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.2 KiB
Python
22 lines
1.2 KiB
Python
import sys,os,json,pickle,csv,unicodedata,numpy as np,cv2,glob
|
|
sys.path.insert(0,'code'); from ocr import VisionOcr
|
|
S=os.path.dirname(__file__)
|
|
def norm(s):
|
|
s=unicodedata.normalize('NFKC',s).upper()
|
|
tr=str.maketrans('ッャュョァィゥェォヮヵヶユ','ツヤユヨアイウエオワカケユ'); s=s.translate(tr)
|
|
s=s.replace('キユーピー','キユピ').replace('キューピー','キユピ')
|
|
return ''.join(ch for ch in s if ch.isalnum() or ch=='%')
|
|
def grams(s): return {s[i:i+2] for i in range(len(s)-1)}
|
|
def sim(a,b):
|
|
A,B=grams(norm(a)),grams(norm(b))
|
|
return len(A&B)/max(1,len(A)) if A else 0.0 # share of OCR bigrams explained by signature
|
|
# leak-free signatures: catalog names (all SKUs) + OCR of enrolled packshots
|
|
names={r['SKU_ID']:r['product_name'] for r in csv.DictReader(open('stock-lopia/stock.csv'))}
|
|
if __name__=='__main__':
|
|
o=VisionOcr(languages=['ja-JP','en-US']); sig={}
|
|
for k,n in names.items():
|
|
sig[k]=n
|
|
p=f'stock-lopia-clean/{k}.jpg'
|
|
if os.path.exists(p): sig[k]+=' '+' '.join(l.text for l in o.read(cv2.imread(p)).lines)
|
|
for k,v in sig.items(): print(k, v[:90])
|
|
pickle.dump(sig,open(f'{S}/sig.pkl','wb'))
|