import math import numpy as np _LEFT_EYE_EAR = [33, 160, 158, 133, 153, 145] _RIGHT_EYE_EAR = [362, 385, 387, 263, 373, 380] _LEFT_IRIS_CENTER = 468 _RIGHT_IRIS_CENTER = 473 _LEFT_EYE_INNER = 133 _LEFT_EYE_OUTER = 33 _RIGHT_EYE_INNER = 362 _RIGHT_EYE_OUTER = 263 _LEFT_EYE_TOP = 159 _LEFT_EYE_BOTTOM = 145 _RIGHT_EYE_TOP = 386 _RIGHT_EYE_BOTTOM = 374 _MOUTH_TOP = 13 _MOUTH_BOTTOM = 14 _MOUTH_LEFT = 78 _MOUTH_RIGHT = 308 _MOUTH_UPPER_1 = 82 _MOUTH_UPPER_2 = 312 _MOUTH_LOWER_1 = 87 _MOUTH_LOWER_2 = 317 MAR_YAWN_THRESHOLD = 0.55 def _distance(p1: np.ndarray, p2: np.ndarray) -> float: return float(np.linalg.norm(p1 - p2)) def compute_ear(landmarks: np.ndarray, eye_indices: list[int]) -> float: p1 = landmarks[eye_indices[0], :2] p2 = landmarks[eye_indices[1], :2] p3 = landmarks[eye_indices[2], :2] p4 = landmarks[eye_indices[3], :2] p5 = landmarks[eye_indices[4], :2] p6 = landmarks[eye_indices[5], :2] vertical1 = _distance(p2, p6) vertical2 = _distance(p3, p5) horizontal = _distance(p1, p4) if horizontal < 1e-6: return 0.0 return (vertical1 + vertical2) / (2.0 * horizontal) def compute_avg_ear(landmarks: np.ndarray) -> float: left_ear = compute_ear(landmarks, _LEFT_EYE_EAR) right_ear = compute_ear(landmarks, _RIGHT_EYE_EAR) return (left_ear + right_ear) / 2.0 def compute_gaze_ratio(landmarks: np.ndarray) -> tuple[float, float]: left_iris = landmarks[_LEFT_IRIS_CENTER, :2] left_inner = landmarks[_LEFT_EYE_INNER, :2] left_outer = landmarks[_LEFT_EYE_OUTER, :2] left_top = landmarks[_LEFT_EYE_TOP, :2] left_bottom = landmarks[_LEFT_EYE_BOTTOM, :2] right_iris = landmarks[_RIGHT_IRIS_CENTER, :2] right_inner = landmarks[_RIGHT_EYE_INNER, :2] right_outer = landmarks[_RIGHT_EYE_OUTER, :2] right_top = landmarks[_RIGHT_EYE_TOP, :2] right_bottom = landmarks[_RIGHT_EYE_BOTTOM, :2] left_h_total = _distance(left_inner, left_outer) right_h_total = _distance(right_inner, right_outer) if left_h_total < 1e-6 or right_h_total < 1e-6: return 0.5, 0.5 left_h_ratio = _distance(left_outer, left_iris) / left_h_total right_h_ratio = _distance(right_outer, right_iris) / right_h_total h_ratio = (left_h_ratio + right_h_ratio) / 2.0 left_v_total = _distance(left_top, left_bottom) right_v_total = _distance(right_top, right_bottom) if left_v_total < 1e-6 or right_v_total < 1e-6: return h_ratio, 0.5 left_v_ratio = _distance(left_top, left_iris) / left_v_total right_v_ratio = _distance(right_top, right_iris) / right_v_total v_ratio = (left_v_ratio + right_v_ratio) / 2.0 return float(np.clip(h_ratio, 0, 1)), float(np.clip(v_ratio, 0, 1)) def compute_mar(landmarks: np.ndarray) -> float: top = landmarks[_MOUTH_TOP, :2] bottom = landmarks[_MOUTH_BOTTOM, :2] left = landmarks[_MOUTH_LEFT, :2] right = landmarks[_MOUTH_RIGHT, :2] upper1 = landmarks[_MOUTH_UPPER_1, :2] lower1 = landmarks[_MOUTH_LOWER_1, :2] upper2 = landmarks[_MOUTH_UPPER_2, :2] lower2 = landmarks[_MOUTH_LOWER_2, :2] horizontal = _distance(left, right) if horizontal < 1e-6: return 0.0 v1 = _distance(upper1, lower1) v2 = _distance(top, bottom) v3 = _distance(upper2, lower2) return (v1 + v2 + v3) / (2.0 * horizontal) class EyeBehaviourScorer: def __init__( self, ear_open: float = 0.30, ear_closed: float = 0.16, gaze_max_offset: float = 0.28, ): self.ear_open = ear_open self.ear_closed = ear_closed self.gaze_max_offset = gaze_max_offset def _ear_score(self, ear: float) -> float: if ear >= self.ear_open: return 1.0 if ear <= self.ear_closed: return 0.0 return (ear - self.ear_closed) / (self.ear_open - self.ear_closed) def _gaze_score(self, h_ratio: float, v_ratio: float) -> float: h_offset = abs(h_ratio - 0.5) v_offset = abs(v_ratio - 0.5) offset = math.sqrt(h_offset**2 + v_offset**2) t = min(offset / self.gaze_max_offset, 1.0) return 0.5 * (1.0 + math.cos(math.pi * t)) def score(self, landmarks: np.ndarray) -> float: left_ear = compute_ear(landmarks, _LEFT_EYE_EAR) right_ear = compute_ear(landmarks, _RIGHT_EYE_EAR) # Use minimum EAR so closing ONE eye is enough to drop the score ear = min(left_ear, right_ear) ear_s = self._ear_score(ear) if ear_s < 0.3: return ear_s h_ratio, v_ratio = compute_gaze_ratio(landmarks) gaze_s = self._gaze_score(h_ratio, v_ratio) return ear_s * gaze_s def detailed_score(self, landmarks: np.ndarray) -> dict: left_ear = compute_ear(landmarks, _LEFT_EYE_EAR) right_ear = compute_ear(landmarks, _RIGHT_EYE_EAR) ear = min(left_ear, right_ear) ear_s = self._ear_score(ear) h_ratio, v_ratio = compute_gaze_ratio(landmarks) gaze_s = self._gaze_score(h_ratio, v_ratio) s_eye = ear_s if ear_s < 0.3 else ear_s * gaze_s return { "ear": round(ear, 4), "ear_score": round(ear_s, 4), "h_gaze": round(h_ratio, 4), "v_gaze": round(v_ratio, 4), "gaze_score": round(gaze_s, 4), "s_eye": round(s_eye, 4), }