The camera moves and the shelf does not, but every measurement so far treats a frame as the world: a facing is "a track id", and that id dies the moment the camera pans away and comes back. Label inheritance, drift checks and restart counters were each correct fixes for that, and all of them are symptoms. `shelf_map.py` keeps a per-session map of the shelf plane. Frames are registered by their content shift, detections are placed in shelf coordinates, and a detection that lands where a facing already is merges into it - so identity votes and tag readings pool on the physical facing. Measured, and reported honestly: - Registration is exact on the synthetic walk: 0.01 px median error against the demo camera's own offsets, over 23 consecutive pairs, with both phase correlation and ORB + RANSAC. That validates the harness, not the method - the walk is a pure translation of one photograph. Real video needs step 1's footage. - Pooling works: a facing seen 15 times across 2 track ids reports one identity at 1.00 agreement. - De-duplication is NOT yet better than the tracker: 179 facings against 156 track ids, where the shelf holds roughly 70. Both over-count, the map slightly more. Two bugs were found by measuring, and both were silent: the content shift is the negation of the camera's motion (getting it backwards turned ~60 facings into 857), and matching purely by box overlap lets jitter split a facing (210 facings, 103 of them seen once; matching by track continuity first brought it to 179 with 38). 175 tests pass. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
77 lines
3.6 KiB
Python
77 lines
3.6 KiB
Python
"""
|
|
What does a shelf map buy over track ids?
|
|
|
|
Walks the demo camera once, feeds every frame through both the live session and a
|
|
ShelfMap, and reports what each counts. Needs a running server on :8077.
|
|
|
|
STEPS=58 python code/bench_shelf_map.py
|
|
"""
|
|
import json, sys, time, urllib.request
|
|
sys.path.insert(0, "code")
|
|
import cv2, numpy as np
|
|
from websockets.sync.client import connect
|
|
from shelf_map import ShelfMap
|
|
|
|
BASE = "http://127.0.0.1:8077"
|
|
STEPS = int(__import__("os").environ.get("STEPS", "58")) # one walk over image6 at pan 0.02
|
|
|
|
def fetch(step):
|
|
url = f"{BASE}/v1/demo/frame?samples_param=image6&step={step}&width=1280&height=720&quality=70&pan=0.02"
|
|
return urllib.request.urlopen(url, timeout=10).read()
|
|
|
|
shelf = ShelfMap()
|
|
track_ids, frames, detections = set(), 0, 0
|
|
started = time.perf_counter()
|
|
with connect("ws://127.0.0.1:8077/v1/stream") as ws:
|
|
ws.recv(timeout=15)
|
|
for step in range(STEPS):
|
|
payload = fetch(step)
|
|
frame = cv2.imdecode(np.frombuffer(payload, np.uint8), cv2.IMREAD_COLOR)
|
|
ws.send(step.to_bytes(4, "big") + payload)
|
|
msg = json.loads(ws.recv(timeout=25))
|
|
if msg.get("type") != "result":
|
|
continue
|
|
frames += 1
|
|
tracks = msg["tracks"]
|
|
detections += len(tracks)
|
|
for track in tracks:
|
|
track_ids.add(track["track_id"])
|
|
shelf.update(step, frame, tracks)
|
|
time.sleep(0.05)
|
|
elapsed = time.perf_counter() - started
|
|
|
|
stats = shelf.stats()
|
|
print(f"{frames} frames in {elapsed:.1f} s, {detections} detections")
|
|
print(f" track ids seen (what the live session counts): {len(track_ids)}")
|
|
print(f" facings on the shelf map (what is actually there): {stats['facings']}")
|
|
print(f" merges {stats['merges']}, registrations {stats['registrations']}, failures {stats['registration_failures']}")
|
|
print(f" median registration response {stats['median_response']}")
|
|
print(f" facings whose pooled identity agrees with itself >= 0.8: {stats['identity_consistent']}")
|
|
|
|
facings = [f for f in shelf.facings() if f.observations >= 3]
|
|
print(f"\nfacings seen at least 3 times: {len(facings)}")
|
|
for facing in sorted(facings, key=lambda f: (f.shelf_box[1], f.shelf_box[0]))[:10]:
|
|
sku, agreement = facing.best_identity()
|
|
tag_sku, tag_agreement = facing.best_tag_identity()
|
|
print(f" shelf x={facing.shelf_box[0]:>5} y={facing.shelf_box[1]:>5} obs={facing.observations:>2} "
|
|
f"tracks={len(facing.seen_track_ids):>2} identity={sku} ({agreement:.2f}) tag={tag_sku} ({tag_agreement:.2f})")
|
|
|
|
# render the map so duplicates are visible rather than inferred
|
|
import numpy as np
|
|
facings = shelf.facings()
|
|
xs = [f.shelf_box[0] for f in facings] + [f.shelf_box[2] for f in facings]
|
|
ys = [f.shelf_box[1] for f in facings] + [f.shelf_box[3] for f in facings]
|
|
x0, x1 = min(xs), max(xs)
|
|
y0, y1 = min(ys), max(ys)
|
|
scale = 0.6
|
|
canvas = np.full((int((y1 - y0) * scale) + 20, int((x1 - x0) * scale) + 20, 3), 20, np.uint8)
|
|
for facing in facings:
|
|
bx = [(facing.shelf_box[0] - x0) * scale + 10, (facing.shelf_box[1] - y0) * scale + 10,
|
|
(facing.shelf_box[2] - x0) * scale + 10, (facing.shelf_box[3] - y0) * scale + 10]
|
|
colour = (0, 200, 0) if facing.observations >= 5 else (0, 120, 255)
|
|
cv2.rectangle(canvas, (int(bx[0]), int(bx[1])), (int(bx[2]), int(bx[3])), colour, 1)
|
|
cv2.imwrite("/tmp/dorayaki-verify/shelf-map.png", canvas)
|
|
print(f"\nmap canvas {canvas.shape[1]}x{canvas.shape[0]} px covering shelf x {x0}..{x1}, y {y0}..{y1}")
|
|
from collections import Counter
|
|
print("observations per facing:", Counter(min(f.observations, 20) for f in facings).most_common(6))
|
|
print("facings per y band (100 px):", Counter((f.shelf_box[1] - y0) // 100 for f in facings).most_common(8))
|