feat(identity): local-feature matcher plus text lane as the default backend
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.
This commit is contained in:
parent
25dbf132e7
commit
aaed5d388f
44 changed files with 7847 additions and 60 deletions
|
|
@ -0,0 +1,28 @@
|
|||
import json,cv2,numpy as np,sys,glob,os
|
||||
d=json.load(open('eval/labels/image6.json'));im=cv2.imread('add/image6.jpg')
|
||||
S=sys.argv[1]
|
||||
H=300
|
||||
def tile(img,txt,col=(0,0,0)):
|
||||
h,w=img.shape[:2];s=H/h;img=cv2.resize(img,(max(1,int(w*s)),H))
|
||||
pad=np.full((H+30,img.shape[1]+6,3),255,np.uint8);pad[30:,3:3+img.shape[1]]=img
|
||||
cv2.putText(pad,txt,(2,20),cv2.FONT_HERSHEY_SIMPLEX,0.5,col,1);return pad
|
||||
groups={}
|
||||
for f in d['facings']:
|
||||
x1,y1,x2,y2=f['box'];c=im[y1:y2,x1:x2]
|
||||
groups.setdefault(f['label'],[]).append(tile(c,f"#{f['index']}"))
|
||||
for lab,ts in groups.items():
|
||||
ref=glob.glob(f'stock-lopia-clean/{lab}.jpg')
|
||||
row=([tile(cv2.imread(ref[0]),'REF',(0,0,255))] if ref else [])+ts
|
||||
rows=[row[i:i+12] for i in range(0,len(row),12)]
|
||||
out=[]
|
||||
for r in rows:
|
||||
w=sum(t.shape[1] for t in r);canvas=np.full((H+30,max(w,10),3),255,np.uint8);x=0
|
||||
for t in r: canvas[:,x:x+t.shape[1]]=t;x+=t.shape[1]
|
||||
out.append(canvas)
|
||||
W=max(o.shape[1] for o in out);out=[np.pad(o,((0,0),(0,W-o.shape[1]),(0,0)),constant_values=255) for o in out]
|
||||
cv2.imwrite(f'{S}/gt_{lab}.jpg',np.vstack(out))
|
||||
vis=im.copy()
|
||||
for f in d['facings']:
|
||||
x1,y1,x2,y2=f['box'];c=(0,0,255) if f['label']=='unknown' else (0,200,0)
|
||||
cv2.rectangle(vis,(x1,y1),(x2,y2),c,2);cv2.putText(vis,f"{f['index']}:{f['label'].replace('sku_','')}",(x1+2,y1+15),cv2.FONT_HERSHEY_SIMPLEX,0.45,c,1)
|
||||
cv2.imwrite(f'{S}/shelf_labels.jpg',vis)
|
||||
Loading…
Add table
Add a link
Reference in a new issue