mlbench123 commited on
Commit
2a60a4e
Β·
verified Β·
1 Parent(s): c7e939c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +140 -139
app.py CHANGED
@@ -1,9 +1,12 @@
 
1
  import cv2
2
  import numpy as np
3
  from collections import deque
4
  from datetime import datetime
5
  from ultralytics import YOLO
6
  import time
 
 
7
 
8
  class RotatingPadShirtCounter:
9
  """
@@ -11,7 +14,7 @@ class RotatingPadShirtCounter:
11
  Logic: Count when empty pad ENTERS the ROI (after shirt was removed)
12
  """
13
  def __init__(self,
14
- model_path='runs/exp2/weights/best.pt',
15
  roi_center=(320, 240),
16
  roi_radius=180,
17
  min_conf=0.5,
@@ -52,33 +55,25 @@ class RotatingPadShirtCounter:
52
  self.debug_mode = True
53
 
54
  def detect_in_roi(self, frame):
55
- """
56
- Run YOLO detection and filter by ROI
57
- Returns: (has_empty_pad, has_occupied_pad, all_detections)
58
- """
59
- # Run YOLO
60
  results = self.model.predict(frame, conf=self.min_conf, verbose=False)
61
 
62
  has_empty_pad_in_roi = False
63
  has_occupied_pad_in_roi = False
64
  all_detections = []
65
 
66
- # Parse results
67
  for result in results:
68
  boxes = result.boxes
69
 
70
  for box in boxes:
71
- # Extract data
72
  x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
73
  conf = float(box.conf[0].cpu().numpy())
74
  class_id = int(box.cls[0].cpu().numpy())
75
  class_name = self.model_names[class_id]
76
 
77
- # Calculate center
78
  center_x = (x1 + x2) / 2
79
  center_y = (y1 + y2) / 2
80
 
81
- # Check if in ROI
82
  dist = np.sqrt((center_x - self.roi_center[0])**2 +
83
  (center_y - self.roi_center[1])**2)
84
 
@@ -97,7 +92,6 @@ class RotatingPadShirtCounter:
97
  if class_name == 'empty_pad':
98
  has_empty_pad_in_roi = True
99
  else:
100
- # Any other detection in ROI means occupied (shirt on pad)
101
  has_occupied_pad_in_roi = True
102
 
103
  return has_empty_pad_in_roi, has_occupied_pad_in_roi, all_detections
@@ -118,26 +112,19 @@ class RotatingPadShirtCounter:
118
  if len(self.state_buffer) < self.stability_frames:
119
  return self.current_state
120
 
121
- # Count occurrences
122
  state_counts = {}
123
  for s in self.state_buffer:
124
  state_counts[s] = state_counts.get(s, 0) + 1
125
 
126
- # Get most common state
127
  stable_state = max(state_counts, key=state_counts.get)
128
 
129
- # Require majority agreement (> 60%)
130
  if state_counts[stable_state] >= len(self.state_buffer) * 0.6:
131
  return stable_state
132
 
133
  return self.current_state
134
 
135
  def should_count(self):
136
- """
137
- KEY COUNTING LOGIC:
138
- Count when worker removes shirt: OCCUPIED_IN_ROI -> EMPTY_IN_ROI
139
- But only if previous PAD_AWAY state lasted >= 80 frames
140
- """
141
  if self.prev_state == "PAD_AWAY" and self.current_state == "OCCUPIED_IN_ROI":
142
  time_since_last = time.time() - self.last_count_time
143
  if (time_since_last >= self.min_time_between_counts and
@@ -148,29 +135,21 @@ class RotatingPadShirtCounter:
148
 
149
  def process_frame(self, frame):
150
  """Main processing loop"""
151
- # Detect
152
  has_empty, has_occupied, detections = self.detect_in_roi(frame)
153
-
154
- # Determine instantaneous state
155
  instant_state = self.determine_state(has_empty, has_occupied)
156
-
157
- # Get stable state
158
  stable_state = self.update_state_buffer(instant_state)
159
 
160
- # Track how long previous state was PAD_AWAY
161
  if self.current_state == "PAD_AWAY":
162
  self.pad_away_frames += 1
163
  else:
164
- self.pad_away_frames = 0 # Reset if not PAD_AWAY
165
 
166
- # Check for state change
167
  state_changed = (stable_state != self.current_state)
168
 
169
  if state_changed:
170
  self.prev_state = self.current_state
171
  self.current_state = stable_state
172
 
173
- # Check if we should count
174
  should_count, reason = self.should_count()
175
 
176
  if should_count:
@@ -181,51 +160,38 @@ class RotatingPadShirtCounter:
181
  else:
182
  self.log_event("STATE_CHANGE", f"{self.prev_state} -> {self.current_state}")
183
 
184
- # Visualize
185
  vis_frame = self.draw_visualization(frame, detections, instant_state)
186
-
187
  return vis_frame
188
 
189
  def draw_visualization(self, frame, detections, instant_state):
190
  """Draw debug information on frame"""
191
  vis = frame.copy()
192
 
193
- # Draw ROI
194
  cv2.circle(vis, self.roi_center, self.roi_radius, (0, 255, 255), 3)
195
  cv2.circle(vis, self.roi_center, 5, (0, 255, 255), -1)
196
 
197
- # Draw all detections
198
  for det in detections:
199
  x1, y1, x2, y2 = map(int, det['bbox'])
200
  conf = det['confidence']
201
  cls = det['class']
202
  in_roi = det['in_roi']
203
 
204
- # Color coding
205
- if cls == 'empty_pad':
206
- color = (0, 255, 0) # Green
207
- else:
208
- color = (0, 0, 255) # Red
209
-
210
  thickness = 3 if in_roi else 2
211
  cv2.rectangle(vis, (x1, y1), (x2, y2), color, thickness)
212
 
213
- # Label
214
  label = f"{cls} {conf:.2f}"
215
  if in_roi:
216
  label += " [ROI]"
217
  cv2.putText(vis, label, (x1, y1-10),
218
  cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
219
 
220
- # Status panel
221
  panel_height = 180
222
  panel = np.zeros((panel_height, vis.shape[1], 3), dtype=np.uint8)
223
 
224
- # Count (BIG and prominent)
225
  cv2.putText(panel, f"SHIRTS COUNTED: {self.shirt_count}", (20, 50),
226
  cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 3)
227
 
228
- # Current state
229
  state_color = {
230
  "EMPTY_IN_ROI": (0, 255, 0),
231
  "OCCUPIED_IN_ROI": (0, 165, 255),
@@ -236,11 +202,9 @@ class RotatingPadShirtCounter:
236
  cv2.putText(panel, f"State: {self.current_state}", (20, 90),
237
  cv2.FONT_HERSHEY_SIMPLEX, 0.8, state_color, 2)
238
 
239
- # Instant vs Stable
240
  cv2.putText(panel, f"Instant: {instant_state}", (20, 120),
241
  cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
242
 
243
- # Buffer visualization
244
  buffer_str = ''.join([
245
  'E' if s == "EMPTY_IN_ROI" else
246
  'O' if s == "OCCUPIED_IN_ROI" else
@@ -250,9 +214,7 @@ class RotatingPadShirtCounter:
250
  cv2.putText(panel, f"Buffer: [{buffer_str}]", (20, 150),
251
  cv2.FONT_HERSHEY_SIMPLEX, 0.6, (180, 180, 180), 1)
252
 
253
- # Combine
254
  vis = np.vstack([panel, vis])
255
-
256
  return vis
257
 
258
  def log_event(self, event_type, details):
@@ -274,82 +236,51 @@ class RotatingPadShirtCounter:
274
  }
275
 
276
 
277
- # ============================================================================
278
- # MAIN CONFIGURATION - EDIT THESE VALUES
279
- # ============================================================================
280
-
281
- # INPUT/OUTPUT FILES
282
- INPUT_VIDEO = "videos/sdcard_0_20251013125904.mp4" # Your input video path
283
- OUTPUT_VIDEO = "videos/output_counted.mp4" # Where to save processed video
284
- MODEL_PATH = "runs/exp2/weights/best.pt" # Your YOLO model path
285
-
286
- # ROI SETTINGS (Region of Interest where pad appears)
287
- ROI_CENTER = None # None = auto-detect (video center), or tuple like (640, 360)
288
- ROI_RADIUS = 180 # Radius in pixels
289
-
290
- # DETECTION SETTINGS
291
- MIN_CONFIDENCE = 0.98 # Minimum YOLO confidence (0.0 to 1.0)
292
- STABILITY_FRAMES = 15 # Frames needed to confirm state change
293
-
294
- # ============================================================================
295
-
296
-
297
- def process_video():
298
- """
299
- Process video file and save output with detections and counting
300
- """
301
- print("="*80)
302
- print("ROTATING PAD SHIRT COUNTER - VIDEO PROCESSOR")
303
- print("="*80)
304
 
305
- # Open input video
306
- cap = cv2.VideoCapture(INPUT_VIDEO)
 
307
  if not cap.isOpened():
308
- print(f"❌ Error: Cannot open video file: {INPUT_VIDEO}")
309
- return
310
 
311
- # Get video properties
312
  fps = int(cap.get(cv2.CAP_PROP_FPS))
313
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
314
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
315
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
316
 
317
- print(f"βœ“ Input Video: {INPUT_VIDEO}")
318
- print(f" Resolution: {width}x{height}")
319
- print(f" FPS: {fps}")
320
- print(f" Total Frames: {total_frames}")
321
-
322
- # Auto-calculate ROI center if not provided
323
- roi_center = ROI_CENTER if ROI_CENTER else (width // 2, height // 2)
324
 
325
- print(f" ROI Center: {roi_center}, Radius: {ROI_RADIUS}")
326
 
327
- # Initialize counter
328
  counter = RotatingPadShirtCounter(
329
- model_path=MODEL_PATH,
330
  roi_center=roi_center,
331
- roi_radius=ROI_RADIUS,
332
- min_conf=MIN_CONFIDENCE,
333
- stability_frames=STABILITY_FRAMES
334
  )
335
 
336
- # Prepare output video writer
337
- output_height = height + 180 # Add panel height
 
 
 
 
 
338
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
339
- out = cv2.VideoWriter(OUTPUT_VIDEO, fourcc, fps, (width, output_height))
340
 
341
  if not out.isOpened():
342
- print(f"❌ Error: Cannot create output video: {OUTPUT_VIDEO}")
343
  cap.release()
344
- return
345
 
346
- print(f"βœ“ Output Video: {OUTPUT_VIDEO}")
347
- print(f" Output Resolution: {width}x{output_height}")
348
- print("-"*80)
349
- print("Processing video...")
350
 
351
  frame_count = 0
352
- start_time = time.time()
353
 
354
  try:
355
  while True:
@@ -358,57 +289,127 @@ def process_video():
358
  break
359
 
360
  frame_count += 1
361
-
362
- # Process frame
363
  vis_frame = counter.process_frame(frame)
364
 
365
- # Add frame number and progress
366
- progress = (frame_count / total_frames) * 100
367
- cv2.putText(vis_frame, f"Frame: {frame_count}/{total_frames} ({progress:.1f}%)",
368
  (width - 350, 30),
369
  cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 0), 2)
370
 
371
- # Write frame
372
  out.write(vis_frame)
373
 
374
- # Progress indicator every 30 frames
375
  if frame_count % 30 == 0:
376
- elapsed = time.time() - start_time
377
- fps_processing = frame_count / elapsed
378
- eta_seconds = (total_frames - frame_count) / fps_processing if fps_processing > 0 else 0
379
- print(f"Progress: {frame_count}/{total_frames} frames "
380
- f"({progress:.1f}%) | "
381
- f"Shirts: {counter.shirt_count} | "
382
- f"ETA: {eta_seconds:.0f}s")
383
 
384
- except KeyboardInterrupt:
385
- print("\n⚠ Processing interrupted by user")
 
 
386
 
387
  finally:
388
- # Cleanup
389
  cap.release()
390
  out.release()
391
-
392
- # Final statistics
393
- elapsed = time.time() - start_time
394
- stats = counter.get_stats()
395
-
396
- print("\n" + "="*80)
397
- print("PROCESSING COMPLETE")
398
- print("="*80)
399
- print(f"Total Frames Processed: {frame_count}")
400
- print(f"Processing Time: {elapsed:.2f} seconds")
401
- print(f"Average FPS: {frame_count/elapsed:.2f}")
402
- print(f"\n🎯 TOTAL SHIRTS COUNTED: {stats['total_shirts']}")
403
- print(f"Final State: {stats['current_state']}")
404
- print("\nEvent Log (Shirt Counts):")
405
- for evt in stats['events']:
406
- if evt['event'] == 'SHIRT_COUNTED':
407
- print(f" βœ“ [{evt['timestamp']}] Shirt #{evt['count']} - {evt['details']}")
408
- print("="*80)
409
- print(f"βœ“ Output saved to: {OUTPUT_VIDEO}")
410
- print("="*80)
411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
 
413
  if __name__ == "__main__":
414
- process_video()
 
1
+ import gradio as gr
2
  import cv2
3
  import numpy as np
4
  from collections import deque
5
  from datetime import datetime
6
  from ultralytics import YOLO
7
  import time
8
+ import tempfile
9
+ import os
10
 
11
  class RotatingPadShirtCounter:
12
  """
 
14
  Logic: Count when empty pad ENTERS the ROI (after shirt was removed)
15
  """
16
  def __init__(self,
17
+ model_path='best.pt',
18
  roi_center=(320, 240),
19
  roi_radius=180,
20
  min_conf=0.5,
 
55
  self.debug_mode = True
56
 
57
  def detect_in_roi(self, frame):
58
+ """Run YOLO detection and filter by ROI"""
 
 
 
 
59
  results = self.model.predict(frame, conf=self.min_conf, verbose=False)
60
 
61
  has_empty_pad_in_roi = False
62
  has_occupied_pad_in_roi = False
63
  all_detections = []
64
 
 
65
  for result in results:
66
  boxes = result.boxes
67
 
68
  for box in boxes:
 
69
  x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
70
  conf = float(box.conf[0].cpu().numpy())
71
  class_id = int(box.cls[0].cpu().numpy())
72
  class_name = self.model_names[class_id]
73
 
 
74
  center_x = (x1 + x2) / 2
75
  center_y = (y1 + y2) / 2
76
 
 
77
  dist = np.sqrt((center_x - self.roi_center[0])**2 +
78
  (center_y - self.roi_center[1])**2)
79
 
 
92
  if class_name == 'empty_pad':
93
  has_empty_pad_in_roi = True
94
  else:
 
95
  has_occupied_pad_in_roi = True
96
 
97
  return has_empty_pad_in_roi, has_occupied_pad_in_roi, all_detections
 
112
  if len(self.state_buffer) < self.stability_frames:
113
  return self.current_state
114
 
 
115
  state_counts = {}
116
  for s in self.state_buffer:
117
  state_counts[s] = state_counts.get(s, 0) + 1
118
 
 
119
  stable_state = max(state_counts, key=state_counts.get)
120
 
 
121
  if state_counts[stable_state] >= len(self.state_buffer) * 0.6:
122
  return stable_state
123
 
124
  return self.current_state
125
 
126
  def should_count(self):
127
+ """KEY COUNTING LOGIC"""
 
 
 
 
128
  if self.prev_state == "PAD_AWAY" and self.current_state == "OCCUPIED_IN_ROI":
129
  time_since_last = time.time() - self.last_count_time
130
  if (time_since_last >= self.min_time_between_counts and
 
135
 
136
  def process_frame(self, frame):
137
  """Main processing loop"""
 
138
  has_empty, has_occupied, detections = self.detect_in_roi(frame)
 
 
139
  instant_state = self.determine_state(has_empty, has_occupied)
 
 
140
  stable_state = self.update_state_buffer(instant_state)
141
 
 
142
  if self.current_state == "PAD_AWAY":
143
  self.pad_away_frames += 1
144
  else:
145
+ self.pad_away_frames = 0
146
 
 
147
  state_changed = (stable_state != self.current_state)
148
 
149
  if state_changed:
150
  self.prev_state = self.current_state
151
  self.current_state = stable_state
152
 
 
153
  should_count, reason = self.should_count()
154
 
155
  if should_count:
 
160
  else:
161
  self.log_event("STATE_CHANGE", f"{self.prev_state} -> {self.current_state}")
162
 
 
163
  vis_frame = self.draw_visualization(frame, detections, instant_state)
 
164
  return vis_frame
165
 
166
  def draw_visualization(self, frame, detections, instant_state):
167
  """Draw debug information on frame"""
168
  vis = frame.copy()
169
 
 
170
  cv2.circle(vis, self.roi_center, self.roi_radius, (0, 255, 255), 3)
171
  cv2.circle(vis, self.roi_center, 5, (0, 255, 255), -1)
172
 
 
173
  for det in detections:
174
  x1, y1, x2, y2 = map(int, det['bbox'])
175
  conf = det['confidence']
176
  cls = det['class']
177
  in_roi = det['in_roi']
178
 
179
+ color = (0, 255, 0) if cls == 'empty_pad' else (0, 0, 255)
 
 
 
 
 
180
  thickness = 3 if in_roi else 2
181
  cv2.rectangle(vis, (x1, y1), (x2, y2), color, thickness)
182
 
 
183
  label = f"{cls} {conf:.2f}"
184
  if in_roi:
185
  label += " [ROI]"
186
  cv2.putText(vis, label, (x1, y1-10),
187
  cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
188
 
 
189
  panel_height = 180
190
  panel = np.zeros((panel_height, vis.shape[1], 3), dtype=np.uint8)
191
 
 
192
  cv2.putText(panel, f"SHIRTS COUNTED: {self.shirt_count}", (20, 50),
193
  cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 3)
194
 
 
195
  state_color = {
196
  "EMPTY_IN_ROI": (0, 255, 0),
197
  "OCCUPIED_IN_ROI": (0, 165, 255),
 
202
  cv2.putText(panel, f"State: {self.current_state}", (20, 90),
203
  cv2.FONT_HERSHEY_SIMPLEX, 0.8, state_color, 2)
204
 
 
205
  cv2.putText(panel, f"Instant: {instant_state}", (20, 120),
206
  cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
207
 
 
208
  buffer_str = ''.join([
209
  'E' if s == "EMPTY_IN_ROI" else
210
  'O' if s == "OCCUPIED_IN_ROI" else
 
214
  cv2.putText(panel, f"Buffer: [{buffer_str}]", (20, 150),
215
  cv2.FONT_HERSHEY_SIMPLEX, 0.6, (180, 180, 180), 1)
216
 
 
217
  vis = np.vstack([panel, vis])
 
218
  return vis
219
 
220
  def log_event(self, event_type, details):
 
236
  }
237
 
238
 
239
+ def process_video(video_path, roi_radius, min_confidence, stability_frames, progress=gr.Progress()):
240
+ """Process uploaded video"""
241
+ if video_path is None:
242
+ return None, "⚠️ Please upload a video first!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
+ progress(0, desc="Opening video...")
245
+
246
+ cap = cv2.VideoCapture(video_path)
247
  if not cap.isOpened():
248
+ return None, "❌ Error: Cannot open video file"
 
249
 
 
250
  fps = int(cap.get(cv2.CAP_PROP_FPS))
251
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
252
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
253
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
254
 
255
+ roi_center = (width // 2, height // 2)
 
 
 
 
 
 
256
 
257
+ progress(0.1, desc="Loading model...")
258
 
 
259
  counter = RotatingPadShirtCounter(
260
+ model_path='best.pt',
261
  roi_center=roi_center,
262
+ roi_radius=int(roi_radius),
263
+ min_conf=min_confidence,
264
+ stability_frames=int(stability_frames)
265
  )
266
 
267
+ output_height = height + 180
268
+
269
+ # Create temporary output file
270
+ temp_output = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
271
+ output_path = temp_output.name
272
+ temp_output.close()
273
+
274
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
275
+ out = cv2.VideoWriter(output_path, fourcc, fps, (width, output_height))
276
 
277
  if not out.isOpened():
 
278
  cap.release()
279
+ return None, "❌ Error: Cannot create output video"
280
 
281
+ progress(0.2, desc="Processing video...")
 
 
 
282
 
283
  frame_count = 0
 
284
 
285
  try:
286
  while True:
 
289
  break
290
 
291
  frame_count += 1
 
 
292
  vis_frame = counter.process_frame(frame)
293
 
294
+ frame_progress = (frame_count / total_frames) * 100
295
+ cv2.putText(vis_frame, f"Frame: {frame_count}/{total_frames} ({frame_progress:.1f}%)",
 
296
  (width - 350, 30),
297
  cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 0), 2)
298
 
 
299
  out.write(vis_frame)
300
 
 
301
  if frame_count % 30 == 0:
302
+ progress(0.2 + (frame_count / total_frames) * 0.75,
303
+ desc=f"Processing: {frame_count}/{total_frames} frames | Shirts: {counter.shirt_count}")
 
 
 
 
 
304
 
305
+ except Exception as e:
306
+ cap.release()
307
+ out.release()
308
+ return None, f"❌ Error during processing: {str(e)}"
309
 
310
  finally:
 
311
  cap.release()
312
  out.release()
313
+
314
+ progress(1.0, desc="Complete!")
315
+
316
+ stats = counter.get_stats()
317
+
318
+ result_text = f"""
319
+ βœ… **Processing Complete!**
320
+
321
+ πŸ“Š **Results:**
322
+ - Total Frames Processed: {frame_count:,}
323
+ - **Shirts Counted: {stats['total_shirts']}**
324
+ - Final State: {stats['current_state']}
 
 
 
 
 
 
 
 
325
 
326
+ πŸ“ **Event Log (Shirt Counts):**
327
+ """
328
+
329
+ for evt in stats['events']:
330
+ if evt['event'] == 'SHIRT_COUNTED':
331
+ result_text += f"\n βœ“ [{evt['timestamp']}] Shirt #{evt['count']} - {evt['details']}"
332
+
333
+ if stats['total_shirts'] == 0:
334
+ result_text += "\n\n⚠️ No shirts detected. Try adjusting parameters or ensure video shows the rotating pad system."
335
+
336
+ return output_path, result_text
337
+
338
+
339
+ # Gradio Interface
340
+ with gr.Blocks(title="Rotating Pad Shirt Counter", theme=gr.themes.Soft()) as demo:
341
+ gr.Markdown("""
342
+ # πŸ‘• Rotating Pad Shirt Counter
343
+
344
+ ### Demo Showcase - Limited Training Model
345
+
346
+ **⚠️ Important Note:** This is a demonstration model trained on only **half of a single video** for showcase purposes.
347
+ Performance may vary with different videos, lighting conditions, or camera angles.
348
+
349
+ ### How it works:
350
+ 1. Upload a video showing a rotating pad system with shirts
351
+ 2. The model detects when shirts are placed on the pad
352
+ 3. System counts shirts as they rotate through the Region of Interest (ROI)
353
+
354
+ ### Best Results:
355
+ - Similar camera angle and lighting to training data
356
+ - Clear view of the rotating pad
357
+ - Videos from the same or similar production line
358
+
359
+ ---
360
+ """)
361
+
362
+ with gr.Row():
363
+ with gr.Column():
364
+ video_input = gr.Video(label="Upload Video", height=400)
365
+
366
+ with gr.Accordion("βš™οΈ Advanced Settings (Optional)", open=False):
367
+ roi_radius = gr.Slider(
368
+ minimum=100, maximum=300, value=180, step=10,
369
+ label="ROI Radius (pixels)",
370
+ info="Detection area size around center"
371
+ )
372
+ min_confidence = gr.Slider(
373
+ minimum=0.5, maximum=0.99, value=0.98, step=0.01,
374
+ label="Minimum Confidence",
375
+ info="Higher = more strict detection"
376
+ )
377
+ stability_frames = gr.Slider(
378
+ minimum=3, maximum=30, value=15, step=1,
379
+ label="Stability Frames",
380
+ info="Frames needed to confirm state change"
381
+ )
382
+
383
+ process_btn = gr.Button("πŸš€ Process Video", variant="primary", size="lg")
384
+
385
+ with gr.Column():
386
+ video_output = gr.Video(label="Processed Output", height=400)
387
+ result_text = gr.Textbox(
388
+ label="Results & Statistics",
389
+ lines=10,
390
+ max_lines=15
391
+ )
392
+
393
+ gr.Markdown("""
394
+ ---
395
+ ### πŸ“Œ Model Information:
396
+ - **Classes Detected:** `empty_pad`, `occupied_pad` (shirt on pad)
397
+ - **Training Data:** Half portion of single production video
398
+ - **Purpose:** Demonstration and proof-of-concept
399
+ - **Limitations:** May not generalize well to different environments
400
+
401
+ ### πŸ’‘ Tips:
402
+ - Start with default settings
403
+ - If no shirts detected, try lowering confidence threshold
404
+ - If too many false counts, increase stability frames
405
+ - ROI radius should cover the area where pad appears
406
+ """)
407
+
408
+ process_btn.click(
409
+ fn=process_video,
410
+ inputs=[video_input, roi_radius, min_confidence, stability_frames],
411
+ outputs=[video_output, result_text]
412
+ )
413
 
414
  if __name__ == "__main__":
415
+ demo.launch()