sku_l09.jpg was 407x305, so its 400 px pyramid level was a 305->400 upscale: a blur with no high-frequency detail to disagree with, which won 31 of 54 crops on the shelf it was cut from. Re-sourced at 1000x1000. The floor is now a hard gate in rebuild_catalog.py - a view under 800 px tall is refused, not reported - because reporting it is what let the 305 px view ship. data/packshots/source/sku_l07.jpg (600x600) is refused on this rule. README records the real sizes and the provenance for every row.
152 lines
8 KiB
Markdown
152 lines
8 KiB
Markdown
# LOPIA clean catalog
|
|
|
|
Official manufacturer pack shots, one front view per SKU. This replaces the
|
|
views in `../stock-lopia/`, which were 71-119 px crops cut from the test shelf
|
|
`add/image6.jpg` itself: one 18 px sliver, duplicate crops, a wrong product on
|
|
`sku_l09`, and four SKUs with no view at all. Enrolling from the test shelf is
|
|
why every dark package collapsed onto one SKU and why the identity head was
|
|
trained on its own exam.
|
|
|
|
Eight of the twelve rows are enrolled; `sku_l04`, `sku_l07`, `sku_l08` and
|
|
`sku_l12` stay text-only (see *Known gaps*). Every enrolled view is at least
|
|
1000 px tall.
|
|
|
|
Rebuild from the staged packshots with:
|
|
|
|
python code/rebuild_catalog.py --source data/packshots/source \
|
|
--output stock-lopia-clean --csv stock-lopia/stock.csv \
|
|
--replace --min-views 1
|
|
|
|
`--min-views 1` because official pages publish a single front view per JAN. The
|
|
default floor of 2 exists to catch accidental single-view enrollment from shelf
|
|
crops, where several views are available.
|
|
|
|
To re-run the audit without touching the catalog, point `--output` at a
|
|
scratch path inside the repo and keep the JSON:
|
|
|
|
python code/rebuild_catalog.py --source data/packshots/source \
|
|
--output data/packshots/audit-check --csv stock-lopia/stock.csv \
|
|
--min-views 1 --json data/packshots/audit-check.json
|
|
|
|
## Provenance
|
|
|
|
Product names are the catalog's, verbatim from `stock.csv`; sizes are measured.
|
|
|
|
| SKU | Catalog name | View | Recorded source |
|
|
|---|---|---|---|
|
|
| sku_l01 | キューピー エッグケア 305g | 455x1000 | not recorded |
|
|
| sku_l02 | キューピー アマニ油マヨネーズ 200g | 468x1000 | not recorded |
|
|
| sku_l03 | キューピー からしマヨネーズ 200g | 450x1000 | not recorded |
|
|
| sku_l05 | キューピー ゼロ ノンコレステロール 310g | 461x1000 | not recorded |
|
|
| sku_l06 | キューピー フィッテ 210g | 654x1500 | [amazon.co.jp](https://www.amazon.co.jp/dp/B08V2676LK) |
|
|
| sku_l09 | ピュアセレクト コクうま カロリーカット65% 360g | 1000x1000 | not recorded |
|
|
| sku_l10 | ピュアセレクト マヨネーズ 400g | 1000x1000 | [rakuten.co.jp](https://item.rakuten.co.jp/matsukiyo/4901001114030/) (JAN 4901001114030) |
|
|
| sku_l11 | キューピー ハーフマヨネーズ 400g | 482x1000 | not recorded |
|
|
|
|
`data/packshots/source/ATTRIBUTION.json` records a URL for `sku_l06` and
|
|
`sku_l10` only. Its `_provenance_gap` note names the other six - l01, l02, l03,
|
|
l05, l09, l11 - as enrolled before any provenance was tracked, and the audit
|
|
reports them under `unattributed_skus` on every run. What *is* verified about
|
|
them, per that note: each is independent of the evaluation photo (template
|
|
match against `add/image6.jpg` peaks at 0.166, against 0.95 for a crop of it),
|
|
each is 1000 px or taller, and each one's own OCR matches its catalog row (see
|
|
*Name/OCR gate*). That is a traceability gap, not a correctness one.
|
|
|
|
`sku_l06` is the current package (`内臓脂肪を減らす`), which replaced the
|
|
superseded `内臓脂肪ケア` view on 2026-09-29. The catalog name was shortened to
|
|
match; the shelf tags were not part of that change.
|
|
|
|
## Name/OCR gate
|
|
|
|
A packshot's OCR has to explain its own catalog row's name, or it is refused.
|
|
Measured over the shipped packshots - every image read in all four rotations,
|
|
every read scored against all 12 catalog names:
|
|
|
|
| Class | n | Score |
|
|
|---|---|---|
|
|
| Correct packshot vs its own row | 8 | 0.9167 - 1.0000 |
|
|
| Wrong product vs any row | 1 image x 12 names | 0.0476 - 0.5833 |
|
|
|
|
`MIN_NAME_OCR = 0.75` is the midpoint of those two bands: 0.167 of headroom
|
|
above the worst correct packshot (l11, whose read omits `マヨネーズ`) and 0.167
|
|
below the wrong-product ceiling. The wrong-product image is
|
|
`references/rejected/kewpie-mayonnaise-450g.jpg`; its best score anywhere is
|
|
0.5833, tied against `キューピー からしマヨネーズ 200g` and
|
|
`キューピー ハーフマヨネーズ 400g`, and it bottoms out at 0.0476 against
|
|
`ピュアセレクト コクうま カロリーカット65% 360g`.
|
|
|
|
Reproduce the band with (run from the recognition root; unindented so it runs
|
|
verbatim):
|
|
|
|
```python
|
|
import csv, os, sys, cv2
|
|
sys.path.insert(0, 'code')
|
|
from ocr import build_ocr_engine
|
|
from rebuild_catalog import best_orientation_text, name_ocr_similarity
|
|
|
|
clean = 'stock-lopia-clean'
|
|
names = {r['SKU_ID']: r['product_name'] for r in csv.DictReader(open(clean + '/stock.csv'))}
|
|
engine = build_ocr_engine(None)
|
|
for n in sorted(os.listdir(clean)):
|
|
if n.endswith('.jpg'):
|
|
sku = n[:-4]
|
|
read = best_orientation_text(engine, cv2.imread(os.path.join(clean, n)), names[sku])
|
|
print(sku, round(name_ocr_similarity(names[sku], read), 4))
|
|
|
|
rej = 'references/rejected/kewpie-mayonnaise-450g.jpg'
|
|
read = best_orientation_text(engine, cv2.imread(os.path.join(clean, rej)), names['sku_l01'])
|
|
print('rejected max', max((round(name_ocr_similarity(v, read), 4), k) for k, v in names.items()))
|
|
```
|
|
|
|
The gate cannot separate siblings, and does not try: the `sku_l09` packshot
|
|
scores 0.8462 against the `sku_l10` name, because both are Ajinomoto
|
|
ピュアセレクト mayonnaise. That case is what `text_discriminated_siblings` flags
|
|
(l09 ~ l10, cosine 0.9817) - it is a cross-SKU geometry problem, not a gap a
|
|
threshold closes.
|
|
|
|
## Pyramid floor
|
|
|
|
Every enrolled view must be at least 800 px tall, or it is refused. This is a
|
|
hard gate, not a report. It exists because of `sku_l09`: the view enrolled at
|
|
305 px could not be stored as a 200/400/800 px pyramid, so its local lane
|
|
upscaled 305->400 px and scored 85-129 inliers against 10-40 for every other
|
|
reference, winning facings it should not. It was re-sourced at 1000x1000 on
|
|
2026-09-29. Reporting the defect instead of refusing it is what let it ship.
|
|
|
|
`data/packshots/source/sku_l07.jpg` is 600x600 and is refused on this rule
|
|
before OCR even runs.
|
|
|
|
## Known gaps
|
|
|
|
Four rows in `../stock-lopia/stock.csv` have no enrolled image, so no image
|
|
lane can match them. They stay in `stock.csv` as text-only rows: the text lane
|
|
builds a signature from the printed name, which is how it can name or veto a
|
|
near-unknown the matcher cannot see. They are the current image-coverage
|
|
ceiling, not a recognition failure.
|
|
|
|
| SKU | Catalog name | Why there is no view |
|
|
|---|---|---|
|
|
| sku_l04 | やまや めんたいマヨネーズタイプ 200g | A packshot is staged and refused at name/OCR 0.00: the image reads only `明太手屋さんが つくつ た コ`, brand text that runs vertically. A correctly oriented or higher-resolution shot is what would clear it. |
|
|
| sku_l07 | 丸和油脂 ラッキーマヨネーズ 200g | A packshot is staged and refused twice: it is 600x600, under the pyramid floor, and scores name/OCR 0.5714. Every product bigram reads (`ラッキー マヨネーズ`); the six it misses are the maker prefix `丸和`/`和油`/`油脂` and the size `20`/`0G`/`00`, which the package does not print. |
|
|
| sku_l08 | スパイシーストーム ペリペリミックス 30g | No official 30 g Peri-Peri Mix found |
|
|
| sku_l12 | ユーラス 野菜をおいしく食べる旨たれ 380g | No pack shot sourced |
|
|
|
|
`sku_l10` left this table on 2026-09-29. Its catalog name said 450 g, which
|
|
Ajinomoto does not make; the shelf tag in `add/image6.jpg` and the
|
|
manufacturer's lineup both say 400 g, so `stock.csv` was corrected and a
|
|
1000x1000 packshot (JAN 4901001114030) enrolled. See
|
|
`data/packshots/source/ATTRIBUTION.json`.
|
|
|
|
Keep the text-only rows when rebuilding: the CSV carry-over must preserve every
|
|
source row, not just the SKUs with a kept view. Dropping these four costs the
|
|
text lane the signatures that cover them.
|
|
|
|
## Rejected
|
|
|
|
`references/rejected/kewpie-mayonnaise-450g.jpg` is Kewpie Mayonnaise 450g (JAN
|
|
4901577042072). It was filed as `sku_l09` and then as `sku_l10` because the
|
|
catalog's names pointed there, but it is neither: l09 is Ajinomoto Pure Select
|
|
コクうま カロリーカット 65% 360g and l10 is Pure Select マヨネーズ 400g. Both now
|
|
hold their own correct views. The rejected image is kept as provenance for that
|
|
mix-up and is never enrolled; the name/OCR gate scores it at most 0.5833
|
|
against any catalog name, below the 0.75 threshold.
|