fix(recognition): calibrate the accept lines against unrelated shelves
Every threshold was fitted on image6, where each crop is a facing of a product the shelf holds, so the set could not price a false accept. Measured on 4580 crops from 64 unrelated Wikimedia shelf photos at the shipped configuration: LOCAL_ACCEPT 2.0 -> 4.0 false accepts 60 (1.31%) -> 3 (0.07%) TEXT_MIN_SHARED 2 -> 4 the two-bigram reads that named sku_l12 via おい/いし On image6 that is 44 shown / 42 correct at full resolution - 95.5% precision, 65.6% coverage - the highest-coverage cell of the joint sweep that passes both G2 (>= 95%) and G3 (<= 1%). At live resolution 56% end-to-end at 97%. The text lane's evidence floor gates its answer only: zeroing the row's score would reorder the ranking and make the margin gate compare a different pair than the one it was measured on. code/sweep_accept_lines.py sweeps both axes jointly through the shipped DecisionPolicy; the earlier table swept one axis while pinning the other, so its baseline rows were not the old line.
This commit is contained in:
parent
4ce41ecb17
commit
5094b5f6ca
12 changed files with 815 additions and 195 deletions
|
|
@ -45,28 +45,38 @@ from rebuild_catalog import bigrams, normalize_text, signature_bigrams, sku_of
|
|||
# wrong label on a shelf is worse than a missing one.
|
||||
#
|
||||
# 4.0 since 2026-09-29: the line was tuned on image6 alone, where every crop is
|
||||
# a facing of a product the shelf actually holds. On 2297 crops from unrelated
|
||||
# Wikimedia shelves, 2.0 accepted 25 of them on geometry alone (scores 2.26-4.81
|
||||
# with no text overlap at all), a 1.1% far-unknown rate against G3's 1%. At 4.0
|
||||
# that falls to 8 (0.35%) while image6 keeps 42 of 48 correct labels at 95.5%
|
||||
# precision, which is the only region passing both G2 and G3 with coverage left.
|
||||
# a facing of a product the shelf actually holds, so that set could not price
|
||||
# it. Re-measured with `code/sweep_accept_lines.py` on 4580 crops from 64
|
||||
# unrelated Wikimedia shelf photos - 1280-long-side frames at
|
||||
# `LiveSettings.conf`, the configuration the phone runs - against
|
||||
# `TEXT_MIN_SHARED = 4`: 2.0 accepts 60 of them (1.31%) and 4.0 accepts 3
|
||||
# (0.07%), against G3's 1%. On image6 at full resolution 4.0 shows 44 labels of
|
||||
# which 42 are right - 95.5% precision, and 65.6% of the 64 facings that hold a
|
||||
# catalog row - and it is the highest-coverage cell in the sweep that passes
|
||||
# both G2 and G3.
|
||||
LOCAL_ACCEPT = 4.0
|
||||
# The text lane's own accept line: a bigram overlap this high, with this much
|
||||
# margin over the runner-up, names the row on its own. Measured: the text lane
|
||||
# picks the right SKU on 41-42 of the 55 facings where it reads anything.
|
||||
TEXT_ACCEPT = 0.5
|
||||
TEXT_MARGIN = 0.15
|
||||
# How many of the read's bigrams the winning row must actually contain. The
|
||||
# ratio alone is not evidence: a two-pair read the row happens to contain scores
|
||||
# 1.0, and "サソース" named sku_l08 that way. Two pairs was the old floor, and it
|
||||
# is too low once the lane answers on unrelated shelves: on 2297 crops from
|
||||
# Wikimedia milk, soy and snack shelves, 20 of 26 text-lane answers rested on
|
||||
# exactly two shared bigrams ("避くて おいしい" -> sku_l12 via おい/いし), an
|
||||
# 11.4% false accept rate against G3's 1%. Four is where the two sides separate
|
||||
# once the local line is also at 4.0: false accepts fall to 8 (0.35%) while
|
||||
# image6 keeps 42 of its 48 correct labels. It is a floor on evidence, not on
|
||||
# the ratio, so a long read of shared brand copy still scores low and is still
|
||||
# refused by the margin.
|
||||
# How many of the read's bigrams the winning row must actually contain before
|
||||
# that read may name a product. A floor on the amount of shared evidence, not
|
||||
# on the ratio: a two-pair read the row happens to contain scores 1.0, and
|
||||
# "サソース" named sku_l08 that way.
|
||||
#
|
||||
# It gates the text lane's *answer* and nothing else. Zeroing the row's score
|
||||
# would be a different policy under the same name: it reorders the ranking, so
|
||||
# a zeroed top row promotes a runner-up and the margin gate ends up comparing a
|
||||
# different pair than the one it was measured on. It would also gate the veto,
|
||||
# which is backwards - the veto only ever refuses a local accept, so a floor
|
||||
# under it would let a two-bigram read rescue a match the local lane earned.
|
||||
# `code/sweep_accept_lines.py` measures the line by calling the policy below
|
||||
# with this value, so the sweep and the shipped lane cannot disagree about what
|
||||
# it means.
|
||||
#
|
||||
# The ratio stays the ranking's own gate, so a long read of shared brand copy
|
||||
# still scores low and is still refused by the margin.
|
||||
TEXT_MIN_SHARED = 4
|
||||
# Reference pyramid heights. A ~1000 px packshot against a 110-250 px crop is a
|
||||
# 4-8x ratio, and ORB's 8x1.2 pyramid tops out at 3.6x, which is why ORB and
|
||||
|
|
@ -348,8 +358,15 @@ def text_similarity(
|
|||
the most real text to match were punished hardest. Bounded this way the
|
||||
same reads score 0.86 and 1.0 against their true rows, and the garbage
|
||||
read ties across three rows and is refused by the margin.
|
||||
|
||||
`vocabulary` defaults to the weight table's own keys, which are by
|
||||
construction the bigrams some signature produces. A second copy of that set
|
||||
on the lane is a second thing to keep in step, and it is the drift the
|
||||
module already paid for once with the folding rules.
|
||||
"""
|
||||
read_grams = bigrams(read)
|
||||
if vocabulary is None and weights is not None:
|
||||
vocabulary = set(weights)
|
||||
if vocabulary is not None:
|
||||
read_grams &= vocabulary
|
||||
if not read_grams:
|
||||
|
|
@ -404,12 +421,6 @@ class TextLane:
|
|||
# Bigram weights over the catalog's own names, so the lane can tell
|
||||
# copy every sibling prints from the words that name one product.
|
||||
self.weights = bigram_weights(list(self.signatures.values()))
|
||||
# Every pair any name can produce. A read's pairs outside this set are
|
||||
# OCR noise or word-boundary artifacts, and counting them in the
|
||||
# denominator charges a row for text it could never have explained.
|
||||
self.vocabulary: Set[str] = set()
|
||||
for text in self.signatures.values():
|
||||
self.vocabulary |= signature_bigrams(text)
|
||||
self.ocr_engine = ocr_engine
|
||||
# The engine's OCR lock. Vision tolerates concurrency but the CPU does
|
||||
# not, and this lane runs one OCR per crop on the classify pool, so it
|
||||
|
|
@ -472,7 +483,7 @@ class TextLane:
|
|||
if len(normalize_text(read)) < 4:
|
||||
return {}
|
||||
return {
|
||||
sku: text_similarity(read, signature, self.weights, self.vocabulary)
|
||||
sku: text_similarity(read, signature, self.weights)
|
||||
for sku, signature in self.signatures.items()
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue