first commit
This commit is contained in:
910
README.md
Normal file
910
README.md
Normal file
@@ -0,0 +1,910 @@
|
||||
# Vase Matcher
|
||||
|
||||
A Flask web application that takes a photograph of a flower arrangement /
|
||||
vase and identifies which item in a fixed template catalogue it most
|
||||
closely resembles — judged independently by **eleven** different computer
|
||||
vision / deep learning signals, combined into one weighted verdict plus a
|
||||
set of independent diagnostic sections (color, shape, texture, flower
|
||||
count, vase identity) that never influence the score but explain *why* a
|
||||
match looks the way it does.
|
||||
|
||||
This document covers the system from High-Level Design (what the pieces
|
||||
are and why they exist) down to Low-Level Design (exact algorithms, data
|
||||
contracts, file-by-file responsibilities).
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#1-overview)
|
||||
2. [High-Level Design](#2-high-level-design)
|
||||
3. [Feature Walkthrough](#3-feature-walkthrough)
|
||||
4. [Low-Level Design](#4-low-level-design)
|
||||
5. [API Reference](#5-api-reference)
|
||||
6. [Configuration Reference](#6-configuration-reference)
|
||||
7. [Environment & Setup](#7-environment--setup)
|
||||
8. [Testing Tools](#8-testing-tools)
|
||||
9. [Known Limitations & Design Tradeoffs](#9-known-limitations--design-tradeoffs)
|
||||
10. [Glossary](#10-glossary)
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
**Problem**: given a customer's photo of a vase/flower arrangement they
|
||||
received, determine which SKU in a small, fixed template catalogue it is,
|
||||
and explain the confidence of that determination in a way a human can
|
||||
sanity-check.
|
||||
|
||||
**Approach**: no single CV method is reliable enough alone (classical
|
||||
keypoint matchers fail on repetitive floral texture; color alone ignores
|
||||
shape; deep matchers alone ignore the vase). So the app runs a stack of
|
||||
independent methods, combines four of them into a literal **weighted sum**
|
||||
for the primary verdict, aggregates the same four by **rank** (Borda count)
|
||||
as a cross-check, and surfaces several more signals (color, shape, texture,
|
||||
flower count, vase identity, an external AI vision check) purely as
|
||||
**explanatory diagnostics** that a human reviewer can use to understand
|
||||
*why* the weighted verdict landed where it did — without ever touching the
|
||||
score itself.
|
||||
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ Upload a vase photo │
|
||||
└──────────────┬───────────────┘
|
||||
│
|
||||
┌──────────────▼───────────────┐
|
||||
│ Which of N catalogue SKUs │
|
||||
│ does this most resemble? │
|
||||
└──────────────┬───────────────┘
|
||||
│
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
│ │ │
|
||||
┌────────▼────────┐ ┌─────────▼─────────┐ ┌────────▼────────┐
|
||||
│ SCORED (feeds │ │ DIAGNOSTIC ONLY │ │ ON-DEMAND │
|
||||
│ the verdict) │ │ (never scored) │ │ (opt-in, slow) │
|
||||
├──────────────────┤ ├────────────────────┤ ├─────────────────┤
|
||||
│ SIFT │ │ Shape matching │ │ Flower count │
|
||||
│ ORB │ │ Texture matching │ │ (SAM3+YOLO-World)│
|
||||
│ SuperPoint+Light- │ │ Color family grid │ │ Vase identity │
|
||||
│ Glue ("SuperGlue")│ │ AI vision verify │ │ (DINOv2+CLIP) │
|
||||
│ LoFTR │ │ │ │ │
|
||||
│ Color space │ │ │ │ │
|
||||
└──────────────────┘ └────────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. High-Level Design
|
||||
|
||||
### 2.1 System Context
|
||||
|
||||
```
|
||||
┌──────────┐ HTTPS ┌─────────────────────────────┐
|
||||
│ Browser │◄────────────────────►│ Flask app (app.py) │
|
||||
│ (user) │ upload / results │ torch17_new (Python 3.8) │
|
||||
└──────────┘ │ │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ pipeline/* (in-process) │ │
|
||||
│ │ SIFT ORB SuperGlue LoFTR │ │
|
||||
│ │ Color Shape Texture │ │
|
||||
│ │ YOLO-World DINOv2 CLIP │ │
|
||||
│ │ rembg (background removal)│ │
|
||||
│ └─────────────────────────┘ │
|
||||
└───────┬───────────────┬───────┘
|
||||
│ │
|
||||
subprocess │ │ HTTPS (optional)
|
||||
(SAM3 only) │ │
|
||||
▼ ▼
|
||||
┌────────────────────────┐ ┌──────────────────┐
|
||||
│ sam3_worker.py │ │ External vision- │
|
||||
│ sam2_env (Python 3.10) │ │ LLM endpoint │
|
||||
│ transformers 5.5 + SAM3 │ │ (Cloudflare tunnel)│
|
||||
│ facebook/sam3, 4-bit NF4 │ │ "AI verification" │
|
||||
└────────────────────────┘ └──────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ NVIDIA GPU (8GB) │
|
||||
│ shared by both │
|
||||
│ processes, │
|
||||
│ never concurrently │
|
||||
│ loaded (see §4.7) │
|
||||
└───────────────────┘
|
||||
```
|
||||
|
||||
Everything runs on a **single 8GB consumer GPU** (GTX 1070, Pascal /
|
||||
compute capability 6.1). That one constraint shapes almost every
|
||||
architectural decision in this project: sequential (not concurrent) model
|
||||
execution, aggressive image downscaling, explicit model unload/reload
|
||||
cycles, 4-bit quantization for the newest model, and a cross-process split
|
||||
for the one dependency that couldn't share a Python environment with
|
||||
everything else.
|
||||
|
||||
### 2.2 Why Two Python Environments?
|
||||
|
||||
`facebook/sam3` (via Hugging Face `transformers`) needs
|
||||
`transformers>=5.5.0`, which itself requires **Python ≥3.10**. The rest of
|
||||
the app (all the deep-learning matchers, rembg, etc.) lives in a
|
||||
`torch17_new` conda environment pinned to **Python 3.8** for compatibility
|
||||
with older CUDA/driver combinations already validated there. Upgrading that
|
||||
shared environment in place was judged too risky (could silently break
|
||||
DINOv2/CLIP/LightGlue/rembg, all already working). Instead:
|
||||
|
||||
- SAM3 runs in a **separate conda env** (`sam2_env`, Python 3.10) that
|
||||
already had `transformers 5.5.0` and `bitsandbytes` installed.
|
||||
- The main Flask app talks to it via a **one-shot subprocess per request**
|
||||
(`sam3_worker.py`), passing a JSON request file and reading back a JSON
|
||||
response + PNG masks. See [§4.6](#46-sam3-cross-process-bridge) for the
|
||||
exact contract.
|
||||
- This keeps the two environments fully decoupled — SAM3 can be upgraded,
|
||||
reinstalled, or even moved to a different machine/GPU without touching
|
||||
the main app at all.
|
||||
|
||||
### 2.3 Core Design Principles
|
||||
|
||||
| Principle | Why |
|
||||
|---|---|
|
||||
| **Sequential GPU usage, never concurrent** | An 8GB card can't hold multiple deep models' activations at once without fragmentation-driven CUDA OOM. Every heavy pipeline runs its methods one at a time, with `torch.cuda.empty_cache()` between them. |
|
||||
| **Aggressive, uniform downscaling** | Every image (upload *and* template) is capped to `MAX_IMAGE_DIM = 1600px` before touching any model — this single choke point is what keeps memory/time bounded regardless of how large the original photo was. |
|
||||
| **Opt-in for heavyweight features** | Flower counting and vase-identity comparison unload the core matching models first (to free GPU headroom) and are only triggered by explicit user action — they must never slow down the default "upload → match" path. |
|
||||
| **Scored vs. diagnostic separation** | Only 5 signals (SIFT, ORB, SuperGlue, LoFTR, Color) ever feed a number into the weighted verdict. Shape, texture, the color family grid, AI verification, flower count, and vase identity are **always** presented as separate, clearly-labeled sections that cannot move the score — this is a hard invariant enforced by keeping their computation entirely out of `_weighted_scores()`/`_overall_best()`. |
|
||||
| **Two independent "final answer" aggregates** | A literal weighted **sum** (`_weighted_scores`) and a rank-based **Borda count** (`_overall_best`) are computed from the same four raw method outputs but can legitimately disagree — the weighted sum is the primary displayed verdict; Borda is a sanity cross-check. |
|
||||
| **Soft-fail everything non-essential** | A failing color analysis, a stale AI-verification tunnel, a SAM3 subprocess crash — none of these ever take down the core match. Every optional section is wrapped in try/except and reported as its own `error` field. |
|
||||
| **Concept prompting over heuristic filtering** | SAM3 replaced an earlier SAM1-based approach that had to *guess* what counted as "a flower" from size/position/overlap heuristics. Prompting SAM3 directly with the word "flower" (or "vase") does that semantic work at the model level instead. |
|
||||
|
||||
### 2.4 Technology Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|---|---|
|
||||
| Backend web framework | Flask (development server, threaded) |
|
||||
| Classical CV | OpenCV (SIFT, ORB, FLANN matching, RANSAC homography, K-means, GLCM via scikit-image) |
|
||||
| Deep local features | SuperPoint + LightGlue (via the `lightglue` package) |
|
||||
| Deep dense matching | LoFTR (via `kornia.feature`) |
|
||||
| Background removal | `rembg` (BiRefNet-general-lite model), ONNX Runtime (CUDA + CPU fallback) |
|
||||
| Concept segmentation | SAM3 (`facebook/sam3`, Hugging Face `transformers`, 4-bit NF4 via `bitsandbytes`) |
|
||||
| Open-vocabulary detection | YOLO-World (`yolov8s-worldv2.pt`, via `ultralytics`) |
|
||||
| Embedding similarity | DINOv2 (`facebook/dinov2-base`) + CLIP (`openai/clip-vit-base-patch32`), via `transformers` |
|
||||
| External AI check | Third-party vision-LLM endpoint (Cloudflare tunnel), consumed over HTTPS |
|
||||
| Frontend | Vanilla JS (no framework), hand-rolled DOM building, CSS custom properties for theming |
|
||||
| Batch testing | Standalone Python scripts reusing the production pipeline (`testVaseMatcher.py`, `tester.py`) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Feature Walkthrough
|
||||
|
||||
Presented in the order they were built, which is also roughly the order of
|
||||
increasing sophistication:
|
||||
|
||||
1. **Core matching (SIFT / ORB / SuperGlue / LoFTR)** — four independent
|
||||
keypoint/dense matchers, each scored by RANSAC inlier count against
|
||||
every template, run strictly sequentially.
|
||||
2. **Weighted final verdict + Borda overall-best** — two different ways of
|
||||
combining the four raw scores into one winning template.
|
||||
3. **Color space section** — HS-histogram intersection, shown as its own
|
||||
section but also folded into the weighted verdict (`Color` is a 5th
|
||||
contributor there only).
|
||||
4. **Background-removal size fix** — uploads over 15MB are downscaled +
|
||||
re-encoded rather than rejected outright.
|
||||
5. **Color family grid** — dynamic K-means color-region discovery +
|
||||
side-by-side region-matching visualization, purely informational.
|
||||
6. **Shape matching** — Hu-moment contour distance + canonical-silhouette
|
||||
IoU, its own section, never scored.
|
||||
7. **Texture matching** — Local Binary Patterns + GLCM/Haralick features,
|
||||
its own section, never scored.
|
||||
8. **AI verification** — sends the matched template photo + the user's
|
||||
original upload to an external vision-LLM endpoint for a free-text
|
||||
QC-style comparison, parsed into a structured Match/Confidence/
|
||||
Discrepancies/Description card.
|
||||
9. **Flower counting v1 (SAM1)** — Segment Anything (ViT-B) in automatic
|
||||
"segment everything" mode, filtered by size/position heuristics to
|
||||
guess which proposals were flowers. Superseded by step 11.
|
||||
10. **YOLO-World cross-check + vase exclusion** — open-vocabulary detection
|
||||
used first to strip the vase/ribbon out of SAM1's flower count, and
|
||||
kept afterward as an independent second opinion shown side by side.
|
||||
11. **Flower counting v2 (SAM3) + tuned recall** — SAM1 replaced with SAM3
|
||||
concept-prompted segmentation (prompt: `"flower"`), which never
|
||||
proposes the vase/ribbon in the first place — no more heuristic
|
||||
filtering needed. (An intermediate step also tuned SAM1's own
|
||||
`points_stride`/`conf_thres`/`stability_score_thresh` for better
|
||||
recall before the SAM3 replacement landed; that tuning knowledge is
|
||||
preserved in this document for context even though the code path is
|
||||
gone.)
|
||||
12. **Vase-identity comparison (DINOv2 + CLIP)** — crops the vase out of
|
||||
both photos using SAM3's precise mask (not just YOLO's bounding box),
|
||||
background-blacks-out everything else in the crop, and reports a
|
||||
Same/Uncertain/Different verdict from a weighted DINOv2+CLIP cosine
|
||||
similarity.
|
||||
13. **Flower-count mismatch explanation** — a plain-English note ("your
|
||||
photo has 23 flowers vs. 9 in the template") surfaced wherever counts
|
||||
differ, explaining a likely contributor to a lower match score.
|
||||
14. **`tester.py`** — a lean batch-accuracy script that also emits one
|
||||
combined report image per test photo (input segmentation, template
|
||||
segmentation, counts, vase comparison) using the real production
|
||||
pipeline.
|
||||
15. **UI reorder** — the "Weighted final match" section moved to the very
|
||||
top of the results, so the final verdict is visible without scrolling.
|
||||
16. **Auto-triggered "Flower check"** — a lightweight, CLIP-only (no
|
||||
DINOv2) flower-count + flower-similarity mini-panel shown beside the
|
||||
weighted card, fired automatically on every confident match (unlike
|
||||
the full flower-count feature, which stays behind its own button) —
|
||||
with a loading spinner while it computes.
|
||||
|
||||
---
|
||||
|
||||
## 4. Low-Level Design
|
||||
|
||||
### 4.1 Directory Structure
|
||||
|
||||
```
|
||||
featureTransform/
|
||||
├── app.py Flask routes, logging setup, request glue
|
||||
├── config.py All tunables; env-var capping; HF_TOKEN loading
|
||||
├── sam3_worker.py Standalone SAM3 subprocess entrypoint (sam2_env)
|
||||
├── tester.py Lean batch-accuracy + composite report images
|
||||
├── testVaseMatcher.py Fuller batch-accuracy report (CSV, per-method)
|
||||
├── .env HF_TOKEN=hf_... (gitignored, gated model access)
|
||||
│
|
||||
├── pipeline/
|
||||
│ ├── engine.py Orchestrator: bootstrap, per-request pipeline,
|
||||
│ │ weighted/Borda aggregation, opt-in features
|
||||
│ ├── bg_removal.py rembg wrapper, disk caching, mask creation
|
||||
│ ├── classical.py SIFT / ORB extraction + FLANN matching
|
||||
│ ├── deep.py SuperPoint+LightGlue, LoFTR
|
||||
│ ├── color.py HS-histogram + dominant-color comparison
|
||||
│ ├── color_grid.py Dynamic K-means color-family grid
|
||||
│ ├── shape_match.py Hu moments + canonical-silhouette IoU
|
||||
│ ├── texture_match.py LBP + GLCM/Haralick
|
||||
│ ├── flower_count.py SAM3-backed per-instance flower counting
|
||||
│ ├── yolo_world.py Open-vocabulary flower/vase/ribbon detection
|
||||
│ ├── vase_compare.py DINOv2 + CLIP vase-identity comparison
|
||||
│ ├── sam3_client.py Subprocess bridge INTO sam3_worker.py
|
||||
│ ├── verify.py External AI-verification HTTP client
|
||||
│ └── utils.py Template listing, score annotation, compression
|
||||
│
|
||||
├── templates/
|
||||
│ └── index.html Single-page UI (Jinja for the template gallery)
|
||||
│
|
||||
├── static/
|
||||
│ ├── css/style.css
|
||||
│ └── js/main.js All client-side rendering logic
|
||||
│
|
||||
├── cache/
|
||||
│ ├── templates_nobg/ Background-removed templates, cached by name
|
||||
│ └── uploads_nobg/ Background-removed uploads, cached by content hash
|
||||
│
|
||||
├── uploads/<request_id>/ Per-request working files (originals, overlays,
|
||||
│ crops, family grids) -- swept after 6 hours
|
||||
├── logs/app.log Rotating log, all pipeline.* + werkzeug output
|
||||
└── test_reports/ tester.py's composite report images
|
||||
```
|
||||
|
||||
### 4.2 Module Responsibilities
|
||||
|
||||
#### `config.py`
|
||||
The single source of truth for every tunable constant in the system, plus
|
||||
two pieces of process-startup plumbing that **must** run before any heavy
|
||||
import:
|
||||
1. Caps `OMP_NUM_THREADS`/`OPENBLAS_NUM_THREADS`/`MKL_NUM_THREADS`/
|
||||
`NUMEXPR_NUM_THREADS`/`ORT_NUM_THREADS` to `min(4, cpu_count)` — done via
|
||||
`os.environ.setdefault` at import time, before `cv2`/`onnxruntime`/
|
||||
`torch` are imported anywhere in the process. Left uncapped, each
|
||||
library grabs one thread per core, which starves everything else on a
|
||||
shared machine.
|
||||
2. Sets `PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True` — the caching
|
||||
allocator's own recommended mitigation for fragmentation-driven CUDA
|
||||
OOM on a small card.
|
||||
|
||||
It also reads `HF_TOKEN` once (env var first, then a tiny hand-rolled
|
||||
`.env` parser as fallback) so the SAM3 subprocess can be handed it
|
||||
explicitly without needing a `python-dotenv` dependency.
|
||||
|
||||
#### `pipeline/engine.py`
|
||||
The orchestrator. Owns:
|
||||
- `bootstrap()` — precomputes every template's background-removed image
|
||||
and every method's template-side features/embeddings **once**, at
|
||||
startup, so a live request only ever has to process the single uploaded
|
||||
image.
|
||||
- `process_upload()` / `_process_upload_locked()` — the `/api/match` code
|
||||
path: background removal → sequential SIFT/ORB/SuperGlue/LoFTR →
|
||||
best-image annotation → color/shape/texture analysis → weighted +
|
||||
Borda aggregation → color family grid → response assembly.
|
||||
- `_weighted_scores()` / `_overall_best()` — the two aggregation
|
||||
algorithms (see [§4.4](#44-aggregation-layers)).
|
||||
- `count_flowers()` — the full opt-in flower-count + vase-comparison
|
||||
feature (SAM3 + YOLO-World + DINOv2/CLIP, renders visuals, returns
|
||||
everything for the "Count flowers" button).
|
||||
- `flower_summary()` — the lightweight, auto-triggered companion (SAM3 +
|
||||
CLIP only, no YOLO-World, no DINOv2, no rendered images) shown beside
|
||||
the weighted card on every confident match.
|
||||
- A single `threading.Lock` (`_pipeline_lock`) serializes every request's
|
||||
pipeline end-to-end, so at most one heavy CPU/GPU operation is ever in
|
||||
flight process-wide.
|
||||
|
||||
#### `pipeline/bg_removal.py`
|
||||
Wraps `rembg` (BiRefNet-general-lite). Lazily creates an ONNX Runtime
|
||||
session (`CUDAExecutionProvider` first, falls back to CPU on failure —
|
||||
this fallback is "sticky" for the process's lifetime, a known limitation).
|
||||
Disk-caches results: uploads by content hash, templates by filename.
|
||||
`resize_max_dim()` is the single choke point enforcing `MAX_IMAGE_DIM`
|
||||
everywhere. `unload_session()` drops the session so `count_flowers()`/
|
||||
`flower_summary()` can free its GPU memory before the heavier optional
|
||||
models load.
|
||||
|
||||
#### `pipeline/classical.py`
|
||||
SIFT and ORB feature extraction + FLANN-based matching. Template
|
||||
descriptors are precomputed once and cached in-memory (`_template_features`).
|
||||
See [§4.3](#43-scoring-algorithms) for the exact scoring formula.
|
||||
|
||||
#### `pipeline/deep.py`
|
||||
SuperPoint+LightGlue (labeled "SuperGlue" in the UI, matching an existing
|
||||
naming convention) and LoFTR. `unload_models()` drops all three model
|
||||
objects (SuperPoint, LightGlue, LoFTR) so the opt-in flower/vase features
|
||||
can reclaim their GPU memory; they lazily reload on the next `/api/match`
|
||||
call, identical to a fresh process start.
|
||||
|
||||
#### `pipeline/color.py`
|
||||
Independent, non-scored-looking-but-actually-scored (folded into the
|
||||
weighted verdict via `Color`) comparison: Hue+Saturation 2D histogram
|
||||
(Value/brightness deliberately excluded for lighting invariance),
|
||||
compared via histogram intersection. Also computes dominant colors
|
||||
(K-means in a bounded sample) and per-color Lab-distance similarity pairs
|
||||
for the visual palette display.
|
||||
|
||||
#### `pipeline/color_grid.py`
|
||||
Dynamic K-means color-region discovery in LAB space (lightness
|
||||
down-weighted so shadows of the same hue cluster together), rendered as a
|
||||
tile grid, region-matched by nearest average color, with an
|
||||
area-match-percentage per region and an overall area-weighted headline
|
||||
number. Purely visual — tied to whichever template the *weighted* verdict
|
||||
already picked.
|
||||
|
||||
#### `pipeline/shape_match.py`
|
||||
Two complementary shape signals, averaged into one `match_pct`:
|
||||
- **Hu moments** (`cv2.matchShapes`, `CONTOURS_MATCH_I1`) — translation/
|
||||
rotation/scale-invariant contour distance, converted to a similarity %.
|
||||
- **Canonical-silhouette IoU** — both masks cropped to their own bounding
|
||||
box, resized/centered into a fixed square canvas, then measured as
|
||||
direct pixel overlap. This also produces the side-by-side + overlay
|
||||
visualization images.
|
||||
|
||||
#### `pipeline/texture_match.py`
|
||||
Two complementary texture signals, averaged into one `match_pct`:
|
||||
- **Local Binary Patterns** (uniform method), compared as a histogram
|
||||
intersection — catches fine, repetitive patterns (fabric weave, petal
|
||||
grain).
|
||||
- **GLCM/Haralick features** (contrast, homogeneity, energy, correlation),
|
||||
compared as a normalized relative difference, averaged across
|
||||
properties and across 4 angles/2 distances — catches coarser
|
||||
smooth-vs-rough material differences.
|
||||
|
||||
#### `pipeline/flower_count.py`
|
||||
SAM3-backed per-instance flower counting. `count_flowers(bgr, mask,
|
||||
workdir, instances_raw=...)` accepts already-fetched SAM3 instances (so
|
||||
`engine.py` can batch multiple images/prompts into one subprocess call
|
||||
and hand each result set to this function separately), applies a light
|
||||
foreground-overlap sanity filter, then color-clusters the survivors (LAB
|
||||
K-means, capped at `SAM_MAX_KIND_CLUSTERS`) as a rough proxy for distinct
|
||||
flower "kinds". `union_mask()` OR-combines every instance mask into one —
|
||||
used to crop "just the flower material" for CLIP similarity.
|
||||
`render_instances()` draws the numbered, color-coded overlay.
|
||||
|
||||
#### `pipeline/yolo_world.py`
|
||||
Open-vocabulary detection (`yolov8s-worldv2.pt`) prompted with
|
||||
`["flower", "vase", "ribbon", "bow"]`. Kept as an **independent second
|
||||
opinion** shown side by side with SAM3's per-instance count — it draws
|
||||
one box per contiguous flower region rather than per bloom, so it's a
|
||||
coarser, corroborating signal, not a replacement.
|
||||
|
||||
#### `pipeline/vase_compare.py`
|
||||
- `crop_mask(bgr, mask_bool)` — crops the bounding box of any mask (padded
|
||||
by `VASE_CROP_PAD_FRAC`), blacking out every pixel the mask doesn't
|
||||
cover. Generic despite the module name — also used for the flower-only
|
||||
crop in `flower_summary()`.
|
||||
- `compare_vases()` — DINOv2 + CLIP cosine similarity, combined as a
|
||||
weighted average (`VASE_DINO_WEIGHT=0.6`, `VASE_CLIP_WEIGHT=0.4`),
|
||||
bucketed into `same`/`uncertain`/`different`.
|
||||
- `clip_similarity_pct()` — CLIP-only variant (never touches DINOv2),
|
||||
used by the lightweight auto-triggered flower check.
|
||||
- `unload_models()` — drops both model objects; a no-op for whichever one
|
||||
was never loaded (e.g. DINOv2 stays unloaded entirely on the
|
||||
CLIP-only path).
|
||||
|
||||
#### `pipeline/sam3_client.py`
|
||||
The subprocess bridge (runs *in* the main app's Python 3.8 process). Saves
|
||||
each named input image to a temp PNG, writes a JSON request file
|
||||
describing every `(image, prompt, threshold)` job, invokes
|
||||
`sam3_worker.py` under `sam2_env`'s Python interpreter with `HF_TOKEN`
|
||||
passed through the subprocess environment, waits (bounded by
|
||||
`SAM3_TIMEOUT_SECONDS`), and parses the JSON response + PNG masks back
|
||||
into `{(image_key, prompt): [{"mask": bool ndarray, "score": float, "box": [...]}]}`.
|
||||
Always cleans up its temp directory, even on failure.
|
||||
|
||||
#### `sam3_worker.py`
|
||||
Standalone script, **runs under `sam2_env`'s Python 3.10**, has no
|
||||
dependency on this app's `config`/`pipeline` modules. Loads `Sam3Model` +
|
||||
`Sam3Processor` 4-bit quantized (NF4 via `BitsAndBytesConfig`), processes
|
||||
every job in the request against the already-loaded model (one model load
|
||||
serves every job in a request), writes one PNG per detected instance plus
|
||||
a `response.json` manifest.
|
||||
|
||||
#### `pipeline/verify.py`
|
||||
Thin HTTP client for the external AI-verification endpoint. Posts both
|
||||
images as multipart form data, parses the endpoint's free-text `result`
|
||||
field with a tolerant regex (`DISCREP\w*` rather than a literal spelling,
|
||||
since the LLM behind it doesn't always spell "DISCREPANCIES" consistently)
|
||||
into `{match, confidence, discrepancies, description}`.
|
||||
|
||||
#### `pipeline/utils.py`
|
||||
Template file listing, score-annotation overlay text, PNG encoding, and
|
||||
`compress_image_bytes()` — downscales + re-encodes as JPEG only if an
|
||||
upload exceeds `COMPRESS_ABOVE_BYTES`, reusing `MAX_IMAGE_DIM` since the
|
||||
pipeline downsamples to that anyway.
|
||||
|
||||
### 4.3 Scoring Algorithms
|
||||
|
||||
#### SIFT / ORB (`pipeline/classical.py`)
|
||||
```
|
||||
1. Extract keypoints + descriptors (masked to foreground)
|
||||
2. FLANN k=2 nearest-neighbor match against template descriptors
|
||||
3. Lowe's ratio test: keep match if best.distance < 0.75 * secondBest.distance
|
||||
4. RANSAC homography (5.0px threshold) across surviving matches
|
||||
5. score = RANSAC inlier count (raw integer)
|
||||
confidence = inliers / good_matches * 100 (%)
|
||||
```
|
||||
|
||||
#### SuperGlue = SuperPoint + LightGlue (`pipeline/deep.py`)
|
||||
Same shape as above, but SuperPoint (neural keypoints/descriptors) replaces
|
||||
SIFT/ORB's detector and LightGlue (learned matcher) replaces FLANN+ratio
|
||||
test. Matches filtered to inside the foreground mask before the same
|
||||
RANSAC-inlier-count scoring.
|
||||
|
||||
#### LoFTR (`pipeline/deep.py`)
|
||||
Dense matcher — no keypoint detection step. Directly regresses pixel
|
||||
correspondences, keeps only ones above `LOFTR_CONFIDENCE_THRESHOLD` (0.5),
|
||||
filters to inside the mask, then the same RANSAC-inlier-count scoring.
|
||||
|
||||
#### Color space (`pipeline/color.py`)
|
||||
```
|
||||
hist = HS 2D histogram (Hue×Saturation only, Value excluded), normalized to sum=1
|
||||
match_pct = Σ min(hist_input[i], hist_template[i]) × 100 (histogram intersection)
|
||||
```
|
||||
|
||||
#### Shape matching (`pipeline/shape_match.py`)
|
||||
```
|
||||
hu_similarity = max(0, 100 × (1 - matchShapes(contour_a, contour_b) / 1.5))
|
||||
iou = |mask_a ∩ mask_b| / |mask_a ∪ mask_b| × 100 (canonical-aligned)
|
||||
match_pct = (hu_similarity + iou) / 2
|
||||
```
|
||||
|
||||
#### Texture matching (`pipeline/texture_match.py`)
|
||||
```
|
||||
lbp_similarity = Σ min(hist_a[i], hist_b[i]) × 100 (LBP histogram intersection)
|
||||
glcm_similarity = mean over 4 properties of max(0, 1 - |a-b|/max(|a|,|b|)) × 100
|
||||
match_pct = (lbp_similarity + glcm_similarity) / 2
|
||||
```
|
||||
|
||||
#### Vase identity (`pipeline/vase_compare.py`)
|
||||
```
|
||||
cosine_pct(a, b) = max(0, min(1, a·b)) × 100 (a, b are L2-normalized embeddings)
|
||||
|
||||
dino_pct = cosine_pct(DINOv2(crop_a), DINOv2(crop_b))
|
||||
clip_pct = cosine_pct(CLIP(crop_a), CLIP(crop_b))
|
||||
combined_pct = 0.6 × dino_pct + 0.4 × clip_pct
|
||||
verdict = "same" if combined_pct >= 75
|
||||
"uncertain" if combined_pct >= 60
|
||||
"different" otherwise
|
||||
```
|
||||
|
||||
#### Flower CLIP similarity (`flower_summary`, CLIP-only variant)
|
||||
```
|
||||
flower_clip_pct = cosine_pct(CLIP(union_of_flower_crops_a), CLIP(union_of_flower_crops_b))
|
||||
```
|
||||
|
||||
### 4.4 Aggregation Layers
|
||||
|
||||
Two genuinely different "who won" computations exist side by side, and are
|
||||
allowed to disagree:
|
||||
|
||||
**Weighted final match** (`engine._weighted_scores`) — a literal weighted
|
||||
**sum** of raw scores:
|
||||
```
|
||||
weighted_score(template) = Σ_method WEIGHT[method] × raw_score(method, template)
|
||||
|
||||
WEIGHT = { LoFTR: 0.75, SuperGlue: 0.15, SIFT: 0.10, ORB: 0.10, Color: 0.7 }
|
||||
```
|
||||
Because this sums *raw* scores (inlier counts, which run much higher for
|
||||
LoFTR/SuperGlue than SIFT/ORB, plus Color's 0-100 percentage), a method
|
||||
with naturally larger magnitudes pulls harder on the total even at a
|
||||
similar or lower weight. That's the deliberate behavior of the formula as
|
||||
specified, not a bug — see the comment block in `config.py` above
|
||||
`METHOD_WEIGHTS`.
|
||||
|
||||
**Overall best** (`engine._overall_best`, Borda count) — rank-based, and
|
||||
excludes `Color` entirely (only the original four methods participate):
|
||||
```
|
||||
for each method:
|
||||
rank all templates by that method's own raw score
|
||||
template at rank i (0-indexed) earns (n - i) points
|
||||
overall_best = template with the highest total points across all methods
|
||||
```
|
||||
Rank-based aggregation means no single method's raw-score magnitude can
|
||||
dominate the way it can in the weighted sum — this is why the two verdicts
|
||||
are kept, and shown, separately rather than collapsed into one number.
|
||||
|
||||
### 4.5 Request Lifecycles
|
||||
|
||||
#### `POST /api/match`
|
||||
```
|
||||
Browser Flask (app.py) engine.py GPU
|
||||
│ upload image │ │ │
|
||||
├─────────────────────────► │ │
|
||||
│ │ compress if >15MB │ │
|
||||
│ ├──────────────────────────► │ │
|
||||
│ │ │ bg_removal (rembg) │
|
||||
│ │ ├────────────────────►│
|
||||
│ │ │◄────────────────────┤
|
||||
│ │ │ SIFT (sequential) │
|
||||
│ │ ├────────────────────►│
|
||||
│ │ │ ORB │
|
||||
│ │ ├────────────────────►│
|
||||
│ │ │ SuperGlue │
|
||||
│ │ ├────────────────────►│
|
||||
│ │ │ LoFTR │
|
||||
│ │ ├────────────────────►│
|
||||
│ │ │ Color / Shape / │
|
||||
│ │ │ Texture (CPU) │
|
||||
│ │ │ weighted + Borda │
|
||||
│ │ │ color family grid │
|
||||
│ │◄───────────────────────────┤ │
|
||||
│◄───────────────────────── JSON result │ │
|
||||
```
|
||||
SuperPoint/LightGlue/LoFTR **stay loaded** in memory after this request
|
||||
(for fast repeated matching) — they are only unloaded when an opt-in
|
||||
flower/vase feature explicitly needs the GPU headroom.
|
||||
|
||||
#### `POST /api/count_flowers` (opt-in, "Count flowers" button)
|
||||
```
|
||||
Browser app.py engine.count_flowers() sam3_client → sam3_worker.py (sam2_env) YOLO-World DINOv2+CLIP
|
||||
│ click │ │ │ │ │
|
||||
├───────────────► │ │ │ │
|
||||
│ ├────────────────────► │ │ │
|
||||
│ │ │ unload SuperPoint/LightGlue/LoFTR + rembg session │ │
|
||||
│ │ │ yolo_world.detect() ────────────────────────────────────────────►│ │
|
||||
│ │ │ batch: input+flower, input+vase, [template+vase, template+flower] │ │
|
||||
│ │ ├─────────────────────────────► │ │
|
||||
│ │ │ (one SAM3 model load serves every job) │ │
|
||||
│ │ │◄─────────────────────────────┤ │ │
|
||||
│ │ │ cluster + render SAM instances (input & template) │ │
|
||||
│ │ │ render YOLO-World boxes │ │
|
||||
│ │ │ vase_compare (DINOv2+CLIP) ──────────────────────────────────────────────────►│
|
||||
│ │ │◄─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │ │ unload YOLO-World, DINOv2, CLIP │ │
|
||||
│ │◄───────────────────┤ │ │
|
||||
│◄─────────────── JSON + image URLs │ │ │
|
||||
```
|
||||
|
||||
#### `POST /api/flower_summary` (auto-triggered, beside the weighted card)
|
||||
```
|
||||
Browser app.py engine.flower_summary() sam3_client/worker CLIP only
|
||||
│ (fires automatically │ │ │
|
||||
│ right after a confident match) │ │ │
|
||||
├────────────────────────────────────► │ │
|
||||
│ │ unload matching models │ │
|
||||
│ │ batch: input+flower, │ │
|
||||
│ │ template+flower │ │
|
||||
│ ├─────────────────────────────►│ │
|
||||
│ │◄─────────────────────────────┤ │
|
||||
│ │ count comparison (no render)│ │
|
||||
│ │ union masks, crop, CLIP-only similarity ────────────►│
|
||||
│ │◄──────────────────────────────────────────────────────┤
|
||||
│ │ unload CLIP (DINOv2 never loaded) │
|
||||
│◄──────────────────────────────────── │
|
||||
```
|
||||
Deliberately skips YOLO-World, DINOv2, and any rendered overlay images — it
|
||||
exists purely to be fast enough to run unconditionally on every match.
|
||||
|
||||
#### `POST /api/verify` (AI verification, auto-fires after a confident match)
|
||||
```
|
||||
Browser app.py verify.py External vision-LLM endpoint
|
||||
│ │ │ │
|
||||
├──────────────► │ │
|
||||
│ ├────────────────► POST multipart (image1, image2)
|
||||
│ │ ├─────────────────────────►│
|
||||
│ │ │◄─────────────────────────┤
|
||||
│ │ │ parse MATCH/CONFIDENCE/ │
|
||||
│ │ │ DISCREPANCIES/description│
|
||||
│ │◄──────────────── │
|
||||
│◄────────────── JSON (or 502 if endpoint unreachable) │
|
||||
```
|
||||
Failures here (stale Cloudflare tunnel, timeout) are surfaced as a soft
|
||||
error in the UI, never a hard failure of the page.
|
||||
|
||||
### 4.6 SAM3 Cross-Process Bridge
|
||||
|
||||
**Request** (JSON file, written by `sam3_client.py`, read by `sam3_worker.py`):
|
||||
```json
|
||||
{
|
||||
"images": {
|
||||
"input": "/path/to/uploads/<request_id>/sam3_xxxx/input.png",
|
||||
"template": "/path/to/uploads/<request_id>/sam3_xxxx/template.png"
|
||||
},
|
||||
"jobs": [
|
||||
{"image": "input", "prompt": "flower", "threshold": 0.5},
|
||||
{"image": "input", "prompt": "vase", "threshold": 0.3},
|
||||
{"image": "template", "prompt": "vase", "threshold": 0.3}
|
||||
],
|
||||
"output_dir": "/path/to/uploads/<request_id>/sam3_xxxx/out"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (`response.json`, written by the worker):
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{"image": "input", "prompt": "flower", "instances": [
|
||||
{"mask_file": "input_flower_0_0.png", "score": 0.7486, "box": [x1, y1, x2, y2]},
|
||||
{"mask_file": "input_flower_0_1.png", "score": 0.7006, "box": [...]}
|
||||
]},
|
||||
{"image": "input", "prompt": "vase", "instances": [
|
||||
{"mask_file": "input_vase_1_0.png", "score": 0.9391, "box": [...]}
|
||||
]}
|
||||
],
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
Each `mask_file` is a standalone grayscale PNG (0/255) at the original
|
||||
image's resolution — `sam3_client.py` reads it back with
|
||||
`cv2.imread(..., IMREAD_GRAYSCALE) > 127` to reconstruct a boolean mask.
|
||||
On any failure, `error` is a string (worker traceback included) and the
|
||||
process exits non-zero; `sam3_client.py` raises `Sam3Error` either way,
|
||||
which every caller treats as a soft failure.
|
||||
|
||||
**Why 4-bit (NF4) quantization**: the unquantized checkpoint is ~3.2GB
|
||||
(`model.safetensors`). Loading it naively (8-bit, `device_map="cuda:0"`)
|
||||
hit a CUDA OOM during Transformers' own memory pre-flight ("caching
|
||||
allocator warmup") step on this 8GB card, because that warmup reserves
|
||||
capacity based on the *original* dtype size before quantization actually
|
||||
reduces it. Switching to `load_in_4bit=True` + `bnb_4bit_quant_type="nf4"`
|
||||
+ `low_cpu_mem_usage=True` fixed this: ~700MB resident, ~1.9GB peak during
|
||||
inference, ~6s to load, ~2s per prompt.
|
||||
|
||||
**Why `sam.predictor(...)` isn't called directly for automatic-mode
|
||||
tuning** (historical note, from the SAM1 era): Ultralytics' high-level
|
||||
`model.predict()`/`model()` call validates every kwarg against a fixed
|
||||
CLI-style config schema, which rejects automatic-mode-only parameters like
|
||||
`points_stride`/`conf_thres` with a `SyntaxError`. The fix at the time was
|
||||
calling `model.predictor(...)` directly (after a one-time warmup call to
|
||||
force predictor construction), bypassing that validation entirely since
|
||||
`BasePredictor.__call__` forwards arbitrary kwargs straight through. This
|
||||
code path no longer exists (SAM1 was fully replaced by SAM3), but the
|
||||
technique is documented here in case a similar situation recurs with
|
||||
another Ultralytics-wrapped model (e.g. YOLO-World).
|
||||
|
||||
### 4.7 GPU Memory Management Strategy
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────┐
|
||||
│ Idle / after /api/match │
|
||||
│ SuperPoint + LightGlue + LoFTR RESIDENT │
|
||||
│ (~200-900MB, kept warm for fast matching) │
|
||||
└──────────────────┬────────────────────────┘
|
||||
│ user clicks "Count flowers"
|
||||
│ or a confident match fires
|
||||
│ the auto flower-check
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ deep.unload_models() │
|
||||
│ bg_removal.unload_session() │
|
||||
│ torch.cuda.empty_cache() │
|
||||
│ → GPU now near-baseline (~200-900MB) │
|
||||
└──────────────────┬────────────────────────┘
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ SAM3 (separate PROCESS, sam2_env) │
|
||||
│ ~700MB resident / ~1.9GB peak │
|
||||
│ + optionally YOLO-World (~200MB) │
|
||||
│ + optionally DINOv2 (~350MB) + CLIP (~600MB) │
|
||||
└──────────────────┬────────────────────────┘
|
||||
│ request finishes
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ yolo_world.unload_model() │
|
||||
│ vase_compare.unload_models() │
|
||||
│ (SAM3's own process has already exited — │
|
||||
│ its GPU memory is freed by the OS/driver, │
|
||||
│ nothing to unload on the main-process side) │
|
||||
│ → back to near-baseline │
|
||||
└───────────────────────────────────────────┘
|
||||
```
|
||||
Every model getter (`deep.get_superpoint()`, `flower_count`'s SAM3 client,
|
||||
`vase_compare._get_dino()`/`_get_clip()`, `yolo_world.get_model()`) is a
|
||||
lazy singleton — `None` until first use, reset to `None` by the matching
|
||||
`unload_*()` function. This means "unload" is always safe to call even if
|
||||
the model was never loaded (checked via `is not None`), and every model
|
||||
transparently reloads itself on the next call that needs it, with no
|
||||
special-casing required anywhere else in the codebase.
|
||||
|
||||
### 4.8 Frontend Structure (`static/js/main.js`)
|
||||
|
||||
Single IIFE, no framework, no build step. Key structure:
|
||||
- **Upload flow**: drag/drop or file picker → `fetch POST /api/match` →
|
||||
`renderResults(data)`.
|
||||
- **`renderResults(data)`** populates every section in DOM order (weighted
|
||||
card first, then overall-best, query images, method grid, flower count,
|
||||
color/shape/texture/family-grid sections), then fires two
|
||||
**auto-triggered, non-blocking** follow-ups: `runVerification()` (AI
|
||||
check) and `runFlowerSummary()` (the lightweight flower-check panel) —
|
||||
neither blocks the initial render, both show their own loading state and
|
||||
fail independently.
|
||||
- **`renderWeighted(data)`** builds the terracotta-gradient weighted card
|
||||
(input/template thumbnails, breakdown table, Strong/Moderate/Weak
|
||||
confidence pill derived purely from the *margin* between 1st and 2nd
|
||||
place — display-only, cannot feed back into the algorithm).
|
||||
- **Opt-in flower count**: `countFlowersBtn` click handler → `fetch POST
|
||||
/api/count_flowers` → `renderFlowerCount(data)`, which in turn calls
|
||||
`renderFlowerCountMismatch()` and `renderVaseComparison()`.
|
||||
- Every async section follows the same pattern: hide body / show spinner →
|
||||
fetch → on success show body, on failure show a scoped error message —
|
||||
never a page-level failure.
|
||||
|
||||
---
|
||||
|
||||
## 5. API Reference
|
||||
|
||||
| Method | Path | Body | Returns |
|
||||
|---|---|---|---|
|
||||
| GET | `/` | — | Rendered HTML page (template gallery + upload form) |
|
||||
| GET | `/template_image/<filename>` | — | Raw template image bytes |
|
||||
| GET | `/uploads/<request_id>/<filename>` | — | Any file written into that request's working directory |
|
||||
| POST | `/api/match` | multipart `image` file | Full match result: per-method scores, weighted verdict, Borda overall-best, color/shape/texture/family-grid analysis |
|
||||
| POST | `/api/count_flowers` | JSON `{request_id, template}` | SAM3 + YOLO-World flower counts (with visuals), vase comparison, flower-count-mismatch note |
|
||||
| POST | `/api/flower_summary` | JSON `{request_id, template}` | Lightweight flower-count comparison + CLIP-only flower similarity (no images) |
|
||||
| POST | `/api/verify` | JSON `{request_id, template}` | External AI vision-LLM verdict: match/confidence/discrepancies/description |
|
||||
|
||||
All POST endpoints return `{"error": "..."}` with a non-200 status on
|
||||
failure; the frontend treats every one of these as scoped/local, never
|
||||
page-fatal.
|
||||
|
||||
---
|
||||
|
||||
## 6. Configuration Reference
|
||||
|
||||
All of the following live in `config.py`, grouped by feature:
|
||||
|
||||
| Setting | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `MAX_IMAGE_DIM` | 1600 | Every image downscaled to this before any model touches it |
|
||||
| `METHOD_WEIGHTS` | `{LoFTR: .75, SuperGlue: .15, SIFT: .10, ORB: .10, Color: .7}` | Weighted-sum verdict weights |
|
||||
| `SCORE_THRESHOLD` | per-method | "Confident match" badge cutoff (doesn't affect ranking) |
|
||||
| `MAX_CONTENT_LENGTH_BYTES` | 50MB | Hard reject ceiling |
|
||||
| `COMPRESS_ABOVE_BYTES` | 15MB | Soft threshold — downscale+recompress instead of reject |
|
||||
| `SHAPE_HU_DISTANCE_SCALE` | 1.5 | Divisor turning Hu-moment distance into a 0-100% similarity |
|
||||
| `TEXTURE_LBP_RADIUS` / `TEXTURE_GLCM_LEVELS` | 2 / 32 | Texture descriptor parameters |
|
||||
| `FAMILY_GRID_K` | 5 | Number of color families discovered per image |
|
||||
| `SAM3_FLOWER_THRESHOLD` / `SAM3_VASE_THRESHOLD` | 0.5 / 0.3 | Confidence cutoff per SAM3 concept prompt |
|
||||
| `SAM3_TIMEOUT_SECONDS` | 180 | Subprocess timeout for the SAM3 worker |
|
||||
| `VASE_DINO_WEIGHT` / `VASE_CLIP_WEIGHT` | 0.6 / 0.4 | Vase-comparison combined-score weights |
|
||||
| `VASE_SAME_THRESHOLD` / `VASE_UNCERTAIN_THRESHOLD` | 75 / 60 | Vase verdict cutoffs (untuned — no ground-truth calibration set exists yet) |
|
||||
| `YOLO_WORLD_CLASSES` | `["flower", "vase", "ribbon", "bow"]` | Open-vocabulary prompt classes |
|
||||
| `VERIFY_ENDPOINT_URL` | Cloudflare tunnel URL | Can go stale if the tunnel restarts — treated as a soft failure |
|
||||
|
||||
---
|
||||
|
||||
## 7. Environment & Setup
|
||||
|
||||
### Two conda environments
|
||||
|
||||
| Env | Python | Purpose | Key packages |
|
||||
|---|---|---|---|
|
||||
| `torch17_new` | 3.8 | Main Flask app | `torch`, `opencv-python`, `kornia`, `lightglue`, `rembg`, `ultralytics`, `transformers==4.46.3` |
|
||||
| `sam2_env` | 3.10 | SAM3 subprocess only | `torch`, `transformers>=5.5.0`, `bitsandbytes`, `accelerate` |
|
||||
|
||||
### Model checkpoints / weights
|
||||
|
||||
| Model | Source | Notes |
|
||||
|---|---|---|
|
||||
| SIFT / ORB | Built into OpenCV | No download needed |
|
||||
| SuperPoint + LightGlue | `lightglue` package | Auto-downloaded on first use |
|
||||
| LoFTR | `kornia.feature` | Auto-downloaded on first use |
|
||||
| rembg (BiRefNet-general-lite) | `rembg` package | Auto-downloaded on first use |
|
||||
| YOLO-World | `yolov8s-worldv2.pt` | Downloaded via `ultralytics`, cached locally |
|
||||
| DINOv2-base | `facebook/dinov2-base` | Hugging Face, public, no token needed |
|
||||
| CLIP ViT-B/32 | `openai/clip-vit-base-patch32` | Hugging Face, public, no token needed |
|
||||
| **SAM3** | `facebook/sam3` | **Gated** on Hugging Face — requires requesting access on the model page and an approved access token |
|
||||
|
||||
### `.env` file
|
||||
|
||||
```
|
||||
HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
Required only for the SAM3-backed features (flower counting, vase
|
||||
comparison, the auto-triggered flower check). Without it, those specific
|
||||
features fail softly with a clear "SAM3 unavailable" message — core
|
||||
matching is entirely unaffected.
|
||||
|
||||
### Running the app
|
||||
|
||||
```bash
|
||||
cd /media/suman/Backup_of_extra_/Sasi/featureTransform
|
||||
conda activate torch17_new
|
||||
python app.py
|
||||
# serves on http://0.0.0.0:5053
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Testing Tools
|
||||
|
||||
Two batch-test scripts exist, covering different needs:
|
||||
|
||||
| | `testVaseMatcher.py` | `tester.py` |
|
||||
|---|---|---|
|
||||
| Output | Detailed CSV + per-method accuracy table | Console pass/fail + one composite report image per photo |
|
||||
| Coverage | All 4 methods + weighted + Borda + color pick, individually tallied | Weighted verdict only |
|
||||
| Flower/vase check | Numbers only (family-grid area-match %), no images | Full: SAM3 flower segmentation (input vs. template), counts, vase comparison — all rendered into one PNG |
|
||||
| Speed | Faster (no SAM3/CLIP round trip) | Slower (~15-25s/image extra for the flower/vase step); `--skip-flowers` for a fast pass |
|
||||
| Use case | Regression-testing every individual method's accuracy | Visual QA — "does this match make sense at a glance" |
|
||||
|
||||
Both expect the same input layout:
|
||||
```
|
||||
<test_dir>/
|
||||
SKU_1/ photo1.jpg photo2.jpg ...
|
||||
SKU_2/ ...
|
||||
```
|
||||
where each subfolder name must exactly match an existing template name.
|
||||
**Common mistake**: pointing either script directly at a `SKU_X/` folder
|
||||
instead of its parent — the script silently finds 0 images, since it
|
||||
expects to `listdir()` a folder of *subfolders*, not a folder of images.
|
||||
|
||||
```bash
|
||||
python tester.py # ./flowers, full report images
|
||||
python tester.py /path/to/flowers --limit 3
|
||||
python tester.py --skip-flowers # fast accuracy-only pass
|
||||
python testVaseMatcher.py /path/to/flowers --csv report.csv
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Known Limitations & Design Tradeoffs
|
||||
|
||||
- **rembg's CUDA fallback is sticky for the process's lifetime.** A
|
||||
transient CUDA OOM during session creation permanently falls back that
|
||||
process to CPU-only background removal (much slower) until restarted.
|
||||
No auto-retry/self-healing exists yet.
|
||||
- **Vase-identity and flower-count-mismatch thresholds are untuned.**
|
||||
There is no ground-truth "same/different vase" or "acceptable flower
|
||||
count delta" labeled dataset — the cutoffs in `config.py` are
|
||||
reasonable defaults, not calibrated ones.
|
||||
- **The weighted verdict's literal-sum formula means raw-score magnitude
|
||||
matters, not just weight.** LoFTR/SuperGlue's inlier counts run far
|
||||
higher than SIFT/ORB's, so they can dominate the total even when their
|
||||
configured weight isn't proportionally larger. This is documented as
|
||||
intentional (matches the formula as originally specified), not a bug.
|
||||
- **YOLO-World's own "flower" count is coarse** — one box per contiguous
|
||||
flower region, not per bloom — so it will never numerically match SAM3's
|
||||
per-instance count; it's shown as an independent corroborating signal,
|
||||
not something expected to agree.
|
||||
- **The AI-verification endpoint is a Cloudflare tunnel URL** that can go
|
||||
stale if restarted on the other end; failures are always soft (shown as
|
||||
"AI verification unavailable"), never block the page.
|
||||
- **SAM3 requires a gated, manually-approved Hugging Face token.** The app
|
||||
cannot obtain this automatically — a human must request access at
|
||||
`huggingface.co/facebook/sam3` and provide a token with access granted.
|
||||
- **Everything assumes a single physical GPU with ~8GB VRAM** and Pascal
|
||||
or later compute capability. The sequential-execution and
|
||||
unload/reload discipline exists specifically because of that constraint
|
||||
— a larger card would allow (but doesn't require) more concurrency.
|
||||
|
||||
---
|
||||
|
||||
## 10. Glossary
|
||||
|
||||
| Term | Meaning |
|
||||
|---|---|
|
||||
| **Inlier count** | Number of matched keypoints that agree with one consistent RANSAC-fit geometric transform between two images — the raw score for SIFT/ORB/SuperGlue/LoFTR |
|
||||
| **Weighted verdict** | The primary displayed "final answer" — literal weighted sum of 5 methods' raw scores |
|
||||
| **Borda count / Overall best** | A secondary, rank-based cross-check aggregate of 4 methods (excludes Color) |
|
||||
| **Concept segmentation** | SAM3's mode of operation: prompted with a plain-English word ("flower"), returns instance masks for exactly that concept, rather than segmenting everything indiscriminately |
|
||||
| **NF4** | "NormalFloat4" — the 4-bit quantization scheme used (via `bitsandbytes`) to fit SAM3 in this GPU's memory budget |
|
||||
| **Flower check** | The lightweight, auto-triggered (not opt-in), CLIP-only flower-count + flower-similarity panel shown beside the weighted card |
|
||||
| **Count flowers** | The full, opt-in, button-triggered feature: SAM3 + YOLO-World flower counts, rendered overlays, and DINOv2+CLIP vase comparison |
|
||||
Reference in New Issue
Block a user