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.
19 lines
932 B
Python
19 lines
932 B
Python
import sys,os,glob,pickle,numpy as np,cv2,json
|
|
sys.path.insert(0,'code'); sys.path.insert(0,os.path.dirname(__file__))
|
|
os.environ['YOLO_OFFLINE']='1'
|
|
from detector import ShelfProductDetector
|
|
from variants import Matcher
|
|
from PIL import Image
|
|
det=ShelfProductDetector()
|
|
res={}
|
|
for cat in sys.argv[1:]:
|
|
name=os.path.basename(cat); m=Matcher(cat,root=True,geom='affine',scales=(200,400)); rows=[]
|
|
for p in sorted(glob.glob('add/*')):
|
|
if 'image6' in p: continue
|
|
im=cv2.cvtColor(np.array(Image.open(p).convert('RGB')),cv2.COLOR_RGB2BGR)
|
|
for b in det.detect(im).get('boxes',[]):
|
|
x1,y1,x2,y2=[int(v) for v in b['box']]; c=im[max(0,y1):y2,max(0,x1):x2]
|
|
if c.size==0 or min(c.shape[:2])<20: continue
|
|
rows.append(dict(img=p,ev=m.score(c)))
|
|
res[name]=rows; print(name,len(rows),'distractor crops')
|
|
pickle.dump(res,open(f'{os.path.dirname(__file__)}/distract.pkl','wb'))
|