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.
This commit is contained in:
tngo 2026-09-28 22:27:21 +09:00
parent 25dbf132e7
commit aaed5d388f
44 changed files with 7847 additions and 60 deletions

View file

@ -126,10 +126,34 @@ python code/stream_client.py --source 0 # webcam
Bất kỳ client nào gửi được ảnh JPEG đều dùng được: điện thoại, iPad, camera gắn xe đẩy, camera đeo cổ, robot. Giao thức không phụ thuộc trình duyệt.
For the native app demo, `./run-demo-server.sh` uses `stock-lopia` by default,
matching its `image6` replay frame. Pass another stock directory explicitly to
For the native app demo, `./run-demo-server.sh` uses `stock-lopia-clean` by
default: `stock-lopia` is shelf crops cut from the `image6` evaluation photo, so
scoring against it is memorization. Pass another stock directory explicitly to
change stores; the script writes ignored local app endpoints and a local token.
### Identity backends
`--identity` picks what names a product. The default is `local`:
- `local` — RootSIFT over a per-SKU reference pyramid, similarity-constrained
RANSAC, a hue check, then a text lane (OCR of the crop against each catalog
row's name plus its packshot OCR) that vetoes a match whose package text names
a different row and answers on its own when geometry finds nothing. Measured
on the corrected `image6` labels at live resolution: 59% of in-catalog facings
named at 95% precision, 0 of 48 unknowns accepted. Rows with no enrolled image
are still answerable, because the text lane builds their signature from the
catalog name.
- `matcher` — Van's original SURF/ORB/BRISK composite, kept as the E-1 baseline
lane for comparison. Always answers, so it accepts near-unknown siblings.
- `gallery` — the DINOv2 embedding gallery. Fenced off: it is not the default
and it refuses to start without the pinned weights. Its recall collapses as
the catalog grows (7/42 recall@10 at 493 SKUs), so it is not the recognition
path.
The text lane needs OCR. On macOS that is Vision, which is already wired in
`code/ocr.py`; `DORAYAKI_OCR=none` disables it and the local lane then stands
alone (43% coverage at 79% precision on the same labels).
---