fix(edge): forget per-track state on lost tracks (FIX 5)
fall._states, immobility._still_since/_fired, zone._inside_since/_fired, and engine._last_emit were never pruned when a track was lost (ghost already pruned via on_lost). On a 24/7 edge device this is unbounded growth as track IDs keep incrementing. Add forget(track_id) to FallDetector, ImmobilityWatchdog, and ZoneIntrusionDetector (drops that track's per-track keys/sets). EventEngine.forget(track_id) calls each sub-detector's forget and removes _last_emit cooldown keys whose tuple's second element is that track_id (covers FALL/NO_MOTION/IMMOBILITY/zone/GHOST — GHOST keys are (GHOST, track_id) after FIX 4). EventEngine.process now calls self.forget(tr.track_id) for every track in lost_tracks after emitting, since ghost.on_lost already consumed that track's ghost-relevant state at the top of process(). app.py run_once already calls pool.forget(tr.track_id) for lost tracks as part of FIX 1, completing the cleanup chain (smoother pool + detector state + dedup cache). Tests: tests/test_engine.py adds test_forget_prunes_state_on_lost — fires a FALL event, then passes the track as lost, and asserts engine._last_emit no longer has that track's keys and fall._states no longer has the track id.
This commit is contained in:
parent
f7266f5980
commit
752aabaff1
5 changed files with 39 additions and 0 deletions
|
|
@ -36,6 +36,9 @@ class FallDetector:
|
|||
self.cfg = cfg
|
||||
self._states: dict[int, _State] = {}
|
||||
|
||||
def forget(self, track_id: int) -> None:
|
||||
self._states.pop(track_id, None)
|
||||
|
||||
def _prone(self, kps, bbox, st: _State) -> tuple[bool, dict]:
|
||||
angle = torso_angle_deg(kps)
|
||||
ar = bbox.aspect
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ class ImmobilityWatchdog:
|
|||
self._still_since: dict[int, float] = {}
|
||||
self._fired: set[int] = set()
|
||||
|
||||
def forget(self, track_id: int) -> None:
|
||||
self._still_since.pop(track_id, None)
|
||||
self._fired.discard(track_id)
|
||||
|
||||
def _displacement(self, track: Track) -> float:
|
||||
if len(track.frames) < 2:
|
||||
return 1.0
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ class ZoneIntrusionDetector:
|
|||
self._inside_since: dict[tuple[int, str], float] = {}
|
||||
self._fired: set[tuple[int, str]] = set()
|
||||
|
||||
def forget(self, track_id: int) -> None:
|
||||
self._inside_since = {k: v for k, v in self._inside_since.items() if k[0] != track_id}
|
||||
self._fired = {k for k in self._fired if k[0] != track_id}
|
||||
|
||||
def _foot(self, track: Track) -> tuple[float, float]:
|
||||
kps = track.latest.keypoints
|
||||
a, b = kps[L_ANKLE], kps[R_ANKLE]
|
||||
|
|
|
|||
|
|
@ -32,6 +32,14 @@ class EventEngine:
|
|||
self._last_emit[key] = now
|
||||
return True
|
||||
|
||||
def forget(self, track_id: int) -> None:
|
||||
"""트랙 소실 시 하위 감지기 + dedup 캐시의 트랙별 상태를 정리 (24/7 장치 메모리 누수 방지)."""
|
||||
self.fall.forget(track_id)
|
||||
self.immobility.forget(track_id)
|
||||
self.zone.forget(track_id)
|
||||
self._last_emit = {k: v for k, v in self._last_emit.items()
|
||||
if not (len(k) >= 2 and k[1] == track_id)}
|
||||
|
||||
def _mk(self, etype: EventType, now: float, track_id=None, zone_id=None,
|
||||
metrics=None, spatial=None) -> Event:
|
||||
m = metrics or {}
|
||||
|
|
@ -73,4 +81,8 @@ class EventEngine:
|
|||
spatial=g["spatial"],
|
||||
metrics={"last_state": g["last_state"],
|
||||
"waited_s": g["waited_s"]}))
|
||||
|
||||
# ghost.on_lost already consumed lost tracks' state above, so forgetting here is safe.
|
||||
for tr in lost_tracks:
|
||||
self.forget(tr.track_id)
|
||||
return events
|
||||
|
|
|
|||
|
|
@ -69,6 +69,22 @@ def test_no_motion_event_carries_duration():
|
|||
assert e.duration_s == e.metrics["immobility_s"]
|
||||
|
||||
|
||||
def test_forget_prunes_state_on_lost():
|
||||
eng = _engine()
|
||||
tr = Track(1, deque(maxlen=90))
|
||||
_lying_track(tr, 0.0); eng.process([tr], [], now=0.0)
|
||||
_lying_track(tr, 0.6)
|
||||
events = eng.process([tr], [], now=0.6) # FALL fires
|
||||
assert any(e.event_type == EventType.FALL for e in events)
|
||||
assert (EventType.FALL, 1) in eng._last_emit
|
||||
assert 1 in eng.fall._states
|
||||
|
||||
# track is now lost -> engine must prune its per-track state
|
||||
eng.process([], [tr], now=1.0)
|
||||
assert not any(len(k) >= 2 and k[1] == 1 for k in eng._last_emit)
|
||||
assert 1 not in eng.fall._states
|
||||
|
||||
|
||||
def test_allow_cooldown_primitive():
|
||||
eng = _engine() # EngineConfig(camera_id="cam-01", cooldown_s=30.0)
|
||||
key = ("FALL", 1)
|
||||
|
|
|
|||
Loading…
Reference in a new issue