Sohan2004 commited on
Commit
2b73233
Β·
verified Β·
1 Parent(s): d4ee395

Create main.ipynb

Browse files
Files changed (1) hide show
  1. 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!")