fix(identity): calibrate both accept lines against unrelated shelves
The gates were tuned on image6 alone, where every crop is a facing of a product the shelf actually holds. Wikimedia supermarket photos of milk, soy and snack shelves share nothing with the LOPIA catalog, so every accept there is a false accept by construction - and at the old lines there were 83 of them. Text lane: TEXT_MIN_SHARED 2 -> 4. The vocabulary filter drops bigrams no name contains, so a short garbage read can end up with a perfect ratio on two coincidental pairs. 20 of 26 text-lane answers on those shelves rested on exactly two: '避くて おいしい' named sku_l12 through おい/いし. Local lane: LOCAL_ACCEPT 2.0 -> 4.0. The residual accepts were geometric matches with no text overlap at all, scoring 2.26-4.81. Measured, 2297 distractor crops and 109 image6 facings: accept floor image6 shown/correct precision false accepts 2.0 2 54 / 49 90.7% 83 (3.61%) 2.0 4 52 / 48 92.3% 30 (1.31%) 4.0 4 44 / 42 95.5% 8 (0.35%) 5.0 6 36 / 36 100.0% 0 (0.00%) 4.0/4 is the only region passing both G2 (>=95% precision) and G3 (<=1% far-unknown) with coverage left. On the shipped evaluator: 66% end-to-end at 95% precision, 2 false accepts, against 77%/91%/5 before. The two tests that pinned the old floors now pin the new ones, with the reads that separate them.
This commit is contained in:
parent
d13ce94123
commit
07de2bd37d
4 changed files with 166 additions and 22 deletions
|
|
@ -136,14 +136,19 @@ class TextLaneSignatureTests(unittest.TestCase):
|
|||
self.assertGreaterEqual(ranked[0][1], TEXT_ACCEPT)
|
||||
self.assertGreaterEqual(ranked[0][1] - ranked[1][1], TEXT_MARGIN)
|
||||
|
||||
def test_one_shared_pair_is_not_enough_to_name_a_row(self):
|
||||
"""A ratio of 1.0 on a single pair is not evidence.
|
||||
def test_a_thin_read_is_not_enough_to_name_a_row(self):
|
||||
"""A ratio of 1.0 on a few pairs is not evidence.
|
||||
|
||||
"XXイス" contains exactly one pair the catalog knows, and the row that
|
||||
contains it scores 1.0 with a full margin over every other row. On
|
||||
image6 that shape named sku_l08 from the read "サソース". The floor
|
||||
asks how much of the read the row actually explains, so one pair is
|
||||
refused while the same read with two is accepted.
|
||||
image6 that shape named sku_l08 from the read "サソース". The floor asks
|
||||
how much of the read the row actually explains, so one pair and two
|
||||
pairs are both refused while a real read of the name is accepted.
|
||||
|
||||
Two pairs used to be enough. It is not: on 2297 crops from unrelated
|
||||
Wikimedia shelves, 20 of 26 text-lane answers rested on exactly two
|
||||
shared bigrams - "避くて おいしい" named sku_l12 through おい/いし - for
|
||||
an 11.4% false accept rate. The floor is 4.
|
||||
"""
|
||||
from identity_lanes import DecisionPolicy, TextLane
|
||||
|
||||
|
|
@ -153,16 +158,22 @@ class TextLaneSignatureTests(unittest.TestCase):
|
|||
]
|
||||
lane = TextLane.from_catalog(rows, "no-such-dir", ocr_engine=None)
|
||||
|
||||
thin = sorted(lane.scores_from_text("XXイス").items(), key=lambda kv: kv[1], reverse=True)
|
||||
self.assertEqual(thin[0][1], 1.0, "the ratio alone cannot see the difference")
|
||||
self.assertIsNone(
|
||||
DecisionPolicy(signatures=lane.signatures).decide([], thin, text_read="XXイス")["best_sku"]
|
||||
)
|
||||
for thin_read in ("XXイス", "ZZソース"):
|
||||
thin = sorted(lane.scores_from_text(thin_read).items(),
|
||||
key=lambda kv: kv[1], reverse=True)
|
||||
self.assertEqual(thin[0][1], 1.0, "the ratio alone cannot see the difference")
|
||||
self.assertIsNone(
|
||||
DecisionPolicy(signatures=lane.signatures).decide(
|
||||
[], thin, text_read=thin_read
|
||||
)["best_sku"],
|
||||
f"{thin_read!r} shares too few pairs to name a row",
|
||||
)
|
||||
|
||||
enough = sorted(lane.scores_from_text("ZZソース").items(), key=lambda kv: kv[1], reverse=True)
|
||||
enough = sorted(lane.scores_from_text("オイスター").items(),
|
||||
key=lambda kv: kv[1], reverse=True)
|
||||
self.assertEqual(
|
||||
DecisionPolicy(signatures=lane.signatures).decide(
|
||||
[], enough, text_read="ZZソース"
|
||||
[], enough, text_read="オイスター"
|
||||
)["best_sku"],
|
||||
"sku_l08",
|
||||
)
|
||||
|
|
@ -616,8 +627,11 @@ class DecisionPolicyTests(unittest.TestCase):
|
|||
the margin over the runner-up. Re-sorting by value names a different SKU
|
||||
than the lane chose - here sku_b has the larger value but sku_a is the
|
||||
lane's own first choice, and sku_a is what must be answered.
|
||||
|
||||
Both values clear LOCAL_ACCEPT, so the test is about the ordering and
|
||||
not about the gate.
|
||||
"""
|
||||
decision = self.policy().decide([("sku_a", 3.0), ("sku_b", 5.0)], [])
|
||||
decision = self.policy().decide([("sku_a", 4.5), ("sku_b", 5.0)], [])
|
||||
self.assertEqual(decision["best_sku"], "sku_a")
|
||||
|
||||
def test_a_size_only_read_does_not_fill_in(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue