From a653513c87dae090f07e07c0ca35499ab46237a4 Mon Sep 17 00:00:00 2001 From: Samata Bag <2024itb047.samata@students.iiests.ac.in> Date: Sat, 16 May 2026 22:52:19 +0530 Subject: [PATCH 1/9] Add per-zone max_age overrides for tracker lifecycle --- services/detection/zones.py | 3 + services/tracking/tracker.py | 207 +++++++++++++++++++++++------------ 2 files changed, 141 insertions(+), 69 deletions(-) diff --git a/services/detection/zones.py b/services/detection/zones.py index d6f064b..13e38d2 100644 --- a/services/detection/zones.py +++ b/services/detection/zones.py @@ -14,6 +14,7 @@ class Zone: polygon: list[tuple[int, int]] # list of (x, y) corner points alert_on_entry: bool = True color_bgr: tuple[int, int, int] = (0, 0, 255) # red by default + max_age_override: int | None = None def as_array(self) -> np.ndarray: """Return polygon as numpy array for cv2.pointPolygonTest.""" @@ -27,12 +28,14 @@ def as_array(self) -> np.ndarray: polygon=[(540, 200), (740, 200), (740, 480), (540, 480)], alert_on_entry=True, color_bgr=(0, 0, 255), # red + max_age_override=60, ), Zone( name="keypad_area", polygon=[(620, 280), (720, 280), (720, 420), (620, 420)], alert_on_entry=True, color_bgr=(0, 165, 255), # orange + max_age_override=15, ), Zone( name="safe_corridor", diff --git a/services/tracking/tracker.py b/services/tracking/tracker.py index 67a96e9..cf7b875 100644 --- a/services/tracking/tracker.py +++ b/services/tracking/tracker.py @@ -22,7 +22,7 @@ from deep_sort_realtime.deepsort_tracker import DeepSort # ── adjust sys.path so we can import sibling packages ────────────────────── -import sys, os +import sys sys.path.insert(0, str(Path(__file__).resolve().parents[2])) # repo root from libs.schemas.detection import DetectionFrameSchema @@ -106,9 +106,11 @@ def update( if det.label != "person": # track persons only in this phase continue b = det.bbox - l, t = b.x1, b.y1 - w, h = b.x2 - b.x1, b.y2 - b.y1 - ds_input.append(([l, t, w, h], float(det.confidence), "person")) + left, top = b.x1, b.y1 + w, h = b.x2 - b.x1, b.y2 - b.y1 + ds_input.append( + ([left, top, w, h], float(det.confidence), "person") +) # ── Run tracker ──────────────────────────────────────────────────── raw_tracks = self._tracker.update_tracks(ds_input, frame=raw_frame) @@ -124,114 +126,181 @@ def update( tid = int(t.track_id) # ── ReID matching ───────────────────────────────────── # ── ReID matching ───────────────────────────────────── - if hasattr(t, "features") and t.features: + if hasattr(t, "features") and t.features: - new_embedding = t.features[-1] + new_embedding = t.features[-1] - for lost_id, data in list(self._lost_embeddings.items()): + for lost_id, data in list(self._lost_embeddings.items()): - age = self._frame_id - data["last_seen"] + age = self._frame_id - data["last_seen"] - if age > self.max_age: - continue + if age > self.max_age: + continue - similarity = self._cosine_similarity( - new_embedding, - data["embedding"], - ) + similarity = self._cosine_similarity( + new_embedding, + data["embedding"], + ) - if similarity > self.REID_SIMILARITY_THRESHOLD: + if similarity > self.REID_SIMILARITY_THRESHOLD: - # Restore original ID - tid = lost_id - t.track_id = lost_id + # Restore original ID + tid = lost_id + t.track_id = lost_id - del self._lost_embeddings[lost_id] + del self._lost_embeddings[lost_id] - logger.info( - f"ReID matched: restored track #{lost_id}" - ) + logger.info( + f"ReID matched: restored track #{lost_id}" + ) + + break - break - ltwh = t.to_ltwh() x1 = float(ltwh[0]) y1 = float(ltwh[1]) x2 = x1 + float(ltwh[2]) y2 = y1 + float(ltwh[3]) - cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 + + cx = (x1 + x2) / 2 + cy = (y1 + y2) / 2 zones = [z.name for z in get_zones_for_point(cx, cy)] - # ── Lifecycle: BORN ─────────────────────────────────────────── if tid not in self._known_ids: self._known_ids.add(tid) - self._emit_lifecycle(TrackState.BORN, tid, zones, 0.0) - logger.info(f"Track BORN: #{tid} in zones={zones}") - # ── Dwell time ──────────────────────────────────────────────── + self._emit_lifecycle( + TrackState.BORN, + tid, + zones, + 0.0, + ) + + logger.info( + f"Track BORN: #{tid} in zones={zones}" + ) + prev = self._active_tracks.get(tid) - dwell_frames = (prev.dwell_time_frames + 1) if prev else 1 - dwell_secs = dwell_frames / self.fps - # ── Trajectory ──────────────────────────────────────────────── + dwell_frames = ( + prev.dwell_time_frames + 1 + ) if prev else 1 + + dwell_secs = dwell_frames / self.fps + prev_traj = prev.trajectory if prev else [] - new_point = TrajectoryPoint(x=cx, y=cy, frame_id=self._frame_id) - trajectory = (prev_traj + [new_point])[-self.MAX_TRAJECTORY_LEN:] + + new_point = TrajectoryPoint( + x=cx, + y=cy, + frame_id=self._frame_id, + ) + + trajectory = ( + prev_traj + [new_point] + )[-self.MAX_TRAJECTORY_LEN:] obj = TrackedObject( - track_id = tid, - label = "person", - bbox = [x1, y1, x2, y2], - confidence = float(t.det_conf or 0.0), - center = (cx, cy), - dwell_time_frames = dwell_frames, - dwell_time_seconds = round(dwell_secs, 2), - state = TrackState.ACTIVE, - trajectory = trajectory, - zones_present = zones, - last_seen_frame = self._frame_id, + track_id=tid, + label="person", + bbox=[x1, y1, x2, y2], + confidence=float(t.det_conf or 0.0), + center=(cx, cy), + dwell_time_frames=dwell_frames, + dwell_time_seconds=round(dwell_secs, 2), + state=TrackState.ACTIVE, + trajectory=trajectory, + zones_present=zones, + last_seen_frame=self._frame_id, ) + self._active_tracks[tid] = obj + current_ids.add(tid) - tracked_objects.append(obj) - # ── Lifecycle: LOST for tracks that disappeared ──────────────────── + tracked_objects.append(obj) + + # ── Lifecycle: LOST for tracks that disappeared ──────────────────── for tid, prev_obj in list(self._active_tracks.items()): + if tid not in current_ids: - frames_since = self._frame_id - prev_obj.last_seen_frame + + frames_since = ( + self._frame_id - prev_obj.last_seen_frame + ) + if frames_since == 1: - track = next((t for t in raw_tracks if int(t.track_id) == tid), None) + track = next( + ( + t for t in raw_tracks + if int(t.track_id) == tid + ), + None, + ) - if track is not None and hasattr(track, "features") and track.features: - self._lost_embeddings[tid] = { - "embedding": track.features[-1], - "last_seen": self._frame_id, - } + if ( + track is not None + and hasattr(track, "features") + and track.features + ): + self._lost_embeddings[tid] = { + "embedding": track.features[-1], + "last_seen": self._frame_id, + } self._emit_lifecycle( - TrackState.LOST, tid, + TrackState.LOST, + tid, prev_obj.zones_present, prev_obj.dwell_time_seconds, ) - if frames_since > self._tracker.max_age: + + effective_max_age = self.max_age + + if prev_obj.zones_present: + + zone_name = prev_obj.zones_present[0] + + from services.detection.zones import DEFAULT_ZONES + + zone = next( + ( + z for z in DEFAULT_ZONES + if z.name == zone_name + ), + None, + ) + + if ( + zone + and zone.max_age_override is not None + ): + effective_max_age = ( + zone.max_age_override + ) + + if frames_since > effective_max_age: + self._emit_lifecycle( - TrackState.DEAD, tid, + TrackState.DEAD, + tid, prev_obj.zones_present, prev_obj.dwell_time_seconds, ) - del self._active_tracks[tid] - logger.info(f"Track DEAD: #{tid} after {prev_obj.dwell_time_seconds:.1f}s") - # ── Cleanup expired ReID embeddings ────────────────── - expired_ids = [ - tid for tid, data in self._lost_embeddings.items() - if self._frame_id - data["last_seen"] > self.max_age - ] - - for tid in expired_ids: - del self._lost_embeddings[tid] - - return TrackedFrame( + logger.info( + f"Track DEAD: #{tid} after " + f"{prev_obj.dwell_time_seconds:.1f}s" + ) + # ── Cleanup expired ReID embeddings ────────────────── + expired_ids = [ + tid for tid, data in self._lost_embeddings.items() + if self._frame_id - data["last_seen"] > self.max_age + ] + + for tid in expired_ids: + del self._lost_embeddings[tid] + return TrackedFrame( frame_id = self._frame_id, camera_id = self.camera_id, tracks = tracked_objects, From f99282667e87d21c357af67aec02bac84d9614da Mon Sep 17 00:00:00 2001 From: Samata Bag <2024itb047.samata@students.iiests.ac.in> Date: Sat, 16 May 2026 23:06:24 +0530 Subject: [PATCH 2/9] Resolve merge conflicts for per-zone max_age overrides --- services/tracking/tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/tracking/tracker.py b/services/tracking/tracker.py index cf7b875..1b28077 100644 --- a/services/tracking/tracker.py +++ b/services/tracking/tracker.py @@ -274,10 +274,10 @@ def update( if ( zone - and zone.max_age_override is not None + and zone.get("max_age_override") is not None ): effective_max_age = ( - zone.max_age_override + zone.get("max_age_override") ) if frames_since > effective_max_age: From f15a882581209257e09b2e2b5237a0b32ea6eda6 Mon Sep 17 00:00:00 2001 From: Samata Bag <2024itb047.samata@students.iiests.ac.in> Date: Sat, 16 May 2026 23:16:58 +0530 Subject: [PATCH 3/9] Fix unused import lint issue --- services/memory/memory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/memory/memory.py b/services/memory/memory.py index 5de85ef..6ceac8b 100644 --- a/services/memory/memory.py +++ b/services/memory/memory.py @@ -31,7 +31,7 @@ import json import logging -import time + from typing import Optional import numpy as np From d99707dd14a12f3bec7c862b85b639bd788eb8a7 Mon Sep 17 00:00:00 2001 From: Samata Bag <2024itb047.samata@students.iiests.ac.in> Date: Tue, 19 May 2026 11:31:24 +0530 Subject: [PATCH 4/9] fix(tracker): restore tracked objects and add zone precedence --- services/tracking/tracker.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/services/tracking/tracker.py b/services/tracking/tracker.py index 00d0dba..c92ef6d 100644 --- a/services/tracking/tracker.py +++ b/services/tracking/tracker.py @@ -166,7 +166,19 @@ def update( cx = (x1 + x2) / 2 cy = (y1 + y2) / 2 - zones = [z.name for z in get_zones_for_point(cx, cy)] + matched_zones = get_zones_for_point(cx, cy) + + ZONE_PRIORITY = { + "keypad_area": 2, + "restricted_door": 1, + } + + matched_zones.sort( + key=lambda z: ZONE_PRIORITY.get(z.name, 0), + reverse=True, + ) + + zones = [z.name for z in matched_zones] if tid not in self._known_ids: self._known_ids.add(tid) @@ -183,8 +195,6 @@ def update( ) prev = self._active_tracks.get(tid) - dwell_frames = (prev.dwell_time_frames + 1) if prev else 1 - dwell_secs = dwell_frames / self.fps dwell_frames = ( prev.dwell_time_frames + 1 @@ -211,6 +221,7 @@ def update( ) self._active_tracks[tid] = obj + tracked_objects.append(obj) current_ids.add(tid) From 47c26f46c9165f0c772c13713829c68d17ee6412 Mon Sep 17 00:00:00 2001 From: Samata Bag <2024itb047.samata@students.iiests.ac.in> Date: Tue, 19 May 2026 11:52:27 +0530 Subject: [PATCH 5/9] fix: remove duplicate definitions --- services/memory/memory.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/memory/memory.py b/services/memory/memory.py index 77abde0..21b9e6c 100644 --- a/services/memory/memory.py +++ b/services/memory/memory.py @@ -36,7 +36,11 @@ import numpy as np from libs.observability.metrics import redis_write_latency -from libs.schemas.memory import ActionHint, TrackEvent, TrackSequence +from libs.schemas.memory import ( + ActionHint, + TrackEvent, + TrackSequence, +) from libs.schemas.tracking import TrackLifecycleEvent, TrackState from services.tracking.cross_camera_reid import CrossCameraReID From 701bd06eaac85699cf3ad0d39bfbfd6992a068c9 Mon Sep 17 00:00:00 2001 From: Samata Bag <2024itb047.samata@students.iiests.ac.in> Date: Tue, 19 May 2026 12:33:40 +0530 Subject: [PATCH 6/9] fix: resolve lint issues From 37f217016118ec99b0ffcc6b821feefb8396ed9b Mon Sep 17 00:00:00 2001 From: Samata Bag <2024itb047.samata@students.iiests.ac.in> Date: Tue, 19 May 2026 12:43:16 +0530 Subject: [PATCH 7/9] trigger fresh github actions From 947c386f383b46e6a1a109f9529b397170af54fc Mon Sep 17 00:00:00 2001 From: Samata Bag <2024itb047.samata@students.iiests.ac.in> Date: Tue, 19 May 2026 12:49:31 +0530 Subject: [PATCH 8/9] fix: remove ActionHint dependency --- services/memory/memory.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/services/memory/memory.py b/services/memory/memory.py index 21b9e6c..f7b06c6 100644 --- a/services/memory/memory.py +++ b/services/memory/memory.py @@ -37,7 +37,6 @@ from libs.observability.metrics import redis_write_latency from libs.schemas.memory import ( - ActionHint, TrackEvent, TrackSequence, ) @@ -312,7 +311,11 @@ def get_sequence(self, track_id: int, last_n: Optional[int] = None) -> "TrackSeq def get_zone_entry_count(self, track_id: int, zone: str) -> int: seq = self.get_sequence(track_id) - return sum(1 for e in seq.events if e.zone == zone and e.action_hint == ActionHint.ZONE_ENTRY) + return sum( + 1 + for e in seq.events + if e.zone == zone and getattr(e, "action_hint", None) == "ZONE_ENTRY" + ) def get_active_track_ids(self, camera_id: str) -> set[int]: members = self._r.smembers(self._active_key(camera_id)) From 6533985816bde1fc6c3d2acc327efcb5359b77c8 Mon Sep 17 00:00:00 2001 From: Samata Bag <2024itb047.samata@students.iiests.ac.in> Date: Tue, 19 May 2026 13:00:30 +0530 Subject: [PATCH 9/9] fix: sync memory lint fixes --- services/memory/memory.py | 56 ++++++++------------------------------- 1 file changed, 11 insertions(+), 45 deletions(-) diff --git a/services/memory/memory.py b/services/memory/memory.py index 8d9a57b..c459510 100644 --- a/services/memory/memory.py +++ b/services/memory/memory.py @@ -315,52 +315,18 @@ def get_sequence(self, track_id: int, last_n: Optional[int] = None) -> "TrackSeq zones_visited=zones_visited, ) - def store_event(self, event) -> None: - """ - Append a ``TrackEvent`` to the ring buffer for its track. - - Enforces the ``MAX_EVENTS_PER_TRACK`` cap by trimming the oldest - entry whenever the list exceeds the limit. Also maintains the - zones-visited set, per-zone entry counts, and the active-tracks set. - - Args: - event: ``TrackEvent`` instance (from ``libs.schemas.memory``). - """ - from libs.schemas.memory import ActionHint - - key = self._seq_key(event.track_id) - serialised = event.model_dump_json() - - pipe = self._r.pipeline() - pipe.rpush(key, serialised) - pipe.ltrim(key, -MAX_EVENTS_PER_TRACK, -1) - pipe.sadd(self._active_key(), str(event.track_id)) - - if event.zone: - pipe.sadd(self._zones_key(event.track_id), event.zone) - if event.action_hint == ActionHint.ZONE_ENTRY: - pipe.incr(self._zone_count_key(event.track_id, event.zone)) - - pipe.execute() - - def get_sequence(self, track_id: int, last_n: Optional[int] = None): - """ - Return a ``TrackSequence`` for the given track. - - Args: - track_id: Track identifier. - last_n: If given, return only the most recent *n* events. - - Returns: - ``TrackSequence`` (empty if the track has no stored events). - """ - from libs.schemas.memory import TrackEvent, TrackSequence - - key = self._seq_key(track_id) - raw_list = self._r.lrange(key, -last_n, -1) if last_n else self._r.lrange(key, 0, -1) + def get_zone_entry_count(self, track_id: int, zone: str) -> int: + seq = self.get_sequence(track_id) + return sum( + 1 + for e in seq.events + if e.zone == zone and getattr(e, "action_hint", None) == "ZONE_ENTRY" + ) - events: list[TrackEvent] = [] - for raw in raw_list: + def get_active_track_ids(self, camera_id: str) -> set[int]: + members = self._r.smembers(self._active_key(camera_id)) + result: set[int] = set() + for m in members: try: data = json.loads(raw if isinstance(raw, str) else raw.decode()) events.append(TrackEvent(**data))