ans123 commited on
Commit
2347e7f
·
verified ·
1 Parent(s): edfd32e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +334 -189
app.py CHANGED
@@ -1,201 +1,346 @@
 
1
  import cv2
2
- import sounddevice as sd
3
- import mediapipe as mp
4
  import numpy as np
5
  import pandas as pd
6
- import librosa
7
- import threading
8
  import time
9
- import csv
10
- from collections import deque
11
-
12
- # --- Configuration ---
13
- SAMPLE_RATE = 16000
14
- AUDIO_CHANNELS = 1
15
- BUFFER_DURATION_SECONDS = 10 # Keep last 10s of data
16
- PROCESSING_INTERVAL_SECONDS = 4.0
17
- CSV_FILENAME = "metrics_log.csv"
18
-
19
- # --- Buffers (use thread-safe versions if needed) ---
20
- frame_buffer = deque(maxlen=int(BUFFER_DURATION_SECONDS * 30)) # Assuming ~30fps
21
- audio_buffer = deque(maxlen=int(BUFFER_DURATION_SECONDS * SAMPLE_RATE))
22
- frame_timestamps = deque(maxlen=int(BUFFER_DURATION_SECONDS * 30))
23
- audio_timestamps = deque(maxlen=int(BUFFER_DURATION_SECONDS * SAMPLE_RATE)) # Timestamps per chunk
24
-
25
- # --- MediaPipe Setup ---
26
  mp_face_mesh = mp.solutions.face_mesh
27
  mp_drawing = mp.solutions.drawing_utils
28
- drawing_spec = mp_drawing.DrawingSpec(thickness=1, circle_radius=1)
29
- face_mesh = mp_face_mesh.FaceMesh(
30
- max_num_faces=1,
31
- refine_landmarks=True, # Crucial for iris/pupil
32
- min_detection_confidence=0.5,
33
- min_tracking_confidence=0.5)
34
-
35
- # --- Placeholder Functions (Requires detailed implementation) ---
36
- def analyze_video_window(frames, timestamps):
37
- print(f"Analyzing {len(frames)} frames...")
38
- # TODO:
39
- # - Run MediaPipe Face Mesh + Iris on each frame
40
- # - Extract face presence, landmarks, blink status, pupil data per frame
41
- # - Aggregate: % face detected, avg emotion scores (if using FER), avg pupil proxy, total blinks
42
- # - Return aggregated features
43
- blink_count = np.random.randint(0, 5) # Placeholder
44
- avg_pupil_proxy = np.random.rand() # Placeholder
45
- face_detected_ratio = np.random.rand() # Placeholder
46
- avg_valence_proxy = (np.random.rand() - 0.5) * 2 # Placeholder [-1, 1]
47
- avg_arousal_proxy_face = np.random.rand() # Placeholder [0, 1]
48
- return {
49
- "blink_count": blink_count,
50
- "avg_pupil_proxy": avg_pupil_proxy,
51
- "face_detected_ratio": face_detected_ratio,
52
- "avg_valence_proxy": avg_valence_proxy,
53
- "avg_arousal_proxy_face": avg_arousal_proxy_face
54
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- def analyze_audio_window(audio_chunks, timestamps):
57
- if not audio_chunks:
58
- return {"avg_rms": 0, "avg_pitch": 0} # Default
59
- print(f"Analyzing {len(audio_chunks)} audio chunks...")
60
- # TODO:
61
- # - Concatenate chunks carefully based on timestamps / expected samples
62
- # - Run librosa: calculate RMS, pitch (e.g., pyin), maybe pauses
63
- # - Return aggregated features
64
- full_audio = np.concatenate(audio_chunks)
65
- avg_rms = np.sqrt(np.mean(full_audio**2)) # Basic RMS
66
- # Pitch estimation can be computationally expensive
67
- # pitches, magnitudes = librosa.pyin(full_audio, fmin=librosa.note_to_hz('C2'), fmax=librosa.note_to_hz('C7'), sr=SAMPLE_RATE)
68
- # avg_pitch = np.nanmean(pitches) if pitches is not None and len(pitches) > 0 else 0
69
- avg_pitch = np.random.randint(80, 300) # Placeholder
70
- return {"avg_rms": avg_rms, "avg_pitch": avg_pitch}
71
-
72
-
73
- def calculate_final_metrics(video_features, audio_features):
74
- # TODO: Combine features into the final 0-1 metrics
75
- # This requires defining heuristics or a simple model based on the features
76
- valence = (video_features.get("avg_valence_proxy", 0) + 1) / 2 # Normalize [-1,1] to [0,1]
77
-
78
- # Combine multiple arousal indicators (weights are examples)
79
- arousal_face = video_features.get("avg_arousal_proxy_face", 0)
80
- arousal_voice_rms = min(audio_features.get("avg_rms", 0) * 10, 1.0) # Scale RMS
81
- arousal_pupil = video_features.get("avg_pupil_proxy", 0.5) # Assuming pupil proxy is 0-1
82
- arousal = (0.4 * arousal_face + 0.3 * arousal_voice_rms + 0.3 * arousal_pupil)
83
-
84
- engagement = video_features.get("face_detected_ratio", 0) # Simple proxy
85
- # Could add logic based on blink rate deviations, gaze stability etc.
86
-
87
- # Stress based on neg valence, high arousal
88
- stress = max(0, (1.0 - valence) * arousal) # Example heuristic
89
-
90
- # Cog load based on blink rate, pupil dilation
91
- blink_rate = video_features.get("blink_count", 0) / PROCESSING_INTERVAL_SECONDS
92
- # Normalize blink rate based on expected range (e.g. 0-1 Hz)
93
- norm_blink_rate = min(blink_rate, 1.0)
94
- cog_load = (0.5 * arousal_pupil + 0.5 * norm_blink_rate) # Example heuristic
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  return {
97
- "Timestamp": time.strftime('%Y-%m-%d %H:%M:%S'),
98
- "Valence": round(valence, 3),
99
- "Arousal": round(arousal, 3),
100
- "Engagement_Proxy": round(engagement, 3),
101
- "Stress_Proxy": round(stress, 3),
102
- "Cognitive_Load_Proxy": round(cog_load, 3),
103
- "Blink_Rate_Hz": round(blink_rate, 3),
104
- "Pupil_Size_Proxy": round(video_features.get("avg_pupil_proxy", 0), 3)
105
- # --- Exclude Traits ---
106
  }
107
 
108
- def log_to_csv(filename, metrics_dict):
109
- file_exists = os.path.isfile(filename)
110
- with open(filename, 'a', newline='') as csvfile:
111
- writer = csv.DictWriter(csvfile, fieldnames=metrics_dict.keys())
112
- if not file_exists:
113
- writer.writeheader() # Write header only once
114
- writer.writerow(metrics_dict)
115
-
116
- # --- Capture Threads (Simplified Example - Needs proper implementation) ---
117
- video_active = True
118
- audio_active = True
119
-
120
- def video_capture_thread():
121
- cap = cv2.VideoCapture(0)
122
- while video_active:
123
- ret, frame = cap.read()
124
- if ret:
125
- ts = time.time()
126
- # Make copies to avoid issues if buffer processes frame later
127
- frame_buffer.append(frame.copy())
128
- frame_timestamps.append(ts)
129
- time.sleep(1/30.0) # Limit capture rate
130
- cap.release()
131
- print("Video thread stopped.")
132
-
133
- def audio_capture_callback(indata, frames, time_info, status):
134
- """This is called (from a separate thread) for each audio block."""
135
- if status:
136
- print(status)
137
- ts = time.time() # Timestamp the arrival of the chunk
138
- # Make copies to avoid issues if buffer processes chunk later
139
- audio_buffer.append(indata.copy())
140
- audio_timestamps.append(ts) # Add timestamp for the chunk
141
-
142
- def audio_capture_thread():
143
- with sd.InputStream(samplerate=SAMPLE_RATE, channels=AUDIO_CHANNELS, callback=audio_capture_callback):
144
- print("Audio stream started. Press Ctrl+C to stop.")
145
- while audio_active:
146
- sd.sleep(1000) # Keep thread alive while stream is running
147
- print("Audio thread stopped.")
148
-
149
- # --- Main Processing Logic ---
150
- import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  if __name__ == "__main__":
152
- print("Starting capture threads...")
153
- vid_thread = threading.Thread(target=video_capture_thread, daemon=True)
154
- aud_thread = threading.Thread(target=audio_capture_thread, daemon=True)
155
- vid_thread.start()
156
- aud_thread.start()
157
-
158
- last_process_time = time.time()
159
-
160
- try:
161
- while True:
162
- current_time = time.time()
163
- if current_time - last_process_time >= PROCESSING_INTERVAL_SECONDS:
164
- print(f"\n--- Processing window ending {time.strftime('%H:%M:%S')} ---")
165
- window_end_time = current_time
166
- window_start_time = window_end_time - PROCESSING_INTERVAL_SECONDS
167
-
168
- # --- Get data for the window (Needs thread safety - locks!) ---
169
- # This part is tricky: efficiently select items in the timestamp range
170
- # Simple non-thread-safe example:
171
- frames_in_window = [f for f, ts in zip(list(frame_buffer), list(frame_timestamps)) if window_start_time <= ts < window_end_time]
172
- audio_in_window = [a for a, ts in zip(list(audio_buffer), list(audio_timestamps)) if window_start_time <= ts < window_end_time]
173
- # In practice, you'd remove processed items from the buffer
174
-
175
- if not frames_in_window:
176
- print("No frames in window, skipping.")
177
- last_process_time = current_time # Or += PROCESSING_INTERVAL_SECONDS
178
- continue
179
-
180
- # --- Analyze ---
181
- video_features = analyze_video_window(frames_in_window, []) # Pass timestamps if needed
182
- audio_features = analyze_audio_window(audio_in_window, []) # Pass timestamps if needed
183
-
184
- # --- Calculate & Log ---
185
- final_metrics = calculate_final_metrics(video_features, audio_features)
186
- print("Calculated Metrics:", final_metrics)
187
- log_to_csv(CSV_FILENAME, final_metrics)
188
-
189
- last_process_time = current_time # Reset timer accurately
190
-
191
-
192
- time.sleep(0.1) # Prevent busy-waiting
193
-
194
- except KeyboardInterrupt:
195
- print("Stopping...")
196
- video_active = False
197
- audio_active = False
198
- # Wait for threads to finish
199
- vid_thread.join(timeout=2.0)
200
- # Audio thread stops when sd.sleep ends or stream closes
201
- print("Done.")
 
1
+ import gradio as gr
2
  import cv2
 
 
3
  import numpy as np
4
  import pandas as pd
 
 
5
  import time
6
+ import mediapipe as mp
7
+ import matplotlib.pyplot as plt
8
+ from matplotlib.colors import LinearSegmentedColormap
9
+ from matplotlib.collections import LineCollection
10
+ import os # Potentially needed if saving plots temporarily
11
+
12
+ # --- MediaPipe Initialization (Keep as is) ---
 
 
 
 
 
 
 
 
 
 
13
  mp_face_mesh = mp.solutions.face_mesh
14
  mp_drawing = mp.solutions.drawing_utils
15
+ mp_drawing_styles = mp.solutions.drawing_styles
16
+
17
+ # Create Face Mesh instance globally (or manage creation/closing if resource intensive)
18
+ # Using try-except block for safer initialization if needed in complex setups
19
+ try:
20
+ face_mesh = mp_face_mesh.FaceMesh(
21
+ max_num_faces=1,
22
+ refine_landmarks=True,
23
+ min_detection_confidence=0.5,
24
+ min_tracking_confidence=0.5)
25
+ except Exception as e:
26
+ print(f"Error initializing MediaPipe Face Mesh: {e}")
27
+ face_mesh = None # Handle potential initialization errors
28
+
29
+ # --- Metrics Definition (Keep as is) ---
30
+ metrics = [
31
+ "valence", "arousal", "dominance", "cognitive_load",
32
+ "emotional_stability", "openness", "agreeableness",
33
+ "neuroticism", "conscientiousness", "extraversion",
34
+ "stress_index", "engagement_level"
35
+ ]
36
+ # Initial DataFrame structure for the state
37
+ initial_metrics_df = pd.DataFrame(columns=['timestamp'] + metrics)
38
+
39
+
40
+ # --- Analysis Functions (Keep exactly as you provided) ---
41
+ # Ensure these functions handle None input for landmarks gracefully
42
+ def extract_face_landmarks(image, face_mesh_instance):
43
+ if image is None or face_mesh_instance is None:
44
+ return None
45
+ # Process the image
46
+ image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
47
+ image_rgb.flags.writeable = False # Optimize
48
+ results = face_mesh_instance.process(image_rgb)
49
+ image_rgb.flags.writeable = True
50
+
51
+ if results.multi_face_landmarks:
52
+ return results.multi_face_landmarks[0]
53
+ return None
54
+
55
+ def calculate_ear(landmarks): # Keep as is
56
+ if not landmarks: return 0.0
57
+ LEFT_EYE = [33, 160, 158, 133, 153, 144]
58
+ RIGHT_EYE = [362, 385, 387, 263, 373, 380]
59
+ def get_landmark_coords(landmark_indices):
60
+ return np.array([(landmarks.landmark[idx].x, landmarks.landmark[idx].y) for idx in landmark_indices])
61
+ left_eye_points = get_landmark_coords(LEFT_EYE)
62
+ right_eye_points = get_landmark_coords(RIGHT_EYE)
63
+ def eye_aspect_ratio(eye_points):
64
+ v1 = np.linalg.norm(eye_points[1] - eye_points[5])
65
+ v2 = np.linalg.norm(eye_points[2] - eye_points[4])
66
+ h = np.linalg.norm(eye_points[0] - eye_points[3])
67
+ return (v1 + v2) / (2.0 * h) if h > 0 else 0.0
68
+ left_ear = eye_aspect_ratio(left_eye_points)
69
+ right_ear = eye_aspect_ratio(right_eye_points)
70
+ return (left_ear + right_ear) / 2.0
71
+
72
+ def calculate_mar(landmarks): # Keep as is
73
+ if not landmarks: return 0.0
74
+ MOUTH_OUTLINE = [61, 291, 39, 181, 0, 17, 269, 405]
75
+ mouth_points = np.array([(landmarks.landmark[idx].x, landmarks.landmark[idx].y) for idx in MOUTH_OUTLINE])
76
+ height = np.mean([
77
+ np.linalg.norm(mouth_points[1] - mouth_points[5]),
78
+ np.linalg.norm(mouth_points[2] - mouth_points[6]),
79
+ np.linalg.norm(mouth_points[3] - mouth_points[7])
80
+ ])
81
+ width = np.linalg.norm(mouth_points[0] - mouth_points[4])
82
+ return height / width if width > 0 else 0.0
83
 
84
+ def calculate_eyebrow_position(landmarks): # Keep as is
85
+ if not landmarks: return 0.0
86
+ LEFT_EYEBROW = 107; RIGHT_EYEBROW = 336
87
+ LEFT_EYE = 159; RIGHT_EYE = 386
88
+ left_eyebrow_y = landmarks.landmark[LEFT_EYEBROW].y
89
+ right_eyebrow_y = landmarks.landmark[RIGHT_EYEBROW].y
90
+ left_eye_y = landmarks.landmark[LEFT_EYE].y
91
+ right_eye_y = landmarks.landmark[RIGHT_EYE].y
92
+ left_distance = left_eye_y - left_eyebrow_y
93
+ right_distance = right_eye_y - right_eyebrow_y
94
+ avg_distance = (left_distance + right_distance) / 2.0
95
+ normalized = (avg_distance - 0.02) / 0.06 # Approximate normalization
96
+ return max(0.0, min(1.0, normalized))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
+ def estimate_head_pose(landmarks): # Keep as is
99
+ if not landmarks: return 0.0, 0.0
100
+ NOSE_TIP = 4; LEFT_EYE = 159; RIGHT_EYE = 386
101
+ nose = np.array([landmarks.landmark[NOSE_TIP].x, landmarks.landmark[NOSE_TIP].y, landmarks.landmark[NOSE_TIP].z])
102
+ left_eye = np.array([landmarks.landmark[LEFT_EYE].x, landmarks.landmark[LEFT_EYE].y, landmarks.landmark[LEFT_EYE].z])
103
+ right_eye = np.array([landmarks.landmark[RIGHT_EYE].x, landmarks.landmark[RIGHT_EYE].y, landmarks.landmark[RIGHT_EYE].z])
104
+ eye_level = (left_eye[1] + right_eye[1]) / 2.0
105
+ vertical_tilt = nose[1] - eye_level
106
+ horizontal_mid = (left_eye[0] + right_eye[0]) / 2.0
107
+ horizontal_tilt = nose[0] - horizontal_mid
108
+ vertical_tilt = max(-1.0, min(1.0, vertical_tilt * 10)) # Normalize approx
109
+ horizontal_tilt = max(-1.0, min(1.0, horizontal_tilt * 10)) # Normalize approx
110
+ return vertical_tilt, horizontal_tilt
111
+
112
+ def calculate_metrics(landmarks): # Keep as is
113
+ if not landmarks:
114
+ # Return default/neutral values when no face is detected
115
+ return {metric: 0.5 for metric in metrics}
116
+ # --- Calculations --- (Same as before)
117
+ ear = calculate_ear(landmarks)
118
+ mar = calculate_mar(landmarks)
119
+ eyebrow_position = calculate_eyebrow_position(landmarks)
120
+ vertical_tilt, horizontal_tilt = estimate_head_pose(landmarks)
121
+ cognitive_load = max(0, min(1, 1.0 - ear * 2.5))
122
+ valence = max(0, min(1, mar * 2.0 * (1.0 - eyebrow_position)))
123
+ arousal = max(0, min(1, (mar + (1.0 - ear) + eyebrow_position) / 3.0))
124
+ dominance = max(0, min(1, 0.5 + vertical_tilt))
125
+ neuroticism = max(0, min(1, (cognitive_load * 0.6) + ((1.0 - valence) * 0.4)))
126
+ emotional_stability = 1.0 - neuroticism
127
+ extraversion = max(0, min(1, (arousal * 0.5) + (valence * 0.5)))
128
+ openness = max(0, min(1, 0.5 + ((mar - 0.5) * 0.5)))
129
+ agreeableness = max(0, min(1, (valence * 0.7) + ((1.0 - arousal) * 0.3)))
130
+ conscientiousness = max(0, min(1, (1.0 - abs(arousal - 0.5)) * 0.7 + (emotional_stability * 0.3)))
131
+ stress_index = max(0, min(1, (cognitive_load * 0.5) + (eyebrow_position * 0.3) + ((1.0 - valence) * 0.2)))
132
+ engagement_level = max(0, min(1, (arousal * 0.7) + ((1.0 - abs(horizontal_tilt)) * 0.3)))
133
+ # --- Return dictionary ---
134
  return {
135
+ 'valence': valence, 'arousal': arousal, 'dominance': dominance,
136
+ 'cognitive_load': cognitive_load, 'emotional_stability': emotional_stability,
137
+ 'openness': openness, 'agreeableness': agreeableness, 'neuroticism': neuroticism,
138
+ 'conscientiousness': conscientiousness, 'extraversion': extraversion,
139
+ 'stress_index': stress_index, 'engagement_level': engagement_level
 
 
 
 
140
  }
141
 
142
+
143
+ # --- Visualization Function (Keep as is, ensure it handles None input) ---
144
+ def update_metrics_visualization(metrics_values):
145
+ # Create a blank figure if no metrics are available
146
+ if not metrics_values:
147
+ fig, ax = plt.subplots(figsize=(10, 8)) # Match approx size
148
+ ax.text(0.5, 0.5, "Waiting for analysis...", ha='center', va='center')
149
+ ax.axis('off')
150
+ # Ensure background matches Gradio theme potentially
151
+ fig.patch.set_facecolor('#FFFFFF') # Set background if needed
152
+ ax.set_facecolor('#FFFFFF')
153
+ return fig
154
+
155
+ # Calculate grid size
156
+ num_metrics = len([k for k in metrics_values if k != 'timestamp'])
157
+ nrows = (num_metrics + 2) // 3
158
+ fig, axs = plt.subplots(nrows, 3, figsize=(10, nrows * 2.5), facecolor='#FFFFFF') # Match background
159
+ axs = axs.flatten()
160
+
161
+ # Colormap and normalization
162
+ colors = [(0.1, 0.1, 0.9), (0.9, 0.9, 0.1), (0.9, 0.1, 0.1)] # Blue to Yellow to Red
163
+ cmap = LinearSegmentedColormap.from_list("custom_cmap", colors, N=100)
164
+ norm = plt.Normalize(0, 1)
165
+
166
+ metric_idx = 0
167
+ for key, value in metrics_values.items():
168
+ if key == 'timestamp': continue
169
+
170
+ ax = axs[metric_idx]
171
+ ax.set_title(key.replace('_', ' ').title(), fontsize=10)
172
+ ax.set_xlim(0, 1); ax.set_ylim(0, 0.5); ax.set_aspect('equal'); ax.axis('off')
173
+ ax.set_facecolor('#FFFFFF') # Match background
174
+
175
+ r = 0.4 # radius
176
+ theta = np.linspace(np.pi, 0, 100) # Flipped for gauge direction
177
+ x_bg = 0.5 + r * np.cos(theta); y_bg = 0.1 + r * np.sin(theta)
178
+ ax.plot(x_bg, y_bg, 'k-', linewidth=3, alpha=0.2) # Background arc
179
+
180
+ # Value arc calculation
181
+ value_angle = np.pi * (1 - value) # Map value [0,1] to angle [pi, 0]
182
+ # Ensure there are at least 2 points for the line segment, even for value=0
183
+ num_points = max(2, int(100 * value))
184
+ value_theta = np.linspace(np.pi, value_angle, num_points)
185
+ x_val = 0.5 + r * np.cos(value_theta); y_val = 0.1 + r * np.sin(value_theta)
186
+
187
+ # Create line segments for coloring if there are points to draw
188
+ if len(x_val) > 1:
189
+ points = np.array([x_val, y_val]).T.reshape(-1, 1, 2)
190
+ segments = np.concatenate([points[:-1], points[1:]], axis=1)
191
+ segment_values = np.linspace(0, value, len(segments)) # Color based on value
192
+ lc = LineCollection(segments, cmap=cmap, norm=norm)
193
+ lc.set_array(segment_values); lc.set_linewidth(5)
194
+ ax.add_collection(lc)
195
+
196
+ # Add value text
197
+ ax.text(0.5, 0.15, f"{value:.2f}", ha='center', va='center', fontsize=11,
198
+ fontweight='bold', bbox=dict(facecolor='white', alpha=0.7, boxstyle='round,pad=0.2'))
199
+ metric_idx += 1
200
+
201
+ # Hide unused subplots
202
+ for i in range(metric_idx, len(axs)):
203
+ axs[i].axis('off')
204
+
205
+ plt.tight_layout(pad=0.5)
206
+ return fig
207
+
208
+
209
+ # --- Gradio Processing Function ---
210
+ app_start_time = time.time() # Use a fixed start time for the app session
211
+
212
+ def process_frame(
213
+ frame,
214
+ analysis_freq,
215
+ analyze_flag,
216
+ # --- State variables ---
217
+ metrics_data_state,
218
+ last_analysis_time_state,
219
+ latest_metrics_state,
220
+ latest_landmarks_state
221
+ ):
222
+
223
+ if frame is None:
224
+ # Return default/empty outputs if no frame
225
+ default_plot = update_metrics_visualization(latest_metrics_state)
226
+ return frame, default_plot, metrics_data_state, \
227
+ metrics_data_state, last_analysis_time_state, \
228
+ latest_metrics_state, latest_landmarks_state
229
+
230
+ annotated_frame = frame.copy()
231
+ current_time = time.time()
232
+ perform_analysis = False
233
+ current_landmarks = None # Landmarks detected in *this* frame run
234
+
235
+ # --- Decide whether to perform analysis ---
236
+ if analyze_flag and face_mesh and (current_time - last_analysis_time_state >= analysis_freq):
237
+ perform_analysis = True
238
+ last_analysis_time_state = current_time # Update time immediately
239
+
240
+ # --- Perform Analysis (if flag is set and frequency met) ---
241
+ if perform_analysis:
242
+ current_landmarks = extract_face_landmarks(frame, face_mesh)
243
+ calculated_metrics = calculate_metrics(current_landmarks)
244
+
245
+ # Update state variables
246
+ latest_landmarks_state = current_landmarks # Store landmarks from this run
247
+ latest_metrics_state = calculated_metrics
248
+
249
+ # Log data only if a face was detected
250
+ if current_landmarks:
251
+ elapsed_time = current_time - app_start_time
252
+ new_row = {'timestamp': elapsed_time, **calculated_metrics}
253
+ new_row_df = pd.DataFrame([new_row])
254
+ metrics_data_state = pd.concat([metrics_data_state, new_row_df], ignore_index=True)
255
+
256
+ # --- Drawing ---
257
+ # Always try to draw the latest known landmarks stored in state
258
+ landmarks_to_draw = latest_landmarks_state
259
+ if landmarks_to_draw:
260
+ mp_drawing.draw_landmarks(
261
+ image=annotated_frame,
262
+ landmark_list=landmarks_to_draw,
263
+ connections=mp_face_mesh.FACEMESH_TESSELATION,
264
+ landmark_drawing_spec=None,
265
+ connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_tesselation_style())
266
+ mp_drawing.draw_landmarks(
267
+ image=annotated_frame,
268
+ landmark_list=landmarks_to_draw,
269
+ connections=mp_face_mesh.FACEMESH_CONTOURS,
270
+ landmark_drawing_spec=None,
271
+ connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_contours_style())
272
+
273
+ # --- Generate Metrics Plot ---
274
+ metrics_plot = update_metrics_visualization(latest_metrics_state)
275
+
276
+ # --- Return updated values for outputs AND state ---
277
+ return annotated_frame, metrics_plot, metrics_data_state, \
278
+ metrics_data_state, last_analysis_time_state, \
279
+ latest_metrics_state, latest_landmarks_state
280
+
281
+
282
+ # --- Create Gradio Interface ---
283
+ with gr.Blocks(theme=gr.themes.Soft(), title="Gradio Facial Analysis") as iface:
284
+ gr.Markdown("# Basic Facial Analysis (Gradio Version)")
285
+ gr.Markdown("Analyzes webcam feed for facial landmarks and estimates metrics. *Estimations are for demonstration only.*")
286
+
287
+ # Define State Variables
288
+ # Need to initialize them properly
289
+ metrics_data = gr.State(value=initial_metrics_df.copy())
290
+ last_analysis_time = gr.State(value=time.time())
291
+ latest_metrics = gr.State(value=None) # Initially no metrics
292
+ latest_landmarks = gr.State(value=None) # Initially no landmarks
293
+
294
+ with gr.Row():
295
+ with gr.Column(scale=1):
296
+ webcam_input = gr.Image(sources="webcam", streaming=True, label="Webcam Input", type="numpy")
297
+ analysis_freq_slider = gr.Slider(minimum=0.5, maximum=5.0, step=0.5, value=1.0, label="Analysis Frequency (s)")
298
+ analyze_checkbox = gr.Checkbox(value=True, label="Enable Analysis Calculation")
299
+ status_text = gr.Markdown("Status: Analysis Enabled" if analyze_checkbox.value else "Status: Analysis Paused") # Initial status text
300
+
301
+ # Update status text dynamically (though Gradio handles this implicitly via reruns)
302
+ # Might need a more complex setup with event listeners if precise text update is needed without full rerun
303
+ with gr.Column(scale=1):
304
+ processed_output = gr.Image(label="Processed Feed", type="numpy")
305
+ metrics_plot_output = gr.Plot(label="Estimated Metrics")
306
+ dataframe_output = gr.Dataframe(label="Data Log", headers=['timestamp'] + metrics, wrap=True, height=300)
307
+
308
+
309
+ # Define the connections for the live interface
310
+ webcam_input.stream(
311
+ fn=process_frame,
312
+ inputs=[
313
+ webcam_input,
314
+ analysis_freq_slider,
315
+ analyze_checkbox,
316
+ # Pass state variables as inputs
317
+ metrics_data,
318
+ last_analysis_time,
319
+ latest_metrics,
320
+ latest_landmarks
321
+ ],
322
+ outputs=[
323
+ processed_output,
324
+ metrics_plot_output,
325
+ dataframe_output,
326
+ # Return updated state variables
327
+ metrics_data,
328
+ last_analysis_time,
329
+ latest_metrics,
330
+ latest_landmarks
331
+ ]
332
+ )
333
+
334
+ # --- Launch the App ---
335
  if __name__ == "__main__":
336
+ if face_mesh is None:
337
+ print("Face Mesh could not be initialized. Gradio app might not function correctly.")
338
+ iface.launch(debug=True) # Enable debug for more detailed errors if needed
339
+
340
+ # Optional: Add cleanup logic if needed, although launching blocks execution
341
+ # try:
342
+ # iface.launch()
343
+ # finally:
344
+ # if face_mesh:
345
+ # face_mesh.close() # Close mediapipe resources if app is stopped
346
+ # print("MediaPipe FaceMesh closed.")