- BlinkDetector: EAR-based blink detection (left/right/both), 6 tests - GazeEstimator: iris-to-eye-corner ratio gaze direction, frozen GazeDirection dataclass, 4 tests - HeadPoseEstimator: velocity-based nod/shake/tilt detection (stateful, no tests — daemon smoke test) - HandGestureDetector: normalize_hand + tip-distance open/pinch/fist classifier (no tests — daemon smoke test) - TDD: blink and gaze followed RED→GREEN cycle; Black applied to all 6 files
73 lines
1.9 KiB
Python
73 lines
1.9 KiB
Python
import numpy as np
|
|
import pytest
|
|
from merlin.features.blink import BlinkDetector, BlinkEvent, eye_aspect_ratio
|
|
|
|
LEFT_EYE = [33, 160, 158, 133, 153, 144]
|
|
RIGHT_EYE = [362, 385, 387, 263, 373, 380]
|
|
|
|
|
|
def _face(n: int = 478) -> np.ndarray:
|
|
return np.zeros((n, 3), dtype=np.float32)
|
|
|
|
|
|
def _set_eye_open(face: np.ndarray, indices: list[int]) -> None:
|
|
"""Set 6 EAR landmarks so EAR = ~0.35 (open eye)."""
|
|
p1, p2, p3, p4, p5, p6 = indices
|
|
face[p1] = [0.0, 0.0, 0.0]
|
|
face[p4] = [1.0, 0.0, 0.0]
|
|
face[p2] = [0.25, 0.2, 0.0]
|
|
face[p6] = [0.25, -0.2, 0.0]
|
|
face[p3] = [0.75, 0.2, 0.0]
|
|
face[p5] = [0.75, -0.2, 0.0]
|
|
|
|
|
|
def _set_eye_closed(face: np.ndarray, indices: list[int]) -> None:
|
|
"""Set 6 EAR landmarks so EAR ≈ 0 (closed)."""
|
|
for i in indices:
|
|
face[i] = [0.0, 0.0, 0.0]
|
|
|
|
|
|
def test_ear_open_eye():
|
|
face = _face()
|
|
_set_eye_open(face, LEFT_EYE)
|
|
ear = eye_aspect_ratio(face, LEFT_EYE)
|
|
assert ear > 0.20
|
|
|
|
|
|
def test_ear_closed_eye():
|
|
face = _face()
|
|
_set_eye_closed(face, LEFT_EYE)
|
|
ear = eye_aspect_ratio(face, LEFT_EYE)
|
|
assert ear < 0.05
|
|
|
|
|
|
def test_no_blink_when_both_open():
|
|
detector = BlinkDetector(threshold=0.20)
|
|
face = _face()
|
|
_set_eye_open(face, LEFT_EYE)
|
|
_set_eye_open(face, RIGHT_EYE)
|
|
assert detector.detect(face) is None
|
|
|
|
|
|
def test_left_blink_detected():
|
|
detector = BlinkDetector(threshold=0.20)
|
|
face = _face()
|
|
_set_eye_closed(face, LEFT_EYE)
|
|
_set_eye_open(face, RIGHT_EYE)
|
|
assert detector.detect(face) == BlinkEvent.LEFT
|
|
|
|
|
|
def test_right_blink_detected():
|
|
detector = BlinkDetector(threshold=0.20)
|
|
face = _face()
|
|
_set_eye_open(face, LEFT_EYE)
|
|
_set_eye_closed(face, RIGHT_EYE)
|
|
assert detector.detect(face) == BlinkEvent.RIGHT
|
|
|
|
|
|
def test_both_blink_detected():
|
|
detector = BlinkDetector(threshold=0.20)
|
|
face = _face()
|
|
_set_eye_closed(face, LEFT_EYE)
|
|
_set_eye_closed(face, RIGHT_EYE)
|
|
assert detector.detect(face) == BlinkEvent.BOTH
|