fix(recognition): calibrate the accept lines against unrelated shelves

Every threshold was fitted on image6, where each crop is a facing of a product
the shelf holds, so the set could not price a false accept. Measured on 4580
crops from 64 unrelated Wikimedia shelf photos at the shipped configuration:

  LOCAL_ACCEPT 2.0 -> 4.0   false accepts 60 (1.31%) -> 3 (0.07%)
  TEXT_MIN_SHARED 2 -> 4    the two-bigram reads that named sku_l12 via おい/いし

On image6 that is 44 shown / 42 correct at full resolution - 95.5% precision,
65.6% coverage - the highest-coverage cell of the joint sweep that passes both
G2 (>= 95%) and G3 (<= 1%). At live resolution 56% end-to-end at 97%.

The text lane's evidence floor gates its answer only: zeroing the row's score
would reorder the ranking and make the margin gate compare a different pair
than the one it was measured on.

code/sweep_accept_lines.py sweeps both axes jointly through the shipped
DecisionPolicy; the earlier table swept one axis while pinning the other, so
its baseline rows were not the old line.
This commit is contained in:
kiyo9w 2026-09-29 09:28:13 +09:00
parent 4ce41ecb17
commit 5094b5f6ca
12 changed files with 815 additions and 195 deletions

View file

@ -5,8 +5,11 @@ 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.
Measured the way the server measures: a 1280-long-side frame detected at
`LiveSettings.conf`. The sources are 4032-4512 px photos, and detecting them at
full resolution and at the default `detect_conf` instead measures a
configuration the phone never runs, which is how a threshold change reads as a
pass on evidence the product does not produce.
.venv/bin/python code/eval_distractors.py --stock stock-lopia-clean \
data/external-shelves/wikimedia-commons
@ -28,7 +31,13 @@ if current_dir not in sys.path:
import cv2 # noqa: E402
from eval_identity import LOCAL_ACCEPT, LocalLaneAdapter # noqa: E402
from eval_identity import ( # noqa: E402
LIVE_LONG_SIDE,
LOCAL_ACCEPT,
LocalLaneAdapter,
at_live_resolution,
)
from identity_lanes import TEXT_MIN_SHARED # noqa: E402
def main(argv: List[str] | None = None) -> int:
@ -40,9 +49,11 @@ def main(argv: List[str] | None = None) -> int:
args = parser.parse_args(argv)
from engine import build_default_engine, EngineSettings
from live import LiveSettings
engine = build_default_engine(EngineSettings(stock_dir=args.stock))
lane = LocalLaneAdapter(args.stock)
conf = LiveSettings().conf
for directory in args.directories:
manifest_path = os.path.join(directory, "manifest.json")
try:
@ -65,14 +76,15 @@ def main(argv: List[str] | None = None) -> int:
if image is None:
continue
started = time.perf_counter()
detections = engine.detect(image)
frame = at_live_resolution(image)
detections = engine.detect(frame, conf=conf)
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]
crop = frame[max(0, y1):y2, max(0, x1):x2]
if crop.size == 0:
continue
decision = lane.decide(crop, lane.rank(crop), accept=LOCAL_ACCEPT,
@ -93,6 +105,14 @@ def main(argv: List[str] | None = None) -> int:
"dataset": os.path.basename(directory.rstrip("/")),
"catalog": args.stock,
"note": "Unrelated shelves: every accept is a false accept by construction.",
# The configuration the numbers above were measured under, so the
# stored evidence is checkable against the constants it claims.
"config": {
"frame_long_side": LIVE_LONG_SIDE,
"detect_conf": conf,
"local_accept": LOCAL_ACCEPT,
"text_min_shared": TEXT_MIN_SHARED,
},
"images": len(rows),
"detections": detections,
"false_accepts": accepts,