Spaces:
Sleeping
Sleeping
File size: 5,378 Bytes
c86c45b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | 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),
}
|