""" Head pose estimation — detect nod, shake, and tilt from Face Mesh landmarks. Velocity-based: compares nose tip position across consecutive frames. """ from __future__ import annotations from enum import Enum from typing import Optional import numpy as np _NOSE_TIP = 1 _L_CHEEK = 234 _R_CHEEK = 454 NOD_THRESHOLD = 0.015 SHAKE_THRESHOLD = 0.015 TILT_THRESHOLD = 0.012 class HeadGesture(str, Enum): NOD = "head_nod" SHAKE = "head_shake" TILT_LEFT = "head_tilt_left" TILT_RIGHT = "head_tilt_right" class HeadPoseEstimator: """Detect head gestures by comparing nose tip position across frames.""" def __init__(self) -> None: self._prev: Optional[np.ndarray] = None def update(self, face_landmarks: np.ndarray) -> Optional[HeadGesture]: """ Args: face_landmarks: (478, 3) float32 — current frame. Returns: HeadGesture if detected, else None. """ nose = face_landmarks[_NOSE_TIP] if self._prev is None: self._prev = nose.copy() return None delta = nose - self._prev self._prev = nose.copy() dy = float(delta[1]) dx = float(delta[0]) l_cheek = face_landmarks[_L_CHEEK] r_cheek = face_landmarks[_R_CHEEK] roll = float(l_cheek[1] - r_cheek[1]) if abs(dy) > NOD_THRESHOLD and abs(dy) > abs(dx): return HeadGesture.NOD if abs(dx) > SHAKE_THRESHOLD and abs(dx) > abs(dy): return HeadGesture.SHAKE if abs(roll) > TILT_THRESHOLD: return HeadGesture.TILT_LEFT if roll > 0 else HeadGesture.TILT_RIGHT return None