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.
29 lines
1.8 KiB
Python
29 lines
1.8 KiB
Python
import sys,os,json,pickle,numpy as np,cv2,time
|
|
sys.path.insert(0,'code'); sys.path.insert(0,os.path.dirname(__file__))
|
|
from ocr import VisionOcr
|
|
from diag import crops
|
|
S=os.path.dirname(__file__)
|
|
L=json.load(open(f'{S}/image6_v2.json'))
|
|
# distinguishing tokens per SKU (any hit counts); includes siblings for near-unknowns
|
|
KEY={'sku_l01':['エッグケア','卵を使って'],'sku_l02':['アマニ'],'sku_l03':['からし','Spicy'],'sku_l04':['めんたい'],
|
|
'sku_l05':['ゼロ','ノンコレ'],'sku_l06':['フィッテ','内臓脂肪'],'sku_l07':['ラッキー'],'sku_l08':['PERI','ペリ'],
|
|
'sku_l09':['コクうま','65'],'sku_l10':['ピュアセレクト','マヨネーズ'],'sku_l11':['ハーフ','1/2'],'kewpie450':['KEWPIE','マヨネーズ']}
|
|
o=VisionOcr(languages=['ja-JP','en-US']); out={}
|
|
for res in ['full','live']:
|
|
rows=[]
|
|
for fc,c in crops('add/image6.jpg',L,res=='live'):
|
|
if fc['label']=='exclude' or c.size==0 or min(c.shape[:2])<8: continue
|
|
for up in [1,2]:
|
|
cc=cv2.resize(c,None,fx=up,fy=up,interpolation=cv2.INTER_CUBIC) if up>1 else c
|
|
t=time.perf_counter(); r=o.read(cc); ms=(time.perf_counter()-t)*1000
|
|
txt=' '.join(l.text for l in r.lines)
|
|
rows.append(dict(index=fc['index'],truth=fc['label'],up=up,text=txt,ms=ms))
|
|
out[res]=rows
|
|
pickle.dump(out,open(f'{S}/ocr.pkl','wb'))
|
|
for res,rows in out.items():
|
|
for up in [1,2]:
|
|
rr=[r for r in rows if r['up']==up]; tr=[r for r in rr if r['truth'] in KEY]
|
|
hit=sum(any(k in r['text'] for k in KEY[r['truth']]) for r in tr)
|
|
print(res,'up',up,f'in-catalog key-token hit {hit}/{len(tr)}','median ms',round(np.median([r['ms'] for r in rr]),1))
|
|
for r in out['live']:
|
|
if r['up']==2 and r['truth']!='unknown': print(r['index'],r['truth'],'|',r['text'][:80])
|