File size: 5,538 Bytes
eb4abb8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Server-side face mesh and HUD drawing for WebRTC/WS video frames."""

from __future__ import annotations

import cv2
import numpy as np

from mediapipe.tasks.python.vision import FaceLandmarksConnections
from models.face_mesh import FaceMeshDetector

_FONT = cv2.FONT_HERSHEY_SIMPLEX
_CYAN = (255, 255, 0)
_GREEN = (0, 255, 0)
_MAGENTA = (255, 0, 255)
_ORANGE = (0, 165, 255)
_RED = (0, 0, 255)
_WHITE = (255, 255, 255)
_LIGHT_GREEN = (144, 238, 144)

_TESSELATION_CONNS = [(c.start, c.end) for c in FaceLandmarksConnections.FACE_LANDMARKS_TESSELATION]
_CONTOUR_CONNS = [(c.start, c.end) for c in FaceLandmarksConnections.FACE_LANDMARKS_CONTOURS]
_LEFT_EYEBROW = [70, 63, 105, 66, 107, 55, 65, 52, 53, 46]
_RIGHT_EYEBROW = [300, 293, 334, 296, 336, 285, 295, 282, 283, 276]
_NOSE_BRIDGE = [6, 197, 195, 5, 4, 1, 19, 94, 2]
_LIPS_OUTER = [61, 146, 91, 181, 84, 17, 314, 405, 321, 375, 291, 409, 270, 269, 267, 0, 37, 39, 40, 185, 61]
_LIPS_INNER = [78, 95, 88, 178, 87, 14, 317, 402, 318, 324, 308, 415, 310, 311, 312, 13, 82, 81, 80, 191, 78]
_LEFT_EAR_POINTS = [33, 160, 158, 133, 153, 145]
_RIGHT_EAR_POINTS = [362, 385, 387, 263, 373, 380]


def _lm_px(lm: np.ndarray, idx: int, w: int, h: int) -> tuple[int, int]:
    return (int(lm[idx, 0] * w), int(lm[idx, 1] * h))


def _draw_polyline(
    frame: np.ndarray, lm: np.ndarray, indices: list[int], w: int, h: int, color: tuple, thickness: int
) -> None:
    for i in range(len(indices) - 1):
        cv2.line(
            frame,
            _lm_px(lm, indices[i], w, h),
            _lm_px(lm, indices[i + 1], w, h),
            color,
            thickness,
            cv2.LINE_AA,
        )


def draw_face_mesh(frame: np.ndarray, lm: np.ndarray, w: int, h: int) -> None:
    """Draw tessellation, contours, eyebrows, nose, lips, eyes, irises, gaze lines on frame."""
    overlay = frame.copy()
    for s, e in _TESSELATION_CONNS:
        cv2.line(overlay, _lm_px(lm, s, w, h), _lm_px(lm, e, w, h), (200, 200, 200), 1, cv2.LINE_AA)
    cv2.addWeighted(overlay, 0.3, frame, 0.7, 0, frame)
    for s, e in _CONTOUR_CONNS:
        cv2.line(frame, _lm_px(lm, s, w, h), _lm_px(lm, e, w, h), _CYAN, 1, cv2.LINE_AA)
    _draw_polyline(frame, lm, _LEFT_EYEBROW, w, h, _LIGHT_GREEN, 2)
    _draw_polyline(frame, lm, _RIGHT_EYEBROW, w, h, _LIGHT_GREEN, 2)
    _draw_polyline(frame, lm, _NOSE_BRIDGE, w, h, _ORANGE, 1)
    _draw_polyline(frame, lm, _LIPS_OUTER, w, h, _MAGENTA, 1)
    _draw_polyline(frame, lm, _LIPS_INNER, w, h, (200, 0, 200), 1)
    left_pts = np.array([_lm_px(lm, i, w, h) for i in FaceMeshDetector.LEFT_EYE_INDICES], dtype=np.int32)
    cv2.polylines(frame, [left_pts], True, _GREEN, 2, cv2.LINE_AA)
    right_pts = np.array([_lm_px(lm, i, w, h) for i in FaceMeshDetector.RIGHT_EYE_INDICES], dtype=np.int32)
    cv2.polylines(frame, [right_pts], True, _GREEN, 2, cv2.LINE_AA)
    for indices in [_LEFT_EAR_POINTS, _RIGHT_EAR_POINTS]:
        for idx in indices:
            cv2.circle(frame, _lm_px(lm, idx, w, h), 3, (0, 255, 255), -1, cv2.LINE_AA)
    for iris_idx, eye_inner, eye_outer in [
        (FaceMeshDetector.LEFT_IRIS_INDICES, 133, 33),
        (FaceMeshDetector.RIGHT_IRIS_INDICES, 362, 263),
    ]:
        iris_pts = np.array([_lm_px(lm, i, w, h) for i in iris_idx], dtype=np.int32)
        center = iris_pts[0]
        if len(iris_pts) >= 5:
            radii = [np.linalg.norm(iris_pts[j] - center) for j in range(1, 5)]
            radius = max(int(np.mean(radii)), 2)
            cv2.circle(frame, tuple(center), radius, _MAGENTA, 2, cv2.LINE_AA)
            cv2.circle(frame, tuple(center), 2, _WHITE, -1, cv2.LINE_AA)
        eye_cx = int((lm[eye_inner, 0] + lm[eye_outer, 0]) / 2.0 * w)
        eye_cy = int((lm[eye_inner, 1] + lm[eye_outer, 1]) / 2.0 * h)
        dx, dy = center[0] - eye_cx, center[1] - eye_cy
        cv2.line(
            frame,
            tuple(center),
            (int(center[0] + dx * 3), int(center[1] + dy * 3)),
            _RED,
            1,
            cv2.LINE_AA,
        )


def draw_hud(frame: np.ndarray, result: dict, model_name: str) -> None:
    """Draw status bar and detail overlay (FOCUSED/NOT FOCUSED, conf, s_face, s_eye, MAR, yawn)."""
    h, w = frame.shape[:2]
    is_focused = result["is_focused"]
    status = "FOCUSED" if is_focused else "NOT FOCUSED"
    color = _GREEN if is_focused else _RED
    cv2.rectangle(frame, (0, 0), (w, 55), (0, 0, 0), -1)
    cv2.putText(frame, status, (10, 28), _FONT, 0.8, color, 2, cv2.LINE_AA)
    cv2.putText(frame, model_name.upper(), (w - 150, 28), _FONT, 0.45, _WHITE, 1, cv2.LINE_AA)
    conf = result.get("mlp_prob", result.get("raw_score", 0.0))
    mar_s = f" MAR:{result['mar']:.2f}" if result.get("mar") is not None else ""
    sf, se = result.get("s_face", 0), result.get("s_eye", 0)
    detail = f"conf:{conf:.2f} S_face:{sf:.2f} S_eye:{se:.2f}{mar_s}"
    cv2.putText(frame, detail, (10, 48), _FONT, 0.4, _WHITE, 1, cv2.LINE_AA)
    if result.get("yaw") is not None:
        cv2.putText(
            frame,
            f"yaw:{result['yaw']:+.0f} pitch:{result['pitch']:+.0f} roll:{result['roll']:+.0f}",
            (w - 280, 48),
            _FONT,
            0.4,
            (180, 180, 180),
            1,
            cv2.LINE_AA,
        )
    if result.get("is_yawning"):
        cv2.putText(frame, "YAWN", (10, 75), _FONT, 0.7, _ORANGE, 2, cv2.LINE_AA)


def get_tesselation_connections() -> list[tuple[int, int]]:
    """Return tessellation edge pairs for client-side face mesh (cached by client)."""
    return list(_TESSELATION_CONNS)