Spaces:
Sleeping
Sleeping
| """ | |
| Face scan stress feature extraction using MediaPipe FaceMesh. | |
| Ported from src/lib/ai/face-mesh.ts | |
| """ | |
| from __future__ import annotations | |
| import math | |
| from dataclasses import dataclass | |
| from typing import Optional | |
| import numpy as np | |
| LANDMARKS = { | |
| "LEFT_EYE_OUTER": 33, | |
| "LEFT_EYE_INNER": 133, | |
| "LEFT_EYE_TOP": 159, | |
| "LEFT_EYE_BOTTOM": 145, | |
| "RIGHT_EYE_OUTER": 263, | |
| "RIGHT_EYE_INNER": 362, | |
| "RIGHT_EYE_TOP": 386, | |
| "RIGHT_EYE_BOTTOM": 374, | |
| "LEFT_EYEBROW_INNER": 107, | |
| "LEFT_EYEBROW_OUTER": 70, | |
| "RIGHT_EYEBROW_INNER": 336, | |
| "RIGHT_EYEBROW_OUTER": 300, | |
| "MOUTH_TOP": 13, | |
| "MOUTH_BOTTOM": 14, | |
| "MOUTH_LEFT": 61, | |
| "MOUTH_RIGHT": 291, | |
| } | |
| class StressFeatures: | |
| left_eye_aspect: float | |
| right_eye_aspect: float | |
| brow_tension: float | |
| mouth_tension: float | |
| eye_symmetry: float | |
| mouth_opening: float | |
| timestamp: float | |
| def _distance(p1, p2) -> float: | |
| return math.sqrt( | |
| (p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2 | |
| ) | |
| def _ear(outer, inner, top, bottom) -> float: | |
| v = _distance(top, bottom) | |
| h = _distance(outer, inner) | |
| return v / h if h > 0 else 0 | |
| def extract_stress_features(landmarks: list) -> Optional[StressFeatures]: | |
| """Extract 7 stress features from 478 MediaPipe face landmarks.""" | |
| if not landmarks or len(landmarks) < 468: | |
| return None | |
| def p(idx): | |
| lm = landmarks[idx] | |
| return (lm.x, lm.y, lm.z) | |
| left_ear = _ear( | |
| p(LANDMARKS["LEFT_EYE_OUTER"]), | |
| p(LANDMARKS["LEFT_EYE_INNER"]), | |
| p(LANDMARKS["LEFT_EYE_TOP"]), | |
| p(LANDMARKS["LEFT_EYE_BOTTOM"]), | |
| ) | |
| right_ear = _ear( | |
| p(LANDMARKS["RIGHT_EYE_OUTER"]), | |
| p(LANDMARKS["RIGHT_EYE_INNER"]), | |
| p(LANDMARKS["RIGHT_EYE_TOP"]), | |
| p(LANDMARKS["RIGHT_EYE_BOTTOM"]), | |
| ) | |
| brow_tension = ( | |
| _distance(p(LANDMARKS["LEFT_EYEBROW_INNER"]), p(LANDMARKS["LEFT_EYE_TOP"])) | |
| + _distance(p(LANDMARKS["RIGHT_EYEBROW_INNER"]), p(LANDMARKS["RIGHT_EYE_TOP"])) | |
| ) / 2 | |
| mouth_width = _distance(p(LANDMARKS["MOUTH_LEFT"]), p(LANDMARKS["MOUTH_RIGHT"])) | |
| mouth_height = _distance(p(LANDMARKS["MOUTH_TOP"]), p(LANDMARKS["MOUTH_BOTTOM"])) | |
| mouth_tension = mouth_width / mouth_height if mouth_height > 0 else 1.0 | |
| eye_symmetry = abs(left_ear - right_ear) / ((left_ear + right_ear) / 2 + 0.001) | |
| mouth_opening = mouth_height / mouth_width if mouth_width > 0 else 0.1 | |
| import time | |
| return StressFeatures( | |
| left_eye_aspect=left_ear, | |
| right_eye_aspect=right_ear, | |
| brow_tension=brow_tension, | |
| mouth_tension=mouth_tension, | |
| eye_symmetry=eye_symmetry, | |
| mouth_opening=mouth_opening, | |
| timestamp=time.time(), | |
| ) | |
| def features_to_array(features: StressFeatures) -> np.ndarray: | |
| """Convert StressFeatures to a 7-element numpy array for the ONNX model.""" | |
| return np.array( | |
| [ | |
| features.left_eye_aspect, | |
| features.right_eye_aspect, | |
| features.brow_tension, | |
| features.mouth_tension, | |
| features.eye_symmetry, | |
| features.mouth_opening, | |
| features.timestamp % 86400 / 86400, # normalized time-of-day | |
| ], | |
| dtype=np.float32, | |
| ) | |
| def run_face_scan(image: np.ndarray) -> Optional[StressFeatures]: | |
| """Run MediaPipe FaceMesh on a BGR image and extract stress features.""" | |
| import mediapipe as mp | |
| mp_face_mesh = mp.solutions.face_mesh | |
| with mp_face_mesh.FaceMesh( | |
| static_image_mode=True, | |
| max_num_faces=1, | |
| refine_landmarks=True, | |
| min_detection_confidence=0.5, | |
| ) as face_mesh: | |
| results = face_mesh.process(image) | |
| if not results.multi_face_landmarks: | |
| return None | |
| landmarks = results.multi_face_landmarks[0].landmark | |
| return extract_stress_features(landmarks) | |