Compare commits
34 commits
443a1957a9
...
a716f5aab1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a716f5aab1 | ||
|
|
684934e042 | ||
|
|
4d1c17f15d | ||
|
|
81a2ab10e7 | ||
|
|
01d9a5e614 | ||
|
|
5eb4bdf6c8 | ||
|
|
4bdad66bbe | ||
|
|
07de2bd37d | ||
|
|
d13ce94123 | ||
|
|
21f1d97cca | ||
|
|
962cdaab5f | ||
|
|
11826cc43c | ||
|
|
8db280d220 | ||
|
|
20fb459ee7 | ||
|
|
0376a74418 | ||
|
|
a1c69f72fe | ||
|
|
56cff09292 | ||
|
|
e25fcda67d | ||
|
|
dfdaef4e61 | ||
|
|
6a45fb990d | ||
| 5fe3258614 | |||
|
|
8754738dc1 | ||
|
|
306ea9b3e8 | ||
|
|
30bfe42bf6 | ||
|
|
22db40eb93 | ||
|
|
1e62554944 | ||
|
|
a9f0f61611 | ||
|
|
74e9afed7d | ||
|
|
ddead84546 | ||
|
|
aa838f7b2b | ||
|
|
d604c6a20a | ||
|
|
2b1566e8e2 | ||
|
|
601d72296c | ||
| 68061635d9 |
11
recognization/.gitignore
vendored
|
|
@ -25,3 +25,14 @@ stock-*/identity_head.pt
|
|||
.gallery_cache_*.npz
|
||||
.demo-token
|
||||
config/local.env
|
||||
|
||||
# Fetched images. Every set keeps its manifest.json (source URL, licence,
|
||||
# artist) and its results in git; the pixels are re-fetchable from those URLs
|
||||
# and are 59 MB for the Wikimedia shelves alone.
|
||||
data/external-shelves/*/*.jpg
|
||||
data/external-shelves/*/*.jpeg
|
||||
data/external-shelves/*/*.JPG
|
||||
data/external-shelves/*/*.png
|
||||
data/packshots/research/*/images/
|
||||
data/packshots/research/*/pages/
|
||||
data/packshots/research/*/*.jpg
|
||||
|
|
|
|||
|
|
@ -1010,8 +1010,9 @@ def main() -> None:
|
|||
parser.add_argument("--live-method", type=str, default=None, choices=list(CLASSIFY_METHODS))
|
||||
parser.add_argument("--live-conf", type=float, default=None)
|
||||
parser.add_argument("--identity", type=str, default=None,
|
||||
choices=["matcher", "local", "gallery"],
|
||||
help="which mechanism names products (default local)")
|
||||
choices=["matcher", "local", "gallery", "van"],
|
||||
help="which mechanism names products (default local; "
|
||||
"van is the always-argmax baseline)")
|
||||
parser.add_argument("--live-inflight", type=int, default=None,
|
||||
help="classifications in flight per session")
|
||||
parser.add_argument("--live-attempts", type=int, default=None,
|
||||
|
|
|
|||
42
recognization/code/bench_ocr.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"""OCR cost per crop, on production-shaped crops.
|
||||
|
||||
The decision record claims ~18-26 ms per crop on Mac and re-measured 18.0 ms on
|
||||
40 production crops. This is that measurement, so the number is reproducible
|
||||
rather than remembered.
|
||||
|
||||
.venv/bin/python code/bench_ocr.py
|
||||
"""
|
||||
import json, statistics, sys, time
|
||||
sys.path.insert(0, 'code')
|
||||
import cv2
|
||||
from eval_identity import LocalLaneAdapter, crop_of
|
||||
|
||||
lane = LocalLaneAdapter('stock-lopia-clean')
|
||||
labels = json.load(open('eval/labels/image6.json'))
|
||||
image = cv2.imread('add/image6.jpg')
|
||||
|
||||
crops = []
|
||||
for facing in labels['facings']:
|
||||
if facing.get('label') in (None, 'exclude'):
|
||||
continue
|
||||
crop = crop_of(image, facing['box'])
|
||||
if crop is not None and crop.size:
|
||||
crops.append(crop)
|
||||
if len(crops) >= 40:
|
||||
break
|
||||
|
||||
# Warm the recognizer: the first call loads the Vision model.
|
||||
lane.text_lane.read(crops[0])
|
||||
|
||||
times = []
|
||||
for crop in crops:
|
||||
started = time.perf_counter()
|
||||
lane.text_lane.read(crop)
|
||||
times.append((time.perf_counter() - started) * 1000)
|
||||
|
||||
times.sort()
|
||||
print(f'{len(times)} crops')
|
||||
print(f' p50 {statistics.median(times):.1f} ms')
|
||||
print(f' mean {statistics.mean(times):.1f} ms')
|
||||
print(f' p95 {times[int(len(times) * 0.95)]:.1f} ms')
|
||||
print(f' min {times[0]:.1f} max {times[-1]:.1f}')
|
||||
|
|
@ -129,7 +129,9 @@ def image_info(titles: list) -> dict:
|
|||
|
||||
|
||||
def main() -> None:
|
||||
root = sys.argv[1] if len(sys.argv) > 1 else os.path.expanduser("~/dorayaki-data/shelf-images")
|
||||
default = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"data", "external-shelves", "wikimedia-commons")
|
||||
root = sys.argv[1] if len(sys.argv) > 1 else default
|
||||
os.makedirs(root, exist_ok=True)
|
||||
|
||||
print("searching Commons...")
|
||||
|
|
@ -139,7 +141,7 @@ def main() -> None:
|
|||
info = image_info(sorted(found))
|
||||
print(f" metadata for {len(info)} files")
|
||||
|
||||
kept, skipped = [], {"license": 0, "size": 0, "missing": 0}
|
||||
kept, skipped = [], {"license": 0, "size": 0, "missing": 0, "not_a_photo": 0}
|
||||
for title, entry in sorted(info.items()):
|
||||
if not entry.get("url"):
|
||||
skipped["missing"] += 1
|
||||
|
|
@ -148,6 +150,12 @@ def main() -> None:
|
|||
if not any(token in licence for token in PERMISSIVE):
|
||||
skipped["license"] += 1
|
||||
continue
|
||||
# A book scan is not a shelf. The search terms pull in .djvu volumes
|
||||
# whose page 1 is a title page, and Commons serves those as JPEG
|
||||
# thumbnails, so the extension check has to be on the source title.
|
||||
if any(title.lower().endswith(ext) for ext in (".djvu", ".pdf", ".svg", ".tif", ".tiff")):
|
||||
skipped["not_a_photo"] += 1
|
||||
continue
|
||||
if (entry.get("width") or 0) < 1000 or (entry.get("height") or 0) < 700:
|
||||
skipped["size"] += 1
|
||||
continue
|
||||
|
|
@ -156,9 +164,13 @@ def main() -> None:
|
|||
print(f" keeping {len(kept)} (skipped: {skipped})")
|
||||
manifest = []
|
||||
for index, (title, entry) in enumerate(kept):
|
||||
stem = os.path.basename(urllib.parse.urlparse(entry["url"]).path).replace(" ", "_")
|
||||
stem = urllib.parse.unquote(stem)[:60]
|
||||
name = f"{index:03d}_{stem}"
|
||||
# Truncate the stem, never the extension: a 60-character cut on the
|
||||
# whole basename drops it, and the file then has no type for cv2 or
|
||||
# for anyone reading the directory.
|
||||
stem, extension = os.path.splitext(
|
||||
os.path.basename(urllib.parse.urlparse(entry["url"]).path).replace(" ", "_")
|
||||
)
|
||||
name = f"{index:03d}_{urllib.parse.unquote(stem)[:60]}{extension.lower()}"
|
||||
path = os.path.join(root, name)
|
||||
if not os.path.exists(path):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -89,8 +89,11 @@ class EngineSettings:
|
|||
# calibrated open-set gate: measured on the corrected image6 labels it shows
|
||||
# 36 of 61 in-catalog facings at 95% precision with the text lane, against
|
||||
# 35 at 35% for Van's always-argmax matcher. `matcher` keeps Van's composite
|
||||
# as a selectable baseline lane (E-1). `gallery` is fenced off: raw DINOv2
|
||||
# is 12-21% top-1 on 7 SKUs and its recall@10 falls to 7/42 at 493 SKUs.
|
||||
# as a selectable baseline lane (E-1). `van` is that same scorer with the
|
||||
# gate removed - it answers its top SKU every time, which is Van's actual
|
||||
# contract and the only way to reproduce the stream comparison in
|
||||
# DECISION.md. `gallery` is fenced off: raw DINOv2 is 12-21% top-1 on 7
|
||||
# SKUs and its recall@10 falls to 7/42 at 493 SKUs.
|
||||
identity_backend: str = "local"
|
||||
# Cosine similarity below which the gallery calls a facing unknown. Measured:
|
||||
# same-product pairs sit above 0.6, different products below 0.5.
|
||||
|
|
@ -469,6 +472,8 @@ class RecognitionEngine:
|
|||
return "cosine"
|
||||
if backend == "local":
|
||||
return "local"
|
||||
if backend == "van":
|
||||
return "van"
|
||||
return "inlier"
|
||||
|
||||
def identity_text_lane(self) -> Any:
|
||||
|
|
|
|||
116
recognization/code/eval_distractors.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""Open-world false-accept probe: unrelated shelf photos through the shipped lane.
|
||||
|
||||
data/external-shelves/ holds Wikimedia Commons supermarket photos of shelves
|
||||
that share nothing with the LOPIA catalog. Every accept here is a false accept
|
||||
by construction, which is what makes the set useful: the image6 shelf can only
|
||||
measure false accepts on the products it happens to contain.
|
||||
|
||||
The stored eval.json predates the open-set gate, so it is re-run rather than
|
||||
trusted.
|
||||
|
||||
.venv/bin/python code/eval_distractors.py --stock stock-lopia-clean \
|
||||
data/external-shelves/wikimedia-commons
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Dict, List
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if current_dir not in sys.path:
|
||||
sys.path.insert(0, current_dir)
|
||||
|
||||
import cv2 # noqa: E402
|
||||
|
||||
from eval_identity import LOCAL_ACCEPT, LocalLaneAdapter # noqa: E402
|
||||
|
||||
|
||||
def main(argv: List[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("directories", nargs="+")
|
||||
parser.add_argument("--stock", default="stock-lopia-clean")
|
||||
parser.add_argument("--write", action="store_true")
|
||||
parser.add_argument("--limit", type=int, default=0, help="images per set (0 = all)")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
from engine import build_default_engine, EngineSettings
|
||||
|
||||
engine = build_default_engine(EngineSettings(stock_dir=args.stock))
|
||||
lane = LocalLaneAdapter(args.stock)
|
||||
for directory in args.directories:
|
||||
manifest_path = os.path.join(directory, "manifest.json")
|
||||
try:
|
||||
with open(manifest_path) as handle:
|
||||
manifest = json.load(handle)
|
||||
except (OSError, ValueError):
|
||||
manifest = {"images": []}
|
||||
meta = {row["file"]: row for row in manifest.get("images", [])}
|
||||
rows: List[Dict[str, Any]] = []
|
||||
|
||||
paths = sorted(
|
||||
path for path in glob.glob(os.path.join(directory, "*"))
|
||||
if os.path.splitext(path)[1].lower() in (".jpg", ".jpeg", ".png")
|
||||
)
|
||||
if args.limit:
|
||||
paths = paths[: args.limit]
|
||||
for path in paths:
|
||||
name = os.path.basename(path)
|
||||
image = cv2.imread(path)
|
||||
if image is None:
|
||||
continue
|
||||
started = time.perf_counter()
|
||||
detections = engine.detect(image)
|
||||
accepted = 0
|
||||
for detection in detections:
|
||||
box = detection.get("box") or detection.get("bbox")
|
||||
if not box:
|
||||
continue
|
||||
x1, y1, x2, y2 = [int(v) for v in box]
|
||||
crop = image[max(0, y1):y2, max(0, x1):x2]
|
||||
if crop.size == 0:
|
||||
continue
|
||||
decision = lane.decide(crop, lane.rank(crop), accept=LOCAL_ACCEPT,
|
||||
margin=0.0, scale="full")
|
||||
if decision.get("best_sku"):
|
||||
accepted += 1
|
||||
rows.append({
|
||||
"file": name,
|
||||
"title": meta.get(name, {}).get("title", ""),
|
||||
"detections": len(detections),
|
||||
"false_accepts": accepted,
|
||||
"ms": round((time.perf_counter() - started) * 1000),
|
||||
})
|
||||
|
||||
detections = sum(r["detections"] for r in rows)
|
||||
accepts = sum(r["false_accepts"] for r in rows)
|
||||
report = {
|
||||
"dataset": os.path.basename(directory.rstrip("/")),
|
||||
"catalog": args.stock,
|
||||
"note": "Unrelated shelves: every accept is a false accept by construction.",
|
||||
"images": len(rows),
|
||||
"detections": detections,
|
||||
"false_accepts": accepts,
|
||||
"false_accept_rate": round(accepts / detections, 4) if detections else None,
|
||||
"rows": rows,
|
||||
}
|
||||
print(f"{report['dataset']}: {len(rows)} images, {detections} detections, "
|
||||
f"{accepts} false accepts ({report['false_accept_rate'] or 0:.2%})")
|
||||
for row in sorted(rows, key=lambda r: -r["false_accepts"])[:5]:
|
||||
if row["false_accepts"]:
|
||||
print(f" {row['false_accepts']:>3} {row['file'][:55]}")
|
||||
if args.write:
|
||||
out = os.path.join(directory, "eval_result.json")
|
||||
with open(out, "w") as handle:
|
||||
json.dump(report, handle, ensure_ascii=False, indent=1)
|
||||
print(f" wrote {out}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
127
recognization/code/eval_external.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
"""Score external-proxy image sets through the shipped identity lane.
|
||||
|
||||
These sets are not Tier 2 (see data/external-eval/README.md). They exist to test
|
||||
the pipeline on data it had no part in building, so the one thing this script
|
||||
must never do is score an image that has since been enrolled - that is
|
||||
training-set accuracy wearing a held-out label.
|
||||
|
||||
`leaked()` is the same check the shelf evaluator runs, and it is applied here
|
||||
for that reason: enrolling a packshot from an external set is a normal thing to
|
||||
do (it is how F-12 was closed), and it silently converts one of these rows into
|
||||
a leak unless something refuses it.
|
||||
|
||||
.venv/bin/python code/eval_external.py --stock stock-lopia-clean \
|
||||
data/external-eval/kewpie data/external-eval/other-mayo
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict, List
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if current_dir not in sys.path:
|
||||
sys.path.insert(0, current_dir)
|
||||
|
||||
import cv2 # noqa: E402
|
||||
|
||||
from eval_identity import LOCAL_ACCEPT, LocalLaneAdapter, leaked # noqa: E402
|
||||
|
||||
|
||||
def score_set(lane: Any, directory: str, stock_dir: str) -> Dict[str, Any]:
|
||||
rows: List[Dict[str, Any]] = []
|
||||
for path in sorted(glob.glob(os.path.join(directory, "*.jpg"))):
|
||||
name = os.path.basename(path)
|
||||
truth = "sku_" + name.split("_")[1]
|
||||
image = cv2.imread(path)
|
||||
if image is None:
|
||||
rows.append({"file": name, "truth": truth, "verdict": "unreadable"})
|
||||
continue
|
||||
# The whole image is the crop: an external packshot is not a shelf facing.
|
||||
box = (0, 0, image.shape[1], image.shape[0])
|
||||
leak = leaked(image, box, stock_dir)
|
||||
if leak:
|
||||
rows.append({
|
||||
"file": name, "truth": truth, "verdict": "leaked",
|
||||
"leaked_into": leak,
|
||||
"note": "enrolled in the catalog; scoring it would be training accuracy",
|
||||
})
|
||||
continue
|
||||
decision = lane.decide(
|
||||
image, lane.rank(image), accept=LOCAL_ACCEPT, margin=0.0, scale="full"
|
||||
)
|
||||
best = decision.get("best_sku")
|
||||
verdict = "correct" if best == truth else ("abstain" if not best else "wrong")
|
||||
rows.append({
|
||||
"file": name, "truth": truth, "best": best,
|
||||
"confidence": decision.get("confidence"),
|
||||
"status": decision.get("status"),
|
||||
"source": decision.get("source"),
|
||||
"verdict": verdict,
|
||||
})
|
||||
|
||||
scored = [r for r in rows if r["verdict"] in ("correct", "abstain", "wrong")]
|
||||
counts = {v: sum(1 for r in scored if r["verdict"] == v)
|
||||
for v in ("correct", "abstain", "wrong")}
|
||||
# Per SKU, because a set total hides which products the lane can and cannot
|
||||
# name. Every SKU here has images from two independent source domains, so a
|
||||
# SKU that scores the same on both is not one lucky photograph.
|
||||
by_sku: Dict[str, Dict[str, int]] = {}
|
||||
for row in scored:
|
||||
bucket = by_sku.setdefault(row["truth"], {"correct": 0, "abstain": 0, "wrong": 0})
|
||||
bucket[row["verdict"]] += 1
|
||||
return {
|
||||
"dataset": os.path.basename(directory.rstrip("/")),
|
||||
"catalog": stock_dir,
|
||||
"note": "External proxy images, not Tier 2. Scored through the shipped "
|
||||
"LocalLaneAdapter + DecisionPolicy.",
|
||||
"rows": rows,
|
||||
"scored": len(scored),
|
||||
"leaked": sum(1 for r in rows if r["verdict"] == "leaked"),
|
||||
"by_sku": by_sku,
|
||||
**counts,
|
||||
}
|
||||
|
||||
|
||||
def main(argv: List[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("directories", nargs="+", help="external set directories")
|
||||
parser.add_argument("--stock", default="stock-lopia-clean")
|
||||
parser.add_argument("--write", action="store_true",
|
||||
help="write eval_result.json into each set directory")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
lane = LocalLaneAdapter(args.stock)
|
||||
totals = {"correct": 0, "abstain": 0, "wrong": 0, "leaked": 0, "scored": 0}
|
||||
for directory in args.directories:
|
||||
report = score_set(lane, directory, args.stock)
|
||||
for key in totals:
|
||||
totals[key] += report[key]
|
||||
print(f"{report['dataset']}: correct {report['correct']} | "
|
||||
f"abstain {report['abstain']} | wrong {report['wrong']} | "
|
||||
f"leaked {report['leaked']} of {report['scored']} scored")
|
||||
for sku, bucket in sorted(report["by_sku"].items()):
|
||||
print(f" {sku:<10} correct {bucket['correct']} "
|
||||
f"abstain {bucket['abstain']} wrong {bucket['wrong']}")
|
||||
for row in report["rows"]:
|
||||
if row["verdict"] in ("wrong", "leaked"):
|
||||
print(f" {row['verdict'].upper():<7} {row['file']} "
|
||||
f"truth={row['truth']} best={row.get('best')} "
|
||||
f"{row.get('leaked_into', '')}")
|
||||
if args.write:
|
||||
out = os.path.join(directory, "eval_result.json")
|
||||
with open(out, "w") as handle:
|
||||
json.dump(report, handle, ensure_ascii=False, indent=1)
|
||||
print(f" wrote {out}")
|
||||
print(f"\nTOTAL: {totals['scored']} scored, correct {totals['correct']}, "
|
||||
f"abstain {totals['abstain']}, FALSE ACCEPTS {totals['wrong']}, "
|
||||
f"leaked {totals['leaked']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
"""
|
||||
Run the pipeline over store photos it has never seen.
|
||||
|
||||
python code/eval_external_images.py [~/dorayaki-data/shelf-images]
|
||||
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__)))
|
||||
|
|
@ -13,8 +17,9 @@ from engine import build_default_engine, EngineSettings
|
|||
from tags import TagService, TagSettings
|
||||
from reconcile import attach_tags_to_boxes
|
||||
|
||||
ROOT = os.path.expanduser(sys.argv[1] if len(sys.argv) > 1 else "~/dorayaki-data/shelf-images")
|
||||
manifest = json.load(open(os.path.join(ROOT, "manifest.json")))
|
||||
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()}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,12 @@ LIVE_LONG_SIDE = 1280
|
|||
# the enrollment protocol's session manifest (DECISION §8); this is the cheap
|
||||
# backstop that runs without one.
|
||||
LEAK_SIGNATURE_MAD = 20.0
|
||||
# A signature hit is confirmed by correlation before it counts as a leak. A
|
||||
# genuine re-cut, re-encode or rescale of an enrolled view correlates at 0.99+;
|
||||
# two different photographs of the same product sit near 0.3. Measured on the
|
||||
# l09 pair that the signature alone misjudged (0.28) and on re-encoded and
|
||||
# rescaled copies of the enrolled view (0.999, 0.997).
|
||||
LEAK_CORRELATION = 0.90
|
||||
|
||||
|
||||
def load_labels(path: str) -> Dict[str, Any]:
|
||||
|
|
@ -219,6 +225,24 @@ def _catalog_templates(stock_dir: str) -> List[Tuple[str, str, Tuple[int, int],
|
|||
return cached
|
||||
|
||||
|
||||
def _correlation(left: Any, right: Any) -> float:
|
||||
"""Mean-subtracted correlation of two images, resizing `right` to `left`.
|
||||
|
||||
Scale- and re-encode-tolerant, unlike a pixel comparison: the enrolled view
|
||||
and a re-saved or rescaled copy of it correlate at 0.99+, while two
|
||||
different photographs of the same product sit near 0.3.
|
||||
"""
|
||||
if right.shape[:2] != left.shape[:2]:
|
||||
right = cv2.resize(right, (left.shape[1], left.shape[0]),
|
||||
interpolation=cv2.INTER_AREA)
|
||||
a = cv2.cvtColor(left, cv2.COLOR_BGR2GRAY).astype(np.float32).ravel()
|
||||
b = cv2.cvtColor(right, cv2.COLOR_BGR2GRAY).astype(np.float32).ravel()
|
||||
a -= a.mean()
|
||||
b -= b.mean()
|
||||
denom = float(np.linalg.norm(a) * np.linalg.norm(b))
|
||||
return float((a * b).sum() / denom) if denom else 0.0
|
||||
|
||||
|
||||
def leaked(image: Any, box: Sequence[int], stock_dir: str) -> Optional[str]:
|
||||
"""Name of the catalog template this crop is, or None when it is held out.
|
||||
|
||||
|
|
@ -238,9 +262,21 @@ def leaked(image: Any, box: Sequence[int], stock_dir: str) -> Optional[str]:
|
|||
return None
|
||||
crop_signatures = _signatures(crop)
|
||||
for stem, name, shape, signatures in _catalog_templates(stock_dir):
|
||||
if min(float(np.abs(signature - crop_signature).mean())
|
||||
for signature in signatures for crop_signature in crop_signatures) < LEAK_SIGNATURE_MAD:
|
||||
return stem
|
||||
close = min(float(np.abs(signature - crop_signature).mean())
|
||||
for signature in signatures for crop_signature in crop_signatures)
|
||||
if close < LEAK_SIGNATURE_MAD:
|
||||
# The signature is a 64x64 grayscale thumbnail, so it is coarse
|
||||
# enough to fire on two different photos of the same product: the
|
||||
# l09 rear label from a retailer page sits 19.5 from the enrolled
|
||||
# front packshot, under the 20.0 line, while being a different
|
||||
# photograph (resized MAD 26.3). Confirm before calling it a leak.
|
||||
# A genuine re-cut or re-encode correlates at 0.99+; that pair
|
||||
# correlates at 0.28, so the two signals together are decisive
|
||||
# where either alone is not.
|
||||
template = cv2.imread(os.path.join(stock_dir, name))
|
||||
if template is None or _correlation(crop, template) >= LEAK_CORRELATION:
|
||||
return stem
|
||||
continue
|
||||
if shape != crop.shape[:2]:
|
||||
continue
|
||||
template = cv2.imread(os.path.join(stock_dir, name))
|
||||
|
|
@ -377,10 +413,14 @@ class LocalLaneAdapter(Lane):
|
|||
|
||||
name = "local"
|
||||
|
||||
def __init__(self, stock_dir: str, ocr: Any = None) -> None:
|
||||
def __init__(self, stock_dir: str, ocr: Any = None, use_text: bool = True) -> None:
|
||||
from identity_lanes import LocalLane, TextLane
|
||||
|
||||
self.lane = LocalLane(stock_dir)
|
||||
# E-3 asks what the text lane is worth, which is only answerable by
|
||||
# running the same crops without it. Building the lane anyway keeps the
|
||||
# comparison to one variable: the OCR cost is paid either way.
|
||||
self.use_text = use_text
|
||||
self.text_lane = TextLane.from_catalog(
|
||||
_catalog_rows(stock_dir), stock_dir, ocr if ocr is not None else _ocr_engine()
|
||||
)
|
||||
|
|
@ -392,6 +432,10 @@ class LocalLaneAdapter(Lane):
|
|||
margin: float, scale: str) -> Dict[str, Any]:
|
||||
from identity_lanes import DecisionPolicy
|
||||
|
||||
if not self.use_text:
|
||||
from identity import decide_identity
|
||||
|
||||
return decide_identity(dict(ranked), scale="local", min_score=accept)
|
||||
read = self.text_lane.read(crop)
|
||||
text_rank = sorted(
|
||||
self.text_lane.scores_from_text(read).items(),
|
||||
|
|
@ -605,6 +649,8 @@ def main() -> int:
|
|||
parser.add_argument("--lanes", default="local,matcher,gallery,hybrid-verify")
|
||||
parser.add_argument("--shortlist", type=int, default=5)
|
||||
parser.add_argument("--accept", type=float, default=None, help="override the lane's own floor")
|
||||
parser.add_argument("--no-text", action="store_true",
|
||||
help="run the local lane without the text lane (E-3)")
|
||||
parser.add_argument("--margin", type=float, default=None)
|
||||
parser.add_argument("--live", action="store_true", help="also score at live resolution")
|
||||
parser.add_argument("--json", help="write the report here")
|
||||
|
|
@ -644,7 +690,7 @@ def main() -> int:
|
|||
if "van" in wanted:
|
||||
lanes["van"] = VanLane(args.stock_dir)
|
||||
if "local" in wanted:
|
||||
lanes["local"] = LocalLaneAdapter(args.stock_dir)
|
||||
lanes["local"] = LocalLaneAdapter(args.stock_dir, use_text=not args.no_text)
|
||||
if "gallery" in wanted or "hybrid-verify" in wanted:
|
||||
lanes["gallery"] = GalleryLane(args.stock_dir, catalog_skus)
|
||||
if "hybrid-verify" in wanted:
|
||||
|
|
|
|||
|
|
@ -292,7 +292,9 @@ def main() -> None:
|
|||
parser.add_argument("--epochs", type=int, default=60)
|
||||
parser.add_argument("--views", type=int, default=2, help="enrolment views per class")
|
||||
parser.add_argument("--augment", type=int, default=12, help="augmented copies per enrolment view")
|
||||
parser.add_argument("--image", default=os.path.expanduser("~/dorayaki-data/shelf-images"))
|
||||
parser.add_argument("--image", default=os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"data", "external-shelves", "wikimedia-commons"))
|
||||
parser.add_argument("--stock", default=None,
|
||||
help="train on a store's enrolled views instead of the milk shelf, "
|
||||
"and write <stock>/identity_head.pt for the gallery to load")
|
||||
|
|
|
|||
|
|
@ -30,8 +30,11 @@ DEFAULT_MIN_CONFIDENCE = 0.55
|
|||
|
||||
# `local` scale (identity_lanes.LocalLane). The value is already a margin over
|
||||
# the runner-up, so the relative-margin test does not apply to it: a single
|
||||
# absolute line is the whole gate.
|
||||
LOCAL_ACCEPT = 2.0
|
||||
# absolute line is the whole gate. Kept equal to identity_lanes.LOCAL_ACCEPT,
|
||||
# which is what the engine's DecisionPolicy uses; this copy exists for the
|
||||
# evaluator's `--no-text` path, and two different lines would score the same
|
||||
# crops two ways.
|
||||
LOCAL_ACCEPT = 4.0
|
||||
|
||||
|
||||
def relative_confidence(top_score: float, runner_up_score: float) -> float:
|
||||
|
|
@ -248,6 +251,26 @@ def decide_identity(
|
|||
"message": message,
|
||||
}
|
||||
|
||||
if scale == "van":
|
||||
# Van's contract: the top SKU, always answered. No gate, no abstention -
|
||||
# this is the baseline the decision is measured against, and it exists
|
||||
# so the stream comparison is reproducible rather than asserted.
|
||||
if top_score <= 0.0:
|
||||
level, message, best = IDENTITY_REJECTED, "No matching evidence", None
|
||||
else:
|
||||
level, message, best = IDENTITY_ACCEPTED, "Always-argmax baseline", top_sku
|
||||
return {
|
||||
"status": "success" if level == IDENTITY_ACCEPTED else level,
|
||||
"method": method,
|
||||
"best_sku": best,
|
||||
"score": round(top_score, 2),
|
||||
"confidence": confidence,
|
||||
"identity_level": level,
|
||||
"ranked_candidates": ranked,
|
||||
"details": top_details,
|
||||
"message": message,
|
||||
}
|
||||
|
||||
if top_score <= 0.0:
|
||||
level = IDENTITY_REJECTED
|
||||
message = "No matching evidence"
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import os
|
|||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if current_dir not in sys.path:
|
||||
|
|
@ -43,7 +43,14 @@ from rebuild_catalog import bigrams, normalize_text, signature_bigrams, sku_of
|
|||
# on the corrected image6 labels: 2.0 keeps 95% of shown labels correct at 59%
|
||||
# coverage, 1.0 keeps 87% at 66%. The stricter line is the default because a
|
||||
# wrong label on a shelf is worse than a missing one.
|
||||
LOCAL_ACCEPT = 2.0
|
||||
#
|
||||
# 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.
|
||||
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.
|
||||
|
|
@ -51,12 +58,16 @@ 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 is the floor that keeps
|
||||
# every correct text answer on image6 (40 of 40) while dropping the wrong ones
|
||||
# from 6 to 2, taking shown-label precision from 87% to 95%. 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.
|
||||
TEXT_MIN_SHARED = 2
|
||||
# 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.
|
||||
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
|
||||
# BRISK find zero inliers at live resolution. Scaling each reference to the
|
||||
|
|
@ -73,6 +84,15 @@ HUE_VALUE_MIN = 50
|
|||
# Quantity tokens: a number with its unit, in either script. Cut whole, because
|
||||
# the unit's own letters ("g", "ml", "%") are what survived a digits-only rule.
|
||||
_QUANTITY = re.compile(r"\d+(?:\.\d+)?\s*(?:KG|G|ML|L|%|個|本|枚|入|袋|缶|パック)")
|
||||
# A package's own size, in mass units. Only mass can contradict a package size:
|
||||
# a percentage ("カロリー 50% カット") is a claim about the contents, not the
|
||||
# package, and counting it made every low-calorie crop look like a 50 g jar.
|
||||
_MASS = re.compile(r"(\d+(?:\.\d+)?)\s*(KG|G)", re.IGNORECASE)
|
||||
# The range a retail package of this class falls in. Below it are fragments of a
|
||||
# longer number ("20" read out of "200g"); above it are two numbers run together
|
||||
# ("5210" out of "210g"). Both are OCR noise, not a different package, and
|
||||
# either would veto a correct answer if taken at face value.
|
||||
PACKAGE_MASS_RANGE = (100.0, 2000.0)
|
||||
# How many surviving bigrams a read needs before it may name a product.
|
||||
IDENTITY_BIGRAM_FLOOR = 3
|
||||
|
||||
|
|
@ -516,6 +536,40 @@ class DecisionPolicy:
|
|||
return self.text_min_shared
|
||||
return len(bigrams(read) & signature_bigrams(signature))
|
||||
|
||||
def _contradicts_size(self, read: str, sku: str) -> bool:
|
||||
"""Does the read print a package size the row's name does not have?
|
||||
|
||||
The near-unknown case the text ranking cannot catch. A crop of a
|
||||
Kewpie 450 g bag reads "マヨネーズ 450g", and every mayonnaise row in
|
||||
the catalog explains "マヨネーズ" equally well, so the ranking ties at
|
||||
1.0 and the strict-greater veto above never fires - the local lane's
|
||||
answer stands. The size is the part that differs, and it is printed
|
||||
plainly, so it is the part worth checking.
|
||||
|
||||
Only a mass in `PACKAGE_MASS_RANGE` counts. Outside it the read is
|
||||
noise, not a package: "20" is a fragment of "200g" and "5210" is two
|
||||
numbers run together, and vetoing on either would cost correct
|
||||
answers. Measured on image6, this vetoes 3 false accepts at full
|
||||
resolution and 1 at live, and no correct answer at either.
|
||||
"""
|
||||
signature = self.signatures.get(sku)
|
||||
if signature is None:
|
||||
return False
|
||||
read_masses = {
|
||||
float(number) for number, _unit in _MASS.findall(normalize_text(read))
|
||||
}
|
||||
plausible = {
|
||||
mass for mass in read_masses
|
||||
if PACKAGE_MASS_RANGE[0] <= mass <= PACKAGE_MASS_RANGE[1]
|
||||
}
|
||||
if not plausible:
|
||||
return False
|
||||
name_masses = {
|
||||
float(number) for number, _unit in _MASS.findall(normalize_text(signature))
|
||||
}
|
||||
return not (read_masses & name_masses)
|
||||
|
||||
|
||||
def decide(
|
||||
self,
|
||||
local_rank: Any,
|
||||
|
|
@ -550,6 +604,11 @@ class DecisionPolicy:
|
|||
and (text_top[1] - text_runner) >= self.text_margin
|
||||
and carries_identity(text_read or "")
|
||||
and self._shared_evidence(text_read or "", text_top[0]) >= self.text_min_shared
|
||||
# A row the read contradicts on size cannot be named by that read
|
||||
# either. Without this the veto above only moves the answer: the
|
||||
# local lane's row is blocked and the text lane then names the same
|
||||
# row, because "マヨネーズ" is what both of them matched.
|
||||
and not self._contradicts_size(text_read or "", text_top[0])
|
||||
else None
|
||||
)
|
||||
|
||||
|
|
@ -562,6 +621,8 @@ class DecisionPolicy:
|
|||
# artwork, so the geometry is real and only the words differ.
|
||||
if text_top and text_top[0] != candidate and text_top[1] > local_text_floor(text, candidate):
|
||||
answer = None
|
||||
elif self._contradicts_size(text_read or "", candidate):
|
||||
answer = None
|
||||
else:
|
||||
answer, source = candidate, "local"
|
||||
if answer is None and text_names:
|
||||
|
|
|
|||
|
|
@ -1301,6 +1301,10 @@ class LiveSession:
|
|||
decision = DecisionPolicy(
|
||||
signatures=text_lane.signatures if text_lane else None
|
||||
).decide(list(scores.items()), text_rank, text_read=text_read)
|
||||
elif scale == "van":
|
||||
# Van's contract on the stream: the aggregate's top SKU, always
|
||||
# answered. Same scorer as `matcher`, no gate.
|
||||
decision = decide_identity(mean_scores, method=method, scale="van")
|
||||
else:
|
||||
decision = decide_identity(mean_scores, method=method, scale=scale)
|
||||
track.candidates = [
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -317,6 +345,25 @@ def audit(
|
|||
return kept, refused
|
||||
|
||||
|
||||
def unattributed(source: str) -> List[str]:
|
||||
"""SKUs whose views have no entry in the source's ATTRIBUTION.json.
|
||||
|
||||
Reported, not refused. A view with no recorded source is still a usable
|
||||
reference - the six that predate the attribution file are independent of
|
||||
the evaluation photo and their OCR matches their catalog names - but the
|
||||
provenance is gone, and provenance is what lets a later audit tell a
|
||||
re-sourced packshot from a leaked shelf crop. Naming them keeps the gap
|
||||
from growing quietly.
|
||||
"""
|
||||
path = os.path.join(source, "ATTRIBUTION.json")
|
||||
try:
|
||||
with open(path) as handle:
|
||||
recorded = json.load(handle)
|
||||
except (OSError, ValueError):
|
||||
recorded = {}
|
||||
return sorted(sku for sku in collect(source) if sku not in recorded)
|
||||
|
||||
|
||||
def text_discriminated_siblings(
|
||||
kept: Dict[str, List[str]], source: str, names: Optional[Dict[str, str]] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
|
@ -535,6 +582,8 @@ 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)
|
||||
no_source = unattributed(args.source)
|
||||
verdict = live_ready_verdict(kept, names, shelf_skus)
|
||||
report = {
|
||||
"source": args.source,
|
||||
|
|
@ -548,6 +597,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
"text_only_skus": text_only,
|
||||
"text_discriminated_siblings": siblings,
|
||||
"below_pyramid_floor": small,
|
||||
"unattributed_skus": no_source,
|
||||
"completeness": verdict,
|
||||
"skus": {sku: views for sku, views in sorted(kept.items())},
|
||||
"refused": refused,
|
||||
|
|
@ -571,6 +621,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
for row in small:
|
||||
print(f" {row['sku']:<10} {row['view']:<20} {row['height']}px tall - "
|
||||
"scores are not comparable with the rest; re-source this view")
|
||||
if no_source:
|
||||
print(f"\nno recorded source ({len(no_source)}): {', '.join(no_source)} - "
|
||||
"usable, but provenance is missing; re-sourcing would close it")
|
||||
if verdict["live_ready"] is None:
|
||||
print(f"\ncompleteness: unknown - {verdict['reason']}")
|
||||
elif verdict["live_ready"]:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
@ -632,6 +646,56 @@ class DecisionPolicyTests(unittest.TestCase):
|
|||
)
|
||||
self.assertIsNone(decision["best_sku"])
|
||||
|
||||
def test_a_read_printing_another_size_vetoes_the_local_answer(self):
|
||||
"""The documented near-unknown: a Kewpie 450 g bag the catalog lacks.
|
||||
|
||||
The crop reads "マヨネーズ 450g", which every mayonnaise row explains
|
||||
equally, so the text ranking ties at 1.0 and the strict-greater veto
|
||||
cannot fire. The size is what differs, and the catalog's Kewpie rows
|
||||
are 200 g, so the read contradicts the row the local lane chose.
|
||||
"""
|
||||
from identity_lanes import DecisionPolicy
|
||||
|
||||
policy = DecisionPolicy(signatures={"sku_a": "キューピー ハーフマヨネーズ 400g"})
|
||||
decision = policy.decide(
|
||||
[("sku_a", 5.0)], [("sku_a", 1.0)], text_read="ピーマヨネーズ 450g"
|
||||
)
|
||||
self.assertIsNone(decision["best_sku"])
|
||||
|
||||
def test_a_read_agreeing_on_size_does_not_veto(self):
|
||||
"""The same read with the row's own size must still answer."""
|
||||
from identity_lanes import DecisionPolicy
|
||||
|
||||
policy = DecisionPolicy(signatures={"sku_a": "キューピー ハーフマヨネーズ 400g"})
|
||||
decision = policy.decide(
|
||||
[("sku_a", 5.0)], [("sku_a", 1.0)], text_read="ピーマヨネーズ 400g"
|
||||
)
|
||||
self.assertEqual(decision["best_sku"], "sku_a")
|
||||
|
||||
def test_ocr_noise_around_a_size_does_not_veto(self):
|
||||
"""A fragment or a run-together number is noise, not a package size.
|
||||
|
||||
Live reads of a correct 200 g row came back as "20" and "5210"; both
|
||||
are outside the range a package of this class falls in, and vetoing on
|
||||
either would cost a correct answer.
|
||||
"""
|
||||
from identity_lanes import DecisionPolicy
|
||||
|
||||
policy = DecisionPolicy(signatures={"sku_a": "キューピー ハーフマヨネーズ 200g"})
|
||||
for read in ("アマニ油 20gs", "内臓脂訪を 滅与ず ッデュービーフィ5 210g", "n0gベ% 50"):
|
||||
decision = policy.decide([("sku_a", 5.0)], [("sku_a", 1.0)], text_read=read)
|
||||
self.assertEqual(decision["best_sku"], "sku_a", read)
|
||||
|
||||
def test_a_percentage_is_not_a_package_size(self):
|
||||
"""A low-calorie claim must not read as a 50 g package."""
|
||||
from identity_lanes import DecisionPolicy
|
||||
|
||||
policy = DecisionPolicy(signatures={"sku_a": "キューピー ハーフマヨネーズ 400g"})
|
||||
decision = policy.decide(
|
||||
[("sku_a", 5.0)], [("sku_a", 1.0)], text_read="カロリー 50% カット 400g"
|
||||
)
|
||||
self.assertEqual(decision["best_sku"], "sku_a")
|
||||
|
||||
def test_a_named_read_with_a_size_still_fills_in(self):
|
||||
decision = self.policy().decide([], [("sku_b", 0.9)], text_read="コクうま 65%")
|
||||
self.assertEqual(decision["best_sku"], "sku_b")
|
||||
|
|
|
|||
|
|
@ -50,3 +50,20 @@ class IdentityRankingTests(unittest.TestCase):
|
|||
self.assertEqual(ranked["identity_level"], IDENTITY_ACCEPTED)
|
||||
self.assertEqual(ranked["best_sku"], "sku_a")
|
||||
self.assertEqual(ranked["status"], "success")
|
||||
|
||||
def test_van_answers_what_the_gate_would_refuse(self):
|
||||
# Van's contract is the baseline the decision is measured against, so it
|
||||
# must answer exactly where the shipped gate abstains. A weak score and
|
||||
# a close pair are both refused above; here both are named.
|
||||
weak = decide_identity({"sku_a": 0.4, "sku_b": 0.0}, scale="van")
|
||||
self.assertEqual(weak["identity_level"], IDENTITY_ACCEPTED)
|
||||
self.assertEqual(weak["best_sku"], "sku_a")
|
||||
close = decide_identity({"sku_a": 20.0, "sku_b": 19.5}, scale="van")
|
||||
self.assertEqual(close["best_sku"], "sku_a")
|
||||
|
||||
def test_van_still_rejects_when_there_is_no_evidence(self):
|
||||
# Always-argmax is not always-answer: with nothing to rank there is no
|
||||
# argmax, and naming a SKU from an empty score set would be a fabrication.
|
||||
ranked = decide_identity({}, scale="van")
|
||||
self.assertEqual(ranked["identity_level"], IDENTITY_REJECTED)
|
||||
self.assertIsNone(ranked["best_sku"])
|
||||
|
|
|
|||
|
|
@ -184,5 +184,95 @@ class BadShelfListFailsBeforeTheSwapTests(unittest.TestCase):
|
|||
self.assertFalse(os.path.exists(output), "the catalog was replaced before the check")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
class OrientationTests(unittest.TestCase):
|
||||
"""A packshot whose identifying text is not horizontal.
|
||||
|
||||
The やまや めんたいマヨネーズ packshot reads its brand only when rotated;
|
||||
the upright pass returns stray kana. Checking one orientation refused a
|
||||
correct image, which is the worst failure this check has - it rejects good
|
||||
data while claiming to validate it.
|
||||
"""
|
||||
class FakeOcr:
|
||||
"""Reads a fixed string per rotation.
|
||||
|
||||
Rotations are identified by aspect ratio, not by exact shape: the
|
||||
caller upscales before OCR, so the fake sees scaled dimensions. The
|
||||
upright and 180-degree passes keep the source's orientation (wide),
|
||||
the two 90-degree passes swap it (tall). A corner marker cannot tell
|
||||
them apart, because `cv2.rotate` carries the corner value through.
|
||||
"""
|
||||
|
||||
name = "fake"
|
||||
|
||||
def __init__(self, by_orientation):
|
||||
self.by_orientation = by_orientation
|
||||
|
||||
def read(self, image):
|
||||
class Line:
|
||||
def __init__(self, text):
|
||||
self.text = text
|
||||
|
||||
class Read:
|
||||
def __init__(self, text):
|
||||
self.lines = [Line(part) for part in text.split("|") if part]
|
||||
|
||||
height, width = image.shape[:2]
|
||||
key = "wide" if width > height else "tall"
|
||||
return Read(self.by_orientation.get(key, ""))
|
||||
|
||||
def marked(self, height, width):
|
||||
return np.zeros((height, width, 3), dtype="uint8")
|
||||
|
||||
def test_a_rotated_read_wins_when_it_matches_the_name(self):
|
||||
"""The upright pass reads noise; the rotated pass reads the product."""
|
||||
from rebuild_catalog import best_orientation_text
|
||||
|
||||
engine = self.FakeOcr({"wide": "つくつ|た", "tall": "明太手屋さんが|マヨネーズ"})
|
||||
text = best_orientation_text(engine, self.marked(40, 60), "やまや めんたいマヨネーズタイプ 200g")
|
||||
self.assertIn("明太手屋さんが", text)
|
||||
|
||||
def test_the_upright_read_wins_when_it_matches(self):
|
||||
from rebuild_catalog import best_orientation_text
|
||||
|
||||
engine = self.FakeOcr({"wide": "ピュアセレクト|マヨネーズ|450g", "tall": "ノイズ"})
|
||||
text = best_orientation_text(engine, self.marked(40, 60), "ピュアセレクト マヨネーズ 450g")
|
||||
self.assertIn("ピュアセレクト", text)
|
||||
|
||||
def test_without_a_name_the_longest_read_wins(self):
|
||||
from rebuild_catalog import best_orientation_text
|
||||
|
||||
engine = self.FakeOcr({"wide": "ab", "tall": "abcdefgh"})
|
||||
self.assertEqual(best_orientation_text(engine, self.marked(40, 60)), "abcdefgh")
|
||||
|
||||
def test_audit_keeps_a_view_whose_name_only_reads_rotated(self):
|
||||
"""The regression: `audit` must try every orientation before refusing.
|
||||
|
||||
This is the boundary that matters. `best_orientation_text` passing its
|
||||
own unit test proves nothing about enrollment - the bug was that
|
||||
`audit` called a single-orientation read, so a correct packshot whose
|
||||
brand text runs vertically was refused as a name/OCR mismatch.
|
||||
"""
|
||||
from rebuild_catalog import audit
|
||||
|
||||
root = tempfile.mkdtemp(prefix="orient-")
|
||||
# Wider than tall, so the upright pass is the "wide" read (noise) and
|
||||
# only a 90-degree rotation produces the "tall" read that matches.
|
||||
cv2.imwrite(os.path.join(root, "sku_x1.jpg"), self.marked(300, 400))
|
||||
engine = self.FakeOcr({"wide": "つくつ|た", "tall": "キユーピー|エッグケア|305g"})
|
||||
kept, refused = audit(root, min_views=1, names={"sku_x1": "キューピー エッグケア 305g"},
|
||||
ocr_engine=engine)
|
||||
self.assertEqual(kept, {"sku_x1": ["sku_x1.jpg"]}, refused)
|
||||
self.assertEqual(refused, [])
|
||||
|
||||
def test_audit_still_refuses_a_view_the_name_never_matches(self):
|
||||
"""The check must keep its teeth: a wrong product is still refused."""
|
||||
from rebuild_catalog import audit
|
||||
|
||||
root = tempfile.mkdtemp(prefix="orient-bad-")
|
||||
cv2.imwrite(os.path.join(root, "sku_x1.jpg"), self.marked(400, 300))
|
||||
engine = self.FakeOcr({"wide": "キューピー|マヨネーズ", "tall": "キューピー|マヨネーズ"})
|
||||
kept, refused = audit(root, min_views=1, names={"sku_x1": "やまや めんたいマヨネーズタイプ 200g"},
|
||||
ocr_engine=engine)
|
||||
self.assertEqual(kept, {})
|
||||
self.assertEqual(len(refused), 1, refused)
|
||||
self.assertIn("name/OCR", refused[0]["reason"])
|
||||
|
|
|
|||
34
recognization/data/README.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Recognition data
|
||||
|
||||
Everything the identity work needs to be reproducible: sourced packshots,
|
||||
evaluation runs, and the audits that produced them. Nothing here is ephemeral;
|
||||
a run that matters is written here, not to a temp directory.
|
||||
|
||||
## `packshots/source/`
|
||||
|
||||
Front packshots as downloaded, before enrollment. `ATTRIBUTION.json` records the
|
||||
source URL, the product as printed, and the manufacturer for each. `ocr-reads.json`
|
||||
holds the OCR text each image produced, so a name/OCR refusal can be re-checked
|
||||
without re-running Vision.
|
||||
|
||||
`rebuild_catalog.py` reads this directory and writes a catalog:
|
||||
|
||||
.venv/bin/python code/rebuild_catalog.py --source data/packshots/source \
|
||||
--output stock-lopia-clean --csv stock-lopia/stock.csv --min-views 1 --replace
|
||||
|
||||
## `evaluations/`
|
||||
|
||||
One directory per session. Each holds the raw artifacts a claim was computed
|
||||
from, so a number in DECISION.md can be traced back to the run that produced it.
|
||||
|
||||
| Directory | Contents |
|
||||
|---|---|
|
||||
| `stream/` | WebSocket track dumps from `stream_client.py`, scored by `score_stream.py`. |
|
||||
| `still/` | `/v1/recognize` responses for the still-image path. |
|
||||
| `catalog-audit/` | `rebuild_catalog.py --json` reports. |
|
||||
| `scratch/` | Intermediate images (rotations, crops) kept because they document a finding. |
|
||||
|
||||
Scoring a stream dump:
|
||||
|
||||
.venv/bin/python code/score_stream.py data/evaluations/<session>/stream/<file>.json \
|
||||
--labels eval/labels/image6.json --stock-dir stock-lopia-clean
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"source": "data/packshots/source",
|
||||
"output": "stock-lopia-clean",
|
||||
"min_width": 120,
|
||||
"min_height": 160,
|
||||
"min_views": 1,
|
||||
"duplicate_cosine": 0.98,
|
||||
"min_name_ocr": 0.7,
|
||||
"ocr": "vision",
|
||||
"text_only_skus": [
|
||||
"sku_l04",
|
||||
"sku_l07",
|
||||
"sku_l08",
|
||||
"sku_l12"
|
||||
],
|
||||
"text_discriminated_siblings": [
|
||||
{
|
||||
"sku": "sku_l09",
|
||||
"view": "sku_l09.jpg",
|
||||
"sibling_sku": "sku_l10",
|
||||
"sibling_view": "sku_l10.jpg",
|
||||
"cosine": 0.9817,
|
||||
"name_similarity": 0.333
|
||||
}
|
||||
],
|
||||
"below_pyramid_floor": [],
|
||||
"completeness": {
|
||||
"live_ready": null,
|
||||
"reason": "no --shelf-tags given; the completeness rule needs the shelf's SKU list",
|
||||
"enrolled": 8,
|
||||
"text_only": [
|
||||
"sku_l04",
|
||||
"sku_l07",
|
||||
"sku_l08",
|
||||
"sku_l12"
|
||||
]
|
||||
},
|
||||
"skus": {
|
||||
"sku_l01": [
|
||||
"sku_l01.jpg"
|
||||
],
|
||||
"sku_l02": [
|
||||
"sku_l02.jpg"
|
||||
],
|
||||
"sku_l03": [
|
||||
"sku_l03.jpg"
|
||||
],
|
||||
"sku_l05": [
|
||||
"sku_l05.jpg"
|
||||
],
|
||||
"sku_l06": [
|
||||
"sku_l06.jpg"
|
||||
],
|
||||
"sku_l09": [
|
||||
"sku_l09.jpg"
|
||||
],
|
||||
"sku_l10": [
|
||||
"sku_l10.jpg"
|
||||
],
|
||||
"sku_l11": [
|
||||
"sku_l11.jpg"
|
||||
]
|
||||
},
|
||||
"refused": [
|
||||
{
|
||||
"sku": "sku_l04",
|
||||
"view": "sku_l04.jpg",
|
||||
"reason": "name/OCR 0.00 below 0.70: 明太手屋さんが つくつ た コ"
|
||||
},
|
||||
{
|
||||
"sku": "sku_l07",
|
||||
"view": "sku_l07.jpg",
|
||||
"reason": "name/OCR 0.57 below 0.70: ラッキー マヨネーズ 調味料(アミノ酸)不使用"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
{
|
||||
"image": "add/image6.jpg",
|
||||
"stock_dir": "stock-lopia-clean",
|
||||
"labels": "eval/labels/image6.json",
|
||||
"provenance": {
|
||||
"catalog_hash": "3f1653d9d962073af96121250006b9b6d86113556797bbee65de68b60e87a64d",
|
||||
"labels_hash": "5aabf029921ced767d4e14a18e8bccb55812dd974852c56e0931a98fa10366cf",
|
||||
"image_hash": "a08b89ba37d27c9d5a81bcd56bcd042e6baf54d2f182bbaaf47e97f14c9cd525",
|
||||
"code_revision": "68061635d9fef3097609945bfd9ac1935791b398"
|
||||
},
|
||||
"facings": 111,
|
||||
"leakage": {
|
||||
"excluded": 0,
|
||||
"labels": []
|
||||
},
|
||||
"hub_share_limit": 0.3,
|
||||
"enrollment": {
|
||||
"catalog_skus": [
|
||||
"sku_l01",
|
||||
"sku_l02",
|
||||
"sku_l03",
|
||||
"sku_l04",
|
||||
"sku_l05",
|
||||
"sku_l06",
|
||||
"sku_l07",
|
||||
"sku_l08",
|
||||
"sku_l09",
|
||||
"sku_l10",
|
||||
"sku_l11",
|
||||
"sku_l12"
|
||||
],
|
||||
"enrolled_skus": [
|
||||
"sku_l01",
|
||||
"sku_l02",
|
||||
"sku_l03",
|
||||
"sku_l05",
|
||||
"sku_l06",
|
||||
"sku_l09",
|
||||
"sku_l10",
|
||||
"sku_l11"
|
||||
],
|
||||
"unenrolled_skus": {
|
||||
"sku_l07": 6,
|
||||
"sku_l04": 1,
|
||||
"sku_l08": 1
|
||||
},
|
||||
"facings_without_enrollment": 8,
|
||||
"scored_facings": 109
|
||||
},
|
||||
"results": {
|
||||
"full": [
|
||||
{
|
||||
"lane": "local",
|
||||
"accept": 2.0,
|
||||
"margin": 0.15,
|
||||
"in_catalog": 61,
|
||||
"out_catalog": 48,
|
||||
"enrolled": 53,
|
||||
"correct": 48,
|
||||
"wrong": 9,
|
||||
"rejected": 52,
|
||||
"catalog_ceiling": 0.8689,
|
||||
"identity_ceiling": 0.9245,
|
||||
"decision_ceiling": 0.8113,
|
||||
"end_to_end_coverage": 0.7869,
|
||||
"false_accepts": 9,
|
||||
"unknown_false_accepts": 8,
|
||||
"unenrolled_false_accepts": 0,
|
||||
"false_accept_rate": 0.1667,
|
||||
"near_unknown_false_accept_rate": 0.0,
|
||||
"precision": 0.8421,
|
||||
"max_sku_share": 0.2281,
|
||||
"hub_sku": "sku_l09",
|
||||
"hub_alarm": false,
|
||||
"histogram": {
|
||||
"sku_l09": 13,
|
||||
"sku_l11": 10,
|
||||
"sku_l10": 9,
|
||||
"sku_l01": 5,
|
||||
"sku_l03": 5,
|
||||
"sku_l07": 5,
|
||||
"sku_l06": 4,
|
||||
"sku_l02": 3,
|
||||
"sku_l05": 3
|
||||
},
|
||||
"per_sku": {
|
||||
"sku_l01": {
|
||||
"correct": 5,
|
||||
"total": 5
|
||||
},
|
||||
"sku_l02": {
|
||||
"correct": 2,
|
||||
"total": 4
|
||||
},
|
||||
"sku_l03": {
|
||||
"correct": 4,
|
||||
"total": 4
|
||||
},
|
||||
"sku_l04": {
|
||||
"correct": 0,
|
||||
"total": 1
|
||||
},
|
||||
"sku_l05": {
|
||||
"correct": 3,
|
||||
"total": 3
|
||||
},
|
||||
"sku_l06": {
|
||||
"correct": 4,
|
||||
"total": 4
|
||||
},
|
||||
"sku_l07": {
|
||||
"correct": 5,
|
||||
"total": 6
|
||||
},
|
||||
"sku_l08": {
|
||||
"correct": 0,
|
||||
"total": 1
|
||||
},
|
||||
"sku_l09": {
|
||||
"correct": 11,
|
||||
"total": 13
|
||||
},
|
||||
"sku_l10": {
|
||||
"correct": 9,
|
||||
"total": 11
|
||||
},
|
||||
"sku_l11": {
|
||||
"correct": 5,
|
||||
"total": 9
|
||||
}
|
||||
},
|
||||
"ms_per_crop": 26.7
|
||||
}
|
||||
],
|
||||
"live": [
|
||||
{
|
||||
"lane": "local",
|
||||
"accept": 2.0,
|
||||
"margin": 0.15,
|
||||
"in_catalog": 61,
|
||||
"out_catalog": 48,
|
||||
"enrolled": 53,
|
||||
"correct": 41,
|
||||
"wrong": 4,
|
||||
"rejected": 64,
|
||||
"catalog_ceiling": 0.8689,
|
||||
"identity_ceiling": 0.8491,
|
||||
"decision_ceiling": 0.6604,
|
||||
"end_to_end_coverage": 0.6721,
|
||||
"false_accepts": 4,
|
||||
"unknown_false_accepts": 3,
|
||||
"unenrolled_false_accepts": 0,
|
||||
"false_accept_rate": 0.0625,
|
||||
"near_unknown_false_accept_rate": 0.0,
|
||||
"precision": 0.9111,
|
||||
"max_sku_share": 0.2,
|
||||
"hub_sku": "sku_l09",
|
||||
"hub_alarm": false,
|
||||
"histogram": {
|
||||
"sku_l09": 9,
|
||||
"sku_l10": 9,
|
||||
"sku_l11": 7,
|
||||
"sku_l07": 6,
|
||||
"sku_l01": 4,
|
||||
"sku_l03": 3,
|
||||
"sku_l05": 3,
|
||||
"sku_l06": 2,
|
||||
"sku_l02": 1,
|
||||
"sku_l04": 1
|
||||
},
|
||||
"per_sku": {
|
||||
"sku_l01": {
|
||||
"correct": 4,
|
||||
"total": 5
|
||||
},
|
||||
"sku_l02": {
|
||||
"correct": 1,
|
||||
"total": 4
|
||||
},
|
||||
"sku_l03": {
|
||||
"correct": 3,
|
||||
"total": 4
|
||||
},
|
||||
"sku_l04": {
|
||||
"correct": 1,
|
||||
"total": 1
|
||||
},
|
||||
"sku_l05": {
|
||||
"correct": 3,
|
||||
"total": 3
|
||||
},
|
||||
"sku_l06": {
|
||||
"correct": 2,
|
||||
"total": 4
|
||||
},
|
||||
"sku_l07": {
|
||||
"correct": 5,
|
||||
"total": 6
|
||||
},
|
||||
"sku_l08": {
|
||||
"correct": 0,
|
||||
"total": 1
|
||||
},
|
||||
"sku_l09": {
|
||||
"correct": 9,
|
||||
"total": 13
|
||||
},
|
||||
"sku_l10": {
|
||||
"correct": 9,
|
||||
"total": 11
|
||||
},
|
||||
"sku_l11": {
|
||||
"correct": 4,
|
||||
"total": 9
|
||||
}
|
||||
},
|
||||
"ms_per_crop": 20.9
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"source": "data/packshots/source",
|
||||
"output": "/tmp/lopia-audit",
|
||||
"min_width": 120,
|
||||
"min_height": 160,
|
||||
"min_views": 1,
|
||||
"duplicate_cosine": 0.98,
|
||||
"min_name_ocr": 0.7,
|
||||
"ocr": "vision",
|
||||
"text_only_skus": [
|
||||
"sku_l04",
|
||||
"sku_l07",
|
||||
"sku_l08",
|
||||
"sku_l09",
|
||||
"sku_l10",
|
||||
"sku_l12"
|
||||
],
|
||||
"text_discriminated_siblings": [],
|
||||
"below_pyramid_floor": [],
|
||||
"completeness": {
|
||||
"live_ready": null,
|
||||
"reason": "no --shelf-tags given; the completeness rule needs the shelf's SKU list",
|
||||
"enrolled": 6,
|
||||
"text_only": [
|
||||
"sku_l04",
|
||||
"sku_l07",
|
||||
"sku_l08",
|
||||
"sku_l09",
|
||||
"sku_l10",
|
||||
"sku_l12"
|
||||
]
|
||||
},
|
||||
"skus": {
|
||||
"sku_l01": [
|
||||
"sku_l01.jpg"
|
||||
],
|
||||
"sku_l02": [
|
||||
"sku_l02.jpg"
|
||||
],
|
||||
"sku_l03": [
|
||||
"sku_l03.jpg"
|
||||
],
|
||||
"sku_l05": [
|
||||
"sku_l05.jpg"
|
||||
],
|
||||
"sku_l06": [
|
||||
"sku_l06.jpg"
|
||||
],
|
||||
"sku_l11": [
|
||||
"sku_l11.jpg"
|
||||
]
|
||||
},
|
||||
"refused": [
|
||||
{
|
||||
"sku": "sku_l04",
|
||||
"view": "sku_l04.jpg",
|
||||
"reason": "name/OCR 0.00 below 0.70: 明太手屋さんが つくつ た コ"
|
||||
},
|
||||
{
|
||||
"sku": "sku_l07",
|
||||
"view": "sku_l07.jpg",
|
||||
"reason": "name/OCR 0.57 below 0.70: ラッキー マヨネーズ 調味料(アミノ酸)不使用"
|
||||
},
|
||||
{
|
||||
"sku": "sku_l09",
|
||||
"view": "sku_l09.jpg",
|
||||
"reason": "name/OCR 0.05 below 0.70: キューピーマヨネーズ KEWPIE MAYONNAISE ® 450g"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
"""Which facings are falsely accepted, and as what."""
|
||||
import sys, cv2
|
||||
sys.path.insert(0, "code")
|
||||
from eval_identity import LocalLaneAdapter, load_labels, at_live_resolution, leaked, UNKNOWN, crop_of
|
||||
from identity_lanes import DecisionPolicy
|
||||
|
||||
payload = load_labels("eval/labels/image6.json")
|
||||
image = cv2.imread("add/image6.jpg")
|
||||
facings = [f for f in payload["facings"] if f["label"] != "exclude"]
|
||||
facings = [f for f in facings if leaked(image, f["box"], "stock-lopia-clean") is None]
|
||||
enrolled = {"sku_l01","sku_l02","sku_l03","sku_l05","sku_l06","sku_l09","sku_l10","sku_l11"}
|
||||
|
||||
for live in (False, True):
|
||||
picture = at_live_resolution(image) if live else image
|
||||
scaled = facings if not live else [
|
||||
{**f, "box": [int(round(v * picture.shape[1] / image.shape[1])) for v in f["box"]]}
|
||||
for f in facings]
|
||||
lane = LocalLaneAdapter("stock-lopia-clean")
|
||||
print(f"=== {'live' if live else 'full'} ===")
|
||||
for f in scaled:
|
||||
crop = crop_of(picture, f["box"])
|
||||
if crop.size == 0: continue
|
||||
ranked = lane.rank(crop)
|
||||
d = lane.decide(crop, ranked, accept=2.0, margin=0.0, scale="local")
|
||||
ans = d.get("best_sku") if d.get("identity_level") == "accepted" else None
|
||||
if ans and ans != f["label"]:
|
||||
print(f" #{f['index']:>3} truth={f['label']:<9} answered={ans:<9} enrolled={f['label'] in enrolled}")
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
"""What the falsely-accepted crops actually read, and what the text lane scores.
|
||||
|
||||
The advisory says crops 82-97 are Kewpie Mayonnaise 450g - a product the
|
||||
catalog does not hold - and that no catalog row can therefore win the text
|
||||
ranking, so the veto at DecisionPolicy.decide never fires. This prints the
|
||||
reads and the scores that claim depends on.
|
||||
"""
|
||||
import csv
|
||||
import sys
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, "code")
|
||||
from identity_lanes import TextLane, carries_identity # noqa: E402
|
||||
from ocr import build_ocr_engine # noqa: E402
|
||||
|
||||
INDICES = [73, 82, 83, 84, 86, 94, 95, 96, 97]
|
||||
|
||||
rows = []
|
||||
with open("stock-lopia-clean/stock.csv", encoding="utf-8") as handle:
|
||||
for record in csv.reader(handle):
|
||||
if len(record) >= 2 and record[0].startswith("sku_"):
|
||||
rows.append({"sku_id": record[0], "product_name": record[1]})
|
||||
|
||||
engine = build_ocr_engine("vision")
|
||||
lane = TextLane.from_catalog(rows, "stock-lopia-clean", ocr_engine=engine)
|
||||
|
||||
labels = {f["index"]: f for f in __import__("json").load(open("eval/labels/image6.json"))["facings"]}
|
||||
image = cv2.imread("add/image6.jpg")
|
||||
|
||||
tiles = []
|
||||
for index in INDICES:
|
||||
facing = labels[index]
|
||||
x1, y1, x2, y2 = facing["box"]
|
||||
crop = image[y1:y2, x1:x2]
|
||||
read = lane.read(crop)
|
||||
scores = lane.scores_from_text(read)
|
||||
top = sorted(scores.items(), key=lambda kv: -kv[1])[:3]
|
||||
print(f"#{index:>3} truth={facing['label']:<9} carries={carries_identity(read)}")
|
||||
print(f" read: {read[:110]}")
|
||||
print(f" text top: {[(s, round(v, 3)) for s, v in top]}")
|
||||
|
||||
height, width = crop.shape[:2]
|
||||
scale = 260.0 / max(1, height)
|
||||
resized = cv2.resize(crop, (max(1, int(width * scale)), 260), interpolation=cv2.INTER_CUBIC)
|
||||
canvas = np.full((300, 220, 3), 255, np.uint8)
|
||||
canvas[30:290, : min(resized.shape[1], 220)] = resized[:, :220]
|
||||
cv2.putText(canvas, str(index), (6, 22), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
|
||||
tiles.append(canvas)
|
||||
|
||||
grid = np.vstack([np.hstack(tiles[:5]), np.hstack(tiles[5:])])
|
||||
cv2.imwrite("data/evaluations/2026-09-29-session/scratch/fa_tiles.jpg", grid)
|
||||
print("tile:", grid.shape)
|
||||
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 307 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
|
@ -0,0 +1,71 @@
|
|||
"""Mass-only size contradiction, with full reads.
|
||||
|
||||
The first pass counted every quantity token, including percentages, so a crop
|
||||
reading "カロリー 50% カット" looked like a 50 g package. Only mass units can
|
||||
contradict a package size, so this restricts to g/kg and prints the whole read
|
||||
for every accepted facing that carries one.
|
||||
"""
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
import cv2
|
||||
|
||||
sys.path.insert(0, "code")
|
||||
from eval_identity import LocalLaneAdapter, at_live_resolution, crop_of, leaked # noqa: E402
|
||||
from identity_lanes import TextLane, normalize_text # noqa: E402
|
||||
from ocr import build_ocr_engine # noqa: E402
|
||||
|
||||
MASS = re.compile(r"(\d+(?:\.\d+)?)\s*(KG|G)", re.IGNORECASE)
|
||||
# Package sizes this product class actually comes in. Below it are OCR fragments
|
||||
# of a longer number ("20" from 200); above it are concatenations ("5210").
|
||||
PLAUSIBLE = (100.0, 2000.0)
|
||||
|
||||
|
||||
def masses(text: str):
|
||||
return {float(number) for number, _unit in MASS.findall(normalize_text(text))}
|
||||
|
||||
|
||||
rows = []
|
||||
with open("stock-lopia-clean/stock.csv", encoding="utf-8") as handle:
|
||||
for record in csv.reader(handle):
|
||||
if len(record) >= 2 and record[0].startswith("sku_"):
|
||||
rows.append({"sku_id": record[0], "product_name": record[1]})
|
||||
names = {row["sku_id"]: row["product_name"] for row in rows}
|
||||
|
||||
engine = build_ocr_engine("vision")
|
||||
text_lane = TextLane.from_catalog(rows, "stock-lopia-clean", ocr_engine=engine)
|
||||
local_lane = LocalLaneAdapter("stock-lopia-clean")
|
||||
|
||||
payload = json.load(open("eval/labels/image6.json"))
|
||||
image = cv2.imread("add/image6.jpg")
|
||||
facings = [f for f in payload["facings"] if f["label"] != "exclude"]
|
||||
facings = [f for f in facings if leaked(image, f["box"], "stock-lopia-clean") is None]
|
||||
|
||||
for live in (False, True):
|
||||
picture = at_live_resolution(image) if live else image
|
||||
scaled = facings if not live else [
|
||||
{**f, "box": [int(round(v * picture.shape[1] / image.shape[1])) for v in f["box"]]}
|
||||
for f in facings]
|
||||
print(f"=== {'live' if live else 'full'} ===")
|
||||
for facing in scaled:
|
||||
crop = crop_of(picture, facing["box"])
|
||||
if crop.size == 0:
|
||||
continue
|
||||
ranked = local_lane.rank(crop)
|
||||
decision = local_lane.decide(crop, ranked, accept=2.0, margin=0.0, scale="local")
|
||||
if decision.get("identity_level") != "accepted":
|
||||
continue
|
||||
answer = decision.get("best_sku")
|
||||
read = text_lane.read(crop)
|
||||
read_masses = masses(read)
|
||||
name_masses = masses(names.get(answer, ""))
|
||||
if not read_masses or not name_masses:
|
||||
continue
|
||||
plausible = {m for m in read_masses if PLAUSIBLE[0] <= m <= PLAUSIBLE[1]}
|
||||
veto = bool(plausible) and not (read_masses & name_masses)
|
||||
correct = "correct" if answer == facing["label"] else "WRONG"
|
||||
print(f" #{facing['index']:>3} {'VETO' if veto else 'keep'} {correct:<7} "
|
||||
f"answer={answer:<9} read_m={sorted(read_masses)} name_m={sorted(name_masses)}")
|
||||
print(f" read: {read[:120]}")
|
||||
|
After Width: | Height: | Size: 238 KiB |
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"endpoint": "/v1/recognize",
|
||||
"image": "add/image6.jpg",
|
||||
"catalog": "stock-lopia-clean",
|
||||
"gates": "LOCAL_ACCEPT=4.0 TEXT_MIN_SHARED=4",
|
||||
"still": {
|
||||
"detections": 111,
|
||||
"named": 44,
|
||||
"matched": 44,
|
||||
"correct": 42,
|
||||
"wrong": 2,
|
||||
"precision": 0.955
|
||||
},
|
||||
"stream": {
|
||||
"seconds": 20,
|
||||
"pan": 12,
|
||||
"tracks_seen": 165,
|
||||
"tracks_labelled": 62,
|
||||
"label_changes": 0,
|
||||
"unexplained_label_changes": 0,
|
||||
"facings_named": "17/27",
|
||||
"precision": 0.84,
|
||||
"wrong": 0,
|
||||
"unenrolled_false_accepts": 0,
|
||||
"unknown_false_accepts": 8
|
||||
},
|
||||
"note": "Final state with every catalog correction and both calibrated accept lines live."
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"endpoint": "/v1/recognize",
|
||||
"image": "add/image6.jpg",
|
||||
"catalog": "stock-lopia-clean",
|
||||
"gates": "LOCAL_ACCEPT=4.0 TEXT_MIN_SHARED=4",
|
||||
"detections": 111,
|
||||
"named": 44,
|
||||
"matched": 44,
|
||||
"correct": 42,
|
||||
"wrong": 2,
|
||||
"precision": 0.955,
|
||||
"wrong_detail": [
|
||||
"sku_l07 vs unknown",
|
||||
"sku_l03 vs unknown"
|
||||
],
|
||||
"previous": {
|
||||
"gates": "2.0/2",
|
||||
"named": 53,
|
||||
"correct": 48,
|
||||
"wrong": 5,
|
||||
"precision": 0.906
|
||||
},
|
||||
"note": "Live still endpoint, box-IoU matched to the corrected labels. The stricter gates trade 9 names for 3 wrong labels."
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"endpoint": "/v1/recognize",
|
||||
"image": "add/image6.jpg",
|
||||
"catalog": "stock-lopia-clean",
|
||||
"catalog_version": "b862c55f4623",
|
||||
"detections": 111,
|
||||
"named": 53,
|
||||
"named_without_product_info": 0,
|
||||
"view_suffixed_ids": [],
|
||||
"elapsed_s": 2.38,
|
||||
"by_sku": {
|
||||
"sku_l10": 9,
|
||||
"sku_l11": 5,
|
||||
"sku_l01": 5,
|
||||
"sku_l06": 4,
|
||||
"sku_l09": 13,
|
||||
"sku_l07": 6,
|
||||
"sku_l02": 3,
|
||||
"sku_l03": 5,
|
||||
"sku_l05": 3
|
||||
},
|
||||
"note": "Final state: l06 current package enrolled (4/4 facings named, was 1/4 at live), l10/l11 400g, l12 renamed. l12 not named: no enrolled view."
|
||||
}
|
||||
1631
recognization/data/evaluations/2026-09-29-session/still/final_0.json
Normal file
1607
recognization/data/evaluations/2026-09-29-session/still/final_2.json
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
{
|
||||
"image": "add/image6.jpg",
|
||||
"stock_dir": "stock-lopia-clean",
|
||||
"labels": "eval/labels/image6.json",
|
||||
"provenance": {
|
||||
"catalog_hash": "10f55211435aa64c0ed4ca24a7b902ce4ed8a30299409680942b78ec9b3d08c8",
|
||||
"labels_hash": "5aabf029921ced767d4e14a18e8bccb55812dd974852c56e0931a98fa10366cf",
|
||||
"image_hash": "a08b89ba37d27c9d5a81bcd56bcd042e6baf54d2f182bbaaf47e97f14c9cd525",
|
||||
"code_revision": "b6c87c45ea2aa3ed59f5764274341f3ffed30fe0"
|
||||
},
|
||||
"facings": 111,
|
||||
"leakage": {
|
||||
"excluded": 0,
|
||||
"labels": []
|
||||
},
|
||||
"hub_share_limit": 0.3,
|
||||
"enrollment": {
|
||||
"catalog_skus": [
|
||||
"sku_l01",
|
||||
"sku_l02",
|
||||
"sku_l03",
|
||||
"sku_l04",
|
||||
"sku_l05",
|
||||
"sku_l06",
|
||||
"sku_l07",
|
||||
"sku_l08",
|
||||
"sku_l09",
|
||||
"sku_l10",
|
||||
"sku_l11",
|
||||
"sku_l12"
|
||||
],
|
||||
"enrolled_skus": [
|
||||
"sku_l01",
|
||||
"sku_l02",
|
||||
"sku_l03",
|
||||
"sku_l05",
|
||||
"sku_l06",
|
||||
"sku_l09",
|
||||
"sku_l11"
|
||||
],
|
||||
"unenrolled_skus": {
|
||||
"sku_l10": 11,
|
||||
"sku_l07": 6,
|
||||
"sku_l04": 1,
|
||||
"sku_l08": 1
|
||||
},
|
||||
"facings_without_enrollment": 19,
|
||||
"scored_facings": 109
|
||||
},
|
||||
"results": {
|
||||
"full": [
|
||||
{
|
||||
"lane": "van",
|
||||
"accept": 1.0,
|
||||
"margin": 0.15,
|
||||
"in_catalog": 61,
|
||||
"out_catalog": 48,
|
||||
"enrolled": 42,
|
||||
"correct": 32,
|
||||
"wrong": 77,
|
||||
"rejected": 0,
|
||||
"catalog_ceiling": 0.6885,
|
||||
"identity_ceiling": 0.7619,
|
||||
"decision_ceiling": 0.7619,
|
||||
"end_to_end_coverage": 0.5246,
|
||||
"false_accepts": 77,
|
||||
"unknown_false_accepts": 48,
|
||||
"unenrolled_false_accepts": 19,
|
||||
"false_accept_rate": 1.0,
|
||||
"near_unknown_false_accept_rate": 1.0,
|
||||
"precision": 0.2936,
|
||||
"max_sku_share": 0.2569,
|
||||
"hub_sku": "sku_l09",
|
||||
"hub_alarm": false,
|
||||
"histogram": {
|
||||
"sku_l09": 28,
|
||||
"sku_l05": 19,
|
||||
"sku_l02": 15,
|
||||
"sku_l03": 14,
|
||||
"sku_l06": 13,
|
||||
"sku_l11": 12,
|
||||
"sku_l01": 8
|
||||
},
|
||||
"per_sku": {
|
||||
"sku_l01": {
|
||||
"correct": 5,
|
||||
"total": 5
|
||||
},
|
||||
"sku_l02": {
|
||||
"correct": 4,
|
||||
"total": 4
|
||||
},
|
||||
"sku_l03": {
|
||||
"correct": 4,
|
||||
"total": 4
|
||||
},
|
||||
"sku_l04": {
|
||||
"correct": 0,
|
||||
"total": 1
|
||||
},
|
||||
"sku_l05": {
|
||||
"correct": 3,
|
||||
"total": 3
|
||||
},
|
||||
"sku_l06": {
|
||||
"correct": 0,
|
||||
"total": 4
|
||||
},
|
||||
"sku_l07": {
|
||||
"correct": 0,
|
||||
"total": 6
|
||||
},
|
||||
"sku_l08": {
|
||||
"correct": 0,
|
||||
"total": 1
|
||||
},
|
||||
"sku_l09": {
|
||||
"correct": 13,
|
||||
"total": 13
|
||||
},
|
||||
"sku_l10": {
|
||||
"correct": 0,
|
||||
"total": 11
|
||||
},
|
||||
"sku_l11": {
|
||||
"correct": 3,
|
||||
"total": 9
|
||||
}
|
||||
},
|
||||
"ms_per_crop": 15.4
|
||||
},
|
||||
{
|
||||
"lane": "local",
|
||||
"accept": 2.0,
|
||||
"margin": 0.15,
|
||||
"in_catalog": 61,
|
||||
"out_catalog": 48,
|
||||
"enrolled": 42,
|
||||
"correct": 36,
|
||||
"wrong": 5,
|
||||
"rejected": 68,
|
||||
"catalog_ceiling": 0.6885,
|
||||
"identity_ceiling": 0.9286,
|
||||
"decision_ceiling": 0.8571,
|
||||
"end_to_end_coverage": 0.5902,
|
||||
"false_accepts": 5,
|
||||
"unknown_false_accepts": 4,
|
||||
"unenrolled_false_accepts": 0,
|
||||
"false_accept_rate": 0.0833,
|
||||
"near_unknown_false_accept_rate": 0.0,
|
||||
"precision": 0.878,
|
||||
"max_sku_share": 0.2927,
|
||||
"hub_sku": "sku_l09",
|
||||
"hub_alarm": false,
|
||||
"histogram": {
|
||||
"sku_l09": 12,
|
||||
"sku_l11": 8,
|
||||
"sku_l03": 6,
|
||||
"sku_l01": 5,
|
||||
"sku_l07": 4,
|
||||
"sku_l02": 3,
|
||||
"sku_l05": 3
|
||||
},
|
||||
"per_sku": {
|
||||
"sku_l01": {
|
||||
"correct": 5,
|
||||
"total": 5
|
||||
},
|
||||
"sku_l02": {
|
||||
"correct": 2,
|
||||
"total": 4
|
||||
},
|
||||
"sku_l03": {
|
||||
"correct": 4,
|
||||
"total": 4
|
||||
},
|
||||
"sku_l04": {
|
||||
"correct": 0,
|
||||
"total": 1
|
||||
},
|
||||
"sku_l05": {
|
||||
"correct": 3,
|
||||
"total": 3
|
||||
},
|
||||
"sku_l06": {
|
||||
"correct": 0,
|
||||
"total": 4
|
||||
},
|
||||
"sku_l07": {
|
||||
"correct": 4,
|
||||
"total": 6
|
||||
},
|
||||
"sku_l08": {
|
||||
"correct": 0,
|
||||
"total": 1
|
||||
},
|
||||
"sku_l09": {
|
||||
"correct": 12,
|
||||
"total": 13
|
||||
},
|
||||
"sku_l10": {
|
||||
"correct": 0,
|
||||
"total": 11
|
||||
},
|
||||
"sku_l11": {
|
||||
"correct": 6,
|
||||
"total": 9
|
||||
}
|
||||
},
|
||||
"ms_per_crop": 26.6
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
2121
recognization/data/evaluations/2026-09-29-session/stream/bl_1.json
Normal file
2209
recognization/data/evaluations/2026-09-29-session/stream/bl_2.json
Normal file
2073
recognization/data/evaluations/2026-09-29-session/stream/bl_3.json
Normal file
2269
recognization/data/evaluations/2026-09-29-session/stream/fin_1.json
Normal file
2357
recognization/data/evaluations/2026-09-29-session/stream/fin_2.json
Normal file
2217
recognization/data/evaluations/2026-09-29-session/stream/fin_3.json
Normal file
2343
recognization/data/evaluations/2026-09-29-session/stream/fin_4.json
Normal file
1817
recognization/data/evaluations/2026-09-29-session/stream/full_1.json
Normal file
1220
recognization/data/evaluations/2026-09-29-session/stream/full_2.json
Normal file
1424
recognization/data/evaluations/2026-09-29-session/stream/full_3.json
Normal file
1697
recognization/data/evaluations/2026-09-29-session/stream/fx_1.json
Normal file
1388
recognization/data/evaluations/2026-09-29-session/stream/fx_2.json
Normal file
1298
recognization/data/evaluations/2026-09-29-session/stream/fx_3.json
Normal file
2455
recognization/data/evaluations/2026-09-29-session/stream/nl_1.json
Normal file
2383
recognization/data/evaluations/2026-09-29-session/stream/nl_2.json
Normal file
2479
recognization/data/evaluations/2026-09-29-session/stream/nl_3.json
Normal file
1707
recognization/data/evaluations/2026-09-29-session/stream/tr_1.json
Normal file
1352
recognization/data/evaluations/2026-09-29-session/stream/tr_2.json
Normal file
1334
recognization/data/evaluations/2026-09-29-session/stream/tr_3.json
Normal file
1376
recognization/data/evaluations/2026-09-29-session/stream/tr_a.json
Normal file
1791
recognization/data/evaluations/2026-09-29-session/stream/tr_b.json
Normal file
1358
recognization/data/evaluations/2026-09-29-session/stream/tr_c.json
Normal file
1382
recognization/data/evaluations/2026-09-29-session/stream/tr_d.json
Normal file
|
|
@ -0,0 +1,554 @@
|
|||
{
|
||||
"windows": [
|
||||
[
|
||||
0,
|
||||
640,
|
||||
1280,
|
||||
1360
|
||||
],
|
||||
[
|
||||
12,
|
||||
640,
|
||||
1292,
|
||||
1360
|
||||
],
|
||||
[
|
||||
24,
|
||||
640,
|
||||
1304,
|
||||
1360
|
||||
],
|
||||
[
|
||||
36,
|
||||
640,
|
||||
1316,
|
||||
1360
|
||||
],
|
||||
[
|
||||
48,
|
||||
640,
|
||||
1328,
|
||||
1360
|
||||
],
|
||||
[
|
||||
60,
|
||||
640,
|
||||
1340,
|
||||
1360
|
||||
],
|
||||
[
|
||||
72,
|
||||
640,
|
||||
1352,
|
||||
1360
|
||||
],
|
||||
[
|
||||
84,
|
||||
640,
|
||||
1364,
|
||||
1360
|
||||
],
|
||||
[
|
||||
96,
|
||||
640,
|
||||
1376,
|
||||
1360
|
||||
],
|
||||
[
|
||||
108,
|
||||
640,
|
||||
1388,
|
||||
1360
|
||||
],
|
||||
[
|
||||
120,
|
||||
640,
|
||||
1400,
|
||||
1360
|
||||
],
|
||||
[
|
||||
132,
|
||||
640,
|
||||
1412,
|
||||
1360
|
||||
],
|
||||
[
|
||||
144,
|
||||
640,
|
||||
1424,
|
||||
1360
|
||||
],
|
||||
[
|
||||
156,
|
||||
640,
|
||||
1436,
|
||||
1360
|
||||
],
|
||||
[
|
||||
168,
|
||||
640,
|
||||
1448,
|
||||
1360
|
||||
],
|
||||
[
|
||||
180,
|
||||
640,
|
||||
1460,
|
||||
1360
|
||||
],
|
||||
[
|
||||
192,
|
||||
640,
|
||||
1472,
|
||||
1360
|
||||
],
|
||||
[
|
||||
204,
|
||||
640,
|
||||
1484,
|
||||
1360
|
||||
],
|
||||
[
|
||||
216,
|
||||
640,
|
||||
1496,
|
||||
1360
|
||||
],
|
||||
[
|
||||
220,
|
||||
640,
|
||||
1500,
|
||||
1360
|
||||
],
|
||||
[
|
||||
216,
|
||||
640,
|
||||
1496,
|
||||
1360
|
||||
],
|
||||
[
|
||||
204,
|
||||
640,
|
||||
1484,
|
||||
1360
|
||||
],
|
||||
[
|
||||
192,
|
||||
640,
|
||||
1472,
|
||||
1360
|
||||
],
|
||||
[
|
||||
180,
|
||||
640,
|
||||
1460,
|
||||
1360
|
||||
],
|
||||
[
|
||||
168,
|
||||
640,
|
||||
1448,
|
||||
1360
|
||||
],
|
||||
[
|
||||
156,
|
||||
640,
|
||||
1436,
|
||||
1360
|
||||
],
|
||||
[
|
||||
144,
|
||||
640,
|
||||
1424,
|
||||
1360
|
||||
],
|
||||
[
|
||||
132,
|
||||
640,
|
||||
1412,
|
||||
1360
|
||||
],
|
||||
[
|
||||
120,
|
||||
640,
|
||||
1400,
|
||||
1360
|
||||
],
|
||||
[
|
||||
108,
|
||||
640,
|
||||
1388,
|
||||
1360
|
||||
],
|
||||
[
|
||||
96,
|
||||
640,
|
||||
1376,
|
||||
1360
|
||||
],
|
||||
[
|
||||
84,
|
||||
640,
|
||||
1364,
|
||||
1360
|
||||
],
|
||||
[
|
||||
72,
|
||||
640,
|
||||
1352,
|
||||
1360
|
||||
],
|
||||
[
|
||||
60,
|
||||
640,
|
||||
1340,
|
||||
1360
|
||||
],
|
||||
[
|
||||
48,
|
||||
640,
|
||||
1328,
|
||||
1360
|
||||
],
|
||||
[
|
||||
36,
|
||||
640,
|
||||
1316,
|
||||
1360
|
||||
],
|
||||
[
|
||||
24,
|
||||
640,
|
||||
1304,
|
||||
1360
|
||||
],
|
||||
[
|
||||
12,
|
||||
640,
|
||||
1292,
|
||||
1360
|
||||
],
|
||||
[
|
||||
0,
|
||||
640,
|
||||
1280,
|
||||
1360
|
||||
],
|
||||
[
|
||||
12,
|
||||
640,
|
||||
1292,
|
||||
1360
|
||||
],
|
||||
[
|
||||
24,
|
||||
640,
|
||||
1304,
|
||||
1360
|
||||
],
|
||||
[
|
||||
36,
|
||||
640,
|
||||
1316,
|
||||
1360
|
||||
],
|
||||
[
|
||||
48,
|
||||
640,
|
||||
1328,
|
||||
1360
|
||||
],
|
||||
[
|
||||
60,
|
||||
640,
|
||||
1340,
|
||||
1360
|
||||
],
|
||||
[
|
||||
72,
|
||||
640,
|
||||
1352,
|
||||
1360
|
||||
],
|
||||
[
|
||||
84,
|
||||
640,
|
||||
1364,
|
||||
1360
|
||||
],
|
||||
[
|
||||
96,
|
||||
640,
|
||||
1376,
|
||||
1360
|
||||
],
|
||||
[
|
||||
108,
|
||||
640,
|
||||
1388,
|
||||
1360
|
||||
],
|
||||
[
|
||||
120,
|
||||
640,
|
||||
1400,
|
||||
1360
|
||||
],
|
||||
[
|
||||
132,
|
||||
640,
|
||||
1412,
|
||||
1360
|
||||
],
|
||||
[
|
||||
144,
|
||||
640,
|
||||
1424,
|
||||
1360
|
||||
],
|
||||
[
|
||||
156,
|
||||
640,
|
||||
1436,
|
||||
1360
|
||||
],
|
||||
[
|
||||
168,
|
||||
640,
|
||||
1448,
|
||||
1360
|
||||
],
|
||||
[
|
||||
180,
|
||||
640,
|
||||
1460,
|
||||
1360
|
||||
],
|
||||
[
|
||||
192,
|
||||
640,
|
||||
1472,
|
||||
1360
|
||||
],
|
||||
[
|
||||
204,
|
||||
640,
|
||||
1484,
|
||||
1360
|
||||
],
|
||||
[
|
||||
216,
|
||||
640,
|
||||
1496,
|
||||
1360
|
||||
],
|
||||
[
|
||||
220,
|
||||
640,
|
||||
1500,
|
||||
1360
|
||||
],
|
||||
[
|
||||
216,
|
||||
640,
|
||||
1496,
|
||||
1360
|
||||
],
|
||||
[
|
||||
204,
|
||||
640,
|
||||
1484,
|
||||
1360
|
||||
],
|
||||
[
|
||||
192,
|
||||
640,
|
||||
1472,
|
||||
1360
|
||||
],
|
||||
[
|
||||
180,
|
||||
640,
|
||||
1460,
|
||||
1360
|
||||
],
|
||||
[
|
||||
168,
|
||||
640,
|
||||
1448,
|
||||
1360
|
||||
],
|
||||
[
|
||||
156,
|
||||
640,
|
||||
1436,
|
||||
1360
|
||||
],
|
||||
[
|
||||
144,
|
||||
640,
|
||||
1424,
|
||||
1360
|
||||
],
|
||||
[
|
||||
132,
|
||||
640,
|
||||
1412,
|
||||
1360
|
||||
],
|
||||
[
|
||||
120,
|
||||
640,
|
||||
1400,
|
||||
1360
|
||||
],
|
||||
[
|
||||
108,
|
||||
640,
|
||||
1388,
|
||||
1360
|
||||
],
|
||||
[
|
||||
96,
|
||||
640,
|
||||
1376,
|
||||
1360
|
||||
],
|
||||
[
|
||||
84,
|
||||
640,
|
||||
1364,
|
||||
1360
|
||||
],
|
||||
[
|
||||
72,
|
||||
640,
|
||||
1352,
|
||||
1360
|
||||
],
|
||||
[
|
||||
60,
|
||||
640,
|
||||
1340,
|
||||
1360
|
||||
],
|
||||
[
|
||||
48,
|
||||
640,
|
||||
1328,
|
||||
1360
|
||||
],
|
||||
[
|
||||
36,
|
||||
640,
|
||||
1316,
|
||||
1360
|
||||
],
|
||||
[
|
||||
24,
|
||||
640,
|
||||
1304,
|
||||
1360
|
||||
],
|
||||
[
|
||||
12,
|
||||
640,
|
||||
1292,
|
||||
1360
|
||||
],
|
||||
[
|
||||
0,
|
||||
640,
|
||||
1280,
|
||||
1360
|
||||
],
|
||||
[
|
||||
12,
|
||||
640,
|
||||
1292,
|
||||
1360
|
||||
],
|
||||
[
|
||||
24,
|
||||
640,
|
||||
1304,
|
||||
1360
|
||||
],
|
||||
[
|
||||
36,
|
||||
640,
|
||||
1316,
|
||||
1360
|
||||
],
|
||||
[
|
||||
48,
|
||||
640,
|
||||
1328,
|
||||
1360
|
||||
],
|
||||
[
|
||||
60,
|
||||
640,
|
||||
1340,
|
||||
1360
|
||||
],
|
||||
[
|
||||
72,
|
||||
640,
|
||||
1352,
|
||||
1360
|
||||
],
|
||||
[
|
||||
84,
|
||||
640,
|
||||
1364,
|
||||
1360
|
||||
],
|
||||
[
|
||||
96,
|
||||
640,
|
||||
1376,
|
||||
1360
|
||||
],
|
||||
[
|
||||
108,
|
||||
640,
|
||||
1388,
|
||||
1360
|
||||
],
|
||||
[
|
||||
120,
|
||||
640,
|
||||
1400,
|
||||
1360
|
||||
],
|
||||
[
|
||||
132,
|
||||
640,
|
||||
1412,
|
||||
1360
|
||||
],
|
||||
[
|
||||
144,
|
||||
640,
|
||||
1424,
|
||||
1360
|
||||
],
|
||||
[
|
||||
156,
|
||||
640,
|
||||
1436,
|
||||
1360
|
||||
],
|
||||
[
|
||||
168,
|
||||
640,
|
||||
1448,
|
||||
1360
|
||||
]
|
||||
],
|
||||
"source": "add/image6.jpg",
|
||||
"pace": "send",
|
||||
"seconds": 8.1,
|
||||
"tracks": []
|
||||
}
|
||||
1883
recognization/data/evaluations/2026-09-29-session/stream/tracks.json
Normal file
|
|
@ -0,0 +1,721 @@
|
|||
{
|
||||
"source": "add/image6.jpg",
|
||||
"pace": "result",
|
||||
"seconds": 12.0,
|
||||
"tracks": [
|
||||
{
|
||||
"track_id": 1,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
580,
|
||||
122,
|
||||
666,
|
||||
332
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 108,
|
||||
"inherited": true,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 2,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
659,
|
||||
760,
|
||||
746,
|
||||
972
|
||||
],
|
||||
"label_frame_id": 71,
|
||||
"result_frame_id": 81,
|
||||
"inherited": true,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 4,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
586,
|
||||
119,
|
||||
674,
|
||||
333
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 115,
|
||||
"inherited": true,
|
||||
"restarts": 5
|
||||
},
|
||||
{
|
||||
"track_id": 5,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
512,
|
||||
755,
|
||||
604,
|
||||
972
|
||||
],
|
||||
"label_frame_id": 98,
|
||||
"result_frame_id": 114,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 6,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
937,
|
||||
764,
|
||||
1036,
|
||||
975
|
||||
],
|
||||
"label_frame_id": 16,
|
||||
"result_frame_id": 25,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 8,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
291,
|
||||
754,
|
||||
389,
|
||||
971
|
||||
],
|
||||
"label_frame_id": 107,
|
||||
"result_frame_id": 114,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 9,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
459,
|
||||
1068,
|
||||
555,
|
||||
1251
|
||||
],
|
||||
"label_frame_id": 67,
|
||||
"result_frame_id": 89,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 11,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
688,
|
||||
1057,
|
||||
789,
|
||||
1142
|
||||
],
|
||||
"label_frame_id": 50,
|
||||
"result_frame_id": 59,
|
||||
"inherited": false,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 12,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1254,
|
||||
1053,
|
||||
1334,
|
||||
1244
|
||||
],
|
||||
"label_frame_id": 61,
|
||||
"result_frame_id": 69,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 13,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1079,
|
||||
762,
|
||||
1176,
|
||||
973
|
||||
],
|
||||
"label_frame_id": 89,
|
||||
"result_frame_id": 115,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 14,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1277,
|
||||
1065,
|
||||
1345,
|
||||
1239
|
||||
],
|
||||
"label_frame_id": 52,
|
||||
"result_frame_id": 63,
|
||||
"inherited": true,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 15,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
798,
|
||||
765,
|
||||
966,
|
||||
971
|
||||
],
|
||||
"label_frame_id": 83,
|
||||
"result_frame_id": 114,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 16,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
430,
|
||||
1070,
|
||||
543,
|
||||
1250
|
||||
],
|
||||
"label_frame_id": 99,
|
||||
"result_frame_id": 113,
|
||||
"inherited": false,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 17,
|
||||
"sku_id": "sku_l10",
|
||||
"label_box": [
|
||||
878,
|
||||
1146,
|
||||
970,
|
||||
1245
|
||||
],
|
||||
"label_frame_id": 90,
|
||||
"result_frame_id": 106,
|
||||
"inherited": false,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 18,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1128,
|
||||
763,
|
||||
1271,
|
||||
973
|
||||
],
|
||||
"label_frame_id": 99,
|
||||
"result_frame_id": 112,
|
||||
"inherited": false,
|
||||
"restarts": 10
|
||||
},
|
||||
{
|
||||
"track_id": 19,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
680,
|
||||
1058,
|
||||
780,
|
||||
1142
|
||||
],
|
||||
"label_frame_id": 97,
|
||||
"result_frame_id": 110,
|
||||
"inherited": false,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 21,
|
||||
"sku_id": "sku_l10",
|
||||
"label_box": [
|
||||
673,
|
||||
1146,
|
||||
773,
|
||||
1247
|
||||
],
|
||||
"label_frame_id": 98,
|
||||
"result_frame_id": 111,
|
||||
"inherited": false,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 22,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
287,
|
||||
775,
|
||||
347,
|
||||
969
|
||||
],
|
||||
"label_frame_id": 2,
|
||||
"result_frame_id": 18,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 23,
|
||||
"sku_id": "sku_l10",
|
||||
"label_box": [
|
||||
917,
|
||||
1146,
|
||||
999,
|
||||
1244
|
||||
],
|
||||
"label_frame_id": 98,
|
||||
"result_frame_id": 112,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 25,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
488,
|
||||
1078,
|
||||
570,
|
||||
1248
|
||||
],
|
||||
"label_frame_id": 33,
|
||||
"result_frame_id": 53,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 26,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
198,
|
||||
755,
|
||||
258,
|
||||
972
|
||||
],
|
||||
"label_frame_id": 2,
|
||||
"result_frame_id": 15,
|
||||
"inherited": false,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 27,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
484,
|
||||
1062,
|
||||
572,
|
||||
1248
|
||||
],
|
||||
"label_frame_id": 89,
|
||||
"result_frame_id": 97,
|
||||
"inherited": true,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 29,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
879,
|
||||
1055,
|
||||
972,
|
||||
1141
|
||||
],
|
||||
"label_frame_id": 57,
|
||||
"result_frame_id": 70,
|
||||
"inherited": true,
|
||||
"restarts": 4
|
||||
},
|
||||
{
|
||||
"track_id": 32,
|
||||
"sku_id": "sku_l10",
|
||||
"label_box": [
|
||||
67,
|
||||
422,
|
||||
248,
|
||||
613
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 47,
|
||||
"inherited": true,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 35,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1117,
|
||||
763,
|
||||
1179,
|
||||
973
|
||||
],
|
||||
"label_frame_id": 85,
|
||||
"result_frame_id": 113,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 58,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1328,
|
||||
1065,
|
||||
1379,
|
||||
1238
|
||||
],
|
||||
"label_frame_id": 20,
|
||||
"result_frame_id": 23,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 59,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
951,
|
||||
124,
|
||||
1090,
|
||||
218
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 104,
|
||||
"inherited": true,
|
||||
"restarts": 5
|
||||
},
|
||||
{
|
||||
"track_id": 114,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1191,
|
||||
1054,
|
||||
1271,
|
||||
1245
|
||||
],
|
||||
"label_frame_id": 99,
|
||||
"result_frame_id": 111,
|
||||
"inherited": false,
|
||||
"restarts": 3
|
||||
},
|
||||
{
|
||||
"track_id": 127,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
243,
|
||||
755,
|
||||
298,
|
||||
972
|
||||
],
|
||||
"label_frame_id": 35,
|
||||
"result_frame_id": 40,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 135,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
521,
|
||||
118,
|
||||
608,
|
||||
330
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 115,
|
||||
"inherited": true,
|
||||
"restarts": 4
|
||||
},
|
||||
{
|
||||
"track_id": 136,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1072,
|
||||
1054,
|
||||
1147,
|
||||
1245
|
||||
],
|
||||
"label_frame_id": 53,
|
||||
"result_frame_id": 59,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 163,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
125,
|
||||
752,
|
||||
191,
|
||||
971
|
||||
],
|
||||
"label_frame_id": 32,
|
||||
"result_frame_id": 48,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 178,
|
||||
"sku_id": "sku_l10",
|
||||
"label_box": [
|
||||
156,
|
||||
419,
|
||||
307,
|
||||
616
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 41,
|
||||
"inherited": true,
|
||||
"restarts": 5
|
||||
},
|
||||
{
|
||||
"track_id": 262,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
79,
|
||||
113,
|
||||
152,
|
||||
331
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 74,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 269,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
197,
|
||||
755,
|
||||
257,
|
||||
973
|
||||
],
|
||||
"label_frame_id": 48,
|
||||
"result_frame_id": 57,
|
||||
"inherited": false,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 295,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
84,
|
||||
1059,
|
||||
179,
|
||||
1148
|
||||
],
|
||||
"label_frame_id": 78,
|
||||
"result_frame_id": 90,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 302,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
941,
|
||||
410,
|
||||
1009,
|
||||
605
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 50,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 315,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
764,
|
||||
414,
|
||||
848,
|
||||
505
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 51,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 331,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
399,
|
||||
1057,
|
||||
532,
|
||||
1255
|
||||
],
|
||||
"label_frame_id": 105,
|
||||
"result_frame_id": 115,
|
||||
"inherited": true,
|
||||
"restarts": 9
|
||||
},
|
||||
{
|
||||
"track_id": 345,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
763,
|
||||
415,
|
||||
854,
|
||||
499
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 115,
|
||||
"inherited": true,
|
||||
"restarts": 3
|
||||
},
|
||||
{
|
||||
"track_id": 353,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1119,
|
||||
1053,
|
||||
1180,
|
||||
1245
|
||||
],
|
||||
"label_frame_id": 99,
|
||||
"result_frame_id": 113,
|
||||
"inherited": false,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 370,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
83,
|
||||
117,
|
||||
129,
|
||||
333
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 69,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 378,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
576,
|
||||
758,
|
||||
667,
|
||||
971
|
||||
],
|
||||
"label_frame_id": 106,
|
||||
"result_frame_id": 114,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 404,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
68,
|
||||
753,
|
||||
131,
|
||||
971
|
||||
],
|
||||
"label_frame_id": 78,
|
||||
"result_frame_id": 85,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 416,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1089,
|
||||
416,
|
||||
1147,
|
||||
604
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 83,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 499,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
443,
|
||||
767,
|
||||
510,
|
||||
968
|
||||
],
|
||||
"label_frame_id": 95,
|
||||
"result_frame_id": 97,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 507,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
149,
|
||||
757,
|
||||
208,
|
||||
971
|
||||
],
|
||||
"label_frame_id": 84,
|
||||
"result_frame_id": 90,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 552,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
757,
|
||||
413,
|
||||
849,
|
||||
507
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 105,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 582,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
701,
|
||||
413,
|
||||
842,
|
||||
505
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 102,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 613,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
477,
|
||||
767,
|
||||
537,
|
||||
969
|
||||
],
|
||||
"label_frame_id": 104,
|
||||
"result_frame_id": 108,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 673,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1087,
|
||||
1051,
|
||||
1143,
|
||||
1245
|
||||
],
|
||||
"label_frame_id": 108,
|
||||
"result_frame_id": 113,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,791 @@
|
|||
{
|
||||
"source": "add/image6.jpg",
|
||||
"pace": "send",
|
||||
"seconds": 12.0,
|
||||
"tracks": [
|
||||
{
|
||||
"track_id": 1,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
527,
|
||||
122,
|
||||
613,
|
||||
331
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 104,
|
||||
"inherited": true,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 2,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
696,
|
||||
760,
|
||||
783,
|
||||
972
|
||||
],
|
||||
"label_frame_id": 70,
|
||||
"result_frame_id": 81,
|
||||
"inherited": true,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 4,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
494,
|
||||
120,
|
||||
582,
|
||||
332
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 108,
|
||||
"inherited": true,
|
||||
"restarts": 5
|
||||
},
|
||||
{
|
||||
"track_id": 5,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
390,
|
||||
757,
|
||||
482,
|
||||
971
|
||||
],
|
||||
"label_frame_id": 106,
|
||||
"result_frame_id": 112,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 6,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
949,
|
||||
763,
|
||||
1049,
|
||||
973
|
||||
],
|
||||
"label_frame_id": 16,
|
||||
"result_frame_id": 25,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 8,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
399,
|
||||
754,
|
||||
497,
|
||||
971
|
||||
],
|
||||
"label_frame_id": 98,
|
||||
"result_frame_id": 112,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 9,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
441,
|
||||
1071,
|
||||
536,
|
||||
1250
|
||||
],
|
||||
"label_frame_id": 74,
|
||||
"result_frame_id": 89,
|
||||
"inherited": false,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 12,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1194,
|
||||
1053,
|
||||
1274,
|
||||
1244
|
||||
],
|
||||
"label_frame_id": 50,
|
||||
"result_frame_id": 69,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 13,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1127,
|
||||
763,
|
||||
1224,
|
||||
973
|
||||
],
|
||||
"label_frame_id": 97,
|
||||
"result_frame_id": 112,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 14,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1277,
|
||||
1065,
|
||||
1345,
|
||||
1239
|
||||
],
|
||||
"label_frame_id": 52,
|
||||
"result_frame_id": 63,
|
||||
"inherited": true,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 15,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
805,
|
||||
764,
|
||||
976,
|
||||
972
|
||||
],
|
||||
"label_frame_id": 89,
|
||||
"result_frame_id": 109,
|
||||
"inherited": false,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 16,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
583,
|
||||
1069,
|
||||
683,
|
||||
1251
|
||||
],
|
||||
"label_frame_id": 99,
|
||||
"result_frame_id": 112,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 17,
|
||||
"sku_id": "sku_l10",
|
||||
"label_box": [
|
||||
867,
|
||||
1147,
|
||||
958,
|
||||
1245
|
||||
],
|
||||
"label_frame_id": 98,
|
||||
"result_frame_id": 109,
|
||||
"inherited": false,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 18,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
926,
|
||||
123,
|
||||
1068,
|
||||
332
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 102,
|
||||
"inherited": true,
|
||||
"restarts": 9
|
||||
},
|
||||
{
|
||||
"track_id": 19,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
694,
|
||||
1057,
|
||||
794,
|
||||
1142
|
||||
],
|
||||
"label_frame_id": 108,
|
||||
"result_frame_id": 112,
|
||||
"inherited": true,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 20,
|
||||
"sku_id": "sku_l10",
|
||||
"label_box": [
|
||||
211,
|
||||
1128,
|
||||
285,
|
||||
1260
|
||||
],
|
||||
"label_frame_id": 6,
|
||||
"result_frame_id": 17,
|
||||
"inherited": false,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 21,
|
||||
"sku_id": "sku_l10",
|
||||
"label_box": [
|
||||
848,
|
||||
1145,
|
||||
948,
|
||||
1246
|
||||
],
|
||||
"label_frame_id": 98,
|
||||
"result_frame_id": 112,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 22,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
287,
|
||||
775,
|
||||
347,
|
||||
969
|
||||
],
|
||||
"label_frame_id": 2,
|
||||
"result_frame_id": 18,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 23,
|
||||
"sku_id": "sku_l10",
|
||||
"label_box": [
|
||||
869,
|
||||
1144,
|
||||
952,
|
||||
1244
|
||||
],
|
||||
"label_frame_id": 104,
|
||||
"result_frame_id": 112,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 25,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
488,
|
||||
1078,
|
||||
570,
|
||||
1248
|
||||
],
|
||||
"label_frame_id": 33,
|
||||
"result_frame_id": 53,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 26,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
198,
|
||||
755,
|
||||
258,
|
||||
972
|
||||
],
|
||||
"label_frame_id": 2,
|
||||
"result_frame_id": 14,
|
||||
"inherited": false,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 27,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
522,
|
||||
1067,
|
||||
610,
|
||||
1248
|
||||
],
|
||||
"label_frame_id": 87,
|
||||
"result_frame_id": 92,
|
||||
"inherited": true,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 29,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
947,
|
||||
1055,
|
||||
1038,
|
||||
1139
|
||||
],
|
||||
"label_frame_id": 58,
|
||||
"result_frame_id": 73,
|
||||
"inherited": true,
|
||||
"restarts": 4
|
||||
},
|
||||
{
|
||||
"track_id": 32,
|
||||
"sku_id": "sku_l10",
|
||||
"label_box": [
|
||||
67,
|
||||
422,
|
||||
248,
|
||||
613
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 47,
|
||||
"inherited": true,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 35,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1057,
|
||||
764,
|
||||
1118,
|
||||
972
|
||||
],
|
||||
"label_frame_id": 75,
|
||||
"result_frame_id": 112,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 58,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1264,
|
||||
1065,
|
||||
1315,
|
||||
1238
|
||||
],
|
||||
"label_frame_id": 14,
|
||||
"result_frame_id": 23,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 59,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1092,
|
||||
126,
|
||||
1229,
|
||||
218
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 112,
|
||||
"inherited": true,
|
||||
"restarts": 5
|
||||
},
|
||||
{
|
||||
"track_id": 114,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1191,
|
||||
1054,
|
||||
1271,
|
||||
1245
|
||||
],
|
||||
"label_frame_id": 99,
|
||||
"result_frame_id": 108,
|
||||
"inherited": false,
|
||||
"restarts": 3
|
||||
},
|
||||
{
|
||||
"track_id": 127,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
354,
|
||||
756,
|
||||
409,
|
||||
973
|
||||
],
|
||||
"label_frame_id": 26,
|
||||
"result_frame_id": 40,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 135,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
658,
|
||||
760,
|
||||
744,
|
||||
972
|
||||
],
|
||||
"label_frame_id": 92,
|
||||
"result_frame_id": 104,
|
||||
"inherited": true,
|
||||
"restarts": 3
|
||||
},
|
||||
{
|
||||
"track_id": 136,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1076,
|
||||
1053,
|
||||
1152,
|
||||
1245
|
||||
],
|
||||
"label_frame_id": 54,
|
||||
"result_frame_id": 59,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 163,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
111,
|
||||
754,
|
||||
177,
|
||||
970
|
||||
],
|
||||
"label_frame_id": 32,
|
||||
"result_frame_id": 50,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 178,
|
||||
"sku_id": "sku_l10",
|
||||
"label_box": [
|
||||
156,
|
||||
419,
|
||||
307,
|
||||
616
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 41,
|
||||
"inherited": true,
|
||||
"restarts": 5
|
||||
},
|
||||
{
|
||||
"track_id": 262,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
449,
|
||||
763,
|
||||
522,
|
||||
968
|
||||
],
|
||||
"label_frame_id": 44,
|
||||
"result_frame_id": 55,
|
||||
"inherited": false,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 269,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
113,
|
||||
758,
|
||||
172,
|
||||
972
|
||||
],
|
||||
"label_frame_id": 46,
|
||||
"result_frame_id": 55,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 295,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
84,
|
||||
1059,
|
||||
179,
|
||||
1148
|
||||
],
|
||||
"label_frame_id": 78,
|
||||
"result_frame_id": 90,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 302,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
929,
|
||||
414,
|
||||
995,
|
||||
603
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 51,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 315,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
757,
|
||||
413,
|
||||
849,
|
||||
507
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 67,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 331,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
422,
|
||||
1057,
|
||||
555,
|
||||
1255
|
||||
],
|
||||
"label_frame_id": 100,
|
||||
"result_frame_id": 112,
|
||||
"inherited": true,
|
||||
"restarts": 9
|
||||
},
|
||||
{
|
||||
"track_id": 345,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
835,
|
||||
1055,
|
||||
927,
|
||||
1140
|
||||
],
|
||||
"label_frame_id": 107,
|
||||
"result_frame_id": 111,
|
||||
"inherited": true,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 353,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1119,
|
||||
1053,
|
||||
1180,
|
||||
1245
|
||||
],
|
||||
"label_frame_id": 99,
|
||||
"result_frame_id": 111,
|
||||
"inherited": false,
|
||||
"restarts": 2
|
||||
},
|
||||
{
|
||||
"track_id": 370,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
48,
|
||||
118,
|
||||
91,
|
||||
331
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 66,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 378,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
696,
|
||||
758,
|
||||
787,
|
||||
971
|
||||
],
|
||||
"label_frame_id": 97,
|
||||
"result_frame_id": 112,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 404,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
45,
|
||||
112,
|
||||
83,
|
||||
331
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 71,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 416,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1089,
|
||||
416,
|
||||
1147,
|
||||
604
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 83,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 476,
|
||||
"sku_id": "sku_l10",
|
||||
"label_box": [
|
||||
63,
|
||||
519,
|
||||
138,
|
||||
614
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 80,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 499,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
79,
|
||||
113,
|
||||
152,
|
||||
331
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 112,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 507,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
128,
|
||||
114,
|
||||
187,
|
||||
331
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 85,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 536,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
619,
|
||||
1075,
|
||||
689,
|
||||
1248
|
||||
],
|
||||
"label_frame_id": 100,
|
||||
"result_frame_id": 108,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 552,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
757,
|
||||
413,
|
||||
849,
|
||||
507
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 105,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 568,
|
||||
"sku_id": "sku_l10",
|
||||
"label_box": [
|
||||
45,
|
||||
512,
|
||||
143,
|
||||
619
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 90,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 582,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
684,
|
||||
413,
|
||||
826,
|
||||
506
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 102,
|
||||
"inherited": true,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 606,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
204,
|
||||
758,
|
||||
247,
|
||||
971
|
||||
],
|
||||
"label_frame_id": 102,
|
||||
"result_frame_id": 112,
|
||||
"inherited": false,
|
||||
"restarts": 1
|
||||
},
|
||||
{
|
||||
"track_id": 613,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
453,
|
||||
773,
|
||||
510,
|
||||
969
|
||||
],
|
||||
"label_frame_id": 100,
|
||||
"result_frame_id": 111,
|
||||
"inherited": false,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 653,
|
||||
"sku_id": "sku_l10",
|
||||
"label_box": [
|
||||
60,
|
||||
510,
|
||||
163,
|
||||
622
|
||||
],
|
||||
"label_frame_id": 0,
|
||||
"result_frame_id": 104,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
},
|
||||
{
|
||||
"track_id": 673,
|
||||
"sku_id": "sku_l09",
|
||||
"label_box": [
|
||||
1087,
|
||||
1051,
|
||||
1143,
|
||||
1245
|
||||
],
|
||||
"label_frame_id": 108,
|
||||
"result_frame_id": 112,
|
||||
"inherited": true,
|
||||
"restarts": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
60
recognization/data/external-eval/README.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# External evidence (2026-09-29)
|
||||
|
||||
Independent-source media used to stress the identity pipeline. **None of this is
|
||||
Tier 2.** DECISION.md's Tier 2 is a second labelled store visit; these are
|
||||
`external_proxy` artifacts, and every manifest here says so.
|
||||
|
||||
| Directory | What it holds |
|
||||
|---|---|
|
||||
| `other-mayo/` | 8 accepted images for `sku_l04`, `sku_l07`, `sku_l09` from 6 source domains, plus the scored result |
|
||||
| `l12-reconciliation/` | What `sku_l12` actually is, from the shelf tag and package pixels |
|
||||
| `kewpie/` | 12 accepted images for six SKUs from 6 source domains, plus the scored result. One is now enrolled, so it is excluded from scoring. |
|
||||
| `tier3-sources/` | Two reusable pan clips and 11 rejected candidates for a future Tier-3 replay |
|
||||
|
||||
## Leak checks
|
||||
|
||||
Two, both in `code/eval_external.py`, both run before any number is reported:
|
||||
|
||||
- **Against the evaluation photo.** Every image is template-matched against
|
||||
`add/image6.jpg`. The highest score across all 20 is 0.675 (`sku_l07_1`), and
|
||||
the 0.95+ band that identifies a crop of the evaluation photo is empty.
|
||||
- **Against the catalog.** An image that has since been enrolled is refused
|
||||
rather than scored. `kewpie/sku_l06_1.jpg` is one: it is now the enrolled l06
|
||||
view, so scoring it would be training accuracy wearing a held-out label.
|
||||
|
||||
## Result: 19 scored, 0 false accepts
|
||||
|
||||
.venv/bin/python code/eval_external.py --stock stock-lopia-clean \
|
||||
data/external-eval/kewpie data/external-eval/other-mayo
|
||||
|
||||
| Set | Scored | Correct | Abstain | False accepts | Leaked |
|
||||
|---|---|---|---|---|---|
|
||||
| `kewpie/` | 11 | 8 | 3 | **0** | 1 |
|
||||
| `other-mayo/` | 8 | 4 | 4 | **0** | 0 |
|
||||
| **Total** | **19** | **12** | **7** | **0** | **1** |
|
||||
|
||||
Two things this establishes that the image6 shelf cannot:
|
||||
|
||||
- **`sku_l07` is answered with no enrolled image.** Both l07 images accept
|
||||
through the text lane alone: `sku_l07_1` reads ラッキー フヨネーズ, and
|
||||
`sku_l07_2` reads the rear label's 製造者:丸和油脂株式会社 with 内容量:200g.
|
||||
The catalog row is 丸和油脂 ラッキーマヨネーズ 200g, so the manufacturer and
|
||||
the size together name it. This is the text lane's coverage claim - that a
|
||||
product with no reference image can still be named - holding on data that was
|
||||
not used to build it.
|
||||
- **The l04 abstentions are correct behaviour, not a miss.** l04 has no
|
||||
enrolled image either, and its reads are weak: `sku_l04_2` reads only
|
||||
コと旨 タ 味 (all rows score 0.0), and `sku_l04_4` reads the rear label with
|
||||
やまや garbled to まや, scoring 0.379 - below the accept line. Abstaining on
|
||||
a weak read is the designed outcome.
|
||||
|
||||
`sku_l07_2` accepts with a negative local score (-0.55) on text evidence alone.
|
||||
That is the lane doing its job, but it is worth naming: the accept rests on two
|
||||
matched tokens (manufacturer + size), not on geometry.
|
||||
|
||||
## What is still missing
|
||||
|
||||
No external source gives per-SKU identity for the whole catalog, and no
|
||||
reusable clip combines the target category with shelf-distance text
|
||||
resolution. The Tier-2 and Tier-3 gates in DECISION.md still need the
|
||||
operator's own recorded walks.
|
||||
150
recognization/data/external-eval/kewpie/eval_result.json
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
{
|
||||
"dataset": "kewpie",
|
||||
"catalog": "stock-lopia-clean",
|
||||
"note": "External proxy images, not Tier 2. Scored through the shipped LocalLaneAdapter + DecisionPolicy.",
|
||||
"rows": [
|
||||
{
|
||||
"file": "sku_l01_1.jpg",
|
||||
"truth": "sku_l01",
|
||||
"best": "sku_l01",
|
||||
"confidence": 13.1618,
|
||||
"status": "success",
|
||||
"source": "local",
|
||||
"verdict": "correct"
|
||||
},
|
||||
{
|
||||
"file": "sku_l01_2.jpg",
|
||||
"truth": "sku_l01",
|
||||
"best": "sku_l01",
|
||||
"confidence": 20.986,
|
||||
"status": "success",
|
||||
"source": "local",
|
||||
"verdict": "correct"
|
||||
},
|
||||
{
|
||||
"file": "sku_l02_1.jpg",
|
||||
"truth": "sku_l02",
|
||||
"best": "sku_l02",
|
||||
"confidence": 10.4288,
|
||||
"status": "success",
|
||||
"source": "text",
|
||||
"verdict": "correct"
|
||||
},
|
||||
{
|
||||
"file": "sku_l02_2.jpg",
|
||||
"truth": "sku_l02",
|
||||
"best": "sku_l02",
|
||||
"confidence": 7.542,
|
||||
"status": "success",
|
||||
"source": "local",
|
||||
"verdict": "correct"
|
||||
},
|
||||
{
|
||||
"file": "sku_l03_1.jpg",
|
||||
"truth": "sku_l03",
|
||||
"best": "sku_l03",
|
||||
"confidence": 14.3581,
|
||||
"status": "success",
|
||||
"source": "local",
|
||||
"verdict": "correct"
|
||||
},
|
||||
{
|
||||
"file": "sku_l03_2.jpg",
|
||||
"truth": "sku_l03",
|
||||
"best": "sku_l03",
|
||||
"confidence": 48.9767,
|
||||
"status": "success",
|
||||
"source": "local",
|
||||
"verdict": "correct"
|
||||
},
|
||||
{
|
||||
"file": "sku_l05_1.jpg",
|
||||
"truth": "sku_l05",
|
||||
"best": null,
|
||||
"confidence": 0.0,
|
||||
"status": "unknown",
|
||||
"source": null,
|
||||
"verdict": "abstain"
|
||||
},
|
||||
{
|
||||
"file": "sku_l05_2.jpg",
|
||||
"truth": "sku_l05",
|
||||
"best": "sku_l05",
|
||||
"confidence": -3.9674,
|
||||
"status": "success",
|
||||
"source": "text",
|
||||
"verdict": "correct"
|
||||
},
|
||||
{
|
||||
"file": "sku_l06_1.jpg",
|
||||
"truth": "sku_l06",
|
||||
"verdict": "leaked",
|
||||
"leaked_into": "sku_l06",
|
||||
"note": "enrolled in the catalog; scoring it would be training accuracy"
|
||||
},
|
||||
{
|
||||
"file": "sku_l06_2.jpg",
|
||||
"truth": "sku_l06",
|
||||
"best": "sku_l06",
|
||||
"confidence": 294.8985,
|
||||
"status": "success",
|
||||
"source": "local",
|
||||
"verdict": "correct"
|
||||
},
|
||||
{
|
||||
"file": "sku_l11_1.jpg",
|
||||
"truth": "sku_l11",
|
||||
"best": null,
|
||||
"confidence": -29.9175,
|
||||
"status": "unknown",
|
||||
"source": null,
|
||||
"verdict": "abstain"
|
||||
},
|
||||
{
|
||||
"file": "sku_l11_2.jpg",
|
||||
"truth": "sku_l11",
|
||||
"best": null,
|
||||
"confidence": 14.1552,
|
||||
"status": "ambiguous",
|
||||
"source": null,
|
||||
"verdict": "abstain"
|
||||
}
|
||||
],
|
||||
"scored": 11,
|
||||
"leaked": 1,
|
||||
"by_sku": {
|
||||
"sku_l01": {
|
||||
"correct": 2,
|
||||
"abstain": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"sku_l02": {
|
||||
"correct": 2,
|
||||
"abstain": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"sku_l03": {
|
||||
"correct": 2,
|
||||
"abstain": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"sku_l05": {
|
||||
"correct": 1,
|
||||
"abstain": 1,
|
||||
"wrong": 0
|
||||
},
|
||||
"sku_l06": {
|
||||
"correct": 1,
|
||||
"abstain": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"sku_l11": {
|
||||
"correct": 0,
|
||||
"abstain": 2,
|
||||
"wrong": 0
|
||||
}
|
||||
},
|
||||
"correct": 8,
|
||||
"abstain": 3,
|
||||
"wrong": 0
|
||||
}
|
||||
191
recognization/data/external-eval/kewpie/manifest.json
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
{
|
||||
"dataset_type": "external_proxy",
|
||||
"overall_note": "Independent-source Internet media for recognition stress testing. This is an external_proxy set: it is not held-out store-visit Tier 2 data and does not satisfy the DECISION.md Tier-2 capture protocol. Every accepted file was visually inspected for the exact Kewpie product and package size. Official Kewpie front packshots already enrolled in stock-lopia-clean were excluded. The two sku_l06 files are current-package retailer images showing 内臓脂肪を減らす キユーピーフィッテ; customer-listing photos showing the superseded 内臓脂肪ケア wrapper were inspected and rejected.",
|
||||
"license_note": "All accepted files come from commercial retailer or customer marketplace pages without an explicit reuse grant. They are marked restricted-research for internal evaluation only and should not be redistributed.",
|
||||
"coverage": {
|
||||
"accepted_images": 12,
|
||||
"accepted_skus": [
|
||||
"sku_l01",
|
||||
"sku_l02",
|
||||
"sku_l03",
|
||||
"sku_l05",
|
||||
"sku_l06",
|
||||
"sku_l11"
|
||||
],
|
||||
"two_images_per_sku": [
|
||||
"sku_l01",
|
||||
"sku_l02",
|
||||
"sku_l03",
|
||||
"sku_l05",
|
||||
"sku_l06",
|
||||
"sku_l11"
|
||||
],
|
||||
"two_source_domains_per_sku": [
|
||||
"sku_l01",
|
||||
"sku_l02",
|
||||
"sku_l03",
|
||||
"sku_l05",
|
||||
"sku_l06",
|
||||
"sku_l11"
|
||||
],
|
||||
"not_found": []
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"file": "sku_l01_1.jpg",
|
||||
"sku": "sku_l01",
|
||||
"product": "キユーピー エッグケア 305g",
|
||||
"source_page": "https://paypayfleamarket.yahoo.co.jp/item/z686464976",
|
||||
"source_title": "キユーピー エッグケア 卵を使っていないマヨネーズタイプ 305g",
|
||||
"media_url": "https://auctions.c.yimg.jp/images.auctions.yahoo.co.jp/image/dr000/auc0209/users/ad8016fc0a2ac99427f4bd970e286c7709594046/i-img1200x1200-1789785070735v2rkwx.jpg",
|
||||
"width": 1200,
|
||||
"height": 1200,
|
||||
"view_type": "customer_front",
|
||||
"license_status": "restricted-research",
|
||||
"notes": "Customer-taken marketplace photo on a household surface. The front package visibly reads エッグケア and 305g; exact title corroborates the variant. Not the official Kewpie enrollment packshot. Source domain: paypayfleamarket.yahoo.co.jp; media host: auctions.c.yimg.jp."
|
||||
},
|
||||
{
|
||||
"file": "sku_l01_2.jpg",
|
||||
"sku": "sku_l01",
|
||||
"product": "キユーピー エッグケア 305g",
|
||||
"source_page": "https://www.amazon.co.jp/dp/B0BSVY8NNH",
|
||||
"source_title": "キユーピー エッグケア (卵不使用) マヨネーズ タイプ 305g",
|
||||
"media_url": "https://m.media-amazon.com/images/I/71lzmm5n5cL._AC_SL1500_.jpg",
|
||||
"width": 1500,
|
||||
"height": 1500,
|
||||
"view_type": "retailer_in_use",
|
||||
"license_status": "restricted-research",
|
||||
"notes": "Retailer alternate image with the exact 305g package beside prepared potato salad; package text and size are visible. Not the enrolled plain front packshot. Source/media domain: amazon.co.jp / media-amazon.com."
|
||||
},
|
||||
{
|
||||
"file": "sku_l02_1.jpg",
|
||||
"sku": "sku_l02",
|
||||
"product": "キユーピー アマニ油マヨネーズ 200g",
|
||||
"source_page": "https://paypayfleamarket.yahoo.co.jp/item/z599237268",
|
||||
"source_title": "キユーピー アマニ油マヨネーズ 200g 2個セット 血圧が高めの方に",
|
||||
"media_url": "https://auctions.c.yimg.jp/images.auctions.yahoo.co.jp/image/dr000/auc0204/users/f571f9a659b398818c6c54e9862bf90f0e2d78ec/i-img900x1200-1777250065888ef77yp.jpg",
|
||||
"width": 900,
|
||||
"height": 1200,
|
||||
"view_type": "customer_front_pair",
|
||||
"license_status": "restricted-research",
|
||||
"notes": "Customer-taken two-pack photo. Both fronts visibly read 血圧ケア アマニ油マヨネーズ and 200g; exact title corroborates size. Source domain: paypayfleamarket.yahoo.co.jp; media host: auctions.c.yimg.jp."
|
||||
},
|
||||
{
|
||||
"file": "sku_l02_2.jpg",
|
||||
"sku": "sku_l02",
|
||||
"product": "キユーピー アマニ油マヨネーズ 200g",
|
||||
"source_page": "https://www.amazon.co.jp/dp/B01F8L6KSQ",
|
||||
"source_title": "キユーピー アマニ油マヨネーズ 200g [機能性表示食品]",
|
||||
"media_url": "https://m.media-amazon.com/images/I/71s+50hXsPL._AC_SL1005_.jpg",
|
||||
"width": 1005,
|
||||
"height": 937,
|
||||
"view_type": "back_label",
|
||||
"license_status": "restricted-research",
|
||||
"notes": "Retailer alternate rear-label view from the exact 200g listing. The unfolded back panel shows the アマニ油マヨネーズ label and 200g content declaration. Not a front enrollment image. Source/media domain: amazon.co.jp / media-amazon.com."
|
||||
},
|
||||
{
|
||||
"file": "sku_l03_1.jpg",
|
||||
"sku": "sku_l03",
|
||||
"product": "キユーピー からしマヨネーズ 200g",
|
||||
"source_page": "https://paypayfleamarket.yahoo.co.jp/item/z624032610",
|
||||
"source_title": "キユーピー からしマヨネーズ 200g 4本セット",
|
||||
"media_url": "https://auctions.c.yimg.jp/images.auctions.yahoo.co.jp/image/dr000/auc0206/users/198e53fdfc1edde3b4933f53fd4f0c02c3346cf5/i-img1200x714-1781002147108o12vd1.jpg",
|
||||
"width": 1200,
|
||||
"height": 714,
|
||||
"view_type": "customer_front_bundle",
|
||||
"license_status": "restricted-research",
|
||||
"notes": "Customer-taken four-pack photo with visible からしマヨネーズ fronts; listing title fixes each unit at 200g. Household lighting and grouped packages differ from enrollment. Source domain: paypayfleamarket.yahoo.co.jp; media host: auctions.c.yimg.jp."
|
||||
},
|
||||
{
|
||||
"file": "sku_l03_2.jpg",
|
||||
"sku": "sku_l03",
|
||||
"product": "キユーピー からしマヨネーズ 200g",
|
||||
"source_page": "https://www.amazon.co.jp/dp/B0069K29EK",
|
||||
"source_title": "キユーピー からしマヨネーズ 200g",
|
||||
"media_url": "https://m.media-amazon.com/images/I/81ZODR6dv8L._AC_SL1500_.jpg",
|
||||
"width": 1500,
|
||||
"height": 1500,
|
||||
"view_type": "retailer_in_use",
|
||||
"license_status": "restricted-research",
|
||||
"notes": "Retailer alternate image showing the exact 200g からしマヨネーズ package beside a prepared dish; the package front and 200g declaration are legible. Not the enrolled plain front packshot. Source/media domain: amazon.co.jp / media-amazon.com."
|
||||
},
|
||||
{
|
||||
"file": "sku_l05_1.jpg",
|
||||
"sku": "sku_l05",
|
||||
"product": "キユーピー ゼロ ノンコレステロール 310g",
|
||||
"source_page": "https://www.amazon.co.jp/dp/B0CBPWDXZJ",
|
||||
"source_title": "キユーピー ゼロ ノンコレステロール マヨネーズ タイプ 310g",
|
||||
"media_url": "https://m.media-amazon.com/images/I/81ykD7vVmIL._AC_SL1500_.jpg",
|
||||
"width": 1305,
|
||||
"height": 1500,
|
||||
"view_type": "retailer_in_use_collage",
|
||||
"license_status": "restricted-research",
|
||||
"notes": "Retailer alternate usage collage with the exact キユーピーゼロ 310g package among prepared dishes. The composition differs materially from the enrolled front packshot. Source/media domain: amazon.co.jp / media-amazon.com."
|
||||
},
|
||||
{
|
||||
"file": "sku_l05_2.jpg",
|
||||
"sku": "sku_l05",
|
||||
"product": "キユーピー ゼロ ノンコレステロール 310g",
|
||||
"source_page": "https://store.shopping.yahoo.co.jp/eight-cat-store/s-b0cnsbqcly-20260717.html",
|
||||
"source_title": "キユーピー ゼロ ノンコレステロール マヨネーズ タイプ 310g ×4個",
|
||||
"media_url": "https://item-shopping.c.yimg.jp/i/n/eight-cat-store_s-b0cnsbqcly-20260717_4_d_20260717075244",
|
||||
"width": 900,
|
||||
"height": 900,
|
||||
"view_type": "retailer_in_use_collage",
|
||||
"license_status": "restricted-research",
|
||||
"notes": "Independent retailer-hosted usage collage with the exact キユーピーゼロ 310g package shown among prepared dishes; the package front and 310g declaration are legible. Chosen over the same listing's plain front image so the set stays non-enrollment. Source domain: store.shopping.yahoo.co.jp; media host: item-shopping.c.yimg.jp."
|
||||
},
|
||||
{
|
||||
"file": "sku_l06_1.jpg",
|
||||
"sku": "sku_l06",
|
||||
"product": "内臓脂肪を減らす キユーピーフィッテ 210g",
|
||||
"source_page": "https://www.amazon.co.jp/dp/B08V2676LK",
|
||||
"source_title": "キユーピー フィッテ 210g",
|
||||
"media_url": "https://m.media-amazon.com/images/I/81zckoUh+vL._AC_SL1500_.jpg",
|
||||
"width": 654,
|
||||
"height": 1500,
|
||||
"view_type": "retailer_current_package_front",
|
||||
"license_status": "restricted-research",
|
||||
"notes": "F-12 current-package evidence. The wrapper visibly says 内臓脂肪を減らす キユーピーフィッテ and 210g; the product page repeats 内臓脂肪を減らす and contains no 内臓脂肪ケア wording. This is not the superseded enrolled package. Source/media domain: amazon.co.jp / media-amazon.com."
|
||||
},
|
||||
{
|
||||
"file": "sku_l06_2.jpg",
|
||||
"sku": "sku_l06",
|
||||
"product": "内臓脂肪を減らす キユーピーフィッテ 210g",
|
||||
"source_page": "https://store.shopping.yahoo.co.jp/ladydrugheartshop/4901577083600.html",
|
||||
"source_title": "キユーピー フィッテ 210g×5個",
|
||||
"media_url": "https://item-shopping.c.yimg.jp/i/n/ladydrugheartshop_4901577083600_i_20260902221452",
|
||||
"width": 500,
|
||||
"height": 500,
|
||||
"view_type": "retailer_current_package_front",
|
||||
"license_status": "restricted-research",
|
||||
"notes": "Second-domain F-12 corroboration. The current wrapper visibly says 内臓脂肪を減らす and 210g. Three PayPay Flea Market listings were also inspected, but all showed the superseded 内臓脂肪ケア wrapper and were rejected. Source domain: store.shopping.yahoo.co.jp; media host: item-shopping.c.yimg.jp."
|
||||
},
|
||||
{
|
||||
"file": "sku_l11_1.jpg",
|
||||
"sku": "sku_l11",
|
||||
"product": "キユーピー ハーフ 400g",
|
||||
"source_page": "https://paypayfleamarket.yahoo.co.jp/item/z692700538",
|
||||
"source_title": "キユーピー ハーフ マヨネーズ 400g 2本セット カロリー50%カット",
|
||||
"media_url": "https://auctions.c.yimg.jp/images.auctions.yahoo.co.jp/image/dr000/auc0209/users/67aace4c38f98ea5a750a86a39665a4c7bc9898d/i-img1200x1200-1790465979767g4gnwe.jpg",
|
||||
"width": 1200,
|
||||
"height": 1200,
|
||||
"view_type": "customer_front_pair",
|
||||
"license_status": "restricted-research",
|
||||
"notes": "Customer-taken two-pack photo. Both fronts visibly show キユーピーハーフ; the title identifies each as 400g. Recorded as 400g, not the stock CSV's 450g wording. Source domain: paypayfleamarket.yahoo.co.jp; media host: auctions.c.yimg.jp."
|
||||
},
|
||||
{
|
||||
"file": "sku_l11_2.jpg",
|
||||
"sku": "sku_l11",
|
||||
"product": "キユーピー ハーフ 400g",
|
||||
"source_page": "https://www.amazon.co.jp/dp/B007XT4FIO",
|
||||
"source_title": "キユーピー ハーフ 400g",
|
||||
"media_url": "https://m.media-amazon.com/images/I/81u8+jlddyL._AC_SL1500_.jpg",
|
||||
"width": 1406,
|
||||
"height": 1500,
|
||||
"view_type": "retailer_marketing_composite",
|
||||
"license_status": "restricted-research",
|
||||
"notes": "Retailer alternate marketing composite with the exact 400g package and product messaging. Not the plain official Kewpie front packshot. Recorded as 400g, matching the pictured package and listing title. Source/media domain: amazon.co.jp / media-amazon.com."
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
recognization/data/external-eval/kewpie/sku_l01_1.jpg
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
recognization/data/external-eval/kewpie/sku_l01_2.jpg
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
recognization/data/external-eval/kewpie/sku_l02_1.jpg
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
recognization/data/external-eval/kewpie/sku_l02_2.jpg
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
recognization/data/external-eval/kewpie/sku_l03_1.jpg
Normal file
|
After Width: | Height: | Size: 140 KiB |