ckcl commited on
Commit
ab21ecf
·
verified ·
1 Parent(s): 73ee22c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +836 -122
app.py CHANGED
@@ -1,38 +1,308 @@
1
  import gradio as gr
2
- import torch
3
- from transformers import ViTForImageClassification, ViTImageProcessor
4
  import numpy as np
5
  import cv2
6
  from PIL import Image
7
  import io
8
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  class DrowsinessDetector:
11
  def __init__(self):
12
  self.model = None
13
- self.processor = None
14
- self.input_shape = (224, 224, 3)
15
  self.face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
16
  self.id2label = {0: "notdrowsy", 1: "drowsy"}
17
  self.label2id = {"notdrowsy": 0, "drowsy": 1}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  def load_model(self):
20
- """Load the ViT model and processor from Hugging Face Hub"""
21
  try:
22
- model_id = "ckcl/driver-drowsiness-detector" # 使用你的模型ID
23
- self.model = ViTForImageClassification.from_pretrained(
24
- model_id,
25
- num_labels=2,
26
- id2label=self.id2label,
27
- label2id=self.label2id,
28
- ignore_mismatched_sizes=True
29
- )
30
- self.model.eval()
31
- self.processor = ViTImageProcessor.from_pretrained("google/vit-base-patch16-224")
32
- print(f"ViT model loaded successfully from {model_id}")
 
 
 
 
 
33
  except Exception as e:
34
- print(f"Error loading ViT model: {str(e)}")
35
  raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  def detect_face(self, frame):
38
  """Detect face in the frame"""
@@ -45,168 +315,612 @@ class DrowsinessDetector:
45
  return None, None
46
 
47
  def preprocess_image(self, image):
48
- """Preprocess the input image for ViT"""
49
  if image is None:
50
  return None
51
- pil_img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
52
- inputs = self.processor(images=pil_img, return_tensors="pt")
53
- return inputs
 
 
 
 
 
 
54
 
55
  def predict(self, image):
56
- """Make prediction on the input image using ViT"""
57
- if self.model is None or self.processor is None:
58
  raise ValueError("Model not loaded. Call load_model() first.")
 
 
 
 
 
 
 
 
 
59
  # Detect face
 
60
  face, face_coords = self.detect_face(image)
 
61
  if face is None:
62
- return None, None, "No face detected"
63
- # Preprocess the face image
 
64
  inputs = self.preprocess_image(face)
65
  if inputs is None:
66
- return None, None, "Error processing image"
67
- # Make prediction
68
- with torch.no_grad():
69
- outputs = self.model(**inputs)
70
- logits = outputs.logits
71
- probs = torch.softmax(logits, dim=1)
72
- pred_class = torch.argmax(probs, dim=1).item()
73
- pred_label = self.id2label[pred_class]
74
- pred_prob = probs[0, pred_class].item()
75
- # Return drowsy probability (class 1)
76
- drowsy_prob = probs[0, 1].item()
77
- return drowsy_prob, face_coords, None
78
-
79
- # Initialize detector
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  detector = DrowsinessDetector()
81
 
82
  def process_image(image):
83
- """Process a single image"""
84
  if image is None:
85
  return None, "No image provided"
86
-
87
  try:
88
- # Convert image to numpy array if it's a PIL Image
89
- if isinstance(image, Image.Image):
90
- image = np.array(image)
91
-
92
- # Convert frame to RGB if needed
93
- if len(image.shape) == 2:
94
- image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB)
95
- elif image.shape[2] == 4:
96
- image = cv2.cvtColor(image, cv2.COLOR_RGBA2RGB)
97
 
98
  # Make prediction
99
- drowsy_prob, face_coords, error = detector.predict(image)
100
 
101
  if error:
102
- return image, error
103
 
104
- if face_coords is not None:
105
- x, y, w, h = face_coords
106
- # Draw rectangle around face
107
- color = (0, 0, 255) if drowsy_prob > 0.7 else (0, 255, 0)
108
- cv2.rectangle(image, (x, y), (x+w, y+h), color, 2)
109
 
110
- # Add text
111
- status = "DROWSY" if drowsy_prob > 0.7 else "ALERT"
112
- cv2.putText(image, f"{status} ({drowsy_prob:.2%})",
113
- (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, color, 2)
114
-
115
- return image, f"Status: {status} (Confidence: {drowsy_prob:.2%})"
 
 
 
 
 
 
 
116
  else:
117
- return image, "No face detected"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
  except Exception as e:
120
- return image, f"Error processing image: {str(e)}"
 
 
 
121
 
122
- def process_video(video):
123
  """Process video input"""
124
  if video is None:
125
  return None, "No video provided"
126
 
127
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  # Get input video properties
129
- cap = cv2.VideoCapture(video)
130
  fps = cap.get(cv2.CAP_PROP_FPS)
 
 
131
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
132
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
 
133
 
134
- # Create temporary output video file
135
- temp_output = "temp_output.mp4"
136
- fourcc = cv2.VideoWriter_fourcc(*'mp4v')
137
- out = cv2.VideoWriter(temp_output, fourcc, fps, (width, height))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
  while True:
140
  ret, frame = cap.read()
141
  if not ret:
 
142
  break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
- processed_frame = process_image(frame)[0]
145
- if processed_frame is not None:
146
- out.write(processed_frame)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
  # Release resources
149
  cap.release()
150
  out.release()
151
 
152
- # Check if video was created
153
- if os.path.exists(temp_output) and os.path.getsize(temp_output) > 0:
154
- return temp_output, "Video processed successfully"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  else:
156
- return None, "Error: Failed to create output video"
 
157
 
158
  except Exception as e:
 
 
 
159
  return None, f"Error processing video: {str(e)}"
160
  finally:
161
- # Clean up temporary file
162
- if 'out' in locals():
163
  out.release()
164
- if 'cap' in locals():
165
  cap.release()
 
 
 
 
 
 
 
166
 
167
- # Load the model at startup
168
- detector.load_model()
 
169
 
170
- # Create interface
171
- with gr.Blocks(title="Driver Drowsiness Detection") as demo:
172
- gr.Markdown("""
173
- # 🚗 Driver Drowsiness Detection System
 
 
 
174
 
175
- This system detects driver drowsiness using computer vision and deep learning.
 
 
 
176
 
177
- ## Features:
178
- - Image analysis
179
- - Video processing
180
- - Face detection and drowsiness prediction
181
- """)
182
 
183
- with gr.Tabs():
184
- with gr.Tab("Image"):
185
- gr.Markdown("Upload an image for drowsiness detection")
186
- with gr.Row():
187
- image_input = gr.Image(label="Input Image", type="numpy")
188
- image_output = gr.Image(label="Processed Image")
189
- with gr.Row():
190
- status_output = gr.Textbox(label="Status")
191
- image_input.change(
192
- fn=process_image,
193
- inputs=[image_input],
194
- outputs=[image_output, status_output]
195
- )
196
-
197
- with gr.Tab("Video"):
198
- gr.Markdown("Upload a video file for drowsiness detection")
199
- with gr.Row():
200
- video_input = gr.Video(label="Input Video")
201
- video_output = gr.Video(label="Processed Video")
202
- with gr.Row():
203
- video_status = gr.Textbox(label="Status")
204
- video_input.change(
205
- fn=process_video,
206
- inputs=[video_input],
207
- outputs=[video_output, video_status]
208
- )
 
 
 
 
 
 
209
 
210
- # Launch the app
211
- if __name__ == "__main__":
212
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
 
 
2
  import numpy as np
3
  import cv2
4
  from PIL import Image
5
  import io
6
  import os
7
+ import json
8
+ import time
9
+ import argparse
10
+ import tensorflow as tf
11
+ from tensorflow import keras
12
+ import math
13
+ from collections import deque
14
+
15
+ class SpeedDetector:
16
+ def __init__(self, history_size=30):
17
+ self.speed_history = deque(maxlen=history_size)
18
+ self.last_update_time = None
19
+ self.current_speed = 0
20
+ self.speed_change_threshold = 5 # km/h
21
+ self.abnormal_speed_changes = 0
22
+ self.speed_deviation_sum = 0
23
+ self.speed_change_score = 0
24
+
25
+ # For optical flow speed estimation
26
+ self.prev_gray = None
27
+ self.prev_points = None
28
+ self.frame_idx = 0
29
+ self.speed_estimate = 60 # Initial estimate
30
+
31
+ def update_speed(self, speed_km_h):
32
+ """Update with current speed in km/h"""
33
+ current_time = time.time()
34
+
35
+ # Add to history
36
+ self.speed_history.append(speed_km_h)
37
+ self.current_speed = speed_km_h
38
+
39
+ # Not enough data yet
40
+ if len(self.speed_history) < 5:
41
+ return 0
42
+
43
+ # Calculate speed variation metrics
44
+ speed_arr = np.array(self.speed_history)
45
+
46
+ # 1. Standard deviation of speed
47
+ speed_std = np.std(speed_arr)
48
+
49
+ # 2. Detect abrupt changes
50
+ for i in range(1, len(speed_arr)):
51
+ change = abs(speed_arr[i] - speed_arr[i-1])
52
+ if change >= self.speed_change_threshold:
53
+ self.abnormal_speed_changes += 1
54
+
55
+ # 3. Calculate average rate of change
56
+ changes = np.abs(np.diff(speed_arr))
57
+ avg_change = np.mean(changes) if len(changes) > 0 else 0
58
+
59
+ # Combine into a score (0-1 range)
60
+ self.speed_deviation_sum = min(5, speed_std) / 5 # Normalize to 0-1
61
+ abnormal_change_factor = min(1, self.abnormal_speed_changes / 5)
62
+ avg_change_factor = min(1, avg_change / self.speed_change_threshold)
63
+
64
+ # Weighted combination
65
+ self.speed_change_score = (
66
+ 0.4 * self.speed_deviation_sum +
67
+ 0.4 * abnormal_change_factor +
68
+ 0.2 * avg_change_factor
69
+ )
70
+
71
+ return self.speed_change_score
72
+
73
+ def detect_speed_from_frame(self, frame):
74
+ """Detect speed from video frame using optical flow"""
75
+ if frame is None:
76
+ return self.current_speed
77
+
78
+ # Convert frame to grayscale
79
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
80
+
81
+ # For the first frame, initialize points to track
82
+ if self.prev_gray is None or self.frame_idx % 30 == 0: # Reset tracking points every 30 frames
83
+ # Detect good features to track
84
+ mask = np.zeros_like(gray)
85
+ # Focus on the lower portion of the frame (road)
86
+ h, w = gray.shape
87
+ mask[h//2:, :] = 255
88
+
89
+ corners = cv2.goodFeaturesToTrack(gray, maxCorners=100, qualityLevel=0.01, minDistance=10, mask=mask)
90
+ if corners is not None and len(corners) > 0:
91
+ self.prev_points = corners
92
+ self.prev_gray = gray.copy()
93
+ else:
94
+ # No good points to track
95
+ self.frame_idx += 1
96
+ return self.current_speed
97
+
98
+ # Calculate optical flow if we have previous points
99
+ if self.prev_gray is not None and self.prev_points is not None:
100
+ # Calculate optical flow
101
+ new_points, status, _ = cv2.calcOpticalFlowPyrLK(self.prev_gray, gray, self.prev_points, None)
102
+
103
+ # Filter only valid points
104
+ if new_points is not None and status is not None:
105
+ good_new = new_points[status == 1]
106
+ good_old = self.prev_points[status == 1]
107
+
108
+ # Calculate flow magnitude
109
+ if len(good_new) > 0 and len(good_old) > 0:
110
+ flow_magnitudes = np.sqrt(
111
+ np.sum((good_new - good_old)**2, axis=1)
112
+ )
113
+ avg_flow = np.mean(flow_magnitudes) if len(flow_magnitudes) > 0 else 0
114
+
115
+ # Map optical flow to speed change
116
+ # Higher flow = faster movement
117
+ # This is a simplified mapping and would need calibration for real-world use
118
+ flow_threshold = 1.0 # Adjust based on testing
119
+
120
+ if avg_flow > flow_threshold:
121
+ # Movement detected, estimate acceleration
122
+ speed_change = min(5, max(-5, (avg_flow - flow_threshold) * 2))
123
+
124
+ # Add some temporal smoothing to avoid sudden changes
125
+ speed_change = speed_change * 0.3 # Reduce magnitude for smoother change
126
+ else:
127
+ # Minimal movement, slight deceleration (coasting)
128
+ speed_change = -0.1
129
+
130
+ # Update speed with detected change
131
+ self.speed_estimate += speed_change
132
+ # Keep speed in reasonable range
133
+ self.speed_estimate = max(40, min(120, self.speed_estimate))
134
+
135
+ # Update tracking points
136
+ self.prev_points = good_new.reshape(-1, 1, 2)
137
+
138
+ # Update previous gray frame
139
+ self.prev_gray = gray.copy()
140
+
141
+ self.frame_idx += 1
142
+
143
+ # Check for dashboard speedometer (would require more sophisticated OCR in a real system)
144
+ # For now, just use our estimated speed
145
+ detected_speed = self.speed_estimate
146
+
147
+ # Update current speed and trigger speed change detection
148
+ self.update_speed(detected_speed)
149
+
150
+ return detected_speed
151
+
152
+ def get_speed_change_score(self):
153
+ """Return a score from 0-1 indicating abnormal speed changes"""
154
+ return self.speed_change_score
155
+
156
+ def reset(self):
157
+ """Reset the detector state"""
158
+ self.speed_history.clear()
159
+ self.abnormal_speed_changes = 0
160
+ self.speed_deviation_sum = 0
161
+ self.speed_change_score = 0
162
+ self.prev_gray = None
163
+ self.prev_points = None
164
+ self.frame_idx = 0
165
+ self.speed_estimate = 60 # Reset to initial estimate
166
 
167
  class DrowsinessDetector:
168
  def __init__(self):
169
  self.model = None
170
+ self.input_shape = (224, 224, 3) # Updated to match model's expected input shape
 
171
  self.face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
172
  self.id2label = {0: "notdrowsy", 1: "drowsy"}
173
  self.label2id = {"notdrowsy": 0, "drowsy": 1}
174
+
175
+ # Speed detector
176
+ self.speed_detector = SpeedDetector()
177
+ self.SPEED_CHANGE_WEIGHT = 0.15 # Weight for speed changes in drowsiness calculation
178
+
179
+ # 嘗試動態 import dlib,並設置 fallback
180
+ self.landmark_detection_enabled = False
181
+ try:
182
+ import dlib
183
+ self.detector = dlib.get_frontal_face_detector()
184
+ predictor_path = "shape_predictor_68_face_landmarks.dat"
185
+ if not os.path.exists(predictor_path):
186
+ print(f"Warning: {predictor_path} not found. Downloading...")
187
+ import urllib.request
188
+ urllib.request.urlretrieve(
189
+ "https://github.com/italojs/facial-landmarks-recognition/raw/master/shape_predictor_68_face_landmarks.dat",
190
+ predictor_path
191
+ )
192
+ self.predictor = dlib.shape_predictor(predictor_path)
193
+ self.landmark_detection_enabled = True
194
+ print("Facial landmark detection enabled")
195
+ except Exception as e:
196
+ print(f"Warning: Facial landmark detection disabled: {e}")
197
+ print("The system will use a simpler detection method. For better accuracy, install CMake and dlib.")
198
+
199
+ # Constants for drowsiness detection
200
+ self.EAR_THRESHOLD = 0.25 # Eye aspect ratio threshold
201
+ self.CONSECUTIVE_FRAMES = 20
202
+ self.ear_counter = 0
203
+ self.GAZE_THRESHOLD = 0.2 # Gaze direction threshold
204
+ self.HEAD_POSE_THRESHOLD = 0.3 # Head pose threshold
205
+
206
+ # Parameters for weighted ensemble
207
+ self.MODEL_WEIGHT = 0.45 # Reduced to accommodate speed factor
208
+ self.EAR_WEIGHT = 0.2
209
+ self.GAZE_WEIGHT = 0.1
210
+ self.HEAD_POSE_WEIGHT = 0.1
211
+
212
+ # For tracking across frames
213
+ self.prev_drowsy_count = 0
214
+ self.drowsy_history = []
215
+ self.current_speed = 0 # Current speed in km/h
216
+
217
+ def update_speed(self, speed_km_h):
218
+ """Update the current speed"""
219
+ self.current_speed = speed_km_h
220
+ return self.speed_detector.update_speed(speed_km_h)
221
+
222
+ def reset_speed_detector(self):
223
+ """Reset the speed detector"""
224
+ self.speed_detector.reset()
225
 
226
  def load_model(self):
227
+ """Load the CNN model from local files"""
228
  try:
229
+ # Use local model files
230
+ config_path = "huggingface_model/config.json"
231
+ model_path = "drowsiness_model.h5"
232
+
233
+ # Load config
234
+ with open(config_path, 'r') as f:
235
+ config = json.load(f)
236
+
237
+ # Load the Keras model directly
238
+ self.model = keras.models.load_model(model_path)
239
+
240
+ # Print model summary for debugging
241
+ print("Model loaded successfully")
242
+ print(f"Model input shape: {self.model.input_shape}")
243
+ self.model.summary()
244
+
245
  except Exception as e:
246
+ print(f"Error loading CNN model: {str(e)}")
247
  raise
248
+
249
+ def eye_aspect_ratio(self, eye):
250
+ """Calculate the eye aspect ratio"""
251
+ # Compute the euclidean distances between the two sets of vertical eye landmarks
252
+ A = dist.euclidean(eye[1], eye[5])
253
+ B = dist.euclidean(eye[2], eye[4])
254
+
255
+ # Compute the euclidean distance between the horizontal eye landmarks
256
+ C = dist.euclidean(eye[0], eye[3])
257
+
258
+ # Calculate the eye aspect ratio
259
+ ear = (A + B) / (2.0 * C)
260
+ return ear
261
+
262
+ def calculate_gaze(self, eye_points, facial_landmarks):
263
+ """Calculate gaze direction"""
264
+ left_eye_region = np.array([(facial_landmarks.part(i).x, facial_landmarks.part(i).y) for i in range(36, 42)])
265
+ right_eye_region = np.array([(facial_landmarks.part(i).x, facial_landmarks.part(i).y) for i in range(42, 48)])
266
+
267
+ # Compute eye centers
268
+ left_eye_center = left_eye_region.mean(axis=0).astype("int")
269
+ right_eye_center = right_eye_region.mean(axis=0).astype("int")
270
+
271
+ # Compute the angle between eye centers
272
+ dY = right_eye_center[1] - left_eye_center[1]
273
+ dX = right_eye_center[0] - left_eye_center[0]
274
+ angle = np.degrees(np.arctan2(dY, dX))
275
+
276
+ # Normalize the angle
277
+ return abs(angle) / 180.0
278
+
279
+ def get_head_pose(self, shape):
280
+ """Calculate the head pose"""
281
+ # Get specific facial landmarks for head pose estimation
282
+ image_points = np.array([
283
+ (shape.part(30).x, shape.part(30).y), # Nose tip
284
+ (shape.part(8).x, shape.part(8).y), # Chin
285
+ (shape.part(36).x, shape.part(36).y), # Left eye left corner
286
+ (shape.part(45).x, shape.part(45).y), # Right eye right corner
287
+ (shape.part(48).x, shape.part(48).y), # Left mouth corner
288
+ (shape.part(54).x, shape.part(54).y) # Right mouth corner
289
+ ], dtype="double")
290
+
291
+ # A simple head pose estimation using the angle of the face
292
+ # Calculate center of the face
293
+ center_x = np.mean([p[0] for p in image_points])
294
+ center_y = np.mean([p[1] for p in image_points])
295
+
296
+ # Calculate angle with respect to vertical
297
+ angle = 0
298
+ if len(image_points) > 2:
299
+ point1 = image_points[0] # Nose
300
+ point2 = image_points[1] # Chin
301
+ angle = abs(math.atan2(point2[1] - point1[1], point2[0] - point1[0]))
302
+
303
+ # Normalize to 0-1 range where 0 is upright and 1 is drooping
304
+ normalized_pose = min(1.0, abs(angle) / (math.pi/2))
305
+ return normalized_pose
306
 
307
  def detect_face(self, frame):
308
  """Detect face in the frame"""
 
315
  return None, None
316
 
317
  def preprocess_image(self, image):
318
+ """Preprocess the input image for CNN"""
319
  if image is None:
320
  return None
321
+ # Convert to RGB
322
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
323
+ # Resize to model input size (224x224)
324
+ image = cv2.resize(image, (self.input_shape[0], self.input_shape[1]))
325
+ # Normalize
326
+ image = image.astype(np.float32) / 255.0
327
+ # Add batch dimension
328
+ image = np.expand_dims(image, axis=0)
329
+ return image
330
 
331
  def predict(self, image):
332
+ """Make prediction on the input image using multiple features"""
333
+ if self.model is None:
334
  raise ValueError("Model not loaded. Call load_model() first.")
335
+
336
+ # Initialize results
337
+ drowsy_prob = 0.0
338
+ face_coords = None
339
+ ear_value = 1.0 # Default to wide open eyes
340
+ gaze_value = 0.0
341
+ head_pose_value = 0.0
342
+ landmark_detection_success = False
343
+
344
  # Detect face
345
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
346
  face, face_coords = self.detect_face(image)
347
+
348
  if face is None:
349
+ return 0.0, None, "No face detected", {}
350
+
351
+ # Get model prediction
352
  inputs = self.preprocess_image(face)
353
  if inputs is None:
354
+ return 0.0, face_coords, "Error processing image", {}
355
+
356
+ outputs = self.model.predict(inputs)
357
+ # Get the drowsiness probability from the model
358
+ if outputs.shape[1] == 1:
359
+ model_prob = outputs[0][0]
360
+ # Convert to probability if needed
361
+ if model_prob < 0 or model_prob > 1:
362
+ model_prob = 1 / (1 + np.exp(-model_prob))
363
+ else:
364
+ # For multi-class model
365
+ probs = tf.nn.softmax(outputs, axis=1).numpy()
366
+ model_prob = probs[0, 1] # Probability of class 1 (drowsy)
367
+
368
+ # Get speed change score from detector
369
+ speed_change_score = self.speed_detector.get_speed_change_score()
370
+
371
+ # Get additional features if landmark detection is enabled
372
+ metrics = {
373
+ "model_prob": model_prob,
374
+ "ear": 1.0,
375
+ "gaze": 0.0,
376
+ "head_pose": 0.0,
377
+ "speed_change": speed_change_score
378
+ }
379
+
380
+ if self.landmark_detection_enabled:
381
+ try:
382
+ import dlib
383
+ from scipy.spatial import distance as dist
384
+
385
+ # Detect faces with dlib for landmark detection
386
+ rects = self.detector(gray, 0)
387
+
388
+ if len(rects) > 0:
389
+ # Get facial landmarks
390
+ shape = self.predictor(gray, rects[0])
391
+
392
+ # Get eye aspect ratio
393
+ left_eye = [(shape.part(i).x, shape.part(i).y) for i in range(36, 42)]
394
+ right_eye = [(shape.part(i).x, shape.part(i).y) for i in range(42, 48)]
395
+
396
+ left_ear = self.eye_aspect_ratio(left_eye)
397
+ right_ear = self.eye_aspect_ratio(right_eye)
398
+ ear_value = (left_ear + right_ear) / 2.0
399
+
400
+ # Get gaze direction
401
+ gaze_value = self.calculate_gaze(None, shape)
402
+
403
+ # Get head pose
404
+ head_pose_value = self.get_head_pose(shape)
405
+
406
+ # Update metrics
407
+ metrics["ear"] = ear_value
408
+ metrics["gaze"] = gaze_value
409
+ metrics["head_pose"] = head_pose_value
410
+
411
+ landmark_detection_success = True
412
+ except Exception as e:
413
+ print(f"Error in landmark detection: {e}")
414
+ else:
415
+ # Use a simplified heuristic approach when dlib is not available
416
+ # Calculate an estimated eye ratio from the grayscale intensity in eye regions
417
+ # This is a simplified approach that is not as accurate as the EAR method
418
+ if face_coords is not None:
419
+ try:
420
+ # Try to estimate eye regions based on face proportions
421
+ face_gray = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY)
422
+ face_height, face_width = face_gray.shape[:2]
423
+
424
+ # Estimate eye regions (these are approximate and may not be accurate for all faces)
425
+ left_eye_region = face_gray[int(face_height*0.2):int(face_height*0.4), int(face_width*0.2):int(face_width*0.4)]
426
+ right_eye_region = face_gray[int(face_height*0.2):int(face_height*0.4), int(face_width*0.6):int(face_width*0.8)]
427
+
428
+ # Simplified metric: use average intensity - lower values might indicate closed eyes
429
+ if left_eye_region.size > 0 and right_eye_region.size > 0:
430
+ left_eye_avg = np.mean(left_eye_region) / 255.0
431
+ right_eye_avg = np.mean(right_eye_region) / 255.0
432
+
433
+ # Invert so that darker regions (potentially closed eyes) have higher values
434
+ left_eye_closed = 1.0 - left_eye_avg
435
+ right_eye_closed = 1.0 - right_eye_avg
436
+
437
+ # Combine into a simple eye closure metric (0-1 range, higher means more closed)
438
+ eye_closure = (left_eye_closed + right_eye_closed) / 2.0
439
+
440
+ # Convert to a rough approximation of EAR
441
+ # Lower values indicate more closed eyes (like EAR)
442
+ estimated_ear = max(0.15, 0.4 - (eye_closure * 0.25))
443
+ ear_value = estimated_ear
444
+ metrics["ear"] = ear_value
445
+ except Exception as e:
446
+ print(f"Error in simplified eye detection: {e}")
447
+
448
+ # Combine features for final drowsiness probability
449
+ if landmark_detection_success:
450
+ # Calculate eye state factor (1.0 when eyes closed, 0.0 when fully open)
451
+ eye_state = max(0, min(1, (self.EAR_THRESHOLD - ear_value) * 5))
452
+
453
+ # Weight the factors
454
+ weighted_avg = (
455
+ self.MODEL_WEIGHT * model_prob +
456
+ self.EAR_WEIGHT * eye_state +
457
+ self.GAZE_WEIGHT * gaze_value +
458
+ self.HEAD_POSE_WEIGHT * head_pose_value +
459
+ self.SPEED_CHANGE_WEIGHT * speed_change_score # Add speed change factor
460
+ )
461
+
462
+ # Update drowsy probability
463
+ drowsy_prob = weighted_avg
464
+ else:
465
+ # If landmark detection failed, use simplified approach
466
+ # Use model probability with higher weight
467
+ if "ear" in metrics and metrics["ear"] < 1.0:
468
+ # We have the simplified eye metric
469
+ eye_state = max(0, min(1, (self.EAR_THRESHOLD - metrics["ear"]) * 5))
470
+ drowsy_prob = (self.MODEL_WEIGHT * model_prob) + ((1 - self.MODEL_WEIGHT - self.SPEED_CHANGE_WEIGHT) * eye_state) + (self.SPEED_CHANGE_WEIGHT * speed_change_score)
471
+ else:
472
+ # Only model and speed are available
473
+ drowsy_prob = (model_prob * 0.85) + (speed_change_score * 0.15)
474
+
475
+ # Apply smoothing with history
476
+ self.drowsy_history.append(drowsy_prob)
477
+ if len(self.drowsy_history) > 10:
478
+ self.drowsy_history.pop(0)
479
+
480
+ # Use median filtering for robustness
481
+ drowsy_prob = np.median(self.drowsy_history)
482
+
483
+ return drowsy_prob, face_coords, None, metrics
484
+
485
+ # Create a global instance
486
  detector = DrowsinessDetector()
487
 
488
  def process_image(image):
489
+ """Process image input"""
490
  if image is None:
491
  return None, "No image provided"
492
+
493
  try:
494
+ # Check for valid image
495
+ if image.size == 0 or image.shape[0] == 0 or image.shape[1] == 0:
496
+ return None, "Invalid image dimensions"
497
+
498
+ # Make a copy of the image to avoid modifying the original
499
+ processed_image = image.copy()
 
 
 
500
 
501
  # Make prediction
502
+ drowsy_prob, face_coords, error, metrics = detector.predict(processed_image)
503
 
504
  if error:
505
+ return None, error
506
 
507
+ if face_coords is None:
508
+ # No face detected - add text to the image and return it
509
+ cv2.putText(processed_image, "No face detected", (30, 30),
510
+ cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
511
+ return processed_image, "No face detected"
512
 
513
+ # Draw bounding box
514
+ x, y, w, h = face_coords
515
+
516
+ # Use a higher threshold (0.7) to reduce false positives
517
+ is_drowsy = drowsy_prob >= 0.7
518
+
519
+ # Determine alert level and color
520
+ if drowsy_prob >= 0.85:
521
+ alert_level = "High Risk"
522
+ color = (0, 0, 255) # Red
523
+ elif drowsy_prob >= 0.7:
524
+ alert_level = "Medium Risk"
525
+ color = (0, 165, 255) # Orange
526
  else:
527
+ alert_level = "Alert"
528
+ color = (0, 255, 0) # Green
529
+
530
+ cv2.rectangle(processed_image, (x, y), (x+w, y+h), color, 2)
531
+
532
+ # Add the metrics as text on image
533
+ y_offset = 25
534
+ cv2.putText(processed_image, f"{'Drowsy' if is_drowsy else 'Alert'} ({drowsy_prob:.2f})",
535
+ (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, color, 2)
536
+
537
+ # Add alert level
538
+ cv2.putText(processed_image, alert_level, (x, y-35),
539
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
540
+
541
+ # Add metrics in bottom left
542
+ cv2.putText(processed_image, f"Model: {metrics['model_prob']:.2f}", (10, processed_image.shape[0]-10-y_offset*3),
543
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
544
+ cv2.putText(processed_image, f"Eye Ratio: {metrics['ear']:.2f}", (10, processed_image.shape[0]-10-y_offset*2),
545
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
546
+ cv2.putText(processed_image, f"Head Pose: {metrics['head_pose']:.2f}", (10, processed_image.shape[0]-10-y_offset),
547
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
548
+
549
+ # Add confidence disclaimer for high model probabilities but good eye metrics
550
+ if metrics['model_prob'] > 0.9 and metrics['ear'] > 0.25:
551
+ cv2.putText(processed_image, "Model conflict - verify manually",
552
+ (10, processed_image.shape[0]-10-y_offset*4),
553
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 165, 255), 1)
554
+
555
+ return processed_image, f"Processed successfully. Drowsiness: {drowsy_prob:.2f}, Alert level: {alert_level}"
556
 
557
  except Exception as e:
558
+ import traceback
559
+ error_details = traceback.format_exc()
560
+ print(f"Error processing image: {str(e)}\n{error_details}")
561
+ return None, f"Error processing image: {str(e)}"
562
 
563
+ def process_video(video, initial_speed=60):
564
  """Process video input"""
565
  if video is None:
566
  return None, "No video provided"
567
 
568
  try:
569
+ # 创建内存缓冲区而不是临时文件
570
+ temp_input = None
571
+
572
+ # Handle video input (can be file path or video data)
573
+ if isinstance(video, str):
574
+ print(f"Processing video from path: {video}")
575
+ # 直接读取原始文件,不复制到临时目录
576
+ cap = cv2.VideoCapture(video)
577
+ else:
578
+ print(f"Processing video from uploaded data")
579
+ # 读取上传的视频数据到内存
580
+ import tempfile
581
+ temp_input = tempfile.NamedTemporaryFile(suffix='.mp4', delete=False)
582
+ temp_input_path = temp_input.name
583
+ with open(temp_input_path, "wb") as f:
584
+ f.write(video)
585
+ cap = cv2.VideoCapture(temp_input_path)
586
+
587
+ if not cap.isOpened():
588
+ return None, "Error: Could not open video"
589
+
590
  # Get input video properties
 
591
  fps = cap.get(cv2.CAP_PROP_FPS)
592
+ if fps <= 0:
593
+ fps = 30 # Default to 30fps if invalid
594
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
595
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
596
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
597
 
598
+ print(f"Video properties: {width}x{height} at {fps}fps, total frames: {total_frames}")
599
+
600
+ # 创建内存缓冲区而不是临时输出文件
601
+ import io
602
+ import base64
603
+
604
+ # 使用临时文件来存储处理后的视频(处理完毕后会删除)
605
+ import tempfile
606
+ temp_output = tempfile.NamedTemporaryFile(suffix='.mp4', delete=False)
607
+ temp_output_path = temp_output.name
608
+
609
+ # Try different codecs on Windows
610
+ if os.name == 'nt': # Windows
611
+ # 使用mp4v编码以确保兼容性
612
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
613
+ else:
614
+ # On other platforms, use MP4V
615
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
616
+
617
+ # Create video writer
618
+ out = cv2.VideoWriter(temp_output_path, fourcc, fps, (width, height))
619
+ if not out.isOpened():
620
+ return None, "Error: Could not create output video file"
621
+
622
+ # Reset speed detector at the start of each video
623
+ detector.reset_speed_detector()
624
+
625
+ # Initialize speed value with the provided initial speed
626
+ current_speed = initial_speed
627
+ detector.speed_detector.speed_estimate = initial_speed
628
+
629
+ # Process each frame
630
+ frame_count = 0
631
+ processed_count = 0
632
+ face_detected_count = 0
633
+ drowsy_count = 0
634
+ high_risk_count = 0
635
+ ear_sum = 0
636
+ model_prob_sum = 0
637
 
638
  while True:
639
  ret, frame = cap.read()
640
  if not ret:
641
+ print(f"End of video or error reading frame at frame {frame_count}")
642
  break
643
+
644
+ frame_count += 1
645
+
646
+ # Detect speed from the current frame
647
+ current_speed = detector.speed_detector.detect_speed_from_frame(frame)
648
+
649
+ try:
650
+ # Try to process the frame
651
+ processed_frame, message = process_image(frame)
652
+
653
+ # Add speed info to the frame
654
+ if processed_frame is not None:
655
+ speed_text = f"Speed: {current_speed:.1f} km/h"
656
+ cv2.putText(processed_frame, speed_text, (10, processed_frame.shape[0]-45),
657
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
658
+
659
+ # Add speed change score
660
+ speed_change_score = detector.speed_detector.get_speed_change_score()
661
+ cv2.putText(processed_frame, f"Speed Variation: {speed_change_score:.2f}",
662
+ (10, processed_frame.shape[0]-70),
663
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
664
 
665
+ if processed_frame is not None:
666
+ out.write(processed_frame)
667
+ processed_count += 1
668
+ if "No face detected" not in message:
669
+ face_detected_count += 1
670
+ if "Drowsiness" in message:
671
+ # Extract drowsiness probability
672
+ try:
673
+ drowsy_text = message.split("Drowsiness: ")[1].split(",")[0]
674
+ drowsy_prob = float(drowsy_text)
675
+
676
+ # Track drowsiness stats
677
+ if drowsy_prob >= 0.7:
678
+ drowsy_count += 1
679
+ if drowsy_prob >= 0.85:
680
+ high_risk_count += 1
681
+
682
+ # Get metrics from the frame
683
+ _, _, _, metrics = detector.predict(frame)
684
+ if 'ear' in metrics:
685
+ ear_sum += metrics['ear']
686
+ if 'model_prob' in metrics:
687
+ model_prob_sum += metrics['model_prob']
688
+ except:
689
+ pass
690
+ else:
691
+ # Fallback: If processing fails, just use the original frame
692
+ # Add text indicating processing failed
693
+ cv2.putText(frame, "Processing failed", (30, 30),
694
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
695
+ out.write(frame)
696
+ processed_count += 1
697
+ print(f"Frame {frame_count}: Processing failed - {message}")
698
+ except Exception as e:
699
+ # If any error occurs during processing, use original frame
700
+ cv2.putText(frame, f"Error: {str(e)[:30]}", (30, 30),
701
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
702
+ out.write(frame)
703
+ processed_count += 1
704
+ print(f"Frame {frame_count}: Exception - {str(e)}")
705
+
706
+ # Print progress for every 10th frame
707
+ if frame_count % 10 == 0:
708
+ print(f"Processed {frame_count}/{total_frames} frames")
709
 
710
  # Release resources
711
  cap.release()
712
  out.release()
713
 
714
+ # Calculate statistics
715
+ drowsy_percentage = (drowsy_count / face_detected_count * 100) if face_detected_count > 0 else 0
716
+ high_risk_percentage = (high_risk_count / face_detected_count * 100) if face_detected_count > 0 else 0
717
+ avg_ear = ear_sum / face_detected_count if face_detected_count > 0 else 0
718
+ avg_model_prob = model_prob_sum / face_detected_count if face_detected_count > 0 else 0
719
+ speed_score = detector.speed_detector.get_speed_change_score()
720
+
721
+ # Check if video was created successfully and return it directly
722
+ if os.path.exists(temp_output_path) and os.path.getsize(temp_output_path) > 0:
723
+ print(f"Video processed successfully with {processed_count} frames")
724
+ print(f"Drowsy frames: {drowsy_count} ({drowsy_percentage:.1f}%), High risk frames: {high_risk_count} ({high_risk_percentage:.1f}%)")
725
+ print(f"Average eye ratio: {avg_ear:.2f}, Average model probability: {avg_model_prob:.2f}")
726
+ print(f"Speed change score: {speed_score:.2f}")
727
+
728
+ # If model prob is high but eye ratio is also high (open eyes), flag potential false positive
729
+ false_positive_warning = ""
730
+ if avg_model_prob > 0.8 and avg_ear > 0.25:
731
+ false_positive_warning = " ⚠️ Possible false positive (eyes open but model detects drowsiness)"
732
+
733
+ result_message = (f"Video processed successfully. Frames: {frame_count}, faces detected: {face_detected_count}, "
734
+ f"drowsy: {drowsy_count} ({drowsy_percentage:.1f}%), high risk: {high_risk_count} ({high_risk_percentage:.1f}%)."
735
+ f" Avg eye ratio: {avg_ear:.2f}, Speed score: {speed_score:.2f}{false_positive_warning}")
736
+
737
+ # 直接返回文件而不保留它
738
+ video_result = temp_output_path
739
+
740
+ return video_result, result_message
741
  else:
742
+ print(f"Failed to create output video. Frames read: {frame_count}, processed: {processed_count}")
743
+ return None, f"Error: Failed to create output video. Frames read: {frame_count}, processed: {processed_count}"
744
 
745
  except Exception as e:
746
+ import traceback
747
+ error_details = traceback.format_exc()
748
+ print(f"Error processing video: {str(e)}\n{error_details}")
749
  return None, f"Error processing video: {str(e)}"
750
  finally:
751
+ # Clean up resources
752
+ if 'out' in locals() and out is not None:
753
  out.release()
754
+ if 'cap' in locals() and cap is not None:
755
  cap.release()
756
+
757
+ # 删除临时输入文件(如果存在)
758
+ if temp_input is not None:
759
+ try:
760
+ os.unlink(temp_input.name)
761
+ except:
762
+ pass
763
 
764
+ def process_webcam(image):
765
+ """Process webcam input - returns processed image and status message"""
766
+ return process_image(image)
767
 
768
+ # Launch the app
769
+ if __name__ == "__main__":
770
+ # Parse command line arguments
771
+ parser = argparse.ArgumentParser(description="Driver Drowsiness Detection App")
772
+ parser.add_argument("--share", action="store_true", help="Create a public link (may trigger security warnings)")
773
+ parser.add_argument("--port", type=int, default=7860, help="Port to run the app on")
774
+ args = parser.parse_args()
775
 
776
+ # Print warning if share is enabled
777
+ if args.share:
778
+ print("WARNING: Running with --share may trigger security warnings on some systems.")
779
+ print("The app will be accessible from the internet through a temporary URL.")
780
 
781
+ # 注册退出时的清理函数
782
+ import atexit
783
+ import glob
784
+ import shutil
 
785
 
786
+ def cleanup_temp_files():
787
+ """清理所有临时文件"""
788
+ try:
789
+ # 删除所有可能留下的临时文件
790
+ import tempfile
791
+ temp_dir = tempfile.gettempdir()
792
+ pattern = os.path.join(temp_dir, "tmp*")
793
+ for file in glob.glob(pattern):
794
+ try:
795
+ if os.path.isfile(file):
796
+ os.remove(file)
797
+ except Exception as e:
798
+ print(f"Failed to delete {file}: {e}")
799
+
800
+ # 确保没有留下.mp4或.avi文件
801
+ for ext in [".mp4", ".avi"]:
802
+ pattern = os.path.join(temp_dir, f"*{ext}")
803
+ for file in glob.glob(pattern):
804
+ try:
805
+ os.remove(file)
806
+ except Exception as e:
807
+ print(f"Failed to delete {file}: {e}")
808
+
809
+ print("Cleaned up temporary files")
810
+ except Exception as e:
811
+ print(f"Error during cleanup: {e}")
812
+
813
+ # 注册清理函数
814
+ atexit.register(cleanup_temp_files)
815
+
816
+ # Load the model at startup
817
+ detector.load_model()
818
 
819
+ # Create interface
820
+ with gr.Blocks(title="Driver Drowsiness Detection") as demo:
821
+ gr.Markdown("""
822
+ # 🚗 Driver Drowsiness Detection System
823
+
824
+ This system detects driver drowsiness using computer vision and deep learning.
825
+
826
+ ## Features:
827
+ - Image analysis
828
+ - Video processing with speed monitoring
829
+ - Webcam detection (PC and mobile)
830
+ - Multi-factor drowsiness prediction (face, eyes, head pose, speed changes)
831
+ """)
832
+
833
+ with gr.Tabs():
834
+ with gr.Tab("Image"):
835
+ gr.Markdown("Upload an image for drowsiness detection")
836
+ with gr.Row():
837
+ image_input = gr.Image(label="Input Image", type="numpy")
838
+ image_output = gr.Image(label="Processed Image")
839
+ with gr.Row():
840
+ status_output = gr.Textbox(label="Status")
841
+ image_input.change(
842
+ fn=process_image,
843
+ inputs=[image_input],
844
+ outputs=[image_output, status_output]
845
+ )
846
+
847
+ with gr.Tab("Video"):
848
+ gr.Markdown("""
849
+ ### 上傳駕駛視頻進行困倦檢測
850
+
851
+ 系統將自動從視頻中檢測以下內容:
852
+ - 駕駛員面部表情和眼睛狀態
853
+ - 車輛速度變化 (通過視頻中的光流分析)
854
+ - 當車速變化超過 ±5 km/h 時將被視為異常駕駛行為
855
+
856
+ **注意:** 處理後的視頻不會保存到本地文件夾,請使用界面右上角的下載按鈕保存結果。
857
+ """)
858
+ with gr.Row():
859
+ video_input = gr.Video(label="輸入視頻")
860
+ video_output = gr.Video(label="處理後視頻 (點擊右上角下載)")
861
+ with gr.Row():
862
+ initial_speed = gr.Slider(minimum=10, maximum=120, value=60, label="初始車速估計值 (km/h)",
863
+ info="僅作為初始估計值,系統會自動從視頻中檢測實際速度變化")
864
+ with gr.Row():
865
+ video_status = gr.Textbox(label="處理狀態")
866
+ with gr.Row():
867
+ process_btn = gr.Button("處理視頻")
868
+ clear_btn = gr.Button("清除")
869
+
870
+ process_btn.click(
871
+ fn=process_video,
872
+ inputs=[video_input, initial_speed],
873
+ outputs=[video_output, video_status]
874
+ )
875
+
876
+ clear_btn.click(
877
+ fn=lambda: (None, "已清除結果"),
878
+ inputs=[],
879
+ outputs=[video_output, video_status]
880
+ )
881
+
882
+ with gr.Tab("Webcam"):
883
+ gr.Markdown("Use your webcam or mobile camera for real-time drowsiness detection")
884
+ with gr.Row():
885
+ webcam_input = gr.Image(label="Camera Feed", type="numpy", streaming=True)
886
+ webcam_output = gr.Image(label="Processed Feed")
887
+ with gr.Row():
888
+ speed_input = gr.Slider(minimum=0, maximum=150, value=60, label="Current Speed (km/h)")
889
+ update_speed_btn = gr.Button("Update Speed")
890
+ with gr.Row():
891
+ webcam_status = gr.Textbox(label="Status")
892
+
893
+ def process_webcam_with_speed(image, speed):
894
+ detector.update_speed(speed)
895
+ return process_image(image)
896
+
897
+ update_speed_btn.click(
898
+ fn=lambda speed: f"Speed updated to {speed} km/h",
899
+ inputs=[speed_input],
900
+ outputs=[webcam_status]
901
+ )
902
+
903
+ webcam_input.change(
904
+ fn=process_webcam_with_speed,
905
+ inputs=[webcam_input, speed_input],
906
+ outputs=[webcam_output, webcam_status]
907
+ )
908
+
909
+ gr.Markdown("""
910
+ ## How It Works
911
+ This system detects drowsiness using multiple factors:
912
+ 1. **Facial features** - Using a trained CNN model
913
+ 2. **Eye openness** - Measuring eye aspect ratio (EAR)
914
+ 3. **Head position** - Detecting head drooping
915
+ 4. **Automatic speed detection** - Using optical flow analysis to track vehicle movement and detect irregular speed changes
916
+
917
+ The system automatically detects speed changes from the video frames using computer vision techniques:
918
+ - **Optical flow** is used to track movement between frames
919
+ - **Irregular speed changes** (±5 km/h) are detected as potential signs of drowsy driving
920
+ - **No external speed data required** - everything is analyzed directly from the video content
921
+
922
+ Combining these factors provides more reliable drowsiness detection than using facial features alone.
923
+ """)
924
+
925
+ # Launch the app
926
+ demo.launch(share=args.share, server_port=args.port)