""" Demo-only: replay a shelf's recognition as if it were live tracking. `DemoRecognition` recognises the whole shelf once, with the real model and the real matcher, and then hands those results to the walk's frames as if the live session had produced them. That is useful for filming and for exercising a client without a camera, and it is *not* a perception result: the boxes do not come from the frame being processed, and no tracking or classification happened on it. Every payload it touches is marked `synthetic: true` so a client can tell. It lives here rather than in `app.py` because it was committed inside an unrelated feature once, which made a film-making device look like part of the server. """ from __future__ import annotations import threading from typing import Any, Dict, List, Optional from demo import DemoSourceError class DemoRecognition: """The demo shortcut. A real walk recognises each facing as it appears, which on a shelf the catalog only partly covers leaves most boxes unlabelled. For a demo the shelf is recognised ONCE, in full, with the same model and matcher, and those results are then handed to the walk's frames as if the live session had just produced them - every box already green, with name and price. Demo-only: enabled by --demo-walk. """ _STILL_TO_LIVE = { # The still payload already speaks the live vocabulary for its states; # the decision-level names are kept for older shapes. "recognized": "recognized", "needs_confirmation": "needs_confirmation", "unrecognized": "unrecognized", "accepted": "recognized", "ambiguous": "needs_confirmation", "unknown": "unrecognized", "rejected": "unrecognized", } def __init__(self, engine: Any, camera: Any, samples: Dict[str, Any], tag_attacher: Any) -> None: self._engine = engine self._camera = camera self._samples = samples self._tag_attacher = tag_attacher self._catalog_by_sku = {product["sku_id"]: product for product in engine.catalog()} self._cache: Dict[str, Any] = {} self._lock = threading.Lock() def steps(self, sample_id: str) -> int: return self._camera.plan([sample_id], (1280, 720)).get("steps", 0) def recognition(self, sample_id: str) -> Any: cached = self._cache.get(sample_id) if cached is not None: return cached import cv2 path = self._samples[sample_id]["path"] image = cv2.imread(path) if image is None: raise DemoSourceError(f"cannot read sample '{sample_id}'") result = self._engine.recognize(image, classify=True, method="hybrid", with_empty_slots=True) if self._tag_attacher is not None: result = self._tag_attacher(result, image) with self._lock: self._cache[sample_id] = (result, int(image.shape[1]), int(image.shape[0])) return self._cache[sample_id] def warm(self, sample_id: str) -> None: self.recognition(sample_id) def apply(self, result: Dict[str, Any], sample_id: str, step: int) -> Dict[str, Any]: """Swap the live tracks for the shelf's recognised ones, in frame space.""" still, _, _ = self.recognition(sample_id) position = self._camera.window_at([sample_id], step) ox, oy = position["x"], position["y"] win_w, win_h = position["width"], position["height"] tracks = [] for index, detection in enumerate(still.get("detections") or []): box = [int(v) for v in detection.get("box") or []] if len(box) != 4: continue x1, y1, x2, y2 = box if x2 - ox <= 0 or y2 - oy <= 0 or x1 - ox >= win_w or y1 - oy >= win_h: continue frame_box = [x1 - ox, y1 - oy, x2 - ox, y2 - oy] identity = detection.get("identity") or {} state = self._STILL_TO_LIVE.get(identity.get("state") or "", "unrecognized") sku_id = identity.get("sku_id") product = self._catalog_by_sku.get(sku_id or "") if sku_id else None if state == "recognized" and product is None: state = "unrecognized" payload: Dict[str, Any] = { "track_id": index + 1, "box": frame_box, "raw_box": frame_box, "confidence": round(float(detection.get("confidence") or 0.0), 4), "first_seen_seq": step, "relabels": 0, "restarts": 0, "inherited": False, "identity": { "state": state, "sku_id": sku_id if state == "recognized" else None, "identity_level": identity.get("identity_level"), "confidence": identity.get("confidence"), "score": identity.get("score"), "candidates": list(identity.get("candidates") or []), "attempts": 1, "method": "hybrid", }, } if product is not None and state == "recognized": payload["product"] = { "sku_id": product.get("sku_id"), "product_name": product.get("product_name"), "price": product.get("price"), } tracks.append(payload) slots = [] for slot in still.get("empty_slots") or []: box = [int(v) for v in slot.get("box") or []] if len(box) != 4: continue x1, y1, x2, y2 = box if x2 - ox <= 0 or y2 - oy <= 0 or x1 - ox >= win_w or y1 - oy >= win_h: continue entry = dict(slot) entry["box"] = [x1 - ox, y1 - oy, x2 - ox, y2 - oy] slots.append(entry) result["tracks"] = tracks result["empty_slots"] = slots counts: Dict[str, int] = {} for track in tracks: counts[track["identity"]["state"]] = counts.get(track["identity"]["state"], 0) + 1 result["summary"] = { "in_view": len(tracks), "recognized": counts.get("recognized", 0), "needs_confirmation": counts.get("needs_confirmation", 0), "unrecognized": counts.get("unrecognized", 0), "detected": 0, "unconfirmed": counts.get("unconfirmed", 0), "identifying": 0, "empty_slots": len(slots), "price_tags": (still.get("price_tags") or {"readable": 0, "unreadable": 0}), "reconciliation": (still.get("reconciliation") or {}), "compliance": (still.get("compliance") or {"violations": 0, "rows": [], "hints": []}), } # A client must be able to tell these boxes did not come from the frame it # just sent. result["synthetic"] = True result["demo"] = {"sample": sample_id, "step": step, "bay": position["bay_index"] + 1} return result