import numpy as np from edge.config import load_config from edge.models import Person, BBox, Keypoint from edge.detectors.zone import Zone from edge.runtime import PipelineRuntime 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, e, snaps): self.events.append(e); return True def test_step_runs_pipeline_and_emits_fall(): cfg = load_config({"FALL_PRONE_S": "0.5"}) sink = _Sink() rt = PipelineRuntime(cfg, _Source(), _Pose(), sink) rt.step(now=0.0) rt.step(now=0.6) from edge.events import EventType assert any(e.event_type == EventType.FALL for e in sink.events) def test_apply_zones_hotswaps(): cfg = load_config({}) rt = PipelineRuntime(cfg, _Source(), _Pose(), _Sink()) rt.apply_zones([Zone("z1", "restricted", [(0, 0), (9, 0), (9, 9)])]) assert rt.zone_detector.zones[0].zone_id == "z1" def test_infer_every_skips_inference_but_still_drains_the_camera(): # T2 저하 티어: 추론은 N틱마다. 스킵 틱에도 프레임은 읽어야 한다(v4l2 버퍼가 밀리면 # 몇 초 전 프레임이 올라와 모든 경보가 늦어진다). class _CountingSource: def __init__(self): self.reads = 0 def read(self): self.reads += 1 return True, np.full((360, 640, 3), 128, np.uint8) class _CountingPose: def __init__(self): self.infers = 0 def infer(self, frame): self.infers += 1 return [_lying_person()] src, pose = _CountingSource(), _CountingPose() cfg = load_config({"INFER_EVERY": "3"}) rt = PipelineRuntime(cfg, src, pose, _Sink()) for t in range(6): rt.step(float(t)) assert src.reads == 6 # 매 틱 프레임을 뽑아 버퍼를 비운다 assert pose.infers == 2 # 추론은 3틱마다 def test_skipped_tick_never_puts_unanalyzed_frames_in_the_evidence_ring(): # 스킵 틱에는 얼굴 박스가 없다. 그 프레임이 링에 들어가면 증거 타임라인에 블러되지 않은 # 얼굴이 실려 나간다(PIPA). 링에는 추론된 프레임만 있어야 한다. cfg = load_config({"INFER_EVERY": "4"}) rt = PipelineRuntime(cfg, _Source(), _Pose(), _Sink()) for t in range(8): rt.step(float(t)) assert len(rt.ring.frames) == 2 # 8틱 중 추론된 2틱만 assert all(boxes for (_t, _f, boxes) in rt.ring.frames) # 전부 얼굴 박스를 동반 def test_dead_camera_still_reports_no_frame_on_a_skipped_tick(): # 스킵 틱이 죽은 카메라를 '정상'으로 보고하면 감시 공백이 초록으로 보인다. class _DeadSource: def read(self): return False, None cfg = load_config({"INFER_EVERY": "2"}) rt = PipelineRuntime(cfg, _DeadSource(), _Pose(), _Sink()) assert rt.step(1.0) is None # 스킵 틱(1 % 2 != 0)에도 None