/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>
119 lines
5 KiB
Python
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()
|