From b621627863e73ec6251e46ce68969fdd141d7b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EC=B0=BD=EC=9A=B1?= Date: Sat, 11 Jul 2026 21:46:43 +0900 Subject: [PATCH] feat(edge): main-loop wiring (run_once) + pipeline factory --- edge/app.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++ tests/test_app.py | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 edge/app.py create mode 100644 tests/test_app.py diff --git a/edge/app.py b/edge/app.py new file mode 100644 index 0000000..aad12b4 --- /dev/null +++ b/edge/app.py @@ -0,0 +1,55 @@ +from edge.config import Config +from edge.tracking import IouTracker +from edge.smoothing import KeypointSmoother +from edge.detectors.fall import FallDetector +from edge.detectors.immobility import ImmobilityWatchdog +from edge.detectors.ghost import GhostIncidentDetector +from edge.detectors.zone import ZoneIntrusionDetector +from edge.engine import EventEngine +from edge.snapshot import RingBuffer, FaceBlurrer, Snapshotter +from edge.models import Person, BBox + + +def _head_boxes(persons: list) -> list: + # 배경인물 머리영역 근사: 코 주변 작은 박스 (MVP; 실제는 얼굴 검출 포트로 교체) + boxes = [] + for p in persons: + nose = p.keypoints[0] + boxes.append(BBox(nose.x - 20, nose.y - 25, nose.x + 20, nose.y + 15)) + return boxes + + +def run_once(source, pose, tracker, smoother, engine, ring, snapshotter, sink, now_fn): + ok, frame = source.read() + if not ok: + return [] + now = now_fn() + persons = pose.infer(frame) + smoothed = [Person(p.bbox, smoother.smooth(p.keypoints, now)) for p in persons] + tracks = tracker.update(smoothed, now) + lost = tracker.lost() + ring.push(now, frame) + events = engine.process(tracks, lost, now) + if events: + snaps = snapshotter.capture([frame], [_head_boxes(persons)]) + for e in events: + sink.try_send(e, snaps) + return events + + +def build_pipeline(config: Config, source, pose) -> dict: + return { + "source": source, + "pose": pose, + "tracker": IouTracker(), + "smoother": KeypointSmoother(), + "engine": EventEngine( + FallDetector(config.fall), + ImmobilityWatchdog(config.immobility), + GhostIncidentDetector(config.ghost), + ZoneIntrusionDetector([], config.zone), + config.engine, + ), + "ring": RingBuffer(), + "snapshotter": Snapshotter(FaceBlurrer()), + } diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..76f650d --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,48 @@ +import numpy as np +from edge.models import Person, BBox, Keypoint +from edge.tracking import IouTracker +from edge.smoothing import KeypointSmoother +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 +from edge.snapshot import RingBuffer, FaceBlurrer, Snapshotter +from edge.events import EventType +from edge.app import run_once + +def _lying_person(): + 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 Person(BBox(90, 290, 210, 320), kps) + +class _Source: + def read(self): return True, np.full((360, 640, 3), 128, np.uint8) + +class _Pose: + def infer(self, frame): return [_lying_person()] + +class _Sink: + def __init__(self): self.events = [] + def try_send(self, event, snapshots): + self.events.append(event); return True + +def test_run_once_pipeline_emits_fall_after_prone(): + src, pose = _Source(), _Pose() + tracker = IouTracker() + smoother = KeypointSmoother() + engine = EventEngine( + FallDetector(FallConfig(prone_confirm_s=0.5)), + ImmobilityWatchdog(ImmobilityConfig(immobile_s=999)), + GhostIncidentDetector(GhostConfig()), + ZoneIntrusionDetector([], ZoneConfig()), + EngineConfig(camera_id="cam-01"), + ) + ring = RingBuffer(); snap = Snapshotter(FaceBlurrer()); sink = _Sink() + times = iter([0.0, 0.6]) + def now_fn(): return next(times) + run_once(src, pose, tracker, smoother, engine, ring, snap, sink, now_fn) + run_once(src, pose, tracker, smoother, engine, ring, snap, sink, now_fn) + assert any(e.event_type == EventType.FALL for e in sink.events)