Replaces the DINO gallery as the recognition path with the measured design from docs/research/2026-09-28-recognition-architecture/DECISION.md: RootSIFT over a per-SKU reference pyramid, similarity-constrained RANSAC and a hue check, fused with an OCR text lane that vetoes a match whose package text names a different catalog row and answers on its own when geometry finds nothing. - identity_lanes.py: LocalLane, TextLane, DecisionPolicy. The text lane builds each row's signature from the catalog name plus its packshot OCR, so a row with no enrolled image is still answerable. - engine.py: every backend groups scores by canonical SKU, so no view-suffixed id reaches the wire and a name and price always resolve. decide_crop fuses the lanes; score_crop_lanes feeds the live path. Both lanes are built in warm_up (non-fatally) so the first frame is not the slow one, and the text lane shares the engine's OCR lock. - live.py: decides per crop on the lane's own rank order. The lane's order comes from its rank key and its value is only the gate, so re-sorting by value named a different SKU than the lane chose; averaging in an empty observation also diluted a good confidence below the accept line. - identity.py: a `local` score scale whose gate is one absolute line, because the value is already a margin over the runner-up. - app.py, run-demo-server.sh: `--identity local` is the default; the demo server no longer starts on the leaked `stock-lopia` catalog. - eval_live.py: the replay vocabulary accepts `local`, so a recording from the shipped server can be scored. - README.md: documents the three backends and what each is for. Measured on the corrected image6 labels at live resolution: 36/61 in-catalog facings named at 95% precision, 0 of 48 unknowns accepted, 11 labels sourced from the text lane alone. Through the real server: /v1/recognize 90% precision, /v1/stream 100% precision, no view-suffixed ids on either. |
||
|---|---|---|
| .. | ||
| an2.py | ||
| analyze.py | ||
| diag.py | ||
| dino_scale.py | ||
| distract.py | ||
| fuse_eval.py | ||
| image6-v1-labels-overlay.jpg | ||
| image6_v2.json | ||
| image6_v2_lfo.json | ||
| lfo_setup.py | ||
| ocr_exp.py | ||
| ocr_fuse.py | ||
| README.md | ||
| scale.py | ||
| sheet.py | ||
| variants.py | ||
Evidence scripts (2026-09-28 audit)
Read-only against product code: every script imports from code/ and writes
only into this folder. Run from recognization/ with .venv/bin/python.
image6_v2.json is the corrected image6 label set (26 of 111 v1 labels
changed; every row carries v1_label, source, and a note where changed).
image6-v1-labels-overlay.jpg shows the v1 labels on the shelf; green = catalog
label, red = unknown. sheet.py regenerates per-SKU contact sheets.
| Step | Command (prefix E=docs/research/2026-09-28-recognition-architecture/evidence) |
Produces |
|---|---|---|
| Original matcher, both catalogs, full/live/live-with-unscaled-boxes | .venv/bin/python $E/diag.py --stock stock-lopia-clean --labels $E/image6_v2.json [--live] [--bug] --out $E/run_stock-lopia-clean_<full|live|livebug>.pkl (repeat with --stock stock-lopia) |
run_*.pkl |
| Score them under v1 or v2 labels | .venv/bin/python $E/analyze.py <labels.json> $E/run_*.pkl |
identity top-1, argmax, current gate |
| Matcher variants | .venv/bin/python $E/variants.py --stock stock-lopia-clean --labels $E/image6_v2.json [--live] --scales 200,400 --root --geom affine --out $E/v_ms_root_aff_<full|live>.pkl (also --tmpl-h 250/400, no flags = raw) |
v_*.pkl |
| Variant ranking + open-set frontier | .venv/bin/python $E/an2.py $E $(basename -a $E/v_*.pkl) (LABELS=image6_v2_lfo.json for the leave-facing-out runs) |
correct at 100/95/90 % precision |
| Leave-facing-out catalog | .venv/bin/python $E/lfo_setup.py, then variants.py --stock $E/cat_<pack|full> --labels $E/image6_v2_lfo.json --scales 200,400 --root --geom affine [--live] --out $E/lfo_cat_<pack|full>_<full|live>.pkl |
lfo_*.pkl |
| Unrelated-shelf distractors | YOLO_OFFLINE=1 .venv/bin/python $E/distract.py stock-lopia-clean $E/cat_full |
distract.pkl |
| Catalog growth, exhaustive vs descriptor index | YOLO_OFFLINE=1 .venv/bin/python $E/scale.py |
stdout |
| DINO retrieval under growth | YOLO_OFFLINE=1 .venv/bin/python $E/dino_scale.py |
stdout |
| OCR read-rate | .venv/bin/python $E/ocr_exp.py |
ocr.pkl |
| OCR signatures, then fusion | .venv/bin/python $E/ocr_fuse.py && .venv/bin/python $E/fuse_eval.py |
sig.pkl, stdout |
| Per-SKU contact sheets + the labelled shelf | .venv/bin/python $E/sheet.py <outdir> |
gt_<label>.jpg, shelf_labels.jpg |
Timings in DECISION.md come from scale.py (single process). The other
scripts ran up to ten processes at once, so their ms figures are relative only.
Verified 2026-09-28
Every command above was run from recognization/ and produced its listed
output. Two rows were wrong as written:
- The table said
python; thepythonon this machine's PATH has nocv2, so every row failed on import. It is.venv/bin/python. an2.pytakes the folder and then file names inside it, sov_*.pklexpanded in the working directory, where those files do not live. It is$(basename -a $E/v_*.pkl).
sheet.py was missing from the table. It takes its output directory as a
positional argument, not a flag: sheet.py --help reads --help as a
directory name, writes nothing, and still exits 0. analyze.py and an2.py
with no arguments raise IndexError; neither validates its own usage.
v1 labels are no longer a file on disk — eval/labels/image6.json is the v2
set. To score a run under v1, rebuild it from the v1_label field:
.venv/bin/python -c "import json;d=json.load(open('$E/image6_v2.json'));\
[f.update(label=f.pop('v1_label')) for f in d['facings']];\
json.dump(d,open('/tmp/image6_v1.json','w'),ensure_ascii=False)"
That reconstruction reproduces DECISION.md's v1 numbers exactly: analyze.py
gives ungated top-1 26/35 at both resolutions and the current gate 24/35 full,
23/35 live (§2 row 15, F-01).