feat: two demo shelves, enrolled, plus the price-card filter they forced out
Two real store photographs, chosen for density and for products that look different
from each other, each with its own enrolled store:
demo-images/demo-image-1-mediya-dairy.jpg CC0 MEDIYA dairy case
21 products kept (13 price cards dropped), 16 recognised, all 14 products named
demo-images/demo-image-2-milk-case.jpg CC BY 2.0 supermarket milk case
56 detections, 49 recognised, all 13 products named
Both were enrolled the way a store would be: the products were read off a contact
sheet of the detections, their crops became the store's gallery (stock-mediya,
stock-milk150), and a head was trained on them. `build_store_from_shelf.py` is that
flow.
The filter came out of the first image. Every Japanese supermarket photo has the same
artifact: the detector boxes the shelf-edge price cards as products, because a card is
roughly the shape of a pack - 13 of 34 detections on image 1 were cards. Their text
separates them cleanly: a card's characters are 30-44% digits and it prints the
tax-excluded price (本体), while a product's are 3-19% digits. Cards are now dropped
before classification, and the count is reported as `price_cards_dropped` rather than
hidden.
demo-images/README.md records how to run both, what the demo shows, and the honest
caveat: both shelves are enrolled, so the recognition rate is in-distribution - what
the app does on a shelf it has been onboarded to, not on one it has never seen.
175 tests pass.
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
2bf916f2aa
commit
383ff9cf2d
72 changed files with 215 additions and 0 deletions
|
|
@ -24,6 +24,7 @@ import hmac
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
|
@ -479,6 +480,48 @@ def create_app(
|
|||
result = _attach_still_tags(result, image)
|
||||
return result
|
||||
|
||||
def _is_price_card(crop: Any) -> bool:
|
||||
"""Is this detection a shelf price card rather than a product?
|
||||
|
||||
Measured on a supermarket photo whose shelf edge carries big price cards:
|
||||
the detector boxes them as products, because a card is roughly the shape of a
|
||||
pack. Their text gives them away - a price card's characters are 30-44 %
|
||||
digits and it prints the tax-excluded price (本体), while a product's are 3-19 %
|
||||
digits. Reporting a price card as a product would put a box labelled with a
|
||||
product name on a price tag, which is worse than not detecting it.
|
||||
"""
|
||||
ocr_engine = getattr(engine, "ocr_engine", None)
|
||||
if ocr_engine is None or not getattr(ocr_engine, "available", lambda: False)():
|
||||
return False
|
||||
try:
|
||||
import cv2
|
||||
|
||||
big = cv2.resize(crop, None, fx=2.0, fy=2.0, interpolation=cv2.INTER_CUBIC)
|
||||
lines = ocr_engine.read(big).lines
|
||||
except Exception:
|
||||
return False
|
||||
text = " ".join(line.text for line in lines)
|
||||
compact = re.sub(r"\s", "", text)
|
||||
if not compact:
|
||||
return False
|
||||
digits = sum(1 for char in compact if char.isdigit() or char in "¥¥./") / len(compact)
|
||||
return ("本体" in text and digits >= 0.20) or digits >= 0.30
|
||||
|
||||
def _drop_price_cards(result: Dict[str, Any], image: Any) -> int:
|
||||
"""Remove detections that are price cards, and say how many were removed."""
|
||||
kept, dropped = [], 0
|
||||
for detection in result["detections"]:
|
||||
crop = engine.crop(image, detection["box"])
|
||||
if crop is not None and crop.size and _is_price_card(crop):
|
||||
dropped += 1
|
||||
continue
|
||||
kept.append(detection)
|
||||
if dropped:
|
||||
result["detections"] = kept
|
||||
result["summary"] = engine.summarize(kept, result.get("empty_slots") or [])
|
||||
result["price_cards_dropped"] = dropped
|
||||
return dropped
|
||||
|
||||
def _attach_still_tags(result: Dict[str, Any], image: Any) -> Dict[str, Any]:
|
||||
"""Run one synchronous tag pass over a still and merge its evidence."""
|
||||
ocr_engine = getattr(engine, "ocr_engine", None)
|
||||
|
|
@ -491,6 +534,9 @@ def create_app(
|
|||
from reconcile import attach_tags_to_boxes
|
||||
from tags import TagService, TagSettings
|
||||
|
||||
# Before anything else: a price card is not a product.
|
||||
_drop_price_cards(result, image)
|
||||
|
||||
boxes = [
|
||||
{"id": detection["id"], "box": detection["box"]} for detection in result["detections"]
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue