Enroll from a repo data dir; read packshots in every orientation
Two enrollment defects, both found by trying to enroll real packshots.
`rebuild_catalog.py` crashed on every successful run: the report referenced
`small` but the call to `below_pyramid_floor` was missing, so the pyramid-floor
check the enrollment protocol asks for never ran and the tool died with a
NameError after writing the catalog.
The name/OCR check read one orientation. A packshot whose identifying text runs
vertically - the やまや めんたいマヨネーズ image reads 明太手屋さんが only when
rotated - was refused as a mismatch, which is the worst failure this check has:
it rejects good data while claiming to validate it. `best_orientation_text`
tries all four right-angle rotations and picks the one that explains the most of
the catalog name, not the one that reads the most characters, because the
upright pass returns stray kana that concatenate longer than the real read.
The regression test goes through `audit`, not the helper: the bug was that
`audit` called a single-orientation read, and a helper unit test would have
passed on the broken code. Verified to fail before the fix and pass after.
Data now lives in the repo, not /tmp:
data/packshots/source/ packshots as downloaded, with ATTRIBUTION.json
data/evaluations/ per-session run artifacts, so a number in
DECISION.md traces to the run that produced it
sku_l09 is re-sourced at 1000x1000 from Matsukiyo's Rakuten image, replacing
the 305px view that the README documents as the reason l09 wins facings it
should not.
This commit is contained in:
parent
443a1957a9
commit
68061635d9
63 changed files with 63861 additions and 3 deletions
|
|
@ -170,6 +170,34 @@ def name_ocr_similarity(name: str, ocr_text: str) -> float:
|
|||
return len(name_bigrams & bigrams(ocr_text)) / max(1, len(name_bigrams))
|
||||
|
||||
|
||||
def best_orientation_text(ocr_engine: Any, image: np.ndarray, name: str = "") -> str:
|
||||
"""OCR text from whichever of the four right-angle rotations matches best.
|
||||
|
||||
A packshot's identifying text is not always horizontal: the やまや
|
||||
めんたいマヨネーズ packshot reads 明太手屋さんが only at 270 degrees, and
|
||||
the upright pass returns three stray characters. Checking one orientation
|
||||
refused a correct image, which is the worst failure this check has - it
|
||||
rejects good data while claiming to validate it.
|
||||
|
||||
The winner is the rotation that explains the most of the catalog name, not
|
||||
the one that reads the most characters: the upright pass on that packshot
|
||||
returns stray kana that concatenate longer than the real read, so length
|
||||
picks the wrong one. With no name to compare against, length is the only
|
||||
signal left.
|
||||
"""
|
||||
rotations = (
|
||||
image,
|
||||
cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE),
|
||||
cv2.rotate(image, cv2.ROTATE_180),
|
||||
cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE),
|
||||
)
|
||||
reads = [" ".join(line.text for line in ocr_engine.read(upscale(rotated)).lines)
|
||||
for rotated in rotations]
|
||||
if name:
|
||||
return max(reads, key=lambda text: name_ocr_similarity(name, text))
|
||||
return max(reads, key=lambda text: len(text.strip()))
|
||||
|
||||
|
||||
def collect(source: str) -> Dict[str, List[str]]:
|
||||
by_sku: Dict[str, List[str]] = {}
|
||||
for name in sorted(os.listdir(source)):
|
||||
|
|
@ -291,7 +319,7 @@ def audit(
|
|||
continue
|
||||
if ocr_engine is not None and names is not None:
|
||||
try:
|
||||
text = " ".join(line.text for line in ocr_engine.read(upscale(image)).lines)
|
||||
text = best_orientation_text(ocr_engine, image, names.get(sku, ""))
|
||||
except OcrUnavailable as error:
|
||||
print(f"OCR skipped for {name}: {error}", file=sys.stderr)
|
||||
else:
|
||||
|
|
@ -535,6 +563,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
# and vetoes it from the CSV alone, so it is a coverage gap, not a refusal.
|
||||
text_only = sorted(set(names) - set(kept))
|
||||
siblings = text_discriminated_siblings(kept, args.source, names)
|
||||
small = below_pyramid_floor(kept, args.source)
|
||||
verdict = live_ready_verdict(kept, names, shelf_skus)
|
||||
report = {
|
||||
"source": args.source,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue