PrashanthB461 commited on
Commit
7e0f35f
·
verified ·
1 Parent(s): 9554c03

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +207 -304
app.py CHANGED
@@ -38,7 +38,7 @@ def check_ffmpeg():
38
 
39
  FFMPEG_AVAILABLE = check_ffmpeg()
40
 
41
- # ========================== # ByteTrack Implementation # ==========================
42
  class BYTETracker:
43
  def __init__(self, track_thresh=0.3, track_buffer=90, match_thresh=0.5, frame_rate=30):
44
  self.track_thresh = track_thresh
@@ -49,10 +49,9 @@ class BYTETracker:
49
  self.tracks = {}
50
  self.worker_history = {}
51
  self.last_positions = {}
52
- self.recently_removed = {} # Store recently removed tracks for re-identification
53
- self.appearance_features = {} # Store appearance features for better re-identification
54
- self.track_continuity = {} # Track temporal continuity
55
- self.similarity_threshold = 0.75 # Higher threshold for appearance similarity
56
 
57
  def update(self, dets, scores, cls):
58
  tracks = []
@@ -65,69 +64,49 @@ class BYTETracker:
65
  stale_ids.append(track_id)
66
 
67
  for track_id in stale_ids:
68
- # Store recently removed tracks for re-identification (for 1.5 seconds)
69
  self.recently_removed[track_id] = {
70
  'bbox': self.tracks[track_id]['bbox'],
71
  'last_seen': current_time,
72
  'last_position': self.last_positions.get(track_id, [0, 0]),
73
- 'appearance': self.appearance_features.get(track_id, None),
74
- 'cls': self.tracks[track_id].get('cls', None)
75
  }
76
  del self.tracks[track_id]
77
  if track_id in self.worker_history:
78
  del self.worker_history[track_id]
79
  if track_id in self.last_positions:
80
  del self.last_positions[track_id]
 
 
81
 
82
- # Clean up recently_removed tracks older than 1.5 seconds
83
  to_remove = []
84
  for track_id, info in self.recently_removed.items():
85
- if current_time - info['last_seen'] > 1.5:
86
  to_remove.append(track_id)
87
  for track_id in to_remove:
88
  del self.recently_removed[track_id]
89
-
90
- # Sort detections by score for high-confidence-first association
91
- detection_indices = np.argsort(-np.array(scores))
92
-
93
- assigned_tracks = set()
94
- matched_detections = set()
95
 
96
- for i in detection_indices:
97
- if i >= len(dets) or scores[i] < self.track_thresh:
98
  continue
99
-
100
- det, score, cl = dets[i], scores[i], cls[i]
101
  x, y, w, h = det
102
-
103
- # Skip if this detection was already matched
104
- if i in matched_detections:
105
- continue
106
-
107
  matched = False
108
  best_iou = 0
109
  best_track_id = None
 
 
 
110
 
111
  # Try to match with active tracks
112
  for track_id, track_info in self.tracks.items():
113
- # Skip if this track was already assigned in this frame
114
- if track_id in assigned_tracks:
115
- continue
116
-
117
  tx, ty, tw, th = track_info['bbox']
118
  iou = self._calculate_iou([x, y, w, h], [tx, ty, tw, th])
119
 
120
- # If similar class and good IOU, consider a match
121
- is_same_class = track_info.get('cls', None) == cl
122
- position_match = self._is_same_worker([x, y], self.last_positions.get(track_id, [0, 0]), threshold=120)
123
-
124
- # Combined matching score with class consistency
125
- match_score = iou
126
- if is_same_class:
127
- match_score += 0.2 # Bonus for same class
128
-
129
- if position_match and match_score > self.match_thresh and match_score > best_iou:
130
- best_iou = match_score
131
  best_track_id = track_id
132
  matched = True
133
 
@@ -139,33 +118,24 @@ class BYTETracker:
139
  'last_seen': current_time
140
  })
141
 
142
- # Update appearance feature with exponential moving average
143
- if best_track_id not in self.appearance_features:
144
- self.appearance_features[best_track_id] = np.array([x, y, w, h, cl])
145
- else:
146
- alpha = 0.7 # Weight for historical data
147
- current_feature = np.array([x, y, w, h, cl])
148
- self.appearance_features[best_track_id] = alpha * self.appearance_features[best_track_id] + (1-alpha) * current_feature
149
-
150
  if best_track_id not in self.worker_history:
151
  self.worker_history[best_track_id] = []
 
 
152
 
153
- # Update position history with trajectory smoothing
154
- if len(self.worker_history[best_track_id]) > 0:
155
- last_x, last_y = self.worker_history[best_track_id][-1]
156
- # Apply slight smoothing to reduce jitter
157
- smooth_x = 0.8 * x + 0.2 * last_x
158
- smooth_y = 0.8 * y + 0.2 * last_y
159
- self.worker_history[best_track_id].append([smooth_x, smooth_y])
160
  else:
161
- self.worker_history[best_track_id].append([x, y])
 
162
 
163
- self.last_positions[best_track_id] = [x, y]
 
 
 
164
 
165
- # Mark as assigned
166
- assigned_tracks.add(best_track_id)
167
- matched_detections.add(i)
168
-
169
  tracks.append({
170
  'id': best_track_id,
171
  'bbox': [x, y, w, h],
@@ -173,124 +143,87 @@ class BYTETracker:
173
  'cls': cl
174
  })
175
  else:
176
- # Try to re-identify with recently removed tracks
177
- reidentified = False
178
- for track_id, info in self.recently_removed.items():
179
- appearance_match = False
180
- if info['appearance'] is not None:
181
- appearance_similarity = self._compute_appearance_similarity(
182
- np.array([x, y, w, h, cl]),
183
- info['appearance']
184
- )
185
- appearance_match = appearance_similarity > self.similarity_threshold
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
- position_match = self._is_same_worker([x, y], info['last_position'], threshold=120)
 
 
 
188
 
189
- # Enhanced re-identification using both position and appearance
190
- if position_match or appearance_match:
191
- self.tracks[track_id] = {
192
- 'bbox': [x, y, w, h],
193
- 'score': score,
194
- 'cls': cl,
195
- 'last_seen': current_time
196
- }
197
-
198
- # Update appearance feature
199
- if track_id in self.appearance_features:
200
- alpha = 0.7 # Weight for historical data
201
- current_feature = np.array([x, y, w, h, cl])
202
- self.appearance_features[track_id] = alpha * self.appearance_features[track_id] + (1-alpha) * current_feature
203
- else:
204
- self.appearance_features[track_id] = np.array([x, y, w, h, cl])
205
-
206
- if track_id not in self.worker_history:
207
- self.worker_history[track_id] = []
208
- self.worker_history[track_id].append([x, y])
209
- self.last_positions[track_id] = [x, y]
210
-
211
- # Mark as assigned
212
- assigned_tracks.add(track_id)
213
- matched_detections.add(i)
214
-
215
- tracks.append({
216
- 'id': track_id,
217
- 'bbox': [x, y, w, h],
218
- 'score': score,
219
- 'cls': cl
220
- })
221
- reidentified = True
222
- del self.recently_removed[track_id]
223
- break
224
-
225
- if not reidentified:
226
- # Check if it matches an existing worker by position
227
- same_worker = False
228
- for worker_id, last_pos in self.last_positions.items():
229
- # Skip if this track was already assigned in this frame
230
- if worker_id in assigned_tracks:
231
- continue
232
-
233
- if self._is_same_worker([x, y], last_pos, threshold=120):
234
- self.tracks[worker_id] = {
235
- 'bbox': [x, y, w, h],
236
- 'score': score,
237
- 'cls': cl,
238
- 'last_seen': current_time
239
- }
240
-
241
- # Update appearance feature
242
- if worker_id in self.appearance_features:
243
- alpha = 0.7 # Weight for historical data
244
- current_feature = np.array([x, y, w, h, cl])
245
- self.appearance_features[worker_id] = alpha * self.appearance_features[worker_id] + (1-alpha) * current_feature
246
- else:
247
- self.appearance_features[worker_id] = np.array([x, y, w, h, cl])
248
-
249
- # Mark as assigned
250
- assigned_tracks.add(worker_id)
251
- matched_detections.add(i)
252
-
253
- tracks.append({
254
- 'id': worker_id,
255
- 'bbox': [x, y, w, h],
256
- 'score': score,
257
- 'cls': cl
258
- })
259
- same_worker = True
260
- break
261
-
262
- if not same_worker:
263
- # Create new track only if it doesn't overlap significantly with existing tracks
264
- should_create_new = True
265
- for track_id in self.tracks:
266
- tx, ty, tw, th = self.tracks[track_id]['bbox']
267
- overlap = self._calculate_iou([x, y, w, h], [tx, ty, tw, th])
268
- if overlap > 0.1: # If significant overlap, don't create new track
269
- should_create_new = False
270
- break
271
-
272
- if should_create_new:
273
- self.tracks[self.next_id] = {
274
- 'bbox': [x, y, w, h],
275
- 'score': score,
276
- 'cls': cl,
277
- 'last_seen': current_time
278
- }
279
- self.appearance_features[self.next_id] = np.array([x, y, w, h, cl])
280
- self.worker_history[self.next_id] = [[x, y]]
281
- self.last_positions[self.next_id] = [x, y]
282
-
283
- # Mark as assigned
284
- assigned_tracks.add(self.next_id)
285
- matched_detections.add(i)
286
-
287
- tracks.append({
288
- 'id': self.next_id,
289
- 'bbox': [x, y, w, h],
290
- 'score': score,
291
- 'cls': cl
292
- })
293
- self.next_id += 1
294
 
295
  return tracks
296
 
@@ -309,32 +242,13 @@ class BYTETracker:
309
  iou = intersection_area / (box1_area + box2_area - intersection_area)
310
  return iou
311
 
312
- def _is_same_worker(self, pos1, pos2, threshold=120):
313
  x1, y1 = pos1
314
  x2, y2 = pos2
315
- distance = np.sqrt((x1 - x2)**2 + (y1 - y2)**2)
316
- return distance < threshold
317
-
318
- def _compute_appearance_similarity(self, feature1, feature2):
319
- # Compute normalized cosine similarity between appearance features
320
- # We weight position/size and class differently
321
- pos_size1 = feature1[:4]
322
- pos_size2 = feature2[:4]
323
-
324
- # Normalize to unit vectors
325
- pos_size1_norm = np.linalg.norm(pos_size1)
326
- pos_size2_norm = np.linalg.norm(pos_size2)
327
-
328
- if pos_size1_norm == 0 or pos_size2_norm == 0:
329
- pos_similarity = 0
330
- else:
331
- pos_similarity = np.dot(pos_size1, pos_size2) / (pos_size1_norm * pos_size2_norm)
332
-
333
- # Class similarity (1 if same, 0 if different)
334
- class_similarity = 1.0 if feature1[4] == feature2[4] else 0.0
335
-
336
- # Combined similarity (weighted more toward position)
337
- return 0.7 * pos_similarity + 0.3 * class_similarity
338
 
339
  # ========================== # Optimized Configuration # ==========================
340
  CONFIG = {
@@ -377,18 +291,17 @@ CONFIG = {
377
  },
378
  "MIN_VIOLATION_FRAMES": 1,
379
  "VIOLATION_COOLDOWN": 30.0,
380
- "WORKER_TRACKING_DURATION": 5.0,
381
  "MAX_PROCESSING_TIME": 60,
382
- "FRAME_SKIP": 2, # Skip more frames for faster processing
383
- "BATCH_SIZE": 8, # Increased batch size for better throughput
384
  "PARALLEL_WORKERS": max(1, cpu_count() - 1),
385
- "TRACK_BUFFER": 90, # 3.0 seconds at 30 fps
386
  "TRACK_THRESH": 0.3,
387
  "MATCH_THRESH": 0.5,
388
  "SNAPSHOT_QUALITY": 95,
389
- "MAX_WORKER_DISTANCE": 120,
390
- "TARGET_RESOLUTION": (384, 384), # Smaller resolution for faster processing
391
- "MAX_WORKERS": 5 # Maximum number of unique workers to track
392
  }
393
 
394
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
@@ -419,12 +332,9 @@ model = load_model()
419
 
420
  # ========================== # Helper Functions # ==========================
421
  def preprocess_frame(frame):
422
- # Faster preprocessing with simpler operations
423
  target_res = CONFIG["TARGET_RESOLUTION"]
424
- if frame.shape[0] != target_res[1] or frame.shape[1] != target_res[0]:
425
- frame = cv2.resize(frame, target_res, interpolation=cv2.INTER_AREA)
426
- # Simple contrast enhancement
427
- frame = cv2.convertScaleAbs(frame, alpha=1.2, beta=10)
428
  return frame
429
 
430
  def draw_detections(frame, detections):
@@ -674,49 +584,6 @@ def verify_and_open_video(video_path):
674
 
675
  return cap
676
 
677
- def process_frames_batch(batch_data, model_path, device_type):
678
- try:
679
- batch_frames, batch_indices = batch_data
680
-
681
- # Load model in this process
682
- local_model = YOLO(model_path)
683
- if device_type == "cuda":
684
- local_model = local_model.to("cuda")
685
- local_model.model.half()
686
-
687
- # Process batch
688
- batch_frames_np = np.array(batch_frames)
689
- batch_frames_tensor = torch.from_numpy(batch_frames_np).permute(0, 3, 1, 2).float() / 255.0
690
-
691
- if device_type == "cuda":
692
- batch_frames_tensor = batch_frames_tensor.to("cuda").half()
693
-
694
- results = local_model(batch_frames_tensor, conf=0.1, verbose=False)
695
-
696
- # Format results
697
- processed_results = []
698
- for i, (result, frame_idx) in enumerate(zip(results, batch_indices)):
699
- boxes = result.boxes
700
- detections = []
701
- for box in boxes:
702
- cls = int(box.cls)
703
- conf = float(box.conf)
704
- bbox = box.xywh.cpu().numpy()[0]
705
- detections.append({
706
- "cls": cls,
707
- "conf": conf,
708
- "bbox": bbox
709
- })
710
- processed_results.append((frame_idx, detections))
711
-
712
- if device_type == "cuda":
713
- torch.cuda.empty_cache()
714
-
715
- return processed_results
716
- except Exception as e:
717
- logger.error(f"Error in process_frames_batch: {e}")
718
- return []
719
-
720
  def process_video(video_data, temp_dir):
721
  video_path = None
722
  output_dir = os.path.join(temp_dir, "output")
@@ -764,41 +631,44 @@ def process_video(video_data, temp_dir):
764
  frame_rate=fps
765
  )
766
 
767
- # Force single worker for all violations (fixes the issue mentioned by the user)
768
- worker_id_mapping = {}
769
- next_worker_id = 1
770
-
771
  unique_violations = {}
772
  violation_frames = {}
773
- worker_violation_count = {} # Track violation count per worker
774
  start_time = time.time()
775
  frame_skip = CONFIG["FRAME_SKIP"]
776
  processed_frames = 0
777
  last_yield_time = start_time
778
-
779
- # Process frames faster with optimized batching
 
 
 
 
 
780
  while processed_frames < total_frames:
 
781
  batch_frames = []
782
  batch_indices = []
783
-
784
- # Create batch
785
- for _ in range(CONFIG["BATCH_SIZE"]):
786
  frame_idx = int(cap.get(cv2.CAP_PROP_POS_FRAMES))
787
  if frame_idx >= total_frames:
788
  break
789
-
790
  ret, frame = cap.read()
791
  if not ret:
792
  logger.warning(f"Failed to read frame {frame_idx}. Skipping.")
793
  break
794
-
 
795
  frame = preprocess_frame(frame)
796
-
797
- # Skip frames to speed up processing
798
  for _ in range(frame_skip - 1):
799
  if not cap.grab():
800
  break
801
-
802
  batch_frames.append(frame)
803
  batch_indices.append(frame_idx)
804
  processed_frames += 1
@@ -807,47 +677,50 @@ def process_video(video_data, temp_dir):
807
  logger.info("No more frames to process.")
808
  break
809
 
 
 
 
 
 
 
 
 
 
810
  try:
811
- # Fast batch processing using GPU
812
  batch_frames_np = np.array(batch_frames)
813
  batch_frames_tensor = torch.from_numpy(batch_frames_np).permute(0, 3, 1, 2).float() / 255.0
814
  batch_frames_tensor = batch_frames_tensor.to(device)
815
  if device.type == "cuda":
816
  batch_frames_tensor = batch_frames_tensor.half()
817
 
 
818
  results = model(batch_frames_tensor, device=device, conf=0.1, verbose=False)
819
  except Exception as e:
820
  logger.error(f"Model inference failed: {e}")
821
  raise ValueError(f"Failed to process video frames with YOLO model: {str(e)}")
822
  finally:
 
823
  batch_frames = []
824
  if device.type == "cuda":
825
  torch.cuda.empty_cache()
826
 
827
- # Update progress
828
- current_time = time.time()
829
- if current_time - last_yield_time > 0.1:
830
- progress = (processed_frames / total_frames) * 100
831
- elapsed_time = current_time - start_time
832
- fps_processed = processed_frames / elapsed_time if elapsed_time > 0 else 0
833
- yield f"Processing video... {progress:.1f}% complete (Frame {processed_frames}/{total_frames}, {fps_processed:.1f} FPS)", "", "", "", ""
834
- last_yield_time = current_time
835
-
836
- # Process results and update tracker
837
  for i, (result, frame_idx) in enumerate(zip(results, batch_indices)):
838
  current_time = frame_idx / fps
839
-
840
  boxes = result.boxes
841
  track_inputs = []
842
-
 
843
  for box in boxes:
844
  cls = int(box.cls)
845
  conf = float(box.conf)
846
  label = CONFIG["VIOLATION_LABELS"].get(cls, None)
847
-
848
  if label is None:
849
  continue
850
-
851
  if conf < CONFIG["CONFIDENCE_THRESHOLDS"].get(label, 0.25):
852
  continue
853
 
@@ -860,35 +733,28 @@ def process_video(video_data, temp_dir):
860
 
861
  if not track_inputs:
862
  continue
863
-
 
864
  tracked_objects = tracker.update(
865
  np.array([t["bbox"] for t in track_inputs]),
866
  np.array([t["conf"] for t in track_inputs]),
867
  np.array([t["cls"] for t in track_inputs])
868
  )
869
 
870
- # Apply the fix: force all detections to be from worker 1
871
  for obj in tracked_objects:
872
  tracker_id = obj['id']
873
-
874
- # Map all tracker IDs to worker ID 1 (fixes the multi-worker issue)
875
- if tracker_id not in worker_id_mapping:
876
- # In a real environment with multiple workers, use the next line instead
877
- # worker_id_mapping[tracker_id] = next_worker_id
878
- # next_worker_id += 1
879
-
880
- # For this specific case, always use worker ID 1
881
- worker_id_mapping[tracker_id] = 1
882
-
883
  label = CONFIG["VIOLATION_LABELS"].get(int(obj['cls']), None)
884
  conf = obj['score']
 
885
 
886
  if label is None:
887
  continue
888
-
889
- worker_id = worker_id_mapping[tracker_id]
890
  violation_key = (worker_id, label)
891
-
 
892
  if violation_key not in unique_violations:
893
  unique_violations[violation_key] = current_time
894
  violation_frames[violation_key] = frame_idx
@@ -901,13 +767,50 @@ def process_video(video_data, temp_dir):
901
  cap.release()
902
  processing_time = time.time() - start_time
903
  logger.info(f"Processing complete in {processing_time:.2f}s")
904
- logger.info(f"Total unique workers detected: {len(set(worker_id_mapping.values()))}")
905
  logger.info(f"Violations per worker: {worker_violation_count}")
906
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  violations = []
908
  for (worker_id, label), detection_time in unique_violations.items():
 
909
  violations.append({
910
- "worker_id": worker_id,
911
  "violation": label,
912
  "timestamp": detection_time,
913
  "confidence": 0.0,
@@ -919,7 +822,7 @@ def process_video(video_data, temp_dir):
919
  yield "No violations detected in the video.", "Safety Score: 100%", "No snapshots captured.", "N/A", "N/A"
920
  return
921
 
922
- # Capture snapshots efficiently
923
  snapshots = []
924
  cap = cv2.VideoCapture(video_path)
925
  for violation in violations:
 
38
 
39
  FFMPEG_AVAILABLE = check_ffmpeg()
40
 
41
+ # ========================== # Improved ByteTrack Implementation # ==========================
42
  class BYTETracker:
43
  def __init__(self, track_thresh=0.3, track_buffer=90, match_thresh=0.5, frame_rate=30):
44
  self.track_thresh = track_thresh
 
49
  self.tracks = {}
50
  self.worker_history = {}
51
  self.last_positions = {}
52
+ self.recently_removed = {}
53
+ self.worker_centroids = {} # Store average positions for each worker
54
+ self.violation_types = {} # Track violation types per worker
 
55
 
56
  def update(self, dets, scores, cls):
57
  tracks = []
 
64
  stale_ids.append(track_id)
65
 
66
  for track_id in stale_ids:
67
+ # Store recently removed tracks for re-identification (for 1 second)
68
  self.recently_removed[track_id] = {
69
  'bbox': self.tracks[track_id]['bbox'],
70
  'last_seen': current_time,
71
  'last_position': self.last_positions.get(track_id, [0, 0]),
72
+ 'violation_types': self.violation_types.get(track_id, set())
 
73
  }
74
  del self.tracks[track_id]
75
  if track_id in self.worker_history:
76
  del self.worker_history[track_id]
77
  if track_id in self.last_positions:
78
  del self.last_positions[track_id]
79
+ # Keep the centroid and violation types for re-identification
80
+ # Don't delete from self.worker_centroids or self.violation_types
81
 
82
+ # Clean up recently_removed tracks older than 1 second
83
  to_remove = []
84
  for track_id, info in self.recently_removed.items():
85
+ if current_time - info['last_seen'] > 1.0:
86
  to_remove.append(track_id)
87
  for track_id in to_remove:
88
  del self.recently_removed[track_id]
 
 
 
 
 
 
89
 
90
+ for i, (det, score, cl) in enumerate(zip(dets, scores, cls)):
91
+ if score < self.track_thresh:
92
  continue
93
+
 
94
  x, y, w, h = det
 
 
 
 
 
95
  matched = False
96
  best_iou = 0
97
  best_track_id = None
98
+
99
+ # Get current violation type
100
+ violation_type = CONFIG["VIOLATION_LABELS"].get(int(cl), "unknown")
101
 
102
  # Try to match with active tracks
103
  for track_id, track_info in self.tracks.items():
 
 
 
 
104
  tx, ty, tw, th = track_info['bbox']
105
  iou = self._calculate_iou([x, y, w, h], [tx, ty, tw, th])
106
 
107
+ # Check if this is the same worker based on position and size
108
+ if iou > self.match_thresh and iou > best_iou:
109
+ best_iou = iou
 
 
 
 
 
 
 
 
110
  best_track_id = track_id
111
  matched = True
112
 
 
118
  'last_seen': current_time
119
  })
120
 
121
+ # Update position history
 
 
 
 
 
 
 
122
  if best_track_id not in self.worker_history:
123
  self.worker_history[best_track_id] = []
124
+ self.worker_history[best_track_id].append([x, y])
125
+ self.last_positions[best_track_id] = [x, y]
126
 
127
+ # Update worker centroid with exponential moving average
128
+ if best_track_id not in self.worker_centroids:
129
+ self.worker_centroids[best_track_id] = [x, y]
 
 
 
 
130
  else:
131
+ self.worker_centroids[best_track_id][0] = 0.7 * self.worker_centroids[best_track_id][0] + 0.3 * x
132
+ self.worker_centroids[best_track_id][1] = 0.7 * self.worker_centroids[best_track_id][1] + 0.3 * y
133
 
134
+ # Update violation types for this worker
135
+ if best_track_id not in self.violation_types:
136
+ self.violation_types[best_track_id] = set()
137
+ self.violation_types[best_track_id].add(violation_type)
138
 
 
 
 
 
139
  tracks.append({
140
  'id': best_track_id,
141
  'bbox': [x, y, w, h],
 
143
  'cls': cl
144
  })
145
  else:
146
+ # Try to match with any known worker based on position
147
+ matched_worker = False
148
+ best_distance = float('inf')
149
+ best_worker_id = None
150
+
151
+ # First check active tracks
152
+ for worker_id, centroid in self.worker_centroids.items():
153
+ if worker_id in self.tracks: # Only consider active tracks
154
+ distance = self._calculate_distance([x, y], centroid)
155
+ if distance < CONFIG["MAX_WORKER_DISTANCE"] and distance < best_distance:
156
+ best_distance = distance
157
+ best_worker_id = worker_id
158
+ matched_worker = True
159
+
160
+ # If no match in active tracks, try recently removed tracks
161
+ if not matched_worker:
162
+ for track_id, info in self.recently_removed.items():
163
+ if track_id in self.worker_centroids:
164
+ distance = self._calculate_distance([x, y], self.worker_centroids[track_id])
165
+ if distance < CONFIG["MAX_WORKER_DISTANCE"] and distance < best_distance:
166
+ best_distance = distance
167
+ best_worker_id = track_id
168
+ matched_worker = True
169
+
170
+ if matched_worker:
171
+ # Reuse the existing worker ID
172
+ self.tracks[best_worker_id] = {
173
+ 'bbox': [x, y, w, h],
174
+ 'score': score,
175
+ 'cls': cl,
176
+ 'last_seen': current_time
177
+ }
178
 
179
+ if best_worker_id not in self.worker_history:
180
+ self.worker_history[best_worker_id] = []
181
+ self.worker_history[best_worker_id].append([x, y])
182
+ self.last_positions[best_worker_id] = [x, y]
183
 
184
+ # Update centroid
185
+ if best_worker_id not in self.worker_centroids:
186
+ self.worker_centroids[best_worker_id] = [x, y]
187
+ else:
188
+ self.worker_centroids[best_worker_id][0] = 0.7 * self.worker_centroids[best_worker_id][0] + 0.3 * x
189
+ self.worker_centroids[best_worker_id][1] = 0.7 * self.worker_centroids[best_worker_id][1] + 0.3 * y
190
+
191
+ # Update violation types
192
+ if best_worker_id not in self.violation_types:
193
+ self.violation_types[best_worker_id] = set()
194
+ self.violation_types[best_worker_id].add(violation_type)
195
+
196
+ # If it was in recently_removed, remove it from there
197
+ if best_worker_id in self.recently_removed:
198
+ del self.recently_removed[best_worker_id]
199
+
200
+ tracks.append({
201
+ 'id': best_worker_id,
202
+ 'bbox': [x, y, w, h],
203
+ 'score': score,
204
+ 'cls': cl
205
+ })
206
+ else:
207
+ # Create a new worker ID
208
+ new_id = self.next_id
209
+ self.tracks[new_id] = {
210
+ 'bbox': [x, y, w, h],
211
+ 'score': score,
212
+ 'cls': cl,
213
+ 'last_seen': current_time
214
+ }
215
+ self.worker_history[new_id] = [[x, y]]
216
+ self.last_positions[new_id] = [x, y]
217
+ self.worker_centroids[new_id] = [x, y]
218
+ self.violation_types[new_id] = {violation_type}
219
+
220
+ tracks.append({
221
+ 'id': new_id,
222
+ 'bbox': [x, y, w, h],
223
+ 'score': score,
224
+ 'cls': cl
225
+ })
226
+ self.next_id += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
  return tracks
229
 
 
242
  iou = intersection_area / (box1_area + box2_area - intersection_area)
243
  return iou
244
 
245
+ def _calculate_distance(self, pos1, pos2):
246
  x1, y1 = pos1
247
  x2, y2 = pos2
248
+ return np.sqrt((x1 - x2)**2 + (y1 - y2)**2)
249
+
250
+ def _is_same_worker(self, pos1, pos2, threshold=150):
251
+ return self._calculate_distance(pos1, pos2) < threshold
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
  # ========================== # Optimized Configuration # ==========================
254
  CONFIG = {
 
291
  },
292
  "MIN_VIOLATION_FRAMES": 1,
293
  "VIOLATION_COOLDOWN": 30.0,
294
+ "WORKER_TRACKING_DURATION": 10.0,
295
  "MAX_PROCESSING_TIME": 60,
296
+ "FRAME_SKIP": 2, # Increased to improve performance
297
+ "BATCH_SIZE": 8, # Increased for better throughput
298
  "PARALLEL_WORKERS": max(1, cpu_count() - 1),
299
+ "TRACK_BUFFER": 150,
300
  "TRACK_THRESH": 0.3,
301
  "MATCH_THRESH": 0.5,
302
  "SNAPSHOT_QUALITY": 95,
303
+ "MAX_WORKER_DISTANCE": 150,
304
+ "TARGET_RESOLUTION": (384, 384)
 
305
  }
306
 
307
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
332
 
333
  # ========================== # Helper Functions # ==========================
334
  def preprocess_frame(frame):
 
335
  target_res = CONFIG["TARGET_RESOLUTION"]
336
+ frame = cv2.resize(frame, target_res, interpolation=cv2.INTER_LINEAR)
337
+ frame = cv2.convertScaleAbs(frame, alpha=1.2, beta=20)
 
 
338
  return frame
339
 
340
  def draw_detections(frame, detections):
 
584
 
585
  return cap
586
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
587
  def process_video(video_data, temp_dir):
588
  video_path = None
589
  output_dir = os.path.join(temp_dir, "output")
 
631
  frame_rate=fps
632
  )
633
 
 
 
 
 
634
  unique_violations = {}
635
  violation_frames = {}
636
+ worker_violation_count = {}
637
  start_time = time.time()
638
  frame_skip = CONFIG["FRAME_SKIP"]
639
  processed_frames = 0
640
  last_yield_time = start_time
641
+
642
+ # Pre-allocate memory for batch processing
643
+ batch_size = CONFIG["BATCH_SIZE"]
644
+ batch_frames = []
645
+ batch_indices = []
646
+
647
+ # Process frames in batches for better performance
648
  while processed_frames < total_frames:
649
+ # Clear previous batch
650
  batch_frames = []
651
  batch_indices = []
652
+
653
+ # Fill the batch
654
+ for _ in range(batch_size):
655
  frame_idx = int(cap.get(cv2.CAP_PROP_POS_FRAMES))
656
  if frame_idx >= total_frames:
657
  break
658
+
659
  ret, frame = cap.read()
660
  if not ret:
661
  logger.warning(f"Failed to read frame {frame_idx}. Skipping.")
662
  break
663
+
664
+ # Preprocess frame (resize and enhance)
665
  frame = preprocess_frame(frame)
666
+
667
+ # Skip frames for performance
668
  for _ in range(frame_skip - 1):
669
  if not cap.grab():
670
  break
671
+
672
  batch_frames.append(frame)
673
  batch_indices.append(frame_idx)
674
  processed_frames += 1
 
677
  logger.info("No more frames to process.")
678
  break
679
 
680
+ # Update progress
681
+ current_time = time.time()
682
+ if current_time - last_yield_time > 0.1:
683
+ progress = (processed_frames / total_frames) * 100
684
+ elapsed_time = current_time - start_time
685
+ fps_processed = processed_frames / elapsed_time if elapsed_time > 0 else 0
686
+ yield f"Processing video... {progress:.1f}% complete (Frame {processed_frames}/{total_frames}, {fps_processed:.1f} FPS)", "", "", "", ""
687
+ last_yield_time = current_time
688
+
689
  try:
690
+ # Convert batch to tensor for efficient processing
691
  batch_frames_np = np.array(batch_frames)
692
  batch_frames_tensor = torch.from_numpy(batch_frames_np).permute(0, 3, 1, 2).float() / 255.0
693
  batch_frames_tensor = batch_frames_tensor.to(device)
694
  if device.type == "cuda":
695
  batch_frames_tensor = batch_frames_tensor.half()
696
 
697
+ # Run inference on batch
698
  results = model(batch_frames_tensor, device=device, conf=0.1, verbose=False)
699
  except Exception as e:
700
  logger.error(f"Model inference failed: {e}")
701
  raise ValueError(f"Failed to process video frames with YOLO model: {str(e)}")
702
  finally:
703
+ # Clear memory
704
  batch_frames = []
705
  if device.type == "cuda":
706
  torch.cuda.empty_cache()
707
 
708
+ # Process results for each frame in the batch
 
 
 
 
 
 
 
 
 
709
  for i, (result, frame_idx) in enumerate(zip(results, batch_indices)):
710
  current_time = frame_idx / fps
711
+
712
  boxes = result.boxes
713
  track_inputs = []
714
+
715
+ # Prepare detection inputs for tracker
716
  for box in boxes:
717
  cls = int(box.cls)
718
  conf = float(box.conf)
719
  label = CONFIG["VIOLATION_LABELS"].get(cls, None)
720
+
721
  if label is None:
722
  continue
723
+
724
  if conf < CONFIG["CONFIDENCE_THRESHOLDS"].get(label, 0.25):
725
  continue
726
 
 
733
 
734
  if not track_inputs:
735
  continue
736
+
737
+ # Update tracker with new detections
738
  tracked_objects = tracker.update(
739
  np.array([t["bbox"] for t in track_inputs]),
740
  np.array([t["conf"] for t in track_inputs]),
741
  np.array([t["cls"] for t in track_inputs])
742
  )
743
 
744
+ # Process tracked objects
745
  for obj in tracked_objects:
746
  tracker_id = obj['id']
 
 
 
 
 
 
 
 
 
 
747
  label = CONFIG["VIOLATION_LABELS"].get(int(obj['cls']), None)
748
  conf = obj['score']
749
+ bbox = obj['bbox']
750
 
751
  if label is None:
752
  continue
753
+
754
+ worker_id = tracker_id
755
  violation_key = (worker_id, label)
756
+
757
+ # Record unique violations
758
  if violation_key not in unique_violations:
759
  unique_violations[violation_key] = current_time
760
  violation_frames[violation_key] = frame_idx
 
767
  cap.release()
768
  processing_time = time.time() - start_time
769
  logger.info(f"Processing complete in {processing_time:.2f}s")
770
+ logger.info(f"Total unique workers detected: {len(tracker.worker_centroids)}")
771
  logger.info(f"Violations per worker: {worker_violation_count}")
772
 
773
+ # Consolidate workers based on spatial proximity
774
+ consolidated_workers = {}
775
+ processed_workers = set()
776
+
777
+ # Sort worker IDs to ensure deterministic consolidation
778
+ worker_ids = sorted(tracker.worker_centroids.keys())
779
+
780
+ for i, worker_id in enumerate(worker_ids):
781
+ if worker_id in processed_workers:
782
+ continue
783
+
784
+ processed_workers.add(worker_id)
785
+ consolidated_workers[worker_id] = [worker_id]
786
+
787
+ for j, other_id in enumerate(worker_ids):
788
+ if i == j or other_id in processed_workers:
789
+ continue
790
+
791
+ # Check if workers are close enough to be considered the same person
792
+ if worker_id in tracker.worker_centroids and other_id in tracker.worker_centroids:
793
+ distance = tracker._calculate_distance(
794
+ tracker.worker_centroids[worker_id],
795
+ tracker.worker_centroids[other_id]
796
+ )
797
+
798
+ if distance < CONFIG["MAX_WORKER_DISTANCE"] * 0.8: # More strict for consolidation
799
+ consolidated_workers[worker_id].append(other_id)
800
+ processed_workers.add(other_id)
801
+
802
+ # Create a mapping from old worker IDs to new consolidated IDs
803
+ worker_id_mapping = {}
804
+ for new_id, old_ids in enumerate(consolidated_workers.values(), 1):
805
+ for old_id in old_ids:
806
+ worker_id_mapping[old_id] = new_id
807
+
808
+ # Update violations with consolidated worker IDs
809
  violations = []
810
  for (worker_id, label), detection_time in unique_violations.items():
811
+ new_worker_id = worker_id_mapping.get(worker_id, worker_id)
812
  violations.append({
813
+ "worker_id": new_worker_id,
814
  "violation": label,
815
  "timestamp": detection_time,
816
  "confidence": 0.0,
 
822
  yield "No violations detected in the video.", "Safety Score: 100%", "No snapshots captured.", "N/A", "N/A"
823
  return
824
 
825
+ # Generate snapshots for each violation
826
  snapshots = []
827
  cap = cv2.VideoCapture(video_path)
828
  for violation in violations: