feat: a synthetic store walk, and the live tuning it forced out

/v1/demo/plan + /v1/demo/frame give a demo the one thing a still image cannot:
motion. The virtual camera pans a shelf row left to right, steps up, and pans
back - a store employee sweeping a shelf - over a real shelf photo, ~1 ms per
frame, deterministic, and wrapping so an unattended demo loops. The client drives
only `step`, so the walk's speed follows whatever frame rate it can sustain.

Three real defects came out of measuring the walk rather than a still:

- Bands were keyed by on-screen position, so a walking camera minted a new key
  every few pixels of pan and re-read the same shelf continuously. Keyed by row
  now; OCR stops chasing the window.
- The classification budget was consumed by retries of products already leaving
  the frame. Retries now skip anything near the edge, and a track gets 2 attempts.
- The pan advance was 0.22 of the window per step: a facing was in view for one
  or two frames, so nothing could ever be classified (9 labelled tracks in 20 s).
  0.06 keeps a product in view for about a second.

Measured on the walk (20 s, 1280x720 frames, one client): orb labels 296 tracks
across 21 SKUs with the first label at 0.2 s, while hybrid manages 13 tracks /
4 SKUs at 5.4 s - so the live default for a demo is the fast method, with hybrid
kept for tap-to-inspect.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
kiyo9w 2026-09-24 18:09:23 +09:00
parent a18a80a21e
commit 00367a95c9
5 changed files with 466 additions and 16 deletions

View file

@ -64,10 +64,13 @@ class LiveSettings:
tags: bool = True
min_hits: int = 2
min_crop_px: int = 40
max_attempts: int = 3
max_attempts: int = 2
retry_gap: int = 8
# A product drifting out of the frame is not worth a retry: the budget belongs
# to what is still in front of the camera.
retry_edge_ratio: float = 0.06
area_growth: float = 1.3
max_inflight_per_session: int = 12
max_inflight_per_session: int = 16
edge_margin_ratio: float = 0.015
glare_fraction: float = 0.02
glare_pixel: int = 245
@ -545,8 +548,14 @@ class LiveSession:
continue
if track.attempts >= self.settings.max_attempts:
continue
if track.attempts > 0 and (seq - track.last_attempt_seq) < self.settings.retry_gap:
if track.area() < self.settings.area_growth * max(1, track.last_attempt_area):
if track.attempts > 0:
if (seq - track.last_attempt_seq) < self.settings.retry_gap:
if track.area() < self.settings.area_growth * max(1, track.last_attempt_area):
continue
retry_margin_x = int(frame_w * self.settings.retry_edge_ratio)
retry_margin_y = int(frame_h * self.settings.retry_edge_ratio)
if (x1 <= retry_margin_x or y1 <= retry_margin_y
or x2 >= frame_w - retry_margin_x or y2 >= frame_h - retry_margin_y):
continue
eligible.append(track)