252 lines
11 KiB
HTML
252 lines
11 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Vase Matcher</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
|
</head>
|
|
<body>
|
|
|
|
<header class="hero">
|
|
<div class="hero-inner">
|
|
<p class="eyebrow">feature matching, four ways</p>
|
|
<h1>Vase Matcher</h1>
|
|
<p class="tagline">Photograph a vase and see which reference template it resembles most —
|
|
judged independently by four different computer-vision methods.</p>
|
|
<a class="scroll-cue" href="#gallery">
|
|
<span>Browse the templates</span>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
</header>
|
|
|
|
<section id="gallery" class="section">
|
|
<div class="section-head">
|
|
<h2>Reference templates</h2>
|
|
<p>{{ templates|length }} vases currently in the catalogue</p>
|
|
</div>
|
|
<div class="template-grid">
|
|
{% for t in templates %}
|
|
<figure class="template-card">
|
|
<img src="{{ url_for('template_image', filename=t.filename) }}" alt="{{ t.name }}" loading="lazy">
|
|
<figcaption>{{ t.name }}</figcaption>
|
|
</figure>
|
|
{% endfor %}
|
|
</div>
|
|
</section>
|
|
|
|
<section id="upload" class="section">
|
|
<div class="section-head">
|
|
<h2>Find your match</h2>
|
|
<p>Upload a photo — background removal and all four matchers run automatically.</p>
|
|
</div>
|
|
|
|
<div class="upload-panel">
|
|
<label id="dropzone" for="file-input" class="dropzone">
|
|
<input id="file-input" type="file" accept="image/png,image/jpeg,image/webp,image/bmp" hidden>
|
|
<div class="dropzone-content" id="dropzone-empty">
|
|
<svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
|
|
<path d="M12 16V4M12 4l-4 4M12 4l4 4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
<path d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
<p><strong>Drop a photo</strong> or click to browse</p>
|
|
<p class="hint">PNG, JPG, WEBP, or BMP — up to 15 MB</p>
|
|
</div>
|
|
<img id="preview-img" class="preview-img" alt="Selected preview" hidden>
|
|
</label>
|
|
|
|
<div class="upload-actions">
|
|
<button id="submit-btn" class="btn-primary" disabled>Match this vase</button>
|
|
<button id="clear-btn" class="btn-ghost" hidden>Choose a different photo</button>
|
|
</div>
|
|
|
|
<p id="error-msg" class="error-msg" hidden></p>
|
|
</div>
|
|
|
|
<div id="loading" class="loading" hidden>
|
|
<div class="spinner"></div>
|
|
<p id="loading-text">Removing background…</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="results" class="section" hidden>
|
|
<div class="section-head">
|
|
<h2>Results</h2>
|
|
<p id="total-time"></p>
|
|
</div>
|
|
|
|
<div class="weighted-section" id="weighted-section">
|
|
<h3>Weighted final match</h3>
|
|
<p class="weighted-sub" id="weighted-sub"></p>
|
|
<div class="weighted-top-row">
|
|
<div class="weighted-card" id="weighted-card"></div>
|
|
<div class="weighted-flower-check" id="weighted-flower-check" hidden>
|
|
<p class="weighted-flower-check-label">Flower check</p>
|
|
<div class="weighted-flower-check-loading" id="weighted-flower-check-loading">
|
|
<div class="spinner spinner-sm"></div>
|
|
<p>Comparing flowers…</p>
|
|
</div>
|
|
<div class="weighted-flower-check-body" id="weighted-flower-check-body" hidden>
|
|
<div class="wfc-stat">
|
|
<span class="wfc-stat-label">Flower count</span>
|
|
<span class="wfc-stat-value" id="wfc-count-value"></span>
|
|
</div>
|
|
<div class="wfc-stat">
|
|
<span class="wfc-stat-label">Flower Vase similarity (CLIP)</span>
|
|
<span class="wfc-stat-value" id="wfc-clip-value"></span>
|
|
</div>
|
|
</div>
|
|
<p class="wfc-error error-msg" id="weighted-flower-check-error" hidden></p>
|
|
</div>
|
|
</div>
|
|
<div class="weighted-ranked" id="weighted-ranked"></div>
|
|
</div>
|
|
|
|
<div class="overall-card" id="overall-card"></div>
|
|
|
|
<div class="query-row" id="query-row"></div>
|
|
|
|
<div class="method-grid" id="method-grid"></div>
|
|
|
|
<div class="flower-count-section" id="flower-count-section">
|
|
<h3>Flower count</h3>
|
|
<p class="flower-count-sub">Optional and separate from matching — two
|
|
independent methods, shown side by side. SAM (Segment Anything) finds individual
|
|
flower-shaped regions and color-clusters them as a rough guide to how many
|
|
distinct kinds are in the bouquet; YOLO-World is prompted to recognize "flower",
|
|
"vase", and "ribbon"/"bow" by name, which is also used to stop the vase and any
|
|
ribbon from being miscounted as flowers by SAM. Not run automatically: it
|
|
temporarily unloads the matching models to free GPU memory, so it can take a
|
|
little longer the first time.</p>
|
|
<button id="count-flowers-btn" class="btn-ghost">Count flowers</button>
|
|
<div id="flower-count-loading" class="loading" hidden>
|
|
<div class="spinner"></div>
|
|
<p>Loading SAM + YOLO-World and segmenting your photo…</p>
|
|
</div>
|
|
<p id="flower-count-error" class="error-msg" hidden></p>
|
|
<div id="flower-count-result" class="flower-count-result" hidden>
|
|
<div class="flower-count-method">
|
|
<p class="flower-count-method-label">SAM — instance segmentation</p>
|
|
<img id="flower-count-image" alt="SAM-detected flower instances">
|
|
<p class="flower-count-total" id="flower-count-total"></p>
|
|
<div class="flower-count-clusters" id="flower-count-clusters"></div>
|
|
</div>
|
|
<div class="flower-count-method">
|
|
<p class="flower-count-method-label">YOLO-World — open-vocabulary detection</p>
|
|
<img id="yolo-count-image" alt="YOLO-World detected flower/vase/ribbon regions">
|
|
<p class="flower-count-total" id="yolo-count-total"></p>
|
|
</div>
|
|
</div>
|
|
<p class="flower-count-time" id="flower-count-time" hidden></p>
|
|
|
|
<div class="flower-count-mismatch" id="flower-count-mismatch" hidden>
|
|
<p id="flower-count-mismatch-text"></p>
|
|
</div>
|
|
|
|
<div class="vase-compare" id="vase-compare" hidden>
|
|
<p class="vase-compare-label">Vase comparison — DINOv2 + CLIP</p>
|
|
<p id="vase-compare-error" class="error-msg" hidden></p>
|
|
<div id="vase-compare-body" class="vase-compare-body" hidden>
|
|
<div class="vase-compare-thumbs">
|
|
<div class="vase-compare-tile">
|
|
<img id="vase-crop-input" alt="Vase cropped from your upload">
|
|
<span>Your upload</span>
|
|
</div>
|
|
<span class="arrow">→</span>
|
|
<div class="vase-compare-tile">
|
|
<img id="vase-crop-template" alt="Vase cropped from the matched template">
|
|
<span id="vase-crop-template-name"></span>
|
|
</div>
|
|
</div>
|
|
<div class="vase-compare-scores">
|
|
<p class="vase-verdict-pill" id="vase-verdict-pill"></p>
|
|
<div class="vase-score-row">
|
|
<span>DINOv2 similarity</span>
|
|
<div class="score-bar-track"><div class="score-bar-fill" id="vase-dino-bar"></div></div>
|
|
<span id="vase-dino-value"></span>
|
|
</div>
|
|
<div class="vase-score-row">
|
|
<span>CLIP similarity</span>
|
|
<div class="score-bar-track"><div class="score-bar-fill" id="vase-clip-bar"></div></div>
|
|
<span id="vase-clip-value"></span>
|
|
</div>
|
|
<div class="vase-score-row combined">
|
|
<span>Combined</span>
|
|
<div class="score-bar-track"><div class="score-bar-fill" id="vase-combined-bar"></div></div>
|
|
<span id="vase-combined-value"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="color-section" id="color-section">
|
|
<h3>Color space</h3>
|
|
<p class="color-sub">How closely each template's color palette matches your upload's —
|
|
separate from the SIFT/ORB/SuperGlue/LoFTR match scores above, though it does contribute
|
|
its own share to the weighted final match below.</p>
|
|
<div class="input-palette" id="input-palette"></div>
|
|
<div class="color-grid" id="color-grid"></div>
|
|
</div>
|
|
|
|
<div class="shape-section" id="shape-section">
|
|
<h3>Shape matching</h3>
|
|
<p class="shape-sub">How closely each template's overall silhouette matches your upload's —
|
|
independent of color and texture, and it doesn't factor into the final verdict either.
|
|
Uses Hu-moment contour matching plus a direct silhouette-overlap check.</p>
|
|
<div class="shape-grid" id="shape-grid"></div>
|
|
|
|
<div class="shape-visual" id="shape-visual" hidden>
|
|
<div class="family-grid-overall" id="shape-overall"></div>
|
|
<div class="family-grid-row" id="shape-visual-row"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="texture-section" id="texture-section">
|
|
<h3>Texture matching</h3>
|
|
<p class="texture-sub">How closely each template's surface texture matches your upload's —
|
|
independent of color and shape, and it doesn't factor into the final verdict either.
|
|
Uses Local Binary Patterns plus GLCM (Haralick) statistical texture features.</p>
|
|
<div class="texture-grid" id="texture-grid"></div>
|
|
|
|
<div class="texture-visual" id="texture-visual" hidden>
|
|
<div class="family-grid-overall" id="texture-overall"></div>
|
|
<div class="family-grid-row" id="texture-visual-row"></div>
|
|
<div class="texture-glcm-table" id="texture-glcm-table"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="family-grid-section" id="family-grid-section" hidden>
|
|
<h3>Color family grid</h3>
|
|
<p class="family-grid-sub">Your upload vs <strong id="family-grid-template-name"></strong>,
|
|
each broken into its own dynamically-discovered color regions, matched up region by region.</p>
|
|
<div class="family-grid-overall" id="family-grid-overall"></div>
|
|
<div class="family-grid-row" id="family-grid-row"></div>
|
|
<div class="family-grid-matches" id="family-grid-matches"></div>
|
|
</div>
|
|
|
|
<div class="verify-section" id="verify-section" hidden>
|
|
<h3>AI verification</h3>
|
|
<p class="verify-sub">Cross-checked against <strong id="verify-template-name"></strong> by an
|
|
external vision model, comparing flowers, vase, and ribbon detail by detail.</p>
|
|
<div class="verify-blob">
|
|
<div class="verify-glass" id="verify-glass"></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<footer class="site-footer">
|
|
<p>Vase Matcher · SIFT & ORB (classical) + SuperPoint/LightGlue & LoFTR (deep learning)</p>
|
|
</footer>
|
|
|
|
<script id="templates-data" type="application/json">{{ templates|tojson }}</script>
|
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
|
</body>
|
|
</html>
|