Spaces:
Running
Running
| """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) | |