product-recognition-demo/recognization/docs/EXTERNAL-EVAL.md
kiyo9w 7d01de8062 feat: an embedding identity backend with a calibrated rejection
The external evaluation showed the matcher cannot say "unknown": on 20 shelves whose
products are absent it claimed 47.8% of facings as recognised, and its raw inlier
score cannot be thresholded (foreign shelves' median 6.0 overlaps the real shelf's
5.2 p10, and the worst offenders score 50-190). So the gallery becomes a selectable
identity backend:

    --identity gallery        (or DORAYAKI_IDENTITY=gallery)

Measured on the same 20 foreign shelves: false accepts fall from 47.8% to 13.8% at
the default threshold and to **5%** at 0.60, with a smooth curve to pick an operating
point from. Detection is unchanged (the gallery only names boxes).

What it does NOT fix, measured on a milk shelf where 14 of 107 facings really are
明治 おいしい牛乳 (labelled by eye): precision is 28-45%, no better than the matcher's
39%, and enrolling the shelf from itself (1-3 views per product) is flat at ~25%
top-1. Off-the-shelf DINOv2 is not discriminative enough for fine-grained SKUs - the
same conclusion the literature reaches, now measured here.

The packaging's own text is a real channel and resolution-bound: 51% recall
single-scale, 60% with the tag reader's multi-scale + red-channel trick, 85% where
the text is large (MEGMILK) and 28% where it is small vertical Japanese.

docs/EXTERNAL-EVAL.md records all of it, including the identity architecture it
implies: reject by default, name from the tag when readable, from the packaging text
when legible, from a fine-tuned embedding otherwise, and hand the rest to the
operator.

175 tests pass.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-25 03:04:18 +09:00

8.1 KiB

Evaluation on real store photos from Wikimedia Commons

Everything else in this repository is measured on two photographs. This evaluates the pipeline on 54 real store photos it has never seen, collected from Wikimedia Commons with their licences (CC0 / CC BY / CC BY-SA / public domain) recorded in a manifest.

Reproduce:

python code/collect_shelf_images.py ~/dorayaki-data/shelf-images   # needs network
python code/eval_external_images.py ~/dorayaki-data/shelf-images

1. Detection transfers, and well

Images with at least one detection 46 of 54
Total detections 2,258
Book scans, illustrations, a photo of a shop front 0 detections (correct)

The detector is the teammate's single-class YOLO trained on one shelf, and it found individual cartons, bottles and packets on dairy cases, soy-sauce shelves, spice racks, a confectionery aisle, an Okinawan supermarket and a Hong Kong grocery - with tight boxes. Detection is not the weak link.

2. Identity transfers when the product is genuinely present

The original 21-SKU catalog contains 明治 おいしい牛乳. On File:2011年4月の牛乳 (5588310773).jpg - a milk case photographed in 2011, entirely unseen - the pipeline found 6 facings of 明治 おいしい牛乳 and labelled them correctly, and on File:Kawasaki-Inryo-1.jpg it found 野菜生活100 and プロビオヨーグルト R-1. So the matcher does transfer to new photographs of products it has templates for.

3. Precision is the failure, and rejection is missing entirely

The same run labelled these as 明治 おいしい牛乳, in the same photograph:

  • 低脂肪殺菌牛乳 (a タカナシ carton, top left)
  • 有機豆乳 (top left corner)
  • 特濃4.5 (three cartons, top middle)
  • 無調整豆乳 (top right)
  • 北海道牛乳 (several, bottom right)

They are all white milk cartons. The matcher has no way to say "unknown", so every look-alike becomes a claim.

Measured over 24 foreign shelves whose products are not in the catalog: 451 facings accepted as recognised products, with the score distribution overlapping the real one and its extremes landing on the wrong images:

accepted median score p10 p90
The shelf the catalog describes (image6) 71 18.2 5.2 372.4
Foreign shelves (24 images) 451 6.0 — —
A bento (「198円 中華弁当」) 1 50.5
A supermarket interior 1 80.6
Deer cookies at Nara 6 33.1 189.9

No threshold on this score separates the two: it would have to sit above 5 (to catch the foreign p10) and below 6 (to keep the real median), and the worst offenders score higher than any correct match on the demo shelf.

4. What this means for the plan

It confirms, on real data, the two conclusions the literature and the review pointed at - and it makes them concrete:

  1. The matcher's raw inlier score is not a confidence. It cannot be used for rejection or for ranking, and it is the reason packaging recognition cannot name products on its own.
  2. Fine-grained look-alikes are the failure mode: milk cartons, soy-milk cartons, and the twenty near-identical mayonnaise packs from the earlier benchmark. The GroceryVision study of 190 open VLMs found the same shape (94.5 % Recall@5, a 17.5-point drop at Recall@1): global embeddings cluster categories well and rank visually similar SKUs badly.

So step 4 of the plan is not a refinement, it is the fix: a shortlist followed by local re-ranking (PRISM's shape: SigLIP shortlist, then LightGlue verification) and a calibrated rejection on an embedding distance where same-product and different-product are actually separable (measured earlier: above 0.6 and below 0.5 respectively, unlike this score).

Attribution

The images are from Wikimedia Commons under their individual licences (CC0, CC BY, CC BY-SA, public domain); manifest.json records the author, licence and source URL of each one. They are evaluation material and are not redistributed here.


Part 2: the identity stack, measured on a shelf where the product IS present

Part 1 measured shelves whose products are absent. This part uses File:2011年4月の牛乳 (5588310773).jpg - a milk case where 14 of 107 facings are 明治 おいしい牛乳, a product the original catalog holds - so precision and recall can be computed instead of just false accepts. The 107 crops were labelled by eye from a contact sheet.

The shipped matcher

claims correct precision recall
matcher 31 12 39 % 86 %

It finds almost every true facing and makes 19 false claims, on cartons of 特濃4.5, 低脂肪殺菌牛乳, 明治ラブ and 北海道牛乳. Correct where the product is, wrong on the look-alikes: the same shape as Part 1.

threshold false accepts on foreign shelves milk shelf: precision recall
0.45 29 % 28 % 79 %
0.50 18 % 29 % 64 %
0.55 11 % 35 % 50 %
0.60 5 % 45 % 36 %
0.65 2 % 0 % 0 %

The gallery's value is the controllable rejection: on shelves where nothing matches, false accepts fall from the matcher's 47.8 % to 5 % at 0.60, and the curve is smooth enough to pick an operating point. But on the shelf where the product is present its precision (28-45 %) is no better than the matcher's 39 % - it buys honesty about the unknown, not finer discrimination.

Enrolling a shelf from itself does not fix it

The flywheel's first step is to enroll a store's own shelf. Measured by taking 1-3 views per product from the labelled milk shelf and testing on the rest:

views per SKU top-1 wrong rejected
1 26 % 30 29
2 25 % 27 27
3 23 % 24 26

Flat at ~25 %, and far below the 72-76 % measured on the demo shelf with the same number of views. The demo shelf's facings are near-identical crops of the same photograph; real milk cartons differ by small text and a colour band. Off-the-shelf DINOv2 is not discriminative enough for fine-grained SKUs, which is what the literature says (RP2K and RPC both fine-tune; GroceryVision reports a 17.5-point drop from Recall@5 to Recall@1). Fine-tuning is not a refinement of the plan, it is a requirement - and its labels are what the tag channel and the confirmation loop produce.

The packaging's own text is a real channel, and resolution-bound

The cartons differ by the text printed on them, so OCR was tested as an identity channel - first single-scale, then with the tag reader's trick (two upscales, the red channel, pooled):

product single scale multi-scale + red
MEGMILK (large Latin text) 85 % 85 %
北海道牛乳 (large kanji) 65 % 65 %
明治ラブ 0 % 75 %
特濃4.5 0 % 50 %
明治 おいしい牛乳 (small vertical text) 7 % 28 %
低脂肪殺菌牛乳 / 無調整豆乳 / 酪農3.6 0 % 0 %
overall 51 % 60 %

Text works where it is large and fails where it is small - the same resolution wall as the price tags. This is a capture-spec question, not a model question.

The identity architecture this implies

channel what it is good for measured
Detection finding facings on unseen shelves 46/54 images, tight boxes
Tag (corroborated) exact identity and price, where readable 15/15 fields on the demo shelf; sparse in the field
Packaging text brand-level discrimination 60 % recall, higher when text is large
Embedding gallery rejection - saying "unknown" 5 % false accepts at 0.60
Fine-tuned embedding fine-grained SKUs (milk cartons) not built; off-the-shelf is ~25 %
Confirmation loop everything the above cannot settle not built

So the honest design is: reject by default, name the product from the tag when it is readable, from the packaging text when it is legible, from a fine-tuned embedding otherwise - and hand the remainder to the operator, whose answers become the next enrollment.