Spaces:
Sleeping
Sleeping
Zaid
fix: resolve OpenCV 5.0.0 blobFromImage colorSpace signature compatibility issue for emotion detection
7100b42 | import os | |
| import cv2 | |
| import numpy as np | |
| # Get absolute path to models directory | |
| MODELS_DIR = os.path.join(os.path.dirname(__file__), "models") | |
| class FaceAnalyzer: | |
| def __init__(self): | |
| # Load ONNX models for Age and Gender | |
| age_onnx = os.path.join(MODELS_DIR, "age_googlenet.onnx") | |
| gender_onnx = os.path.join(MODELS_DIR, "gender_googlenet.onnx") | |
| emotion_onnx = os.path.join(MODELS_DIR, "emotion-ferplus-8.onnx") | |
| self.age_net = None | |
| self.gender_net = None | |
| self.emotion_net = None | |
| if os.path.exists(age_onnx): | |
| try: | |
| self.age_net = cv2.dnn.readNetFromONNX(age_onnx) | |
| print("Age ONNX model loaded successfully.") | |
| except Exception as e: | |
| print(f"Error loading age net: {e}") | |
| if os.path.exists(gender_onnx): | |
| try: | |
| self.gender_net = cv2.dnn.readNetFromONNX(gender_onnx) | |
| print("Gender ONNX model loaded successfully.") | |
| except Exception as e: | |
| print(f"Error loading gender net: {e}") | |
| if os.path.exists(emotion_onnx): | |
| try: | |
| self.emotion_net = cv2.dnn.readNetFromONNX(emotion_onnx) | |
| print("Emotion ONNX model loaded successfully.") | |
| except Exception as e: | |
| print(f"Error loading emotion net: {e}") | |
| self.age_list = ['(0-2)', '(4-6)', '(8-12)', '(15-20)', '(25-32)', '(38-43)', '(48-53)', '(60-100)'] | |
| self.gender_list = ['Male', 'Female'] | |
| self.emotion_list = ['Neutral', 'Happy', 'Surprise', 'Sad', 'Angry', 'Disgust', 'Fear', 'Contempt'] | |
| # GoogLeNet standard mean values (BGR) | |
| self.model_mean_values = (104.0, 117.0, 123.0) | |
| def analyze_face(self, frame, face_landmarks, x_min, y_min, x_max, y_max): | |
| """ | |
| Runs the full analysis on a single face: Face Shape, Eye Color, Emotion, Age, Gender. | |
| """ | |
| h, w, _ = frame.shape | |
| analysis = { | |
| "face_shape": "Unknown", | |
| "eye_color": "Unknown", | |
| "emotion": "Unknown", | |
| "age": "Unknown", | |
| "gender": "Unknown" | |
| } | |
| # 1. Face Shape classification | |
| try: | |
| analysis["face_shape"] = self.classify_face_shape(face_landmarks, w, h) | |
| except Exception as e: | |
| print(f"Error classifying face shape: {e}") | |
| # 2. Eye Color classification | |
| try: | |
| analysis["eye_color"] = self.detect_eye_color(frame, face_landmarks, w, h) | |
| except Exception as e: | |
| print(f"Error detecting eye color: {e}") | |
| # Crop face for DNN models (age, gender, emotion) | |
| pad_w = int((x_max - x_min) * 0.15) | |
| pad_h = int((y_max - y_min) * 0.15) | |
| cx_min = max(0, x_min - pad_w) | |
| cy_min = max(0, y_min - pad_h) | |
| cx_max = min(w, x_max + pad_w) | |
| cy_max = min(h, y_max + pad_h) | |
| if cx_max - cx_min > 10 and cy_max - cy_min > 10: | |
| face_crop = frame[cy_min:cy_max, cx_min:cx_max] | |
| # 3. Emotion classification | |
| if self.emotion_net is not None: | |
| try: | |
| gray_crop = cv2.cvtColor(face_crop, cv2.COLOR_BGR2GRAY) | |
| gray_crop = cv2.resize(gray_crop, (64, 64)) | |
| blob = cv2.dnn.blobFromImage(gray_crop, 1.0, (64, 64)) | |
| self.emotion_net.setInput(blob) | |
| emotion_preds = self.emotion_net.forward() | |
| analysis["emotion"] = self.emotion_list[emotion_preds[0].argmax()] | |
| except Exception as e: | |
| print(f"Error predicting emotion: {e}") | |
| # 4. Age and Gender classification | |
| if self.age_net is not None and self.gender_net is not None: | |
| try: | |
| # GoogLeNet ONNX models expect 224x224 images | |
| blob = cv2.dnn.blobFromImage(face_crop, 1.0, (224, 224), self.model_mean_values, swapRB=False) | |
| # Predict Gender | |
| self.gender_net.setInput(blob) | |
| gender_preds = self.gender_net.forward() | |
| analysis["gender"] = self.gender_list[gender_preds[0].argmax()] | |
| # Predict Age | |
| self.age_net.setInput(blob) | |
| age_preds = self.age_net.forward() | |
| analysis["age"] = self.age_list[age_preds[0].argmax()] | |
| except Exception as e: | |
| print(f"Error predicting age/gender: {e}") | |
| return analysis | |
| def classify_face_shape(self, landmarks, w, h): | |
| """ | |
| Classifies face shape based on ratios calculated from face mesh landmarks: | |
| - Face Length: Chin (152) to Forehead (10) | |
| - Cheekbone Width: Left Cheek (234) to Right Cheek (454) | |
| - Jawline Width: Left Jaw (58) to Right Jaw (288) | |
| - Forehead Width: Left Forehead (109) to Right Forehead (338) | |
| """ | |
| def get_dist(p1_idx, p2_idx): | |
| pt1 = landmarks[p1_idx] | |
| pt2 = landmarks[p2_idx] | |
| # Handle both tasks API landmark objects (which have x, y, z) and legacy dicts/objects | |
| x1, y1 = (pt1.x if hasattr(pt1, 'x') else pt1['x']), (pt1.y if hasattr(pt1, 'y') else pt1['y']) | |
| x2, y2 = (pt2.x if hasattr(pt2, 'x') else pt2['x']), (pt2.y if hasattr(pt2, 'y') else pt2['y']) | |
| return np.sqrt((x1 * w - x2 * w) ** 2 + (y1 * h - y2 * h) ** 2) | |
| face_length = get_dist(10, 152) | |
| cheekbone_width = get_dist(234, 454) | |
| jaw_width = get_dist(58, 288) | |
| forehead_width = get_dist(109, 338) | |
| if face_length == 0 or cheekbone_width == 0: | |
| return "Unknown" | |
| # Ratios | |
| len_width_ratio = face_length / cheekbone_width | |
| forehead_cheek_ratio = forehead_width / cheekbone_width | |
| jaw_cheek_ratio = jaw_width / cheekbone_width | |
| # Simple classification heuristics based on classic face shape dimensions | |
| if len_width_ratio > 1.25: | |
| if jaw_cheek_ratio < 0.75: | |
| return "Heart" | |
| else: | |
| return "Oval" if forehead_cheek_ratio > 0.8 else "Oblong" | |
| else: # Shorter face | |
| if abs(cheekbone_width - jaw_width) < cheekbone_width * 0.1: | |
| return "Square" | |
| else: | |
| return "Round" | |
| def detect_eye_color(self, frame, landmarks, w, h): | |
| """ | |
| Detects eye color by analyzing the iris region from MediaPipe Face Mesh iris landmarks. | |
| Left iris landmarks: 468, 469, 470, 471, 472 | |
| Right iris landmarks: 473, 474, 475, 476, 477 | |
| """ | |
| center_idx = 473 | |
| center_pt = landmarks[center_idx] | |
| cx = int((center_pt.x if hasattr(center_pt, 'x') else center_pt['x']) * w) | |
| cy = int((center_pt.y if hasattr(center_pt, 'y') else center_pt['y']) * h) | |
| # Estimate iris radius as 1/15th of the eye width | |
| pt33 = landmarks[33] | |
| pt133 = landmarks[133] | |
| x33, y33 = (pt33.x if hasattr(pt33, 'x') else pt33['x']) * w, (pt33.y if hasattr(pt33, 'y') else pt33['y']) * h | |
| x133, y133 = (pt133.x if hasattr(pt133, 'x') else pt133['x']) * w, (pt133.y if hasattr(pt133, 'y') else pt133['y']) * h | |
| eye_width = np.sqrt((x33 - x133)**2 + (y33 - y133)**2) | |
| radius = max(2, int(eye_width * 0.15)) | |
| # Crop iris box | |
| y1, y2 = max(0, cy - radius), min(h, cy + radius) | |
| x1, x2 = max(0, cx - radius), min(w, cx + radius) | |
| if (x2 - x1) < 4 or (y2 - y1) < 4: | |
| return "Brown" # Fallback | |
| iris_crop = frame[y1:y2, x1:x2] | |
| # Convert to HSV | |
| hsv = cv2.cvtColor(iris_crop, cv2.COLOR_BGR2HSV) | |
| # We want to filter out pupil (very dark) and sclera/reflection (very bright/white) | |
| mask = cv2.inRange(hsv, (0, 30, 30), (180, 255, 200)) | |
| filtered_pixels = hsv[mask > 0] | |
| if len(filtered_pixels) == 0: | |
| return "Brown" # Fallback if dark/underexposed | |
| median_hue = np.median(filtered_pixels[:, 0]) | |
| median_sat = np.median(filtered_pixels[:, 1]) | |
| median_val = np.median(filtered_pixels[:, 2]) | |
| if median_sat < 50: | |
| return "Grey" | |
| elif 35 <= median_hue <= 85: | |
| return "Green" if median_sat > 70 else "Hazel" | |
| elif 85 < median_hue <= 135: | |
| return "Blue" | |
| else: | |
| return "Brown" | |