fix(eval): confirm a leak signature before calling it one, and score external sets in code
Enrolling a packshot from an external set is normal - it is how F-12 was closed - and it silently turns one of those rows into training data. The external sets were scored by an ad-hoc script that never checked, so sku_l06_1.jpg was being reported as a held-out correct accept while being byte-identical to the enrolled view. code/eval_external.py now scores those sets with the same leaked() check the shelf evaluator runs, and refuses a row that has since been enrolled. That check needed fixing first. Its 64x64 signature fires on two different photographs of the same product: the l09 rear label from a retailer page sits 19.5 from the enrolled front packshot, under the 20.0 line, while being a different photograph (resized MAD 26.3). A signature hit is now confirmed by correlation - a genuine re-cut, re-encode or rescale correlates at 0.99+, that pair at 0.28 - so the two signals together are decisive where either alone is not. Verified against both directions: identical, q55 re-encode and 700px rescale are caught; the different-photo pair is not. Also adds the van backend (always-argmax, no gate) so the stream comparison in DECISION.md is reproducible rather than asserted, with two tests pinning that it answers where the shipped gate abstains and still refuses an empty score set. External result on the corrected basis: 19 scored, 12 correct, 7 abstain, 0 false accepts, 1 leak excluded. Suite 307 -> 309.
This commit is contained in:
parent
dfdaef4e61
commit
e25fcda67d
9 changed files with 252 additions and 51 deletions
|
|
@ -78,6 +78,12 @@ LIVE_LONG_SIDE = 1280
|
|||
# the enrollment protocol's session manifest (DECISION §8); this is the cheap
|
||||
# backstop that runs without one.
|
||||
LEAK_SIGNATURE_MAD = 20.0
|
||||
# A signature hit is confirmed by correlation before it counts as a leak. A
|
||||
# genuine re-cut, re-encode or rescale of an enrolled view correlates at 0.99+;
|
||||
# two different photographs of the same product sit near 0.3. Measured on the
|
||||
# l09 pair that the signature alone misjudged (0.28) and on re-encoded and
|
||||
# rescaled copies of the enrolled view (0.999, 0.997).
|
||||
LEAK_CORRELATION = 0.90
|
||||
|
||||
|
||||
def load_labels(path: str) -> Dict[str, Any]:
|
||||
|
|
@ -219,6 +225,24 @@ def _catalog_templates(stock_dir: str) -> List[Tuple[str, str, Tuple[int, int],
|
|||
return cached
|
||||
|
||||
|
||||
def _correlation(left: Any, right: Any) -> float:
|
||||
"""Mean-subtracted correlation of two images, resizing `right` to `left`.
|
||||
|
||||
Scale- and re-encode-tolerant, unlike a pixel comparison: the enrolled view
|
||||
and a re-saved or rescaled copy of it correlate at 0.99+, while two
|
||||
different photographs of the same product sit near 0.3.
|
||||
"""
|
||||
if right.shape[:2] != left.shape[:2]:
|
||||
right = cv2.resize(right, (left.shape[1], left.shape[0]),
|
||||
interpolation=cv2.INTER_AREA)
|
||||
a = cv2.cvtColor(left, cv2.COLOR_BGR2GRAY).astype(np.float32).ravel()
|
||||
b = cv2.cvtColor(right, cv2.COLOR_BGR2GRAY).astype(np.float32).ravel()
|
||||
a -= a.mean()
|
||||
b -= b.mean()
|
||||
denom = float(np.linalg.norm(a) * np.linalg.norm(b))
|
||||
return float((a * b).sum() / denom) if denom else 0.0
|
||||
|
||||
|
||||
def leaked(image: Any, box: Sequence[int], stock_dir: str) -> Optional[str]:
|
||||
"""Name of the catalog template this crop is, or None when it is held out.
|
||||
|
||||
|
|
@ -238,9 +262,21 @@ def leaked(image: Any, box: Sequence[int], stock_dir: str) -> Optional[str]:
|
|||
return None
|
||||
crop_signatures = _signatures(crop)
|
||||
for stem, name, shape, signatures in _catalog_templates(stock_dir):
|
||||
if min(float(np.abs(signature - crop_signature).mean())
|
||||
for signature in signatures for crop_signature in crop_signatures) < LEAK_SIGNATURE_MAD:
|
||||
return stem
|
||||
close = min(float(np.abs(signature - crop_signature).mean())
|
||||
for signature in signatures for crop_signature in crop_signatures)
|
||||
if close < LEAK_SIGNATURE_MAD:
|
||||
# The signature is a 64x64 grayscale thumbnail, so it is coarse
|
||||
# enough to fire on two different photos of the same product: the
|
||||
# l09 rear label from a retailer page sits 19.5 from the enrolled
|
||||
# front packshot, under the 20.0 line, while being a different
|
||||
# photograph (resized MAD 26.3). Confirm before calling it a leak.
|
||||
# A genuine re-cut or re-encode correlates at 0.99+; that pair
|
||||
# correlates at 0.28, so the two signals together are decisive
|
||||
# where either alone is not.
|
||||
template = cv2.imread(os.path.join(stock_dir, name))
|
||||
if template is None or _correlation(crop, template) >= LEAK_CORRELATION:
|
||||
return stem
|
||||
continue
|
||||
if shape != crop.shape[:2]:
|
||||
continue
|
||||
template = cv2.imread(os.path.join(stock_dir, name))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue