feat: add Van's real baseline lane and stop size tokens naming products
Two review findings, both confirmed by measurement. 1. The evaluator had no always-argmax lane, so every "vs Van" claim compared against the *gated* matcher, which is not Van's contract - the original demo answered every crop with its best-scoring SKU and had no accept line. The new 'van' lane is that contract: same scores, no gate. Against it the local lane wins both axes at both resolutions (full 61%/77% vs 52%/29%; live 59%/95% vs 57%/32%), but the live coverage margin is one facing, not seven, and the 95% figure is live-resolution only - at full resolution the same lane reads 77% with 11 false accepts and a hub alarm. 2. A crop reading only "400g" scored 0.667 on three SKUs, above TEXT_ACCEPT, so the text lane named a product from a size line. The fill-in job now requires two non-digit bigrams; the veto is unchanged, so "65%" still contradicts a local answer. The raw OCR read travels with the ranking through score_crop_lanes for that check. Tests: 3 for the size gate (policy + live) and the van lane's own row. Removing the gate fails all three. Suite 276 -> 280.
This commit is contained in:
parent
13ee7426e2
commit
fd72251160
7 changed files with 180 additions and 22 deletions
|
|
@ -514,17 +514,18 @@ class RecognitionEngine:
|
|||
return decide_identity({}, method="local", scale="local")
|
||||
text_lane = self.identity_text_lane()
|
||||
text_rank: Any = None
|
||||
read = ""
|
||||
if text_lane is not None:
|
||||
read = text_lane.read(crop)
|
||||
text_rank = sorted(
|
||||
text_lane.scores_from_text(read).items(), key=lambda kv: kv[1], reverse=True
|
||||
)
|
||||
return DecisionPolicy().decide(lane.rank(crop), text_rank)
|
||||
return DecisionPolicy().decide(lane.rank(crop), text_rank, text_read=read)
|
||||
|
||||
def score_crop_lanes(
|
||||
self, crop: Any, method: Optional[str] = None
|
||||
) -> Tuple[Dict[str, float], List[Tuple[str, float]]]:
|
||||
"""Both lanes' evidence for one crop: local scores and text ranking.
|
||||
) -> Tuple[Dict[str, float], List[Tuple[str, float]], str]:
|
||||
"""Both lanes' evidence for one crop: local scores, text ranking, raw read.
|
||||
|
||||
The live path accumulates evidence across looks, so it needs the lanes
|
||||
separately rather than one fused decision. How it combines them depends
|
||||
|
|
@ -532,16 +533,18 @@ class RecognitionEngine:
|
|||
margin over the runner-up, and averaging in an empty observation halves
|
||||
it below the accept line), while the cosine and inlier scales average
|
||||
the scores over the track's window. The text reading is always the
|
||||
newest. The still path calls `decide_crop`, which fuses a single crop.
|
||||
newest. The raw read travels with the ranking because the decision needs
|
||||
it to tell a product name from a size line. The still path calls
|
||||
`decide_crop`, which fuses a single crop.
|
||||
"""
|
||||
scores = self.score_crop(crop, method)
|
||||
text_lane = self.identity_text_lane()
|
||||
if text_lane is None:
|
||||
return scores, []
|
||||
return scores, [], ""
|
||||
read = text_lane.read(crop)
|
||||
return scores, sorted(
|
||||
text_lane.scores_from_text(read).items(), key=lambda kv: kv[1], reverse=True
|
||||
)
|
||||
), read
|
||||
|
||||
def identity_lane(self) -> Any:
|
||||
"""The local lane, built once. None when the backend is not `local`."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue