from collections import deque from edge.models import Keypoint, BBox from edge.tracking import Track, TrackFrame from edge.detectors.zone import point_in_polygon, Zone, ZoneIntrusionDetector, ZoneConfig SQUARE = [(0, 0), (100, 0), (100, 100), (0, 100)] def test_point_in_polygon_basic(): assert point_in_polygon(50, 50, SQUARE) is True assert point_in_polygon(150, 50, SQUARE) is False def _track_with_ankles(ax, ay, t): kps = [Keypoint(ax, ay, 0.9) for _ in range(17)] kps[15] = Keypoint(ax, ay, 0.9) # L_ANKLE kps[16] = Keypoint(ax, ay, 0.9) # R_ANKLE return Track(1, deque([TrackFrame(t, BBox(ax - 10, ay - 60, ax + 10, ay), kps)], maxlen=90)) def test_intrusion_fires_after_debounce(): z = Zone("z1", "restricted", SQUARE) det = ZoneIntrusionDetector([z], ZoneConfig(debounce_s=0.5)) tr = _track_with_ankles(50, 50, 0.0) assert det.update(tr, now=0.0) == [] tr = _track_with_ankles(50, 50, 0.6) out = det.update(tr, now=0.6) assert out == [{"zone_id": "z1", "level": "restricted"}] def test_leaving_zone_resets(): z = Zone("z1", "warning", SQUARE) det = ZoneIntrusionDetector([z], ZoneConfig(debounce_s=0.5)) det.update(_track_with_ankles(50, 50, 0.0), now=0.0) det.update(_track_with_ankles(200, 200, 0.6), now=0.6) # 이탈 out = det.update(_track_with_ankles(50, 50, 1.0), now=1.0) # 재진입, 디바운스 재시작 assert out == [] def _track_lowconf_ankles(bbox, t): kps = [Keypoint(0, 0, 0.0) for _ in range(17)] # every keypoint hallucinated at the origin return Track(1, deque([TrackFrame(t, bbox, kps)], maxlen=90)) def test_foot_falls_back_to_bbox_when_ankles_low_confidence(): # Both ankles are hallucinated at (0,0) with conf 0. Averaging them would pin the foot to the # image origin — outside any centred zone. The detector must instead use the bbox # bottom-centre, where the worker actually stands, so the intrusion still fires. z = Zone("z1", "restricted", SQUARE) det = ZoneIntrusionDetector([z], ZoneConfig(debounce_s=0.0)) bbox = BBox(40, 10, 60, 50) # bottom-centre = (cx=50, y2=50), inside SQUARE out = det.update(_track_lowconf_ankles(bbox, 0.0), now=0.0) assert out == [{"zone_id": "z1", "level": "restricted"}] def test_zone_uses_normalized_coordinates_when_frame_size_given(): # The dashboard stores zones as 0..1 of the image (it stretches the snapshot to a fixed # 960x540 canvas). The edge foot is camera-native pixels. Normalizing the foot by the # frame size makes the two comparable regardless of camera resolution — without this, # every zone is silently misplaced. zn = Zone("z1", "restricted", [(0.4, 0.4), (0.6, 0.4), (0.6, 0.6), (0.4, 0.6)]) det = ZoneIntrusionDetector([zn], ZoneConfig(debounce_s=0.0)) # foot at the centre of a 1280x720 frame -> (0.5, 0.5) -> inside the 0.4..0.6 zone inside = det.update(_track_with_ankles(640, 360, 0.0), now=0.0, frame_wh=(1280, 720)) assert inside == [{"zone_id": "z1", "level": "restricted"}] # foot near the corner -> ~(0.008, 0.014) -> outside det2 = ZoneIntrusionDetector([zn], ZoneConfig(debounce_s=0.0)) assert det2.update(_track_with_ankles(10, 10, 0.0), now=0.0, frame_wh=(1280, 720)) == []