FocusGuardBaseModel / models /eye_scorer.py
Kexin-251202's picture
Deploy base model
c86c45b verified
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),
}