""" Blink detection from MediaPipe Face Mesh landmarks. Uses the Eye Aspect Ratio (EAR) method: when the eye closes, the vertical landmark distances shrink relative to the horizontal width, driving EAR toward 0. """ from __future__ import annotations from enum import Enum from typing import Optional import numpy as np _LEFT_EYE = [33, 160, 158, 133, 153, 144] _RIGHT_EYE = [362, 385, 387, 263, 373, 380] class BlinkEvent(str, Enum): LEFT = "left_blink" RIGHT = "right_blink" BOTH = "both_blink" def eye_aspect_ratio(landmarks: np.ndarray, indices: list[int]) -> float: """ EAR = (||p2-p6|| + ||p3-p5||) / (2 * ||p1-p4||) ~0.3 for open eye, ~0.0 for closed. """ p1, p2, p3, p4, p5, p6 = [landmarks[i] for i in indices] vert_a = np.linalg.norm(p2 - p6) vert_b = np.linalg.norm(p3 - p5) horiz = np.linalg.norm(p1 - p4) if horiz < 1e-6: return 0.0 return float((vert_a + vert_b) / (2.0 * horiz)) class BlinkDetector: """Detect left, right, or both-eye blinks from face mesh landmarks.""" def __init__(self, threshold: float = 0.20) -> None: self._threshold = threshold def detect(self, face_landmarks: np.ndarray) -> Optional[BlinkEvent]: """ Args: face_landmarks: (478, 3) float32 — MediaPipe Face Mesh with iris refinement. Returns: BlinkEvent if a blink is detected, else None. """ left_closed = eye_aspect_ratio(face_landmarks, _LEFT_EYE) < self._threshold right_closed = eye_aspect_ratio(face_landmarks, _RIGHT_EYE) < self._threshold if left_closed and right_closed: return BlinkEvent.BOTH if left_closed: return BlinkEvent.LEFT if right_closed: return BlinkEvent.RIGHT return None