Make the catalog hash honest, share the text helpers, drop the duplicate labels

`catalog_hash` hashed the CSV and the view *names*, so replacing
`sku_l09.jpg` with a different product's packshot left the hash unchanged and
a report could claim the same catalog while every l09 decision in it had
moved. It hashes each view's bytes now.

`identity_lanes` carried its own `normalize_text` and `bigrams`, near-copies
of the ones in `rebuild_catalog` that had already drifted (the lane's table
folded ユ to itself, the catalog's did not). The lane imports the catalog's
now, so one read scores the same either way.

`evidence/image6_v2.json` was byte-identical to `eval/labels/image6.json`; the
evidence scripts and the decision document point at the canonical file and the
copy is gone. `score_crop`'s docstring now states the contract its callers
already depend on: the dict's insertion order is the lane's ranking, and
re-sorting by value names a different SKU than the lane chose.

Tests: the lane is shared by the classify pool, so a concurrent-ranking test
pins that one BFMatcher does not cross crops under eight threads.
This commit is contained in:
tngo 2026-09-29 00:48:03 +09:00
parent fc49ba60e6
commit b6c87c45ea
36 changed files with 89 additions and 1524 deletions

View file

@ -27,7 +27,7 @@ for p in sorted(glob.glob('add/*')):
x1,y1,x2,y2=[int(v) for v in b['box']]; c=im[max(0,y1):y2,max(0,x1):x2]
if min(c.shape[:2])>=40: dis.append((f'd_{os.path.basename(p)}_{j}',c))
random.shuffle(dis); print('distractor refs available',len(dis))
L=json.load(open(f'{S}/image6_v2.json')); Q=[(fc,c) for fc,c in crops('add/image6.jpg',L,True) if fc['label']!='exclude' and min(c.shape[:2])>=8]
L=json.load(open(f'{S}/../../../../eval/labels/image6.json')); Q=[(fc,c) for fc,c in crops('add/image6.jpg',L,True) if fc['label']!='exclude' and min(c.shape[:2])>=8]
Qf=[(fc['label'],)+feat(c,None) for fc,c in Q]
bf=cv2.BFMatcher(cv2.NORM_L2)
def verify(kq,dq,r):