feat(edge): EventEngine orchestration with dedup + cooldown
This commit is contained in:
parent
406c0f5b6c
commit
e8e24c266f
2 changed files with 122 additions and 0 deletions
73
edge/engine.py
Normal file
73
edge/engine.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from edge.tracking import Track
|
||||
from edge.events import Event, EventType, severity_for
|
||||
from edge.detectors.fall import FallDetector
|
||||
from edge.detectors.immobility import ImmobilityWatchdog
|
||||
from edge.detectors.ghost import GhostIncidentDetector
|
||||
from edge.detectors.zone import ZoneIntrusionDetector
|
||||
|
||||
|
||||
@dataclass
|
||||
class EngineConfig:
|
||||
camera_id: str = "cam"
|
||||
cooldown_s: float = 30.0
|
||||
|
||||
|
||||
class EventEngine:
|
||||
def __init__(self, fall: FallDetector, immobility: ImmobilityWatchdog,
|
||||
ghost: GhostIncidentDetector, zone: ZoneIntrusionDetector,
|
||||
cfg: EngineConfig):
|
||||
self.fall = fall
|
||||
self.immobility = immobility
|
||||
self.ghost = ghost
|
||||
self.zone = zone
|
||||
self.cfg = cfg
|
||||
self._last_emit: dict[tuple, float] = {}
|
||||
|
||||
def _allow(self, key: tuple, now: float) -> bool:
|
||||
last = self._last_emit.get(key)
|
||||
if last is not None and now - last < self.cfg.cooldown_s:
|
||||
return False
|
||||
self._last_emit[key] = now
|
||||
return True
|
||||
|
||||
def _mk(self, etype: EventType, now: float, track_id=None, zone_id=None,
|
||||
metrics=None, spatial=None) -> Event:
|
||||
return Event(etype, severity_for(etype), self.cfg.camera_id, track_id,
|
||||
zone_id, now, 0.0, metrics or {}, spatial)
|
||||
|
||||
def process(self, active_tracks: list[Track], lost_tracks: list[Track],
|
||||
now: float) -> list[Event]:
|
||||
events: list[Event] = []
|
||||
self.ghost.on_lost(lost_tracks, now)
|
||||
|
||||
for tr in active_tracks:
|
||||
zone_hits = self.zone.update(tr, now)
|
||||
in_zone = bool(self.zone.active_zone_ids(tr.track_id))
|
||||
|
||||
sig, meta = self.fall.update(tr, now)
|
||||
state = self.fall._states.get(tr.track_id)
|
||||
self.ghost.note_state(tr.track_id, state.name if state else "STANDING")
|
||||
if sig == "fall" and self._allow((EventType.FALL, tr.track_id), now):
|
||||
events.append(self._mk(EventType.FALL, now, tr.track_id, metrics=meta))
|
||||
elif sig == "no_motion" and self._allow((EventType.NO_MOTION, tr.track_id), now):
|
||||
events.append(self._mk(EventType.NO_MOTION, now, tr.track_id, metrics=meta))
|
||||
|
||||
fired, imeta = self.immobility.update(tr, now, in_danger_zone=in_zone)
|
||||
if fired and self._allow((EventType.IMMOBILITY, tr.track_id), now):
|
||||
events.append(self._mk(EventType.IMMOBILITY, now, tr.track_id, metrics=imeta))
|
||||
|
||||
for hit in zone_hits:
|
||||
etype = (EventType.ZONE_RESTRICTED if hit["level"] == "restricted"
|
||||
else EventType.ZONE_WARNING)
|
||||
if self._allow((etype, tr.track_id, hit["zone_id"]), now):
|
||||
events.append(self._mk(etype, now, tr.track_id, zone_id=hit["zone_id"]))
|
||||
|
||||
for g in self.ghost.update(active_tracks, now):
|
||||
key = (EventType.GHOST, round(g["spatial"][0]), round(g["spatial"][1]))
|
||||
if self._allow(key, now):
|
||||
events.append(self._mk(EventType.GHOST, now, spatial=g["spatial"],
|
||||
metrics={"last_state": g["last_state"],
|
||||
"waited_s": g["waited_s"]}))
|
||||
return events
|
||||
49
tests/test_engine.py
Normal file
49
tests/test_engine.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from collections import deque
|
||||
from edge.models import Keypoint, BBox
|
||||
from edge.tracking import Track, TrackFrame
|
||||
from edge.events import EventType
|
||||
from edge.detectors.fall import FallDetector, FallConfig
|
||||
from edge.detectors.immobility import ImmobilityWatchdog, ImmobilityConfig
|
||||
from edge.detectors.ghost import GhostIncidentDetector, GhostConfig
|
||||
from edge.detectors.zone import ZoneIntrusionDetector, ZoneConfig
|
||||
from edge.engine import EventEngine, EngineConfig
|
||||
|
||||
def _lying_kps():
|
||||
kps = [Keypoint(150, 300, 0.9) for _ in range(17)]
|
||||
kps[5] = Keypoint(100, 300, 0.9); kps[6] = Keypoint(100, 310, 0.9)
|
||||
kps[11] = Keypoint(200, 300, 0.9); kps[12] = Keypoint(200, 310, 0.9)
|
||||
kps[0] = Keypoint(90, 320, 0.9)
|
||||
return kps
|
||||
|
||||
def _lying_track(track, t):
|
||||
track.frames.append(TrackFrame(t, BBox(90, 290, 210, 320), _lying_kps()))
|
||||
return track
|
||||
|
||||
def _engine():
|
||||
return EventEngine(
|
||||
FallDetector(FallConfig(prone_confirm_s=0.5)),
|
||||
ImmobilityWatchdog(ImmobilityConfig(immobile_s=999)),
|
||||
GhostIncidentDetector(GhostConfig()),
|
||||
ZoneIntrusionDetector([], ZoneConfig()),
|
||||
EngineConfig(camera_id="cam-01", cooldown_s=30.0),
|
||||
)
|
||||
|
||||
def test_fall_produces_fall_event():
|
||||
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)
|
||||
types = [e.event_type for e in events]
|
||||
assert EventType.FALL in types
|
||||
assert events[0].camera_id == "cam-01"
|
||||
|
||||
def test_cooldown_suppresses_duplicate():
|
||||
eng = _engine()
|
||||
tr = Track(1, deque(maxlen=90))
|
||||
_lying_track(tr, 0.0); eng.process([tr], [], now=0.0)
|
||||
_lying_track(tr, 0.6); eng.process([tr], [], now=0.6) # FALL 1회
|
||||
_lying_track(tr, 0.7)
|
||||
again = eng.process([tr], [], now=0.7)
|
||||
assert all(e.event_type != EventType.FALL for e in again)
|
||||
Loading…
Reference in a new issue