Delete main.ipynb
Browse files- main.ipynb +0 -975
main.ipynb
DELETED
|
@@ -1,975 +0,0 @@
|
|
| 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!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|