raven/tests/test_features/test_blink.py
pyr0ball 0dcc25164d feat(features): implement BlinkDetector, GazeEstimator, HeadPoseEstimator, HandGestureDetector
- 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
2026-04-26 21:13:59 -07:00

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