mlbench123 commited on
Commit
dbe2a2d
·
verified ·
1 Parent(s): 840baf2

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +414 -0
app.py ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """
10
+ Robust shirt counter for rotating pad system.
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,
18
+ stability_frames=5):
19
+
20
+ # Load YOLO model
21
+ print(f"Loading YOLO model from: {model_path}")
22
+ self.model = YOLO(model_path)
23
+ self.model_names = self.model.names
24
+ print(f"Model classes: {self.model_names}")
25
+
26
+ # ROI Configuration
27
+ self.roi_center = roi_center
28
+ self.roi_radius = roi_radius
29
+ self.min_conf = min_conf
30
+
31
+ # State tracking
32
+ self.current_state = "UNKNOWN"
33
+ self.prev_state = "UNKNOWN"
34
+ self.state_buffer = deque(maxlen=stability_frames)
35
+ self.stability_frames = stability_frames
36
+
37
+ # Counting logic
38
+ self.shirt_count = 0
39
+
40
+ # Prevent double counting
41
+ self.last_count_time = time.time()
42
+ self.min_time_between_counts = 3.0
43
+
44
+ # Detection history
45
+ self.detection_history = deque(maxlen=30)
46
+
47
+ self.pad_away_frames = 0
48
+ self.min_pad_away_frames = 80
49
+
50
+ # Logging
51
+ self.event_log = []
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
+
85
+ in_roi = dist < self.roi_radius
86
+
87
+ detection = {
88
+ 'bbox': [x1, y1, x2, y2],
89
+ 'center': (center_x, center_y),
90
+ 'confidence': conf,
91
+ 'class': class_name,
92
+ 'in_roi': in_roi
93
+ }
94
+ all_detections.append(detection)
95
+
96
+ if in_roi:
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
104
+
105
+ def determine_state(self, has_empty, has_occupied):
106
+ """Determine current state based on detections"""
107
+ if has_empty:
108
+ return "EMPTY_IN_ROI"
109
+ elif has_occupied:
110
+ return "OCCUPIED_IN_ROI"
111
+ else:
112
+ return "PAD_AWAY"
113
+
114
+ def update_state_buffer(self, state):
115
+ """Add to buffer and return stable state"""
116
+ self.state_buffer.append(state)
117
+
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
144
+ self.pad_away_frames >= self.min_pad_away_frames):
145
+ return True, f"Shirt on pad after PAD_AWAY for {self.pad_away_frames} frames"
146
+
147
+ return False, None
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:
177
+ self.shirt_count += 1
178
+ self.last_count_time = time.time()
179
+ self.log_event("SHIRT_COUNTED", reason)
180
+ print(f"🎯 SHIRT #{self.shirt_count} COUNTED! - {reason}")
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),
232
+ "PAD_AWAY": (255, 0, 0),
233
+ "UNKNOWN": (128, 128, 128)
234
+ }.get(self.current_state, (255, 255, 255))
235
+
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
247
+ 'A' if s == "PAD_AWAY" else '?'
248
+ for s in self.state_buffer
249
+ ])
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):
259
+ """Log events for debugging"""
260
+ self.event_log.append({
261
+ 'timestamp': datetime.now().strftime('%H:%M:%S.%f')[:-3],
262
+ 'event': event_type,
263
+ 'details': details,
264
+ 'count': self.shirt_count,
265
+ 'state': self.current_state
266
+ })
267
+
268
+ def get_stats(self):
269
+ """Get statistics"""
270
+ return {
271
+ 'total_shirts': self.shirt_count,
272
+ 'current_state': self.current_state,
273
+ 'events': self.event_log
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:
356
+ ret, frame = cap.read()
357
+ if not ret:
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()