product-recognition-demo/recognization/eval/labels/README.md
tngo aaed5d388f feat(identity): local-feature matcher plus text lane as the default backend
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.
2026-09-28 22:27:21 +09:00

3.1 KiB

Shelf label files

Ground truth for the identity evaluators (code/eval_identity.py, code/eval_live.py) and for the evidence scripts in docs/research/2026-09-28-recognition-architecture/evidence/.

{"image": "add/image6.jpg", "version": 2, "facings": [ ... ]}

image is the photo the boxes index into. facings is one row per detected product facing, in that photo's own pixel coordinates.

Row schema

Field Required Meaning
index yes Row identity. Stable across label revisions, so a run's per-facing log joins to truth by index, never by array position.
box yes [x1, y1, x2, y2] in image pixels.
label yes Canonical SKU id (sku_l01), unknown for a facing that is not in the catalog, or exclude for a facing no lane can be scored on.
source yes How the label was decided. v1-confirmed-visual = the v1 label was checked against the package and kept; visual-audit-2026-09-28 = the label was re-read from the package in the 2026-09-28 audit.
v1_label yes The label this row carried in v1. Kept so claims computed on v1 stay reproducible and the 26 corrections stay auditable.
note when label != v1_label Why the label changed. Also on a few unchanged rows that need a remark (e.g. #46-48, the tare bottles that may be what stock.csv calls l12).
match no v1 provenance only: the field DECISION.md §3 reads as a match IoU, and as evidence that v1 labels were propagated from tag or box matching rather than read off packages. Values here are 0.0, 0.911, 1.0. No scorer reads it; it is carried through unchanged for traceability.

label takes a canonical SKU id, unknown, or exclude. This file uses sku_l01-sku_l11: l12 has no real facing on the shelf, so no row carries it. exclude rows stay in the file so the facing inventory is complete; every scorer drops them before computing a rate. The evaluators read index, box, and label; the provenance fields are additive.

image6.json is the corrected v2 set: 111 rows, 109 scored (2 exclude), 61 in-catalog across 11 SKUs, 48 unknown, 26 labels changed from v1. It is a copy of docs/research/2026-09-28-recognition-architecture/evidence/image6_v2.json; edit the evidence copy and re-copy, so the two cannot drift.

Schema check

DECISION.md §7 sets the rule for every label file:

Every label row carries source, confidence, and labeler. A label file without them fails a schema check.

The current file does not meet it. All 111 rows carry source and v1_label, but none carries confidence or labeler: the file records where each label came from and what it replaced, not a per-row certainty or a labeler name. Inventing confidence values after the fact would be worse than the gap, so they are absent rather than guessed.

Tier-2 labels must add both. confidence is the labeler's own certainty for that row; labeler names the person or pass that produced it, because Tier 2 is double-labelled and a disagreement has to be traceable to a pass. Tier 1 (image6_v2.json) is dev-only and its numbers never ship, so it stays as it is.