# facial_diagnostics.py import cv2 import numpy as np import math from statistics import median # Facial metric helpers LEFT_EYE_IDX = [33, 160, 158, 133, 153, 144] RIGHT_EYE_IDX = [362, 385, 387, 263, 373, 380] MOUTH_IDX = [13, 14, 78, 308] SMILE_LEFT = 61 SMILE_RIGHT = 291 def eye_aspect_ratio(pts, idx): a = np.linalg.norm(np.array(pts[idx[1]]) - np.array(pts[idx[5]])) b = np.linalg.norm(np.array(pts[idx[2]]) - np.array(pts[idx[4]])) c = np.linalg.norm(np.array(pts[idx[0]]) - np.array(idx[3])) + 1e-8 return float((a + b) / (2.0 * c)) def mouth_ratio(pts): top, bottom, left, right = pts[MOUTH_IDX[0]], pts[MOUTH_IDX[1]], pts[MOUTH_IDX[2]], pts[MOUTH_IDX[3]] return float(np.linalg.norm(np.array(top) - np.array(bottom)) / (np.linalg.norm(np.array(left) - np.array(right)) + 1e-8)) def head_tilt_angle(pts): left = np.mean([pts[33], pts[133]], axis=0) right = np.mean([pts[362], pts[263]], axis=0) diff = np.array(right) - np.array(left) return float(math.degrees(math.atan2(diff[1], diff[0]))) def smile_symmetry(pts): left = np.array(pts[SMILE_LEFT]) right = np.array(pts[SMILE_RIGHT]) center = (left + right) / 2 L = np.linalg.norm(left - center) R = np.linalg.norm(right - center) if L + R == 0: return 1.0 return float(min(L, R) / max(L, R)) # Face mesh import mediapipe as mp mp_face_mesh = mp.solutions.face_mesh FACE_MESH = mp_face_mesh.FaceMesh( static_image_mode=False, max_num_faces=1, refine_landmarks=True, min_detection_confidence=0.6, min_tracking_confidence=0.6 ) def analyze_frame(frame): h, w, _ = frame.shape rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) res = FACE_MESH.process(rgb) if not res.multi_face_landmarks: return {"found": False} lm = res.multi_face_landmarks[0] pts = [(int(p.x * w), int(p.y * h)) for p in lm.landmark] ear = (eye_aspect_ratio(pts, LEFT_EYE_IDX) + eye_aspect_ratio(pts, RIGHT_EYE_IDX)) / 2 mratio = mouth_ratio(pts) tilt = head_tilt_angle(pts) symmetry = smile_symmetry(pts) return { "found": True, "ear": float(ear), "mouth_ratio": float(mratio), "head_tilt": float(tilt), "smile_sym": float(symmetry) } def aggregate_metrics(metrics): if not metrics: return {} ears = [m["ear"] for m in metrics] mouths = [m["mouth_ratio"] for m in metrics] tilts = [m["head_tilt"] for m in metrics] smiles = [m["smile_sym"] for m in metrics] return { "count": len(metrics), "ear_median": median(ears), "mouth_median": median(mouths), "tilt_median": median(tilts), "smile_median": median(smiles) }