raven/tests/test_features/test_gaze.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

57 lines
1.5 KiB
Python

import numpy as np
from merlin.features.gaze import GazeEstimator, GazeDirection
_LEFT_IRIS = 468
_RIGHT_IRIS = 473
_LEFT_INNER = 133
_RIGHT_OUTER = 263
def _face(n: int = 478) -> np.ndarray:
return np.zeros((n, 3), dtype=np.float32)
def _set_gaze_center(face: np.ndarray) -> None:
"""Iris centers at midpoint of eye span → center gaze."""
face[_LEFT_INNER] = [0.3, 0.5, 0.0]
face[_RIGHT_OUTER] = [0.7, 0.5, 0.0]
mid = (face[_LEFT_INNER] + face[_RIGHT_OUTER]) / 2.0
face[_LEFT_IRIS] = mid.copy()
face[_RIGHT_IRIS] = mid.copy()
def _set_gaze_left(face: np.ndarray) -> None:
"""Iris centers shifted left relative to eye span."""
face[_LEFT_INNER] = [0.3, 0.5, 0.0]
face[_RIGHT_OUTER] = [0.7, 0.5, 0.0]
face[_LEFT_IRIS] = [0.35, 0.5, 0.0]
face[_RIGHT_IRIS] = [0.35, 0.5, 0.0]
def test_center_gaze_label():
face = _face()
_set_gaze_center(face)
g = GazeEstimator().estimate(face)
assert g.label == "center"
def test_left_gaze_label():
face = _face()
_set_gaze_left(face)
g = GazeEstimator().estimate(face)
assert g.label == "left"
def test_zero_eye_width_returns_center():
"""Degenerate case: all landmarks at same point → center."""
face = _face()
g = GazeEstimator().estimate(face)
assert g.dx == 0.0 and g.dy == 0.0
def test_gazeresult_is_frozen():
g = GazeDirection(dx=0.1, dy=0.2)
import pytest
with pytest.raises((AttributeError, TypeError)):
g.dx = 0.5