Spaces:
Sleeping
Sleeping
| 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), | |
| } | |