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>
This commit is contained in:
kiyo9w 2026-09-25 03:04:18 +09:00
parent f74366d6a4
commit 7d01de8062
3 changed files with 207 additions and 0 deletions

View file

@ -88,3 +88,95 @@ respectively, unlike this score).
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.
## The embedding gallery: rejection, but not precision
| 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.