Three keypoint-trust hardening fixes that reuse the codebase's existing "visible" confidence convention (no new tuning knobs): - P2-9 zone.py: _foot averaged both ankles unconditionally, so a hallucinated ankle (conf~0, often pinned to (0,0)) dragged the foot point into or out of a zone. Now averages only ankles with conf >= 0.2 and falls back to the bbox bottom-centre when neither is trustworthy. - P2-11 geometry.py: head_below_hips used the NOSE regardless of confidence. Now returns False when the nose is below the visibility threshold, so an unconfident/hallucinated nose can't fire the head-below-hips fall cue. The confident-nose path is unchanged (test_fall.py stays green). - P2-10 movenet_pose.py: the CPU MoveNet fallback is single-person, so at a multi-worker site every worker but one is silently invisible to fall/zone/ghost detection. Construction now logs a loud startup WARNING pointing to the Hailo backend. Tests: test_zone (bbox fallback), test_geometry (low-conf nose), and test_movenet_pose (caplog warning). Full python suite: 157 passed, 2 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
63 lines
3.2 KiB
Python
63 lines
3.2 KiB
Python
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)) == []
|