- 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
57 lines
1.5 KiB
Python
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
|