Files
Vase-Matcher/templates/index.html
2026-08-04 17:09:29 +05:30

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&nbsp;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&nbsp;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&hellip;</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 &mdash; 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&hellip;</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 &mdash; 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 &mdash; 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 &mdash; 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">&rarr;</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 &middot; SIFT &amp; ORB (classical) + SuperPoint/LightGlue &amp; 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>