Files
livekit-cameras/tests/test_rally_face.py
T
root 95bb7c50ee feat: point the fleet at wss LiveKit room uwh-telhai
Publish and subscribe over wss://livekit.uni-wh.de:7800 and refuse
cleartext ws://. Conference room is uwh-telhai. Includes the uncommitted
encoded H.264 publish path, Rally hairpin, and KMS wall overlay.
2026-10-11 00:48:50 +00:00

173 lines
5.1 KiB
Python

"""YuNet face gates for Rally follow — reject ceiling/stand/noise."""
from __future__ import annotations
import numpy as np
from rally_follow import FaceGate, accept_yunet_face, select_yunet_face
def _face(
*,
x=400.0,
y=200.0,
w=80.0,
h=100.0,
score=0.93,
re=None,
le=None,
nose=None,
rm=None,
lm=None,
) -> np.ndarray:
"""YuNet 15-vector: box + 5 landmarks + score, in 960x540 detect space."""
if re is None:
re = (x + 0.30 * w, y + 0.38 * h)
if le is None:
le = (x + 0.70 * w, y + 0.38 * h)
if nose is None:
nose = (x + 0.50 * w, y + 0.55 * h)
if rm is None:
rm = (x + 0.35 * w, y + 0.78 * h)
if lm is None:
lm = (x + 0.65 * w, y + 0.78 * h)
row = np.zeros(15, dtype=np.float32)
row[0:4] = (x, y, w, h)
row[4:6] = re
row[6:8] = le
row[8:10] = nose
row[10:12] = rm
row[12:14] = lm
row[14] = score
return row
FW, FH = 960, 540
def test_good_face_accepted():
assert accept_yunet_face(_face(), FW, FH) is True
def test_tiny_blob_rejected():
# Live FP: 25x34 @ 0.805, nose not between the eyes.
row = _face(x=512, y=250, w=25, h=34, score=0.805,
re=(528.5, 264), le=(532.1, 263), nose=(536.1, 271),
rm=(529.7, 277), lm=(532.1, 278))
assert accept_yunet_face(row, FW, FH) is False
def test_low_score_rejected():
assert accept_yunet_face(_face(score=0.61), FW, FH) is False
def test_ceiling_band_rejected():
row = _face(x=400, y=2, w=80, h=100)
assert accept_yunet_face(row, FW, FH) is False
def test_stand_band_rejected():
row = _face(x=400, y=470, w=80, h=100)
assert accept_yunet_face(row, FW, FH) is False
def test_nose_not_between_eyes_rejected():
row = _face(re=(430, 238), le=(450, 238), nose=(470, 255))
assert accept_yunet_face(row, FW, FH) is False
def test_upside_down_landmarks_rejected():
row = _face(
re=(424, 270), le=(456, 270), nose=(440, 240),
rm=(428, 220), lm=(452, 220),
)
assert accept_yunet_face(row, FW, FH) is False
def test_select_skips_large_invalid_for_smaller_valid():
junk = _face(x=10, y=2, w=80, h=100, score=0.99) # ceiling
good = _face(x=400, y=200, w=80, h=100, score=0.88)
picked = select_yunet_face(np.stack([junk, good]), FW, FH)
assert picked is not None
assert float(picked[0]) == 400.0
def test_select_empty():
assert select_yunet_face(None, FW, FH) is None
assert select_yunet_face(np.zeros((0, 15)), FW, FH) is None
def test_face_gate_needs_confirm():
gate = FaceGate(need=3, max_jump=0.18)
box = (400, 200, 80, 100)
assert gate.update(box, 960, 540) is None
assert gate.update(box, 960, 540) is None
assert gate.update(box, 960, 540) == box
def test_face_gate_jump_resets():
gate = FaceGate(need=3, max_jump=0.18)
a = (400, 200, 80, 100)
b = (800, 400, 80, 100)
assert gate.update(a, 960, 540) is None
assert gate.update(a, 960, 540) is None
assert gate.update(b, 960, 540) is None # new target
assert gate.update(b, 960, 540) is None
assert gate.update(b, 960, 540) == b
def test_face_gate_loss_clears():
gate = FaceGate(need=2, max_jump=0.18)
box = (400, 200, 80, 100)
gate.update(box, 960, 540)
assert gate.update(box, 960, 540) == box
assert gate.update(None, 960, 540) is None
assert gate.update(box, 960, 540) is None
def test_profile_face_accepted():
"""Head turned: eyes cluster, nose not between them — still a face."""
row = _face(
x=400, y=200, w=80, h=100, score=0.90,
re=(418, 238), le=(426, 237),
nose=(410, 255),
rm=(416, 278), lm=(428, 277),
)
assert accept_yunet_face(row, FW, FH) is True
def test_head_hold_keeps_box_on_roi_motion():
from rally_follow import HeadHold
hold = HeadHold(hold_s=1.0)
box = (400, 200, 80, 100)
t = 10.0
assert hold.update(box, moving=False, now=t) == box
assert hold.update(None, moving=True, now=t + 0.3) == box
assert hold.update(None, moving=False, now=t + 0.4) == box # coast
assert hold.update(None, moving=False, now=t + 2.0) is None
def test_roi_motion_ignores_outside_last_box():
from rally_follow import roi_motion
prev = np.zeros((540, 960), dtype=np.uint8)
now = prev.copy()
now[10:40, 10:40] = 200 # motion far from the face
assert roi_motion(prev, now, (400, 200, 80, 100)) is False
now2 = prev.copy()
now2[210:260, 420:460] = 200
assert roi_motion(prev, now2, (400, 200, 80, 100)) is True
def test_stale_hold_freezes_does_not_keep_panning():
"""Last image-space box is not a pan target — that derails as the camera moves."""
from rally_follow import lock_action, steer_speeds
left = (80, 400, 160, 200)
pan, tilt = steer_speeds(left, 1920, 1080, deadband=0.14)
assert pan == -1
assert lock_action(confirmed=left, held=left) == "steer"
assert lock_action(confirmed=None, held=left) == "freeze"
assert lock_action(confirmed=None, held=None) == "lost"
cx = 1920 / 2 - 40
mid = (int(cx - 80), 440, 160, 200)
pan, tilt = steer_speeds(mid, 1920, 1080, deadband=0.14)
assert pan == 0 and tilt == 0