fix: bind tag projection to each frame origin
This commit is contained in:
parent
05cf32e1d9
commit
d5a7a56dff
4 changed files with 146 additions and 35 deletions
|
|
@ -189,8 +189,9 @@ class LiveSession:
|
|||
self._catalog_by_sku: Dict[str, Dict[str, Any]] = {}
|
||||
self._catalog_version = ""
|
||||
self._tags = None
|
||||
# Shelf position of this frame's origin. Updated from a content shift
|
||||
# before the session lock; tag cadence keys rows by it. Not a shelf map.
|
||||
# Shelf position of this frame's origin. Register and origin update
|
||||
# share one gate so overlapping workers cannot reorder either.
|
||||
self._shift_gate = threading.Lock()
|
||||
self._frame_origin = [0.0, 0.0]
|
||||
self._shift_map = None
|
||||
self._compliance_tracker = None
|
||||
|
|
@ -337,22 +338,23 @@ class LiveSession:
|
|||
}
|
||||
|
||||
def reset(self) -> None:
|
||||
with self._lock:
|
||||
self._generation += 1
|
||||
self.tracker.reset()
|
||||
self._tracks.clear()
|
||||
self._last_seen_seq.clear()
|
||||
self._relabel_count.clear()
|
||||
self._restart_count.clear()
|
||||
self._slot_history.clear()
|
||||
self._label_memory.clear()
|
||||
self._slots = []
|
||||
self._latest_image = None
|
||||
self._latest_frame_id = None
|
||||
self._frame_origin = [0.0, 0.0]
|
||||
self._shift_map = None
|
||||
# ponytail: do not touch _waiter here. asyncio.Event is loop-affine;
|
||||
# take_pending_async clears a stale wake on its own loop.
|
||||
with self._shift_gate:
|
||||
with self._lock:
|
||||
self._generation += 1
|
||||
self.tracker.reset()
|
||||
self._tracks.clear()
|
||||
self._last_seen_seq.clear()
|
||||
self._relabel_count.clear()
|
||||
self._restart_count.clear()
|
||||
self._slot_history.clear()
|
||||
self._label_memory.clear()
|
||||
self._slots = []
|
||||
self._latest_image = None
|
||||
self._latest_frame_id = None
|
||||
self._frame_origin = [0.0, 0.0]
|
||||
self._shift_map = None
|
||||
# ponytail: do not touch _waiter here. asyncio.Event is loop-affine;
|
||||
# take_pending_async clears a stale wake on its own loop.
|
||||
|
||||
# ------------------------------------------------------------------ frame path
|
||||
def process_frame(self, frame_id: int, payload: bytes) -> Dict[str, Any]:
|
||||
|
|
@ -367,7 +369,6 @@ class LiveSession:
|
|||
empty_enabled = self.settings.empty_slots
|
||||
generation = self._generation
|
||||
|
||||
|
||||
shelf_offset = self._note_frame_shift(image)
|
||||
|
||||
boxes = self.engine.detect(image, conf=conf)
|
||||
|
|
@ -495,7 +496,7 @@ class LiveSession:
|
|||
summary = self._summary_locked()
|
||||
stats = self._stats_locked()
|
||||
track_payload = [self._track_payload(state) for state in self._tracks.values()]
|
||||
tag_summary = self._apply_tags(seq, track_payload, tag_bands)
|
||||
tag_summary = self._apply_tags(seq, track_payload, tag_bands, shelf_offset, image.shape)
|
||||
if tag_summary is not None:
|
||||
summary["price_tags"] = tag_summary["tags"]
|
||||
summary["compliance"] = tag_summary["compliance"]
|
||||
|
|
@ -996,16 +997,19 @@ class LiveSession:
|
|||
def _note_frame_shift(self, image: Any) -> Tuple[float, float]:
|
||||
"""Shelf position of this frame's origin. Phase correlation only.
|
||||
|
||||
Runs before the session lock. A failed registration keeps the last origin
|
||||
rather than inventing a jump. The content shift moves the origin the
|
||||
other way, same sign as ShelfMap.
|
||||
Register and origin update share one per-session gate, so overlapping
|
||||
workers cannot reorder the previous-frame baseline or publish a later
|
||||
origin first. Correlation stays outside the session lock and outside
|
||||
OCR. A failed registration keeps the last origin rather than inventing
|
||||
a jump. The content shift moves the origin the other way, same sign as
|
||||
ShelfMap.
|
||||
"""
|
||||
if self._shift_map is None:
|
||||
from shelf_map import ShelfMap
|
||||
with self._shift_gate:
|
||||
if self._shift_map is None:
|
||||
from shelf_map import ShelfMap
|
||||
|
||||
self._shift_map = ShelfMap()
|
||||
shift = self._shift_map.register(image)
|
||||
with self._lock:
|
||||
self._shift_map = ShelfMap()
|
||||
shift = self._shift_map.register(image)
|
||||
if shift is not None:
|
||||
self._frame_origin[0] -= float(shift[0])
|
||||
self._frame_origin[1] -= float(shift[1])
|
||||
|
|
@ -1034,19 +1038,27 @@ class LiveSession:
|
|||
LOGGER.warning("tag pass failed: %s", error)
|
||||
|
||||
def _apply_tags(
|
||||
self, seq: int, track_payloads: List[Dict[str, Any]], bands: Sequence[Any]
|
||||
self,
|
||||
seq: int,
|
||||
track_payloads: List[Dict[str, Any]],
|
||||
bands: Sequence[Any],
|
||||
shelf_offset: Sequence[float],
|
||||
image_shape: Sequence[int],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Attach tag + reconciliation to each track and compute compliance.
|
||||
|
||||
Must be called with the session lock held (it reads the tag cache under its
|
||||
own lock, which is never taken the other way round).
|
||||
own lock, which is never taken the other way round). The origin and shape
|
||||
are this frame's, not whatever `plan` last stored.
|
||||
"""
|
||||
service = self._tags
|
||||
if service is None or not self.settings.tags:
|
||||
return None
|
||||
from reconcile import ComplianceTracker, evaluate_compliance, reconcile, summarize_findings
|
||||
|
||||
tag_by_track, all_tags, counts = service.attach(track_payloads)
|
||||
tag_by_track, all_tags, counts = service.attach(
|
||||
track_payloads, shelf_offset=shelf_offset, image_shape=image_shape
|
||||
)
|
||||
reconciliations: Dict[int, Dict[str, Any]] = {}
|
||||
for payload in track_payloads:
|
||||
track_id = int(payload["track_id"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue