product-recognition-demo/recognization/code/eval_external_images.py
kiyo9w 601d72296c fix(catalog): correct sku_l10 to 400g and enroll its packshot
Ajinomoto ピュアセレクト マヨネーズ has no 450g size: the official lineup is
250g/400g/600g/1kg, the shelf tag in add/image6.jpg reads 400g, and DECISION.md
already recorded the mismatch. Both CSVs carried 450g, so every l10 answer
shipped a wrong size.

The packshot is the exact 400g front (JAN 4901001114030, 1000x1000) with its
source URL and the 450g rejection recorded in
data/packshots/research/sku_l10/manifest.json.

Enrolling it moves the catalog ceiling from 69% to 87% of in-catalog facings
answerable by image (l10 was 11 unenrolled facings). Measured on the corrected
image6 labels: local lane 92% identity at 84% precision full, 85% at 91% live.

Also points external-eval defaults at the repository-local data path; the
licensed Wikimedia corpus remains uncommitted pending manifest review.
2026-09-29 02:45:21 +09:00

59 lines
2.8 KiB
Python

"""
Run the pipeline over store photos it has never seen.
python code/eval_external_images.py [data/external-shelves/wikimedia-commons]
Reports detection and identity per image, and writes a JSON summary. See
docs/EXTERNAL-EVAL.md for what the numbers mean.
The images live in the repository (`data/external-shelves/`), not in a home
directory: they are evaluation evidence, and evidence that only exists on one
machine cannot be re-checked.
"""
import json, os, sys, time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import cv2, numpy as np
from engine import build_default_engine, EngineSettings
from tags import TagService, TagSettings
from reconcile import attach_tags_to_boxes
DEFAULT_ROOT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"data", "external-shelves", "wikimedia-commons")
ROOT = os.path.expanduser(sys.argv[1] if len(sys.argv) > 1 else DEFAULT_ROOT)
engine = build_default_engine(EngineSettings(stock_dir="stock-lopia"))
catalog_by_sku = {p["sku_id"]: p for p in engine.catalog()}
service = TagService(engine.ocr_engine, settings=TagSettings())
rows = []
for entry in manifest["images"]:
path = os.path.join(ROOT, entry["file"])
image = cv2.imread(path)
if image is None:
continue
# keep the long side at 1600 so detection cost is comparable across images
height, width = image.shape[:2]
if max(height, width) > 1600:
scale = 1600.0 / max(height, width)
image = cv2.resize(image, (int(width*scale), int(height*scale)), interpolation=cv2.INTER_AREA)
started = time.perf_counter()
result = engine.recognize(image, classify=True, method="hybrid", with_empty_slots=True)
detections = result["detections"]
recognized = sum(1 for d in detections if (d.get("identity") or {}).get("state") == "recognized")
rejected = sum(1 for d in detections if (d.get("identity") or {}).get("state") == "rejected")
rows.append({
"file": entry["file"], "title": entry["title"], "license": entry["license"],
"size": f"{image.shape[1]}x{image.shape[0]}",
"detections": len(detections), "recognized": recognized, "rejected": rejected,
"ms": round((time.perf_counter()-started)*1000),
})
print(f" {entry['file'][:34]:<34} {image.shape[1]}x{image.shape[0]:<5} "
f"det={len(detections):>4} rec={recognized:>3} rej={rejected:>3} {rows[-1]['ms']:>5} ms")
total_det = sum(r["detections"] for r in rows)
total_rec = sum(r["recognized"] for r in rows)
with_det = sum(1 for r in rows if r["detections"] > 0)
print(f"\n{len(rows)} images, {total_det} detections, {total_rec} recognized "
f"({100*total_rec//max(1,total_det)}%), images with any detection: {with_det}")
json.dump(rows, open(os.path.join(ROOT, "eval.json"), "w"), ensure_ascii=False, indent=1)
print(f"summary: {os.path.join(ROOT, 'eval.json')}")