from collections import deque from edge.models import Keypoint, BBox from edge.tracking import Track, TrackFrame from edge.geometry import arm_elevation_deg, knee_flexion_deg from edge.detectors.ergonomics import ErgoMonitor, ErgoConfig def _kps(overrides=None, conf=0.9): """직립 자세 기본형: 몸통 수직, 팔 내림, 다리 곧게. overrides로 관절만 바꾼다.""" base = { 0: (50, 40), # nose 5: (40, 100), 6: (60, 100), # shoulders 7: (40, 140), 8: (60, 140), # elbows (아래로 = 팔 내림) 11: (40, 200), 12: (60, 200), # hips 13: (40, 260), 14: (60, 260), # knees 15: (40, 320), 16: (60, 320), # ankles } if overrides: base.update(overrides) kps = [] for i in range(17): x, y = base.get(i, (50, 150)) kps.append(Keypoint(x, y, conf)) return kps def _feed(mon, track, kps, t): track.frames.append(TrackFrame(t, BBox(20, 30, 80, 330), kps)) mon.update(track, now=t) def test_arm_elevation_down_vs_raised(): assert arm_elevation_deg(_kps()) < 20.0 # 팔 내림 raised = _kps({7: (40, 60), 8: (60, 60)}) # 팔꿈치가 어깨 위 assert arm_elevation_deg(raised) > 90.0 low_conf = [Keypoint(0, 0, 0.1) for _ in range(17)] assert arm_elevation_deg(low_conf) is None # 측정 불가 ≠ 0° def test_knee_flexion_straight_vs_squat(): assert knee_flexion_deg(_kps()) > 160.0 # 곧게 섬 squat = _kps({13: (70, 220), 14: (90, 220), 15: (40, 230), 16: (60, 230)}) assert knee_flexion_deg(squat) < 110.0 def test_upright_posture_accrues_no_risk_exposure(): mon = ErgoMonitor(ErgoConfig(report_interval_s=10.0)) tr = Track(1, deque(maxlen=90)) for i in range(11): _feed(mon, tr, _kps(), t=float(i)) rep = mon.report(now=11.0) assert rep is not None assert rep["observed_s"] == 10.0 assert rep["trunk_high_s"] == 0.0 and rep["trunk_mod_s"] == 0.0 assert rep["arm_high_s"] == 0.0 and rep["squat_s"] == 0.0 def test_bent_trunk_accrues_high_band(): mon = ErgoMonitor(ErgoConfig(report_interval_s=10.0)) tr = Track(1, deque(maxlen=90)) # 어깨가 엉덩이 높이 근처까지 숙임: 몸통이 수평에 가깝다 (>60°) bent = _kps({5: (140, 190), 6: (160, 190)}) for i in range(11): _feed(mon, tr, bent, t=float(i)) rep = mon.report(now=11.0) assert rep["trunk_high_s"] == 10.0 def test_detection_gap_does_not_count_as_exposure(): mon = ErgoMonitor(ErgoConfig(report_interval_s=100.0, max_frame_gap_s=2.0)) tr = Track(1, deque(maxlen=90)) _feed(mon, tr, _kps(), t=0.0) _feed(mon, tr, _kps(), t=60.0) # 60초 공백 — 2초 상한으로만 집계 mon._period_start = -100.0 # 주기 강제 만료 rep = mon.report(now=61.0) assert rep["observed_s"] == 2.0 def test_empty_period_reports_nothing(): mon = ErgoMonitor(ErgoConfig(report_interval_s=5.0)) tr = Track(1, deque(maxlen=90)) _feed(mon, tr, [Keypoint(0, 0, 0.1) for _ in range(17)], t=0.0) # 저신뢰만 _feed(mon, tr, [Keypoint(0, 0, 0.1) for _ in range(17)], t=1.0) assert mon.report(now=6.0) is None # 관측 0 → 리포트 없음 assert mon.report(now=7.0) is None # 주기는 리셋됨 def test_low_confidence_core_is_not_measured(): mon = ErgoMonitor(ErgoConfig(report_interval_s=5.0)) tr = Track(1, deque(maxlen=90)) _feed(mon, tr, _kps(conf=0.2), t=0.0) _feed(mon, tr, _kps(conf=0.2), t=1.0) assert mon._acc["observed_s"] == 0.0