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.
22 lines
1.3 KiB
Python
22 lines
1.3 KiB
Python
"""Build the leave-facing-out catalogs: cat_pack (packshots only) and cat_full
|
|
(packshots + the widest facing of each in-catalog SKU as a shelf view), and
|
|
image6_v2_lfo.json with those reference facings excluded from scoring.
|
|
Same-photo references make this an optimistic proxy, not a held-out result."""
|
|
import json, cv2, os, shutil, collections
|
|
E = os.path.dirname(os.path.abspath(__file__))
|
|
L = json.load(open(f'{E}/image6_v2.json')); im = cv2.imread('add/image6.jpg')
|
|
by = collections.defaultdict(list)
|
|
for f in L['facings']:
|
|
if f['label'] not in ('unknown', 'exclude'): by[f['label']].append(f)
|
|
refs = {k: max(fs, key=lambda f: f['box'][2] - f['box'][0])['index'] for k, fs in by.items() if len(fs) >= 2}
|
|
for cat in ('cat_pack', 'cat_full'):
|
|
d = f'{E}/{cat}'; shutil.rmtree(d, ignore_errors=True); os.makedirs(d)
|
|
for p in os.listdir('stock-lopia-clean'):
|
|
if p.endswith('.jpg'): shutil.copy(f'stock-lopia-clean/{p}', d)
|
|
if cat == 'cat_full':
|
|
for k, i in refs.items():
|
|
x1, y1, x2, y2 = L['facings'][i]['box']; cv2.imwrite(f'{d}/{k}_shelf.jpg', im[y1:y2, x1:x2])
|
|
for f in L['facings']:
|
|
if f['index'] in refs.values(): f['label'] = 'exclude'
|
|
json.dump(L, open(f'{E}/image6_v2_lfo.json', 'w'), ensure_ascii=False)
|
|
print('shelf references', refs)
|