| import numpy as np |
|
|
| EMOTION_LABELS = ["angry", "disgust", "fear", "happy", "sad", "surprise", "neutral"] |
|
|
| |
| def get_cnn_model(): |
| return None |
|
|
| |
| _baseline_metrics = None |
| _calibration_frames = 0 |
| _CALIBRATION_MAX = 5 |
| _metric_buffer = [] |
|
|
| def reset_calibration(): |
| global _baseline_metrics, _calibration_frames, _metric_buffer |
| _baseline_metrics = None |
| _calibration_frames = 0 |
| _metric_buffer = [] |
|
|
| def predict_emotion(frame_rgb, landmarks=None): |
| """ |
| Main emotion prediction entry point. |
| Bypasses CNN completely and uses high-precision Landmark-Ratio Logic. |
| """ |
| return predict_emotion_from_landmarks(landmarks) |
|
|
| def extract_metrics(pts): |
| |
| mouth_width = np.linalg.norm(pts[61] - pts[291]) |
| |
| |
| eye_dist = np.linalg.norm(pts[33] - pts[263]) |
| |
| |
| inner_lip_dist = np.linalg.norm(pts[13] - pts[14]) |
| |
| |
| inner_brow_dist = np.linalg.norm(pts[70] - pts[300]) |
| |
| |
| |
| nose_to_lip_y = ((pts[61][1] + pts[291][1]) / 2.0) - pts[4][1] |
| |
| |
| |
| left_eye_open = np.linalg.norm(pts[159] - pts[145]) |
| right_eye_open = np.linalg.norm(pts[386] - pts[374]) |
| eye_opening = (left_eye_open + right_eye_open) / 2.0 |
| |
| |
| |
| brow_to_eye_y = ((pts[159][1] + pts[386][1]) / 2.0) - ((pts[70][1] + pts[300][1]) / 2.0) |
| |
| |
| mouth_center_y = (pts[13][1] + pts[14][1]) / 2.0 |
| lip_corner_avg_y = (pts[61][1] + pts[291][1]) / 2.0 |
| upward_curve = mouth_center_y - lip_corner_avg_y |
| |
| |
| |
| nose_to_upper_lip = np.linalg.norm(pts[6] - pts[13]) |
| |
| return { |
| "mouth_width": float(mouth_width), |
| "eye_dist": float(eye_dist), |
| "inner_lip_dist": float(inner_lip_dist), |
| "inner_brow_dist": float(inner_brow_dist), |
| "nose_to_lip_y": float(nose_to_lip_y), |
| "eye_opening": float(eye_opening), |
| "brow_to_eye_y": float(brow_to_eye_y), |
| "upward_curve": float(upward_curve), |
| "nose_to_upper_lip": float(nose_to_upper_lip) |
| } |
|
|
| def predict_emotion_from_landmarks(landmarks): |
| global _baseline_metrics, _calibration_frames, _metric_buffer |
| |
| emotion_engagement = { |
| "Happy": 90, "Surprise": 80, "Neutral": 50, |
| "Angry": 30, "Fear": 25, "Sad": 15, "Disgust": 10 |
| } |
| |
| default_response = { |
| "emotion": "Neutral", "confidence": 50, |
| "probabilities": {k.capitalize(): (50 if k=="neutral" else 0) for k in EMOTION_LABELS}, |
| "engagement_score": 50, "provider": "Landmark-Ratio Logic" |
| } |
| |
| if not landmarks or len(landmarks) < 400: |
| return default_response |
| |
| try: |
| if isinstance(landmarks[0], dict): |
| pts = np.array([[l['x'], l['y']] for l in landmarks]) |
| else: |
| pts = np.array([[l.x, l.y] for l in landmarks]) |
| |
| current_metrics = extract_metrics(pts) |
| |
| |
| _metric_buffer.append(current_metrics) |
| if len(_metric_buffer) > 3: |
| _metric_buffer.pop(0) |
| |
| |
| c = {} |
| for key in current_metrics.keys(): |
| c[key] = sum(m[key] for m in _metric_buffer) / len(_metric_buffer) |
| |
| |
| if _calibration_frames < _CALIBRATION_MAX: |
| if _baseline_metrics is None: |
| _baseline_metrics = c.copy() |
| else: |
| for k in c: |
| _baseline_metrics[k] += c[k] |
| |
| _calibration_frames += 1 |
| if _calibration_frames == _CALIBRATION_MAX: |
| for k in _baseline_metrics: |
| _baseline_metrics[k] /= _CALIBRATION_MAX |
| return default_response |
| |
| b = _baseline_metrics |
| |
| |
| scores = {k.capitalize(): 0.0 for k in EMOTION_LABELS} |
| |
| |
| def pct_change(curr, base): |
| return (curr - base) / max(base, 0.001) |
|
|
| |
| mouth_eye_ratio_c = c["mouth_width"] / max(c["eye_dist"], 0.001) |
| mouth_eye_ratio_b = b["mouth_width"] / max(b["eye_dist"], 0.001) |
| happy_ratio_inc = pct_change(mouth_eye_ratio_c, mouth_eye_ratio_b) |
| |
| if happy_ratio_inc > 0.08: |
| intensity = 90.0 + ((happy_ratio_inc - 0.08) / 0.10) * 10.0 |
| if c["inner_lip_dist"] > b["inner_lip_dist"] * 1.5 and c["inner_lip_dist"] > 0.005: |
| intensity += 5.0 |
| scores["Happy"] = min(intensity, 100.0) |
| |
| |
| brow_rise_inc = pct_change(c["brow_to_eye_y"], b["brow_to_eye_y"]) |
| lip_drop_inc = pct_change(c["nose_to_lip_y"], b["nose_to_lip_y"]) |
| |
| if brow_rise_inc > 0.02 and lip_drop_inc > 0.02: |
| intensity = 90.0 + ((lip_drop_inc - 0.02) / 0.05) * 10.0 |
| scores["Sad"] = min(intensity, 100.0) |
| |
| |
| eye_open_inc = pct_change(c["eye_opening"], b["eye_opening"]) |
| jaw_drop_inc = c["inner_lip_dist"] - b["inner_lip_dist"] |
| |
| if eye_open_inc > 0.15 and jaw_drop_inc > 0.01: |
| intensity = 90.0 + ((eye_open_inc - 0.15) / 0.20) * 10.0 |
| scores["Surprise"] = min(intensity, 100.0) |
| |
| |
| brow_dist_dec = pct_change(b["inner_brow_dist"], c["inner_brow_dist"]) |
| lip_stretch_inc = pct_change(c["mouth_width"], b["mouth_width"]) |
| |
| if brow_rise_inc > 0.01 and brow_dist_dec > 0.02 and lip_stretch_inc > 0.03 and c["upward_curve"] < b["upward_curve"] + 0.01: |
| intensity = 90.0 + ((lip_stretch_inc - 0.03) / 0.05) * 10.0 |
| scores["Fear"] = min(intensity, 100.0) |
| |
| |
| brow_lower_dec = pct_change(b["brow_to_eye_y"], c["brow_to_eye_y"]) |
| lip_tight_dec = pct_change(b["inner_lip_dist"], c["inner_lip_dist"]) |
| |
| if brow_dist_dec > 0.10 and brow_lower_dec > 0.02: |
| intensity = 90.0 + ((brow_dist_dec - 0.10) / 0.10) * 10.0 |
| scores["Angry"] = min(intensity, 100.0) |
| |
| |
| nose_short_dec = pct_change(b["nose_to_upper_lip"], c["nose_to_upper_lip"]) |
| |
| if nose_short_dec > 0.02: |
| intensity = 90.0 + ((nose_short_dec - 0.02) / 0.05) * 10.0 |
| scores["Disgust"] = min(intensity, 100.0) |
| |
| |
| max_variance = 0.0 |
| for k in c: |
| var = abs(c[k] - b[k]) / max(b[k], 0.001) |
| if var > max_variance: |
| max_variance = var |
| |
| if max_variance <= 0.03 or sum(scores.values()) == 0: |
| scores["Neutral"] = 100.0 |
| |
| |
| best_emotion = max(scores, key=scores.get) |
| max_score = scores[best_emotion] |
| |
| |
| if max_score < 50.0: |
| best_emotion = "Neutral" |
| scores["Neutral"] = 100.0 |
| max_score = 100.0 |
| |
| |
| total = sum(scores.values()) |
| if total > 0: |
| probs = {k: round((v / total) * 100, 1) for k, v in scores.items()} |
| else: |
| probs = {k.capitalize(): (100.0 if k=="neutral" else 0.0) for k in EMOTION_LABELS} |
| |
| confidence = round(min(max_score, 100.0), 1) |
| |
| return { |
| "emotion": best_emotion, |
| "confidence": confidence, |
| "probabilities": probs, |
| "engagement_score": emotion_engagement.get(best_emotion, 50), |
| "provider": "Landmark-Ratio Logic" |
| } |
| |
| except Exception as e: |
| print(f"Landmark emotion error: {e}") |
| return default_response |
|
|