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),
        }