move_detect/tests/test_ghost.py
유창욱 f7266f5980 fix(edge): identity-aware GHOST dedup keyed on track_id (FIX 4)
edge/engine.py keyed the ghost cooldown as
(GHOST, round(x), round(y)) — purely spatial, ignoring track identity.
Two distinct workers collapsing at ~the same pixel within the cooldown
window would suppress each other's ghost event, potentially masking a
second CRITICAL incident.

edge/detectors/ghost.py GhostIncidentDetector.update now includes the
candidate's track_id in each emitted dict. edge/engine.py keys the
cooldown as (EventType.GHOST, track_id) and sets the emitted Event's
track_id accordingly, matching how FALL/NO_MOTION/IMMOBILITY/zone events
are already keyed per-track.

Tests: tests/test_ghost.py asserts ghosts[0]["track_id"] == 1 on the
existing reappear-timeout test.
2026-07-11 22:17:48 +09:00

33 lines
1.4 KiB
Python

from collections import deque
from edge.models import Keypoint, BBox
from edge.tracking import Track, TrackFrame
from edge.detectors.ghost import GhostIncidentDetector, GhostConfig
def _track(tid, x, y, t):
fr = TrackFrame(t, BBox(x, y, x + 20, y + 60), [Keypoint(x, y, 0.9) for _ in range(17)])
return Track(tid, deque([fr], maxlen=90))
def test_lost_dangerous_track_becomes_ghost_after_wait():
d = GhostIncidentDetector(GhostConfig(reappear_s=1.0))
d.note_state(1, "FALLEN")
d.on_lost([_track(1, 100, 100, 0.0)], now=0.0)
assert d.update([], now=0.5) == [] # 아직 대기
ghosts = d.update([], now=1.5) # 재출현 없음 > 1.0s
assert len(ghosts) == 1
assert ghosts[0]["last_state"] == "FALLEN"
assert ghosts[0]["spatial"] == (110.0, 130.0) # bbox 중심
assert ghosts[0]["track_id"] == 1
def test_non_dangerous_lost_track_is_ignored():
d = GhostIncidentDetector(GhostConfig(reappear_s=1.0))
d.note_state(1, "STANDING")
d.on_lost([_track(1, 100, 100, 0.0)], now=0.0)
assert d.update([], now=2.0) == []
def test_reappearance_near_location_cancels_ghost():
d = GhostIncidentDetector(GhostConfig(reappear_s=1.0, reappear_dist=2.0))
d.note_state(1, "FALLEN")
d.on_lost([_track(1, 100, 100, 0.0)], now=0.0)
# 인근에 새 트랙 재출현
assert d.update([_track(2, 105, 105, 0.5)], now=0.5) == []
assert d.update([], now=2.0) == [] # 취소되어 방출 없음