Create main.ipynb
Browse files- main.ipynb +975 -0
main.ipynb
ADDED
|
@@ -0,0 +1,975 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Complete Stress Detection System - 10 Action Units
|
| 2 |
+
# Real-time Multi-AU Detection with Comprehensive Analysis
|
| 3 |
+
# Research Assistant: [Your Name]
|
| 4 |
+
# Guide: Prof. Anup Nandy
|
| 5 |
+
# Based on Facial Action Coding System (FACS) - Ekman & Friesen
|
| 6 |
+
|
| 7 |
+
import cv2
|
| 8 |
+
import mediapipe as mp
|
| 9 |
+
import numpy as np
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import matplotlib.pyplot as plt
|
| 12 |
+
from matplotlib.gridspec import GridSpec
|
| 13 |
+
from collections import deque
|
| 14 |
+
import time
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
import warnings
|
| 17 |
+
warnings.filterwarnings('ignore')
|
| 18 |
+
|
| 19 |
+
# ==================== CONFIGURATION ====================
|
| 20 |
+
WINDOW_SIZE = 30
|
| 21 |
+
RECORDING_DURATION = 15
|
| 22 |
+
FPS = 30
|
| 23 |
+
|
| 24 |
+
# ==================== MediaPipe Setup ====================
|
| 25 |
+
mp_face_mesh = mp.solutions.face_mesh
|
| 26 |
+
face_mesh = mp_face_mesh.FaceMesh(
|
| 27 |
+
min_detection_confidence=0.5,
|
| 28 |
+
min_tracking_confidence=0.5,
|
| 29 |
+
refine_landmarks=True
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# ==================== LANDMARK INDICES (468 landmarks) ====================
|
| 33 |
+
|
| 34 |
+
# AU01 - Inner Brow Raiser (Surprise, Fear, Sadness)
|
| 35 |
+
AU01_LANDMARKS = {
|
| 36 |
+
'left_inner_brow': 336,
|
| 37 |
+
'right_inner_brow': 107,
|
| 38 |
+
'nose_bridge': 6,
|
| 39 |
+
'left_outer_brow': 285,
|
| 40 |
+
'right_outer_brow': 55
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# AU04 - Brow Lowerer (Anger, Sadness, Concentration)
|
| 44 |
+
AU04_LANDMARKS = {
|
| 45 |
+
'left_inner_brow': 336,
|
| 46 |
+
'right_inner_brow': 107,
|
| 47 |
+
'left_mid_brow': 285,
|
| 48 |
+
'right_mid_brow': 55,
|
| 49 |
+
'left_eyelid': 159,
|
| 50 |
+
'right_eyelid': 386,
|
| 51 |
+
'nose_bridge': 6
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
# AU06 - Cheek Raiser (Genuine Smile - Duchenne)
|
| 55 |
+
AU06_LANDMARKS = {
|
| 56 |
+
'left_cheek': 205,
|
| 57 |
+
'right_cheek': 425,
|
| 58 |
+
'left_lower_eyelid': 145,
|
| 59 |
+
'right_lower_eyelid': 374,
|
| 60 |
+
'left_eye_outer': 33,
|
| 61 |
+
'right_eye_outer': 263
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
# AU07 - Lid Tightener (Concentration, Anger, Disgust)
|
| 65 |
+
AU07_LANDMARKS = {
|
| 66 |
+
'left_upper_lid': 159,
|
| 67 |
+
'right_upper_lid': 386,
|
| 68 |
+
'left_lower_lid': 145,
|
| 69 |
+
'right_lower_lid': 374,
|
| 70 |
+
'left_eye_top': 159,
|
| 71 |
+
'right_eye_top': 386
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
# AU12 - Lip Corner Puller (Happiness)
|
| 75 |
+
AU12_LANDMARKS = {
|
| 76 |
+
'left_corner': 61,
|
| 77 |
+
'right_corner': 291,
|
| 78 |
+
'upper_center': 13,
|
| 79 |
+
'lower_center': 14
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
# AU14 - Dimpler (Smile Intensity)
|
| 83 |
+
AU14_LANDMARKS = {
|
| 84 |
+
'left_dimple': 206,
|
| 85 |
+
'right_dimple': 426,
|
| 86 |
+
'left_corner': 61,
|
| 87 |
+
'right_corner': 291
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
# AU17 - Chin Raiser (Doubt, Sadness, Pouting)
|
| 91 |
+
AU17_LANDMARKS = {
|
| 92 |
+
'chin_center': 152,
|
| 93 |
+
'lower_lip': 17,
|
| 94 |
+
'chin_left': 176,
|
| 95 |
+
'chin_right': 400
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
# AU23 - Lip Tightener (Anger, Tension)
|
| 99 |
+
AU23_LANDMARKS = {
|
| 100 |
+
'left_corner': 61,
|
| 101 |
+
'right_corner': 291,
|
| 102 |
+
'left_outer': 57,
|
| 103 |
+
'right_outer': 287
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
# AU24 - Lip Pressor (Stress, Tension, Anger)
|
| 107 |
+
AU24_LANDMARKS = {
|
| 108 |
+
'upper_lip_top': 0,
|
| 109 |
+
'upper_lip_bottom': 13,
|
| 110 |
+
'lower_lip_top': 14,
|
| 111 |
+
'lower_lip_bottom': 17
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
# AU26 - Jaw Drop (Surprise, Shock, Mouth Opening)
|
| 115 |
+
AU26_LANDMARKS = {
|
| 116 |
+
'upper_lip': 13,
|
| 117 |
+
'lower_lip': 14,
|
| 118 |
+
'chin': 152,
|
| 119 |
+
'nose': 1
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
# ==================== UTILITY FUNCTIONS ====================
|
| 123 |
+
def calculate_distance(point1, point2):
|
| 124 |
+
return np.sqrt((point1[0] - point2[0])**2 + (point1[1] - point2[1])**2)
|
| 125 |
+
|
| 126 |
+
def get_landmark_coords(landmarks, idx, frame_width, frame_height):
|
| 127 |
+
lm = landmarks[idx]
|
| 128 |
+
return np.array([lm.x * frame_width, lm.y * frame_height])
|
| 129 |
+
|
| 130 |
+
# ==================== AU DETECTOR CLASSES ====================
|
| 131 |
+
|
| 132 |
+
class AU01Detector:
|
| 133 |
+
"""AU01 - Inner Brow Raiser (Surprise, Fear, Worry)"""
|
| 134 |
+
def __init__(self, window_size=30):
|
| 135 |
+
self.name = "AU01_InnerBrowRaise"
|
| 136 |
+
self.activation_history = deque(maxlen=window_size)
|
| 137 |
+
self.intensity_history = deque(maxlen=window_size)
|
| 138 |
+
|
| 139 |
+
def detect(self, landmarks, frame_width, frame_height):
|
| 140 |
+
left_inner = get_landmark_coords(landmarks, AU01_LANDMARKS['left_inner_brow'], frame_width, frame_height)
|
| 141 |
+
right_inner = get_landmark_coords(landmarks, AU01_LANDMARKS['right_inner_brow'], frame_width, frame_height)
|
| 142 |
+
left_outer = get_landmark_coords(landmarks, AU01_LANDMARKS['left_outer_brow'], frame_width, frame_height)
|
| 143 |
+
right_outer = get_landmark_coords(landmarks, AU01_LANDMARKS['right_outer_brow'], frame_width, frame_height)
|
| 144 |
+
nose = get_landmark_coords(landmarks, AU01_LANDMARKS['nose_bridge'], frame_width, frame_height)
|
| 145 |
+
|
| 146 |
+
# Calculate inner vs outer brow height
|
| 147 |
+
inner_height = ((nose[1] - left_inner[1]) + (nose[1] - right_inner[1])) / 2
|
| 148 |
+
outer_height = ((nose[1] - left_outer[1]) + (nose[1] - right_outer[1])) / 2
|
| 149 |
+
|
| 150 |
+
# AU01 active when inner brows raised MORE than outer (creates worried look)
|
| 151 |
+
raise_ratio = inner_height / (outer_height + 1e-6)
|
| 152 |
+
|
| 153 |
+
is_active = raise_ratio > 1.15 # Inner brows 15% higher than outer
|
| 154 |
+
intensity = min(100, max(0, (raise_ratio - 1.0) * 500))
|
| 155 |
+
|
| 156 |
+
self.activation_history.append(int(is_active))
|
| 157 |
+
self.intensity_history.append(intensity)
|
| 158 |
+
|
| 159 |
+
return is_active, intensity
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class AU04Detector:
|
| 163 |
+
"""AU04 - Brow Lowerer (Anger, Concentration, Stress)"""
|
| 164 |
+
def __init__(self, window_size=30):
|
| 165 |
+
self.name = "AU04_BrowLower"
|
| 166 |
+
self.activation_history = deque(maxlen=window_size)
|
| 167 |
+
self.intensity_history = deque(maxlen=window_size)
|
| 168 |
+
|
| 169 |
+
def detect(self, landmarks, frame_width, frame_height):
|
| 170 |
+
left_inner_brow = get_landmark_coords(landmarks, AU04_LANDMARKS['left_inner_brow'], frame_width, frame_height)
|
| 171 |
+
right_inner_brow = get_landmark_coords(landmarks, AU04_LANDMARKS['right_inner_brow'], frame_width, frame_height)
|
| 172 |
+
left_eyelid = get_landmark_coords(landmarks, AU04_LANDMARKS['left_eyelid'], frame_width, frame_height)
|
| 173 |
+
right_eyelid = get_landmark_coords(landmarks, AU04_LANDMARKS['right_eyelid'], frame_width, frame_height)
|
| 174 |
+
nose_bridge = get_landmark_coords(landmarks, AU04_LANDMARKS['nose_bridge'], frame_width, frame_height)
|
| 175 |
+
|
| 176 |
+
left_brow_eyelid_dist = left_inner_brow[1] - left_eyelid[1]
|
| 177 |
+
right_brow_eyelid_dist = right_inner_brow[1] - right_eyelid[1]
|
| 178 |
+
avg_brow_eyelid_dist = (left_brow_eyelid_dist + right_brow_eyelid_dist) / 2
|
| 179 |
+
|
| 180 |
+
face_height = calculate_distance(left_inner_brow, nose_bridge)
|
| 181 |
+
normalized_distance = avg_brow_eyelid_dist / (face_height + 1e-6)
|
| 182 |
+
|
| 183 |
+
inner_brow_distance = calculate_distance(left_inner_brow, right_inner_brow)
|
| 184 |
+
outer_eye_distance = calculate_distance(left_eyelid, right_eyelid)
|
| 185 |
+
brow_compression_ratio = inner_brow_distance / (outer_eye_distance + 1e-6)
|
| 186 |
+
|
| 187 |
+
is_active = (normalized_distance > -0.30 or brow_compression_ratio < 0.95)
|
| 188 |
+
intensity = min(100, max(0, (normalized_distance + 0.40) / 0.40 * 100))
|
| 189 |
+
|
| 190 |
+
self.activation_history.append(int(is_active))
|
| 191 |
+
self.intensity_history.append(intensity)
|
| 192 |
+
|
| 193 |
+
return is_active, intensity
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
class AU06Detector:
|
| 197 |
+
"""AU06 - Cheek Raiser (Genuine Smile)"""
|
| 198 |
+
def __init__(self, window_size=30):
|
| 199 |
+
self.name = "AU06_CheekRaise"
|
| 200 |
+
self.activation_history = deque(maxlen=window_size)
|
| 201 |
+
self.intensity_history = deque(maxlen=window_size)
|
| 202 |
+
|
| 203 |
+
def detect(self, landmarks, frame_width, frame_height):
|
| 204 |
+
left_cheek = get_landmark_coords(landmarks, AU06_LANDMARKS['left_cheek'], frame_width, frame_height)
|
| 205 |
+
right_cheek = get_landmark_coords(landmarks, AU06_LANDMARKS['right_cheek'], frame_width, frame_height)
|
| 206 |
+
left_lower_lid = get_landmark_coords(landmarks, AU06_LANDMARKS['left_lower_eyelid'], frame_width, frame_height)
|
| 207 |
+
right_lower_lid = get_landmark_coords(landmarks, AU06_LANDMARKS['right_lower_eyelid'], frame_width, frame_height)
|
| 208 |
+
|
| 209 |
+
# When cheeks raise, distance between cheek and lower eyelid decreases
|
| 210 |
+
left_distance = calculate_distance(left_cheek, left_lower_lid)
|
| 211 |
+
right_distance = calculate_distance(right_cheek, right_lower_lid)
|
| 212 |
+
avg_distance = (left_distance + right_distance) / 2
|
| 213 |
+
|
| 214 |
+
# Also check if lower eyelid moves up
|
| 215 |
+
left_eye_outer = get_landmark_coords(landmarks, AU06_LANDMARKS['left_eye_outer'], frame_width, frame_height)
|
| 216 |
+
eye_height = abs(left_eye_outer[1] - left_lower_lid[1])
|
| 217 |
+
|
| 218 |
+
cheek_raise_score = eye_height / (avg_distance + 1e-6)
|
| 219 |
+
|
| 220 |
+
is_active = cheek_raise_score > 0.8
|
| 221 |
+
intensity = min(100, max(0, (cheek_raise_score - 0.5) * 200))
|
| 222 |
+
|
| 223 |
+
self.activation_history.append(int(is_active))
|
| 224 |
+
self.intensity_history.append(intensity)
|
| 225 |
+
|
| 226 |
+
return is_active, intensity
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
class AU07Detector:
|
| 230 |
+
"""AU07 - Lid Tightener (Tension, Squinting)"""
|
| 231 |
+
def __init__(self, window_size=30):
|
| 232 |
+
self.name = "AU07_LidTighten"
|
| 233 |
+
self.activation_history = deque(maxlen=window_size)
|
| 234 |
+
self.intensity_history = deque(maxlen=window_size)
|
| 235 |
+
|
| 236 |
+
def detect(self, landmarks, frame_width, frame_height):
|
| 237 |
+
left_upper = get_landmark_coords(landmarks, AU07_LANDMARKS['left_upper_lid'], frame_width, frame_height)
|
| 238 |
+
right_upper = get_landmark_coords(landmarks, AU07_LANDMARKS['right_upper_lid'], frame_width, frame_height)
|
| 239 |
+
left_lower = get_landmark_coords(landmarks, AU07_LANDMARKS['left_lower_lid'], frame_width, frame_height)
|
| 240 |
+
right_lower = get_landmark_coords(landmarks, AU07_LANDMARKS['right_lower_lid'], frame_width, frame_height)
|
| 241 |
+
|
| 242 |
+
# Eye opening (smaller = more tightened)
|
| 243 |
+
left_eye_opening = abs(left_upper[1] - left_lower[1])
|
| 244 |
+
right_eye_opening = abs(right_upper[1] - right_lower[1])
|
| 245 |
+
avg_eye_opening = (left_eye_opening + right_eye_opening) / 2
|
| 246 |
+
|
| 247 |
+
# Normalize by face height
|
| 248 |
+
face_ref = calculate_distance(left_upper,
|
| 249 |
+
get_landmark_coords(landmarks, 152, frame_width, frame_height))
|
| 250 |
+
normalized_opening = avg_eye_opening / (face_ref + 1e-6)
|
| 251 |
+
|
| 252 |
+
is_active = normalized_opening < 0.025 # Eyes tightened/squinted
|
| 253 |
+
intensity = min(100, max(0, (0.035 - normalized_opening) / 0.035 * 100))
|
| 254 |
+
|
| 255 |
+
self.activation_history.append(int(is_active))
|
| 256 |
+
self.intensity_history.append(intensity)
|
| 257 |
+
|
| 258 |
+
return is_active, intensity
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
class AU12Detector:
|
| 262 |
+
"""AU12 - Lip Corner Puller (Smile)"""
|
| 263 |
+
def __init__(self, window_size=30):
|
| 264 |
+
self.name = "AU12_LipCornerPull"
|
| 265 |
+
self.activation_history = deque(maxlen=window_size)
|
| 266 |
+
self.intensity_history = deque(maxlen=window_size)
|
| 267 |
+
|
| 268 |
+
def detect(self, landmarks, frame_width, frame_height):
|
| 269 |
+
left_corner = get_landmark_coords(landmarks, AU12_LANDMARKS['left_corner'], frame_width, frame_height)
|
| 270 |
+
right_corner = get_landmark_coords(landmarks, AU12_LANDMARKS['right_corner'], frame_width, frame_height)
|
| 271 |
+
upper_center = get_landmark_coords(landmarks, AU12_LANDMARKS['upper_center'], frame_width, frame_height)
|
| 272 |
+
lower_center = get_landmark_coords(landmarks, AU12_LANDMARKS['lower_center'], frame_width, frame_height)
|
| 273 |
+
|
| 274 |
+
mouth_width = calculate_distance(left_corner, right_corner)
|
| 275 |
+
mouth_height = calculate_distance(upper_center, lower_center)
|
| 276 |
+
mouth_center_y = (upper_center[1] + lower_center[1]) / 2
|
| 277 |
+
|
| 278 |
+
left_corner_lift = mouth_center_y - left_corner[1]
|
| 279 |
+
right_corner_lift = mouth_center_y - right_corner[1]
|
| 280 |
+
avg_corner_lift = (left_corner_lift + right_corner_lift) / 2
|
| 281 |
+
|
| 282 |
+
mouth_ratio = mouth_width / (mouth_height + 1e-6)
|
| 283 |
+
normalized_lift = avg_corner_lift / mouth_height if mouth_height > 0 else 0
|
| 284 |
+
|
| 285 |
+
lift_difference = abs(left_corner_lift - right_corner_lift)
|
| 286 |
+
symmetry_score = 1.0 - min(1.0, lift_difference / (mouth_height + 1e-6))
|
| 287 |
+
|
| 288 |
+
is_active = (normalized_lift > 0.25 and mouth_ratio > 2.8 and symmetry_score > 0.6)
|
| 289 |
+
intensity = min(100, max(0, normalized_lift * 250))
|
| 290 |
+
|
| 291 |
+
self.activation_history.append(int(is_active))
|
| 292 |
+
self.intensity_history.append(intensity)
|
| 293 |
+
|
| 294 |
+
return is_active, intensity
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
class AU14Detector:
|
| 298 |
+
"""AU14 - Dimpler (Smile Depth Indicator)"""
|
| 299 |
+
def __init__(self, window_size=30):
|
| 300 |
+
self.name = "AU14_Dimpler"
|
| 301 |
+
self.activation_history = deque(maxlen=window_size)
|
| 302 |
+
self.intensity_history = deque(maxlen=window_size)
|
| 303 |
+
|
| 304 |
+
def detect(self, landmarks, frame_width, frame_height):
|
| 305 |
+
left_dimple = get_landmark_coords(landmarks, AU14_LANDMARKS['left_dimple'], frame_width, frame_height)
|
| 306 |
+
right_dimple = get_landmark_coords(landmarks, AU14_LANDMARKS['right_dimple'], frame_width, frame_height)
|
| 307 |
+
left_corner = get_landmark_coords(landmarks, AU14_LANDMARKS['left_corner'], frame_width, frame_height)
|
| 308 |
+
right_corner = get_landmark_coords(landmarks, AU14_LANDMARKS['right_corner'], frame_width, frame_height)
|
| 309 |
+
|
| 310 |
+
# Dimples appear when corners pull back and create indentation
|
| 311 |
+
left_depth = calculate_distance(left_dimple, left_corner)
|
| 312 |
+
right_depth = calculate_distance(right_dimple, right_corner)
|
| 313 |
+
avg_depth = (left_depth + right_depth) / 2
|
| 314 |
+
|
| 315 |
+
# Check corner retraction
|
| 316 |
+
corner_distance = calculate_distance(left_corner, right_corner)
|
| 317 |
+
dimple_score = avg_depth / (corner_distance + 1e-6)
|
| 318 |
+
|
| 319 |
+
is_active = dimple_score > 0.15
|
| 320 |
+
intensity = min(100, max(0, (dimple_score - 0.10) * 500))
|
| 321 |
+
|
| 322 |
+
self.activation_history.append(int(is_active))
|
| 323 |
+
self.intensity_history.append(intensity)
|
| 324 |
+
|
| 325 |
+
return is_active, intensity
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
class AU17Detector:
|
| 329 |
+
"""AU17 - Chin Raiser (Doubt, Pouting, Sadness)"""
|
| 330 |
+
def __init__(self, window_size=30):
|
| 331 |
+
self.name = "AU17_ChinRaise"
|
| 332 |
+
self.activation_history = deque(maxlen=window_size)
|
| 333 |
+
self.intensity_history = deque(maxlen=window_size)
|
| 334 |
+
|
| 335 |
+
def detect(self, landmarks, frame_width, frame_height):
|
| 336 |
+
chin = get_landmark_coords(landmarks, AU17_LANDMARKS['chin_center'], frame_width, frame_height)
|
| 337 |
+
lower_lip = get_landmark_coords(landmarks, AU17_LANDMARKS['lower_lip'], frame_width, frame_height)
|
| 338 |
+
|
| 339 |
+
# When chin raises, distance between chin and lower lip decreases
|
| 340 |
+
chin_lip_distance = calculate_distance(chin, lower_lip)
|
| 341 |
+
|
| 342 |
+
# Normalize by face height
|
| 343 |
+
nose = get_landmark_coords(landmarks, 1, frame_width, frame_height)
|
| 344 |
+
face_height = calculate_distance(nose, chin)
|
| 345 |
+
normalized_distance = chin_lip_distance / (face_height + 1e-6)
|
| 346 |
+
|
| 347 |
+
is_active = normalized_distance < 0.08 # Chin pushed up
|
| 348 |
+
intensity = min(100, max(0, (0.12 - normalized_distance) / 0.12 * 100))
|
| 349 |
+
|
| 350 |
+
self.activation_history.append(int(is_active))
|
| 351 |
+
self.intensity_history.append(intensity)
|
| 352 |
+
|
| 353 |
+
return is_active, intensity
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
class AU23Detector:
|
| 357 |
+
"""AU23 - Lip Tightener (Anger, Tension)"""
|
| 358 |
+
def __init__(self, window_size=30):
|
| 359 |
+
self.name = "AU23_LipTighten"
|
| 360 |
+
self.activation_history = deque(maxlen=window_size)
|
| 361 |
+
self.intensity_history = deque(maxlen=window_size)
|
| 362 |
+
|
| 363 |
+
def detect(self, landmarks, frame_width, frame_height):
|
| 364 |
+
left_corner = get_landmark_coords(landmarks, AU23_LANDMARKS['left_corner'], frame_width, frame_height)
|
| 365 |
+
right_corner = get_landmark_coords(landmarks, AU23_LANDMARKS['right_corner'], frame_width, frame_height)
|
| 366 |
+
left_outer = get_landmark_coords(landmarks, AU23_LANDMARKS['left_outer'], frame_width, frame_height)
|
| 367 |
+
right_outer = get_landmark_coords(landmarks, AU23_LANDMARKS['right_outer'], frame_width, frame_height)
|
| 368 |
+
|
| 369 |
+
corner_width = calculate_distance(left_corner, right_corner)
|
| 370 |
+
outer_width = calculate_distance(left_outer, right_outer)
|
| 371 |
+
tightness_ratio = corner_width / (outer_width + 1e-6)
|
| 372 |
+
|
| 373 |
+
is_active = (tightness_ratio < 0.85)
|
| 374 |
+
intensity = min(100, max(0, (0.95 - tightness_ratio) / 0.20 * 100))
|
| 375 |
+
|
| 376 |
+
self.activation_history.append(int(is_active))
|
| 377 |
+
self.intensity_history.append(intensity)
|
| 378 |
+
|
| 379 |
+
return is_active, intensity
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
class AU24Detector:
|
| 383 |
+
"""AU24 - Lip Pressor (Stress, Tension)"""
|
| 384 |
+
def __init__(self, window_size=30):
|
| 385 |
+
self.name = "AU24_LipPress"
|
| 386 |
+
self.activation_history = deque(maxlen=window_size)
|
| 387 |
+
self.intensity_history = deque(maxlen=window_size)
|
| 388 |
+
|
| 389 |
+
def detect(self, landmarks, frame_width, frame_height):
|
| 390 |
+
upper_lip_top = get_landmark_coords(landmarks, AU24_LANDMARKS['upper_lip_top'], frame_width, frame_height)
|
| 391 |
+
upper_lip_bottom = get_landmark_coords(landmarks, AU24_LANDMARKS['upper_lip_bottom'], frame_width, frame_height)
|
| 392 |
+
lower_lip_top = get_landmark_coords(landmarks, AU24_LANDMARKS['lower_lip_top'], frame_width, frame_height)
|
| 393 |
+
lower_lip_bottom = get_landmark_coords(landmarks, AU24_LANDMARKS['lower_lip_bottom'], frame_width, frame_height)
|
| 394 |
+
|
| 395 |
+
upper_lip_thickness = calculate_distance(upper_lip_top, upper_lip_bottom)
|
| 396 |
+
lower_lip_thickness = calculate_distance(lower_lip_top, lower_lip_bottom)
|
| 397 |
+
total_lip_thickness = upper_lip_thickness + lower_lip_thickness
|
| 398 |
+
|
| 399 |
+
mouth_opening = calculate_distance(upper_lip_bottom, lower_lip_top)
|
| 400 |
+
lip_press_score = mouth_opening / (total_lip_thickness + 1e-6)
|
| 401 |
+
|
| 402 |
+
is_active = (lip_press_score < 0.4 and total_lip_thickness < 15)
|
| 403 |
+
intensity = min(100, max(0, (0.6 - lip_press_score) / 0.6 * 100))
|
| 404 |
+
|
| 405 |
+
self.activation_history.append(int(is_active))
|
| 406 |
+
self.intensity_history.append(intensity)
|
| 407 |
+
|
| 408 |
+
return is_active, intensity
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
class AU26Detector:
|
| 412 |
+
"""AU26 - Jaw Drop (Surprise, Shock)"""
|
| 413 |
+
def __init__(self, window_size=30):
|
| 414 |
+
self.name = "AU26_JawDrop"
|
| 415 |
+
self.activation_history = deque(maxlen=window_size)
|
| 416 |
+
self.intensity_history = deque(maxlen=window_size)
|
| 417 |
+
|
| 418 |
+
def detect(self, landmarks, frame_width, frame_height):
|
| 419 |
+
upper_lip = get_landmark_coords(landmarks, AU26_LANDMARKS['upper_lip'], frame_width, frame_height)
|
| 420 |
+
lower_lip = get_landmark_coords(landmarks, AU26_LANDMARKS['lower_lip'], frame_width, frame_height)
|
| 421 |
+
chin = get_landmark_coords(landmarks, AU26_LANDMARKS['chin'], frame_width, frame_height)
|
| 422 |
+
nose = get_landmark_coords(landmarks, AU26_LANDMARKS['nose'], frame_width, frame_height)
|
| 423 |
+
|
| 424 |
+
# Mouth opening
|
| 425 |
+
mouth_opening = calculate_distance(upper_lip, lower_lip)
|
| 426 |
+
|
| 427 |
+
# Jaw drop (distance from nose to chin increases)
|
| 428 |
+
jaw_length = calculate_distance(nose, chin)
|
| 429 |
+
|
| 430 |
+
# Normalize
|
| 431 |
+
mouth_opening_ratio = mouth_opening / (jaw_length + 1e-6)
|
| 432 |
+
|
| 433 |
+
is_active = mouth_opening_ratio > 0.15 # Mouth significantly open
|
| 434 |
+
intensity = min(100, max(0, (mouth_opening_ratio - 0.10) / 0.20 * 100))
|
| 435 |
+
|
| 436 |
+
self.activation_history.append(int(is_active))
|
| 437 |
+
self.intensity_history.append(intensity)
|
| 438 |
+
|
| 439 |
+
return is_active, intensity
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
# ==================== FEATURE EXTRACTOR ====================
|
| 443 |
+
|
| 444 |
+
class MultiAUFeatureExtractor:
|
| 445 |
+
def __init__(self, detectors):
|
| 446 |
+
self.detectors = detectors
|
| 447 |
+
self.feature_log = []
|
| 448 |
+
|
| 449 |
+
def extract_features(self, timestamp):
|
| 450 |
+
features = {'timestamp': timestamp}
|
| 451 |
+
|
| 452 |
+
for detector in self.detectors:
|
| 453 |
+
is_active = detector.activation_history[-1] if detector.activation_history else 0
|
| 454 |
+
intensity = detector.intensity_history[-1] if detector.intensity_history else 0
|
| 455 |
+
|
| 456 |
+
activation_rate = sum(detector.activation_history) / len(detector.activation_history) if detector.activation_history else 0
|
| 457 |
+
avg_intensity = np.mean(detector.intensity_history) if detector.intensity_history else 0
|
| 458 |
+
max_intensity = np.max(detector.intensity_history) if detector.intensity_history else 0
|
| 459 |
+
intensity_std = np.std(detector.intensity_history) if detector.intensity_history else 0
|
| 460 |
+
|
| 461 |
+
features[f'{detector.name}_active'] = is_active
|
| 462 |
+
features[f'{detector.name}_intensity'] = intensity
|
| 463 |
+
features[f'{detector.name}_activation_rate'] = activation_rate
|
| 464 |
+
features[f'{detector.name}_avg_intensity'] = avg_intensity
|
| 465 |
+
features[f'{detector.name}_max_intensity'] = max_intensity
|
| 466 |
+
features[f'{detector.name}_intensity_std'] = intensity_std
|
| 467 |
+
|
| 468 |
+
self.feature_log.append(features)
|
| 469 |
+
return features
|
| 470 |
+
|
| 471 |
+
def get_dataframe(self):
|
| 472 |
+
return pd.DataFrame(self.feature_log)
|
| 473 |
+
|
| 474 |
+
def save_features(self, filename):
|
| 475 |
+
df = self.get_dataframe()
|
| 476 |
+
df.to_csv(filename, index=False)
|
| 477 |
+
print(f"β Features saved to {filename}")
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
# ==================== DETECTION SESSION ====================
|
| 481 |
+
|
| 482 |
+
def run_detection_session(duration_seconds=15, save_data=True):
|
| 483 |
+
# Initialize all 10 AU detectors
|
| 484 |
+
au01 = AU01Detector()
|
| 485 |
+
au04 = AU04Detector()
|
| 486 |
+
au06 = AU06Detector()
|
| 487 |
+
au07 = AU07Detector()
|
| 488 |
+
au12 = AU12Detector()
|
| 489 |
+
au14 = AU14Detector()
|
| 490 |
+
au17 = AU17Detector()
|
| 491 |
+
au23 = AU23Detector()
|
| 492 |
+
au24 = AU24Detector()
|
| 493 |
+
au26 = AU26Detector()
|
| 494 |
+
|
| 495 |
+
detectors = [au01, au04, au06, au07, au12, au14, au17, au23, au24, au26]
|
| 496 |
+
feature_extractor = MultiAUFeatureExtractor(detectors)
|
| 497 |
+
|
| 498 |
+
cap = cv2.VideoCapture(0)
|
| 499 |
+
|
| 500 |
+
print(f"\n{'='*70}")
|
| 501 |
+
print(f" COMPLETE 10-AU STRESS DETECTION SYSTEM")
|
| 502 |
+
print(f" Recording for {duration_seconds} seconds...")
|
| 503 |
+
print(f"{'='*70}\n")
|
| 504 |
+
|
| 505 |
+
start_time = time.time()
|
| 506 |
+
frame_count = 0
|
| 507 |
+
|
| 508 |
+
while True:
|
| 509 |
+
ret, frame = cap.read()
|
| 510 |
+
if not ret:
|
| 511 |
+
break
|
| 512 |
+
|
| 513 |
+
current_time = time.time()
|
| 514 |
+
elapsed = current_time - start_time
|
| 515 |
+
|
| 516 |
+
if elapsed >= duration_seconds:
|
| 517 |
+
break
|
| 518 |
+
|
| 519 |
+
frame = cv2.flip(frame, 1)
|
| 520 |
+
frame_height, frame_width = frame.shape[:2]
|
| 521 |
+
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 522 |
+
|
| 523 |
+
results = face_mesh.process(rgb_frame)
|
| 524 |
+
|
| 525 |
+
if results.multi_face_landmarks:
|
| 526 |
+
landmarks = results.multi_face_landmarks[0].landmark
|
| 527 |
+
|
| 528 |
+
# Detect all 10 AUs
|
| 529 |
+
au01_active, au01_intensity = au01.detect(landmarks, frame_width, frame_height)
|
| 530 |
+
au04_active, au04_intensity = au04.detect(landmarks, frame_width, frame_height)
|
| 531 |
+
au06_active, au06_intensity = au06.detect(landmarks, frame_width, frame_height)
|
| 532 |
+
au07_active, au07_intensity = au07.detect(landmarks, frame_width, frame_height)
|
| 533 |
+
au12_active, au12_intensity = au12.detect(landmarks, frame_width, frame_height)
|
| 534 |
+
au14_active, au14_intensity = au14.detect(landmarks, frame_width, frame_height)
|
| 535 |
+
au17_active, au17_intensity = au17.detect(landmarks, frame_width, frame_height)
|
| 536 |
+
au23_active, au23_intensity = au23.detect(landmarks, frame_width, frame_height)
|
| 537 |
+
au24_active, au24_intensity = au24.detect(landmarks, frame_width, frame_height)
|
| 538 |
+
au26_active, au26_intensity = au26.detect(landmarks, frame_width, frame_height)
|
| 539 |
+
|
| 540 |
+
features = feature_extractor.extract_features(elapsed)
|
| 541 |
+
|
| 542 |
+
# Display (2 columns)
|
| 543 |
+
y_offset = 25
|
| 544 |
+
col1_x = 10
|
| 545 |
+
col2_x = frame_width // 2 + 10
|
| 546 |
+
|
| 547 |
+
# Header
|
| 548 |
+
cv2.putText(frame, f"Time: {elapsed:.1f}s / {duration_seconds}s",
|
| 549 |
+
(10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
|
| 550 |
+
|
| 551 |
+
y_offset += 35
|
| 552 |
+
|
| 553 |
+
# Column 1: Stress Indicators
|
| 554 |
+
cv2.putText(frame, "STRESS INDICATORS:", (col1_x, y_offset),
|
| 555 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
|
| 556 |
+
y_offset += 25
|
| 557 |
+
|
| 558 |
+
stress_aus = [
|
| 559 |
+
(au01_active, au01_intensity, "AU01-BrowRaise"),
|
| 560 |
+
(au04_active, au04_intensity, "AU04-BrowLower"),
|
| 561 |
+
(au07_active, au07_intensity, "AU07-LidTight"),
|
| 562 |
+
(au17_active, au17_intensity, "AU17-ChinRaise"),
|
| 563 |
+
(au23_active, au23_intensity, "AU23-LipTight"),
|
| 564 |
+
(au24_active, au24_intensity, "AU24-LipPress")
|
| 565 |
+
]
|
| 566 |
+
|
| 567 |
+
for active, intensity, name in stress_aus:
|
| 568 |
+
color = (0, 0, 255) if active else (100, 100, 100)
|
| 569 |
+
cv2.putText(frame, f"{name}: {intensity:.0f}%",
|
| 570 |
+
(col1_x, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.4, color, 1)
|
| 571 |
+
y_offset += 20
|
| 572 |
+
|
| 573 |
+
# Column 2: Positive Indicators
|
| 574 |
+
y_offset = 60
|
| 575 |
+
cv2.putText(frame, "POSITIVE INDICATORS:", (col2_x, y_offset),
|
| 576 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
|
| 577 |
+
y_offset += 25
|
| 578 |
+
|
| 579 |
+
positive_aus = [
|
| 580 |
+
(au06_active, au06_intensity, "AU06-CheekRaise"),
|
| 581 |
+
(au12_active, au12_intensity, "AU12-SmilePull"),
|
| 582 |
+
(au14_active, au14_intensity, "AU14-Dimpler"),
|
| 583 |
+
(au26_active, au26_intensity, "AU26-JawDrop")
|
| 584 |
+
]
|
| 585 |
+
|
| 586 |
+
for active, intensity, name in positive_aus:
|
| 587 |
+
color = (0, 255, 0) if active else (100, 100, 100)
|
| 588 |
+
cv2.putText(frame, f"{name}: {intensity:.0f}%",
|
| 589 |
+
(col2_x, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.4, color, 1)
|
| 590 |
+
y_offset += 20
|
| 591 |
+
|
| 592 |
+
# Bottom summary
|
| 593 |
+
stress_count = sum([au01_active, au04_active, au07_active, au17_active, au23_active, au24_active])
|
| 594 |
+
positive_count = sum([au06_active, au12_active, au14_active])
|
| 595 |
+
|
| 596 |
+
cv2.rectangle(frame, (10, frame_height - 60), (frame_width - 10, frame_height - 10), (50, 50, 50), -1)
|
| 597 |
+
cv2.putText(frame, f"Stress AUs: {stress_count}/6 | Positive AUs: {positive_count}/4",
|
| 598 |
+
(20, frame_height - 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
| 599 |
+
|
| 600 |
+
cv2.imshow('Complete 10-AU Stress Detection', frame)
|
| 601 |
+
frame_count += 1
|
| 602 |
+
|
| 603 |
+
if cv2.waitKey(1) & 0xFF == ord('q'):
|
| 604 |
+
break
|
| 605 |
+
|
| 606 |
+
cap.release()
|
| 607 |
+
cv2.destroyAllWindows()
|
| 608 |
+
|
| 609 |
+
if save_data:
|
| 610 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 611 |
+
filename = f"complete_au_features_{timestamp}.csv"
|
| 612 |
+
feature_extractor.save_features(filename)
|
| 613 |
+
|
| 614 |
+
print(f"\nβ Session complete! Processed {frame_count} frames")
|
| 615 |
+
print(f"β Average FPS: {frame_count/duration_seconds:.1f}")
|
| 616 |
+
|
| 617 |
+
return feature_extractor.get_dataframe()
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
# ==================== ADVANCED ANALYSIS ====================
|
| 621 |
+
|
| 622 |
+
def calculate_comprehensive_stress_score(df):
|
| 623 |
+
"""
|
| 624 |
+
Research-based stress scoring using all 10 AUs
|
| 625 |
+
Weights based on affective computing literature
|
| 626 |
+
"""
|
| 627 |
+
|
| 628 |
+
# STRESS INDICATORS (weighted by research evidence)
|
| 629 |
+
# AU04 (Brow Lower) - Primary anger/stress indicator
|
| 630 |
+
au04_score = (df['AU04_BrowLower_intensity'].mean() / 100) * (df['AU04_BrowLower_activation_rate'].mean()) * 25
|
| 631 |
+
|
| 632 |
+
# AU01 (Inner Brow Raise) - Worry/sadness indicator
|
| 633 |
+
au01_score = (df['AU01_InnerBrowRaise_intensity'].mean() / 100) * (df['AU01_InnerBrowRaise_activation_rate'].mean()) * 15
|
| 634 |
+
|
| 635 |
+
# AU07 (Lid Tighten) - Tension indicator
|
| 636 |
+
au07_score = (df['AU07_LidTighten_intensity'].mean() / 100) * (df['AU07_LidTighten_activation_rate'].mean()) * 12
|
| 637 |
+
|
| 638 |
+
# AU24 (Lip Press) - Stress/tension
|
| 639 |
+
au24_score = (df['AU24_LipPress_intensity'].mean() / 100) * (df['AU24_LipPress_activation_rate'].mean()) * 15
|
| 640 |
+
|
| 641 |
+
# AU23 (Lip Tighten) - Anger/tension
|
| 642 |
+
au23_score = (df['AU23_LipTighten_intensity'].mean() / 100) * (df['AU23_LipTighten_activation_rate'].mean()) * 12
|
| 643 |
+
|
| 644 |
+
# AU17 (Chin Raise) - Doubt/sadness
|
| 645 |
+
au17_score = (df['AU17_ChinRaise_intensity'].mean() / 100) * (df['AU17_ChinRaise_activation_rate'].mean()) * 8
|
| 646 |
+
|
| 647 |
+
# POSITIVE INDICATORS (reduce stress score)
|
| 648 |
+
# AU06 + AU12 (Duchenne Smile - genuine happiness)
|
| 649 |
+
duchenne_smile = ((df['AU06_CheekRaise_active'] == 1) & (df['AU12_LipCornerPull_active'] == 1)).sum() / len(df)
|
| 650 |
+
positive_reduction = duchenne_smile * 15
|
| 651 |
+
|
| 652 |
+
# AU12 alone (social smile - may mask stress)
|
| 653 |
+
social_smile = ((df['AU12_LipCornerPull_active'] == 1) & (df['AU06_CheekRaise_active'] == 0)).sum() / len(df)
|
| 654 |
+
masking_indicator = social_smile * 5 # Adds to stress if smiling without cheek raise
|
| 655 |
+
|
| 656 |
+
# TEMPORAL PATTERNS
|
| 657 |
+
# Sustained stress (continuous activation 3+ seconds)
|
| 658 |
+
sustained_stress = 0
|
| 659 |
+
for au_name in ['AU04_BrowLower', 'AU24_LipPress', 'AU23_LipTighten']:
|
| 660 |
+
streak = 0
|
| 661 |
+
for val in df[f'{au_name}_active']:
|
| 662 |
+
if val == 1:
|
| 663 |
+
streak += 1
|
| 664 |
+
if streak >= 90: # 3 seconds at 30fps
|
| 665 |
+
sustained_stress += 1
|
| 666 |
+
break
|
| 667 |
+
else:
|
| 668 |
+
streak = 0
|
| 669 |
+
sustained_score = min(10, sustained_stress * 3)
|
| 670 |
+
|
| 671 |
+
# Co-occurrence of multiple stress AUs
|
| 672 |
+
stress_cols = ['AU01_InnerBrowRaise_active', 'AU04_BrowLower_active',
|
| 673 |
+
'AU07_LidTighten_active', 'AU23_LipTighten_active',
|
| 674 |
+
'AU24_LipPress_active', 'AU17_ChinRaise_active']
|
| 675 |
+
co_occurrence = (df[stress_cols].sum(axis=1) >= 3).sum() / len(df)
|
| 676 |
+
co_occurrence_score = co_occurrence * 8
|
| 677 |
+
|
| 678 |
+
# COMBINED STRESS SCORE
|
| 679 |
+
raw_stress = (au04_score + au01_score + au07_score + au24_score +
|
| 680 |
+
au23_score + au17_score + sustained_score +
|
| 681 |
+
co_occurrence_score + masking_indicator - positive_reduction)
|
| 682 |
+
|
| 683 |
+
stress_score = min(100, max(0, raw_stress))
|
| 684 |
+
|
| 685 |
+
# Classification
|
| 686 |
+
if stress_score < 25:
|
| 687 |
+
classification = "NOT STRESSED"
|
| 688 |
+
color = "π’"
|
| 689 |
+
elif stress_score < 55:
|
| 690 |
+
classification = "POSSIBLY STRESSED"
|
| 691 |
+
color = "π‘"
|
| 692 |
+
else:
|
| 693 |
+
classification = "STRESSED"
|
| 694 |
+
color = "π΄"
|
| 695 |
+
|
| 696 |
+
return {
|
| 697 |
+
'classification': classification,
|
| 698 |
+
'color': color,
|
| 699 |
+
'stress_score': stress_score,
|
| 700 |
+
'components': {
|
| 701 |
+
'au04': au04_score,
|
| 702 |
+
'au01': au01_score,
|
| 703 |
+
'au07': au07_score,
|
| 704 |
+
'au24': au24_score,
|
| 705 |
+
'au23': au23_score,
|
| 706 |
+
'au17': au17_score,
|
| 707 |
+
'sustained': sustained_score,
|
| 708 |
+
'co_occurrence': co_occurrence_score,
|
| 709 |
+
'duchenne_smile': duchenne_smile,
|
| 710 |
+
'social_smile_masking': masking_indicator
|
| 711 |
+
},
|
| 712 |
+
'activation_percentages': {
|
| 713 |
+
'AU01': (df['AU01_InnerBrowRaise_active'].sum() / len(df)) * 100,
|
| 714 |
+
'AU04': (df['AU04_BrowLower_active'].sum() / len(df)) * 100,
|
| 715 |
+
'AU06': (df['AU06_CheekRaise_active'].sum() / len(df)) * 100,
|
| 716 |
+
'AU07': (df['AU07_LidTighten_active'].sum() / len(df)) * 100,
|
| 717 |
+
'AU12': (df['AU12_LipCornerPull_active'].sum() / len(df)) * 100,
|
| 718 |
+
'AU14': (df['AU14_Dimpler_active'].sum() / len(df)) * 100,
|
| 719 |
+
'AU17': (df['AU17_ChinRaise_active'].sum() / len(df)) * 100,
|
| 720 |
+
'AU23': (df['AU23_LipTighten_active'].sum() / len(df)) * 100,
|
| 721 |
+
'AU24': (df['AU24_LipPress_active'].sum() / len(df)) * 100,
|
| 722 |
+
'AU26': (df['AU26_JawDrop_active'].sum() / len(df)) * 100
|
| 723 |
+
}
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
|
| 727 |
+
# ==================== COMPREHENSIVE VISUALIZATION ====================
|
| 728 |
+
|
| 729 |
+
def plot_comprehensive_analysis(df):
|
| 730 |
+
"""Create 10 publication-quality plots"""
|
| 731 |
+
|
| 732 |
+
fig = plt.figure(figsize=(20, 16))
|
| 733 |
+
gs = GridSpec(5, 3, figure=fig, hspace=0.35, wspace=0.3)
|
| 734 |
+
|
| 735 |
+
fig.suptitle('Comprehensive 10-AU Facial Expression Analysis\nStress Detection System',
|
| 736 |
+
fontsize=18, fontweight='bold', y=0.995)
|
| 737 |
+
|
| 738 |
+
# Plot 1: All AU Activations Over Time
|
| 739 |
+
ax1 = fig.add_subplot(gs[0, :2])
|
| 740 |
+
stress_aus = ['AU01_InnerBrowRaise', 'AU04_BrowLower', 'AU07_LidTighten',
|
| 741 |
+
'AU17_ChinRaise', 'AU23_LipTighten', 'AU24_LipPress']
|
| 742 |
+
colors_stress = ['orange', 'red', 'darkred', 'brown', 'purple', 'magenta']
|
| 743 |
+
|
| 744 |
+
for au, color in zip(stress_aus, colors_stress):
|
| 745 |
+
ax1.plot(df['timestamp'], df[f'{au}_active'], label=au.split('_')[1],
|
| 746 |
+
color=color, linewidth=1.5, alpha=0.7)
|
| 747 |
+
|
| 748 |
+
ax1.set_xlabel('Time (seconds)', fontweight='bold')
|
| 749 |
+
ax1.set_ylabel('Activation (Binary)', fontweight='bold')
|
| 750 |
+
ax1.set_title('Stress-Related AU Temporal Patterns')
|
| 751 |
+
ax1.legend(loc='upper right', ncol=3, fontsize=8)
|
| 752 |
+
ax1.grid(True, alpha=0.3)
|
| 753 |
+
ax1.set_ylim(-0.1, 1.1)
|
| 754 |
+
|
| 755 |
+
# Plot 2: Positive AUs Over Time
|
| 756 |
+
ax2 = fig.add_subplot(gs[0, 2])
|
| 757 |
+
positive_aus = ['AU06_CheekRaise', 'AU12_LipCornerPull', 'AU14_Dimpler', 'AU26_JawDrop']
|
| 758 |
+
colors_pos = ['lightgreen', 'green', 'darkgreen', 'blue']
|
| 759 |
+
|
| 760 |
+
for au, color in zip(positive_aus, colors_pos):
|
| 761 |
+
ax2.plot(df['timestamp'], df[f'{au}_active'], label=au.split('_')[1],
|
| 762 |
+
color=color, linewidth=1.5, alpha=0.7)
|
| 763 |
+
|
| 764 |
+
ax2.set_xlabel('Time (s)', fontweight='bold')
|
| 765 |
+
ax2.set_ylabel('Activation', fontweight='bold')
|
| 766 |
+
ax2.set_title('Positive AU Patterns')
|
| 767 |
+
ax2.legend(fontsize=7)
|
| 768 |
+
ax2.grid(True, alpha=0.3)
|
| 769 |
+
ax2.set_ylim(-0.1, 1.1)
|
| 770 |
+
|
| 771 |
+
# Plot 3: Intensity Heatmap (All 10 AUs)
|
| 772 |
+
ax3 = fig.add_subplot(gs[1, :])
|
| 773 |
+
all_aus = ['AU01_InnerBrowRaise', 'AU04_BrowLower', 'AU06_CheekRaise',
|
| 774 |
+
'AU07_LidTighten', 'AU12_LipCornerPull', 'AU14_Dimpler',
|
| 775 |
+
'AU17_ChinRaise', 'AU23_LipTighten', 'AU24_LipPress', 'AU26_JawDrop']
|
| 776 |
+
|
| 777 |
+
intensity_data = df[[f'{au}_intensity' for au in all_aus]].T
|
| 778 |
+
im = ax3.imshow(intensity_data, aspect='auto', cmap='RdYlGn_r', interpolation='nearest')
|
| 779 |
+
ax3.set_yticks(range(10))
|
| 780 |
+
ax3.set_yticklabels([au.split('_')[0] for au in all_aus])
|
| 781 |
+
ax3.set_xlabel('Frame Number', fontweight='bold')
|
| 782 |
+
ax3.set_title('Complete AU Intensity Heatmap (All 10 Action Units)')
|
| 783 |
+
plt.colorbar(im, ax=ax3, label='Intensity (%)')
|
| 784 |
+
|
| 785 |
+
# Plot 4: AU Activation Frequency Bar Chart
|
| 786 |
+
ax4 = fig.add_subplot(gs[2, 0])
|
| 787 |
+
result = calculate_comprehensive_stress_score(df)
|
| 788 |
+
au_names = list(result['activation_percentages'].keys())
|
| 789 |
+
au_values = list(result['activation_percentages'].values())
|
| 790 |
+
colors = ['red' if 'AU04' in au or 'AU24' in au or 'AU23' in au
|
| 791 |
+
else 'orange' if 'AU01' in au or 'AU07' in au or 'AU17' in au
|
| 792 |
+
else 'green' for au in au_names]
|
| 793 |
+
|
| 794 |
+
bars = ax4.barh(au_names, au_values, color=colors, alpha=0.7)
|
| 795 |
+
ax4.set_xlabel('Activation Percentage (%)', fontweight='bold')
|
| 796 |
+
ax4.set_title('AU Activation Frequencies')
|
| 797 |
+
ax4.grid(True, alpha=0.3, axis='x')
|
| 798 |
+
|
| 799 |
+
# Plot 5: Duchenne vs Non-Duchenne Smile Detection
|
| 800 |
+
ax5 = fig.add_subplot(gs[2, 1])
|
| 801 |
+
duchenne = ((df['AU06_CheekRaise_active'] == 1) & (df['AU12_LipCornerPull_active'] == 1)).sum()
|
| 802 |
+
non_duchenne = ((df['AU12_LipCornerPull_active'] == 1) & (df['AU06_CheekRaise_active'] == 0)).sum()
|
| 803 |
+
no_smile = len(df) - duchenne - non_duchenne
|
| 804 |
+
|
| 805 |
+
labels = ['Genuine\n(Duchenne)', 'Social\n(Masking)', 'No Smile']
|
| 806 |
+
sizes = [duchenne, non_duchenne, no_smile]
|
| 807 |
+
colors_pie = ['green', 'yellow', 'lightgray']
|
| 808 |
+
|
| 809 |
+
ax5.pie(sizes, labels=labels, colors=colors_pie, autopct='%1.1f%%', startangle=90)
|
| 810 |
+
ax5.set_title('Smile Type Distribution')
|
| 811 |
+
|
| 812 |
+
# Plot 6: Stress vs Positive Balance
|
| 813 |
+
ax6 = fig.add_subplot(gs[2, 2])
|
| 814 |
+
stress_intensity_avg = df[['AU01_InnerBrowRaise_intensity', 'AU04_BrowLower_intensity',
|
| 815 |
+
'AU07_LidTighten_intensity', 'AU23_LipTighten_intensity',
|
| 816 |
+
'AU24_LipPress_intensity', 'AU17_ChinRaise_intensity']].mean(axis=1)
|
| 817 |
+
positive_intensity_avg = df[['AU06_CheekRaise_intensity', 'AU12_LipCornerPull_intensity',
|
| 818 |
+
'AU14_Dimpler_intensity']].mean(axis=1)
|
| 819 |
+
|
| 820 |
+
ax6.plot(df['timestamp'], stress_intensity_avg, color='red', linewidth=2, label='Stress AUs')
|
| 821 |
+
ax6.plot(df['timestamp'], positive_intensity_avg, color='green', linewidth=2, label='Positive AUs')
|
| 822 |
+
ax6.fill_between(df['timestamp'], stress_intensity_avg, alpha=0.3, color='red')
|
| 823 |
+
ax6.fill_between(df['timestamp'], positive_intensity_avg, alpha=0.3, color='green')
|
| 824 |
+
ax6.set_xlabel('Time (seconds)', fontweight='bold')
|
| 825 |
+
ax6.set_ylabel('Average Intensity (%)', fontweight='bold')
|
| 826 |
+
ax6.set_title('Stress vs Positive Affect Balance')
|
| 827 |
+
ax6.legend()
|
| 828 |
+
ax6.grid(True, alpha=0.3)
|
| 829 |
+
|
| 830 |
+
# Plot 7: Correlation Matrix (All AUs)
|
| 831 |
+
ax7 = fig.add_subplot(gs[3, :2])
|
| 832 |
+
correlation_cols = [f'{au}_intensity' for au in all_aus]
|
| 833 |
+
corr_matrix = df[correlation_cols].corr()
|
| 834 |
+
|
| 835 |
+
im = ax7.imshow(corr_matrix, cmap='coolwarm', vmin=-1, vmax=1, aspect='auto')
|
| 836 |
+
ax7.set_xticks(range(10))
|
| 837 |
+
ax7.set_yticks(range(10))
|
| 838 |
+
ax7.set_xticklabels([au.split('_')[0] for au in all_aus], rotation=45, ha='right')
|
| 839 |
+
ax7.set_yticklabels([au.split('_')[0] for au in all_aus])
|
| 840 |
+
ax7.set_title('Complete AU Correlation Matrix')
|
| 841 |
+
|
| 842 |
+
# Add correlation values
|
| 843 |
+
for i in range(10):
|
| 844 |
+
for j in range(10):
|
| 845 |
+
if abs(corr_matrix.iloc[i, j]) > 0.3: # Only show strong correlations
|
| 846 |
+
ax7.text(j, i, f'{corr_matrix.iloc[i, j]:.2f}',
|
| 847 |
+
ha="center", va="center", color="black", fontsize=7)
|
| 848 |
+
|
| 849 |
+
plt.colorbar(im, ax=ax7)
|
| 850 |
+
|
| 851 |
+
# Plot 8: Time-Windowed Stress Evolution
|
| 852 |
+
ax8 = fig.add_subplot(gs[3, 2])
|
| 853 |
+
window_size = 90 # 3 seconds
|
| 854 |
+
windowed_stress = []
|
| 855 |
+
window_times = []
|
| 856 |
+
|
| 857 |
+
for i in range(0, len(df) - window_size, window_size // 2):
|
| 858 |
+
window_df = df.iloc[i:i+window_size]
|
| 859 |
+
if len(window_df) > 0:
|
| 860 |
+
window_result = calculate_comprehensive_stress_score(window_df)
|
| 861 |
+
windowed_stress.append(window_result['stress_score'])
|
| 862 |
+
window_times.append(window_df['timestamp'].mean())
|
| 863 |
+
|
| 864 |
+
ax8.plot(window_times, windowed_stress, color='red', linewidth=2, marker='o')
|
| 865 |
+
ax8.fill_between(window_times, windowed_stress, alpha=0.3, color='red')
|
| 866 |
+
ax8.axhline(y=25, color='green', linestyle='--', label='Low threshold', alpha=0.5)
|
| 867 |
+
ax8.axhline(y=55, color='orange', linestyle='--', label='High threshold', alpha=0.5)
|
| 868 |
+
ax8.set_xlabel('Time (seconds)', fontweight='bold')
|
| 869 |
+
ax8.set_ylabel('Stress Score', fontweight='bold')
|
| 870 |
+
ax8.set_title('Stress Score Evolution (3s windows)')
|
| 871 |
+
ax8.legend()
|
| 872 |
+
ax8.grid(True, alpha=0.3)
|
| 873 |
+
|
| 874 |
+
# Plot 9: AU Co-occurrence Matrix
|
| 875 |
+
ax9 = fig.add_subplot(gs[4, 0])
|
| 876 |
+
stress_au_cols = [f'{au}_active' for au in stress_aus]
|
| 877 |
+
co_occur_matrix = np.zeros((6, 6))
|
| 878 |
+
|
| 879 |
+
for i in range(6):
|
| 880 |
+
for j in range(6):
|
| 881 |
+
co_occur = ((df[stress_au_cols[i]] == 1) & (df[stress_au_cols[j]] == 1)).sum()
|
| 882 |
+
co_occur_matrix[i, j] = co_occur / len(df) * 100
|
| 883 |
+
|
| 884 |
+
im = ax9.imshow(co_occur_matrix, cmap='Reds', aspect='auto')
|
| 885 |
+
ax9.set_xticks(range(6))
|
| 886 |
+
ax9.set_yticks(range(6))
|
| 887 |
+
ax9.set_xticklabels([au.split('_')[0] for au in stress_aus], rotation=45, ha='right')
|
| 888 |
+
ax9.set_yticklabels([au.split('_')[0] for au in stress_aus])
|
| 889 |
+
ax9.set_title('Stress AU Co-occurrence (%)')
|
| 890 |
+
plt.colorbar(im, ax=ax9)
|
| 891 |
+
|
| 892 |
+
# Plot 10: Comprehensive Summary Report
|
| 893 |
+
ax10 = fig.add_subplot(gs[4, 1:])
|
| 894 |
+
ax10.axis('off')
|
| 895 |
+
|
| 896 |
+
result = calculate_comprehensive_stress_score(df)
|
| 897 |
+
|
| 898 |
+
summary_text = f"""
|
| 899 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 900 |
+
β COMPREHENSIVE STRESS ASSESSMENT REPORT β
|
| 901 |
+
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£
|
| 902 |
+
β β
|
| 903 |
+
β CLASSIFICATION: {result['color']} {result['classification']:<25} | STRESS SCORE: {result['stress_score']:.1f}/100 β
|
| 904 |
+
β β
|
| 905 |
+
β βββββββββββββββββοΏ½οΏ½οΏ½ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£
|
| 906 |
+
β COMPONENT CONTRIBUTIONS: β
|
| 907 |
+
β β’ AU04 (Brow Lower): {result['components']['au04']:.2f} / 25.0 [{result['activation_percentages']['AU04']:5.1f}% active] β
|
| 908 |
+
β β’ AU01 (Inner Brow): {result['components']['au01']:.2f} / 15.0 [{result['activation_percentages']['AU01']:5.1f}% active] β
|
| 909 |
+
β β’ AU07 (Lid Tighten): {result['components']['au07']:.2f} / 12.0 [{result['activation_percentages']['AU07']:5.1f}% active] β
|
| 910 |
+
β β’ AU24 (Lip Press): {result['components']['au24']:.2f} / 15.0 [{result['activation_percentages']['AU24']:5.1f}% active] β
|
| 911 |
+
β β’ AU23 (Lip Tighten): {result['components']['au23']:.2f} / 12.0 [{result['activation_percentages']['AU23']:5.1f}% active] β
|
| 912 |
+
β β’ AU17 (Chin Raise): {result['components']['au17']:.2f} / 8.0 [{result['activation_percentages']['AU17']:5.1f}% active] β
|
| 913 |
+
β β’ Sustained Activation: {result['components']['sustained']:.2f} / 10.0 β
|
| 914 |
+
β β’ Co-occurrence Pattern: {result['components']['co_occurrence']:.2f} / 8.0 β
|
| 915 |
+
β β’ Smile Masking Effect: {result['components']['social_smile_masking']:.2f} (adds stress if present) β
|
| 916 |
+
β β’ Duchenne Smile Bonus: -{result['components']['duchenne_smile']*15:.2f} (reduces stress) β
|
| 917 |
+
β β
|
| 918 |
+
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£
|
| 919 |
+
β POSITIVE AFFECT INDICATORS: β
|
| 920 |
+
β β’ AU06 (Cheek Raise): {result['activation_percentages']['AU06']:5.1f}% active β
|
| 921 |
+
β β’ AU12 (Lip Pull): {result['activation_percentages']['AU12']:5.1f}% active β
|
| 922 |
+
β β’ AU14 (Dimpler): {result['activation_percentages']['AU14']:5.1f}% active β
|
| 923 |
+
β β’ Duchenne Smile Rate: {result['components']['duchenne_smile']*100:.1f}% β
|
| 924 |
+
β β
|
| 925 |
+
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£
|
| 926 |
+
β RESEARCH BASIS: β
|
| 927 |
+
β Weights based on Facial Action Coding System (FACS) research: β
|
| 928 |
+
β β’ AU04 highest weight (Ekman & Friesen, 1978) - primary anger/stress indicator β
|
| 929 |
+
β β’ AU06+AU12 combination identifies genuine happiness (Duchenne marker) β
|
| 930 |
+
β β’ Sustained activation and co-occurrence patterns enhance stress detection accuracy β
|
| 931 |
+
β β’ Temporal windowing allows detection of acute stress episodes vs chronic patterns β
|
| 932 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 933 |
+
"""
|
| 934 |
+
|
| 935 |
+
ax10.text(0.05, 0.5, summary_text, fontsize=8, family='monospace',
|
| 936 |
+
verticalalignment='center',
|
| 937 |
+
bbox=dict(boxstyle='round', facecolor='lightgray', alpha=0.2))
|
| 938 |
+
|
| 939 |
+
plt.tight_layout()
|
| 940 |
+
return fig, result
|
| 941 |
+
|
| 942 |
+
|
| 943 |
+
# ==================== MAIN EXECUTION ====================
|
| 944 |
+
|
| 945 |
+
if __name__ == "__main__":
|
| 946 |
+
print("\n" + "="*70)
|
| 947 |
+
print(" COMPLETE 10-AU STRESS DETECTION SYSTEM")
|
| 948 |
+
print(" Based on Facial Action Coding System (FACS)")
|
| 949 |
+
print(" Research Guide: Prof. Anup Nandy")
|
| 950 |
+
print("="*70)
|
| 951 |
+
|
| 952 |
+
print("\n Action Units Detected:")
|
| 953 |
+
print(" STRESS: AU01, AU04, AU07, AU17, AU23, AU24")
|
| 954 |
+
print(" POSITIVE: AU06, AU12, AU14, AU26")
|
| 955 |
+
print("\n Press Enter to start 15-second recording...")
|
| 956 |
+
|
| 957 |
+
input()
|
| 958 |
+
|
| 959 |
+
df = run_detection_session(duration_seconds=15, save_data=True)
|
| 960 |
+
|
| 961 |
+
print("\n" + "="*70)
|
| 962 |
+
print(" Generating comprehensive analysis...")
|
| 963 |
+
print("="*70 + "\n")
|
| 964 |
+
|
| 965 |
+
fig, result = plot_comprehensive_analysis(df)
|
| 966 |
+
|
| 967 |
+
print(f"\n{result['color']} FINAL ASSESSMENT: {result['classification']}")
|
| 968 |
+
print(f" Stress Score: {result['stress_score']:.1f}/100")
|
| 969 |
+
print(f"\n Data saved with {len(df)} frames")
|
| 970 |
+
print(f" Total features per frame: {len(df.columns) - 1}")
|
| 971 |
+
print("\n" + "="*70)
|
| 972 |
+
|
| 973 |
+
plt.show()
|
| 974 |
+
|
| 975 |
+
print("\nβ Analysis complete!")
|