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.
41 lines
2.2 KiB
Python
41 lines
2.2 KiB
Python
"""Diagnostic identity harness. Logs full per-facing score vectors so every
|
|
ceiling (catalog, identity rank, decision) is separable. Read-only on product code."""
|
|
import sys, os, json, time, argparse, pickle
|
|
sys.path.insert(0, 'code')
|
|
import cv2, numpy as np
|
|
from feature_matcher import ProductFeatureMatcher
|
|
|
|
def sku_of(name):
|
|
p = name.split('_'); return '_'.join(p[:2])
|
|
|
|
def crops(image_path, labels, live, bug=False, long_side=1280):
|
|
im = cv2.imread(image_path); f = 1.0
|
|
if live:
|
|
h, w = im.shape[:2]; f = long_side / max(h, w)
|
|
im = cv2.resize(im, (int(w*f), int(h*f)), interpolation=cv2.INTER_AREA)
|
|
out = []
|
|
for fc in labels['facings']:
|
|
b = fc['box'] if bug else [int(round(v*f)) for v in fc['box']]
|
|
x1, y1, x2, y2 = b
|
|
out.append((fc, im[max(0,y1):y2, max(0,x1):x2]))
|
|
return out
|
|
|
|
def run(stock, image, labels, live, bug, method):
|
|
m = ProductFeatureMatcher(stock_dir=stock)
|
|
rows = []
|
|
for fc, c in crops(image, labels, live, bug):
|
|
if c.size == 0 or min(c.shape[:2]) < 8: rows.append(dict(index=fc["index"], truth=fc["label"], shape=c.shape[:2], scores={}, templ={}, sub={}, ms=0.0)); continue
|
|
t = time.perf_counter(); s, det = m.score_crop(c, method=method); dt = time.perf_counter()-t
|
|
g = {}
|
|
for k, v in s.items(): g[sku_of(k)] = max(g.get(sku_of(k), 0.0), v)
|
|
sub = {k: det[k].get('sub_details') for k in det}
|
|
rows.append(dict(index=fc['index'], truth=fc['label'], shape=c.shape[:2], scores=g, templ=s, sub=sub, ms=dt*1000))
|
|
return dict(stock=stock, live=live, bug=bug, method=method, enrolled=sorted({sku_of(k) for k in m.db.templates}), rows=rows)
|
|
|
|
if __name__ == '__main__':
|
|
ap = argparse.ArgumentParser(); ap.add_argument('--stock'); ap.add_argument('--labels'); ap.add_argument('--live', action='store_true')
|
|
ap.add_argument('--bug', action='store_true'); ap.add_argument('--method', default='hybrid'); ap.add_argument('--out')
|
|
a = ap.parse_args(); L = json.load(open(a.labels))
|
|
r = run(a.stock, 'add/image6.jpg', L, a.live, a.bug, a.method)
|
|
pickle.dump(r, open(a.out, 'wb'))
|
|
print(a.out, len(r['rows']), 'rows', 'median ms', round(float(np.median([x['ms'] for x in r['rows']])),1))
|