product-recognition-demo/recognization/code/test_demo.py
kiyo9w 00367a95c9 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>
2026-09-24 18:09:23 +09:00

119 lines
5 KiB
Python

"""
Contract tests for the demo camera (the synthetic store walk).
The walk is what a demo shows when there is no camera, so its geometry has to be
predictable: inside the image, moving, serpentine, and wrapping forever.
"""
from __future__ import annotations
import os
import sys
import unittest
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
if os.path.exists(os.path.join(project_root, "stock")):
os.chdir(project_root)
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
import numpy as np # noqa: E402
from demo import DemoCamera, DemoSourceError # noqa: E402
class FakeImageCamera(DemoCamera):
"""A camera over a synthetic image, so the geometry is testable without files."""
def __init__(self, width=1500, height=2000, window=(960, 540)):
super().__init__({"synthetic": {"id": "synthetic", "path": "synthetic", "label": "synthetic"}}, window)
self._image_cache = np.zeros((height, width, 3), dtype="uint8")
self._size_cache = (width, height)
def _size(self, sample_id): # noqa: D102
if sample_id != "synthetic":
raise DemoSourceError(f"unknown sample '{sample_id}'")
return self._size_cache
def _image(self, sample_id): # noqa: D102
return self._image_cache
class WalkGeometryTests(unittest.TestCase):
def setUp(self):
self.camera = FakeImageCamera()
self.plan = self.camera.plan(["synthetic"])
def test_plan_is_deterministic_and_covers_the_shelf(self):
self.assertEqual(self.plan["steps"], self.camera.plan(["synthetic"])["steps"])
self.assertGreater(self.plan["steps"], 20, "a shelf walk is not two steps long")
self.assertEqual(self.plan["bays"][0]["sample"], "synthetic")
def test_window_never_leaves_the_image(self):
width, height = 1500, 2000
for step in range(self.plan["steps"]):
position = self.camera.window_at(["synthetic"], step)
self.assertGreaterEqual(position["x"], 0)
self.assertGreaterEqual(position["y"], 0)
self.assertLessEqual(position["x"] + position["width"], width)
self.assertLessEqual(position["y"] + position["height"], height)
def test_the_camera_actually_moves(self):
xs = [self.camera.window_at(["synthetic"], step)["x"] for step in range(6)]
ys = [self.camera.window_at(["synthetic"], step)["y"] for step in range(self.plan["steps"])]
self.assertGreater(len(set(xs)), 1, "the window must pan, not sit still")
self.assertGreater(len(set(ys)), 1, "the walk must climb rows")
def test_it_ascends_the_shelf(self):
first = self.camera.window_at(["synthetic"], 0)["y"]
last_band = self.plan["steps"] - 1
self.assertGreater(first, self.camera.window_at(["synthetic"], last_band)["y"],
"the walk starts at the bottom row and climbs")
def test_step_wraps(self):
first = self.camera.window_at(["synthetic"], 0)
wrapped = self.camera.window_at(["synthetic"], self.plan["steps"])
self.assertEqual((first["x"], first["y"]), (wrapped["x"], wrapped["y"]))
def test_frames_are_deterministic_jpegs(self):
payload, position = self.camera.frame(["synthetic"], 3, quality=70)
again, _ = self.camera.frame(["synthetic"], 3, quality=70)
self.assertTrue(payload.startswith(b"\xff\xd8"), "JPEG magic")
self.assertEqual(payload, again, "the same step must render the same frame")
self.assertIn("sample", position)
def test_unknown_sample_is_rejected(self):
with self.assertRaises(DemoSourceError):
self.camera.plan(["nope"])
def test_window_larger_than_the_image_is_clamped(self):
camera = FakeImageCamera(width=800, height=600, window=(960, 540))
position = camera.window_at(["synthetic"], 0)
self.assertLessEqual(position["width"], 800)
self.assertLessEqual(position["height"], 600)
self.assertEqual(position["x"], 0)
# The walk starts at the bottom of the shelf, so the window is bottom-aligned.
self.assertEqual(position["y"], 600 - position["height"])
class RealShelfTests(unittest.TestCase):
"""One check against the actual demo asset, skipped when it is absent."""
def test_real_sample_walks(self):
from app import get_available_shelf_images
samples = {item["id"]: item for item in get_available_shelf_images("add")}
if "image6" not in samples:
self.skipTest("demo sample image6 not present")
camera = DemoCamera(samples)
plan = camera.plan(["image6"], (1280, 720))
self.assertGreater(plan["steps"], 20, "a 1500x2000 shelf gives a multi-row walk")
payload, position = camera.frame(["image6"], 5, (1280, 720))
self.assertGreater(len(payload), 10_000)
self.assertLessEqual(position["x"] + position["width"], plan["bays"][0]["width"])
self.assertLessEqual(position["y"] + position["height"], plan["bays"][0]["height"])
if __name__ == "__main__":
unittest.main()