matrice-analytics 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of matrice-analytics might be problematic. Click here for more details.

@@ -78,11 +78,12 @@ class ColorDetectionConfig(BaseConfig):
78
78
  alert_config: Optional[AlertConfig] = None
79
79
  time_window_minutes: int = 60
80
80
  enable_unique_counting: bool = True
81
- enable_smoothing: bool = True
81
+ enable_smoothing: bool = False
82
82
  smoothing_algorithm: str = "observability"
83
83
  smoothing_window_size: int = 20
84
84
  smoothing_cooldown_frames: int = 5
85
85
  smoothing_confidence_range_factor: float = 0.5
86
+ detector = True
86
87
 
87
88
  #JBK_720_GATE POLYGON = [[86, 328], [844, 317], [1277, 520], [1273, 707], [125, 713]]
88
89
  zone_config: Optional[Dict[str, List[List[float]]]] = field(
@@ -92,7 +93,7 @@ class ColorDetectionConfig(BaseConfig):
92
93
  }
93
94
  }
94
95
  )
95
- true_import: bool = False
96
+ # true_import: bool = False
96
97
 
97
98
  def validate(self) -> List[str]:
98
99
  errors = super().validate()
@@ -112,6 +113,16 @@ class ColorDetectionConfig(BaseConfig):
112
113
  errors.append("smoothing_confidence_range_factor must be positive")
113
114
  return errors
114
115
 
116
+ def __post_init__(self):
117
+ # Lazy initialization: the ClipProcessor will be created once by the use case
118
+ # to avoid repeated model downloads and to ensure GPU session reuse.
119
+ if self.detector:
120
+ self.detector = ClipProcessor()
121
+ print("ClipProcessor Loaded Successfully!!")
122
+ else:
123
+ print("Clip color detector disabled by config")
124
+ self.detector = None
125
+
115
126
 
116
127
 
117
128
  class ColorDetectionUseCase(BaseProcessor):
@@ -124,32 +135,32 @@ class ColorDetectionUseCase(BaseProcessor):
124
135
  "three wheelers -CNG-": "Three Wheelers (CNG)", "human hauler": "Human Hauler",
125
136
  "van": "Van", "wheelbarrow": "Wheelbarrow"
126
137
  }
127
-
138
+
128
139
  def __init__(self):
129
140
  super().__init__("color_detection")
130
141
  self.category = "visual_appearance"
131
-
142
+
132
143
  self.target_categories = ["car", "bicycle", "bus", "motorcycle"]
133
-
144
+
134
145
  self.CASE_TYPE: Optional[str] = 'color_detection'
135
146
  self.CASE_VERSION: Optional[str] = '1.3'
136
-
147
+
137
148
  self.tracker = None # AdvancedTracker instance
138
149
  self.smoothing_tracker = None # BBoxSmoothingTracker instance
139
150
  self._total_frame_counter = 0 # Total frames processed
140
151
  self._global_frame_offset = 0 # Frame offset for new sessions
141
152
  self._color_total_track_ids = defaultdict(set) # Cumulative track IDs per category-color
142
153
  self._color_current_frame_track_ids = defaultdict(set) # Per-frame track IDs per category-color
143
-
154
+
144
155
  self._tracking_start_time = None
145
-
156
+
146
157
  self._track_aliases: Dict[Any, Any] = {}
147
158
  self._canonical_tracks: Dict[Any, Dict[str, Any]] = {}
148
159
  # Tunable parameters – adjust if necessary for specific scenarios
149
160
  self._track_merge_iou_threshold: float = 0.05 # IoU ≥ 0.05 →
150
161
  self._track_merge_time_window: float = 7.0 # seconds within which to merge
151
162
 
152
- self._ascending_alert_list: List[int] = []
163
+ self._ascending_alert_list: List[int] = []
153
164
  self.current_incident_end_timestamp: str = "N/A"
154
165
  self.color_det_dict = {}
155
166
  self.start_timer = None
@@ -158,12 +169,17 @@ class ColorDetectionUseCase(BaseProcessor):
158
169
  self._zone_total_track_ids = {} # zone_name -> set of all track IDs that have been in zone
159
170
  self._zone_current_counts = {} # zone_name -> current count in zone
160
171
  self._zone_total_counts = {} # zone_name -> total count that have been in zone
161
- self.logger.info("Initialized ColorDetectionUseCase with zone tracking")
162
- self.detector = None #ClipProcessor()
172
+ self.logger.info("Initialized ColorDetectionUseCase with tracking")
173
+ #self.detector = None
163
174
  self.all_color_data = {}
164
175
  self.all_color_counts = {}
176
+ self.total_category_count = {}
177
+ self.category_color = {}
178
+ self.vehicle_tracks = {}
179
+ self.vehicle_stats = defaultdict(lambda: defaultdict(int))
180
+ self.zone_vehicle_stats = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
165
181
  #self.jpeg = TurboJPEG()
166
-
182
+ # data, config, ProcessingContext(), stream_info,input_bytes
167
183
  def process(
168
184
  self,
169
185
  data: Any,
@@ -173,8 +189,10 @@ class ColorDetectionUseCase(BaseProcessor):
173
189
  stream_info: Optional[Dict[str, Any]] = None
174
190
  ) -> ProcessingResult:
175
191
  processing_start = time.time()
176
-
192
+
177
193
  try:
194
+ cwd = os.getcwd()
195
+ print("Current working directory:", cwd)
178
196
  if not isinstance(config, ColorDetectionConfig):
179
197
  return self.create_error_result(
180
198
  "Invalid configuration type for color detection",
@@ -182,17 +200,17 @@ class ColorDetectionUseCase(BaseProcessor):
182
200
  category=self.category,
183
201
  context=context
184
202
  )
185
-
186
- if config.true_import and self.detector is None:
187
- self.detector = ClipProcessor()
188
- self.logger.info("Initialized ClipProcessor for color detection")
203
+
204
+ # if config.true_import and self.detector is None:
205
+ # self.detector = ClipProcessor()
206
+ # self.logger.info("Initialized ClipProcessor for color detection")
189
207
 
190
208
  if context is None:
191
209
  context = ProcessingContext()
192
-
210
+
193
211
  if not input_bytes:
194
212
  print("input_bytes is required for color detection")
195
-
213
+
196
214
  if not data:
197
215
  #print("data",data)
198
216
  print("Detection data is required for color detection")
@@ -201,23 +219,17 @@ class ColorDetectionUseCase(BaseProcessor):
201
219
  context.input_format = input_format
202
220
  context.confidence_threshold = config.confidence_threshold
203
221
 
204
-
205
222
  self.logger.info(f"Processing color detection with format: {input_format.value}")
206
-
223
+
207
224
  # Step 1: Apply confidence filtering
208
225
  processed_data = filter_by_confidence(data, config.confidence_threshold)
209
- #self.logger.debug(f"Applied confidence filtering with threshold {config.confidence_threshold}")
210
-
226
+
211
227
  # Step 2: Apply category mapping if provided
212
228
  if config.index_to_category:
213
- processed_data = apply_category_mapping(processed_data, config.index_to_category)
214
- #self.logger.debug("Applied category mapping")
229
+ color_processed_data = apply_category_mapping(processed_data, config.index_to_category)
215
230
 
216
- if config.target_categories:
217
- color_processed_data = [d for d in processed_data if d.get('category') in self.target_categories]
218
- self.logger.debug("Applied category filtering")
231
+ color_processed_data = [d for d in color_processed_data if d['category'] in self.target_categories]
219
232
 
220
-
221
233
  raw_processed_data = [copy.deepcopy(det) for det in color_processed_data]
222
234
  # Step 3: Apply bounding box smoothing if enabled
223
235
  if config.enable_smoothing:
@@ -232,26 +244,26 @@ class ColorDetectionUseCase(BaseProcessor):
232
244
  )
233
245
  self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
234
246
  color_processed_data = bbox_smoothing(color_processed_data, self.smoothing_tracker.config, self.smoothing_tracker)
235
-
247
+
236
248
  # Step 4: Apply advanced tracking
237
249
  try:
238
250
  from ..advanced_tracker import AdvancedTracker
239
251
  from ..advanced_tracker.config import TrackerConfig
240
-
252
+
241
253
  if self.tracker is None:
242
254
  tracker_config = TrackerConfig()
243
255
  self.tracker = AdvancedTracker(tracker_config)
244
256
  self.logger.info("Initialized AdvancedTracker for color detection tracking")
245
-
257
+
246
258
  color_processed_data = self.tracker.update(color_processed_data)
247
-
259
+
248
260
  except Exception as e:
249
261
  self.logger.warning(f"AdvancedTracker failed: {e}")
250
-
262
+
251
263
 
252
264
  color_processed_data = self._attach_masks_to_detections(color_processed_data, raw_processed_data)
253
265
  self._total_frame_counter += 1
254
-
266
+
255
267
  frame_number = None
256
268
  if stream_info:
257
269
  input_settings = stream_info.get("input_settings", {})
@@ -260,19 +272,55 @@ class ColorDetectionUseCase(BaseProcessor):
260
272
  # If start and end frame are the same, it's a single frame
261
273
  if start_frame is not None and end_frame is not None and start_frame == end_frame:
262
274
  frame_number = start_frame
263
-
275
+
264
276
  # Step 7: Analyze colors in media
265
277
  color_analysis = self._analyze_colors_in_media(
266
- color_processed_data,
267
- input_bytes,
278
+ color_processed_data,
279
+ input_bytes,
268
280
  config
269
281
  )
282
+ if config.zone_config:
283
+ color_processed_data = self._is_in_zone_robust(color_processed_data,config.zone_config)
284
+ print(color_processed_data)
285
+ try:
286
+ print("About to call process_color_in_frame...")
287
+
288
+ if config.detector is None:
289
+ print("ERROR: Detector is None after initialization attempt!")
290
+ curr_frame_color = {}
291
+
292
+ # else:
293
+ # if color_processed_data:
294
+ # t_id = color_processed_data[0].get('track_id')
295
+ # if t_id is not None and t_id not in self.all_color_data:
296
+ # # curr_frame_color = {}
297
+ # curr_frame_color = config.detector.process_color_in_frame(color_processed_data,input_bytes,config.zone_config,stream_info)
298
+ # res_dict[curr_frame_color[t_id]['color']] = curr_frame_color[t_id]['confidence']
299
+ # else:
300
+ # curr_frame_color = {}
301
+ # print("process_color_in_frame completed successfully")
302
+ # else:
303
+ # curr_frame_color = {}
304
+
305
+ #------------------------ORiginal Code to run on all frames-----------------------
306
+ else:
307
+ print(len(color_processed_data))
308
+ curr_frame_color = config.detector.process_color_in_frame(
309
+ color_processed_data,
310
+ input_bytes,
311
+ config.zone_config,
312
+ stream_info,
313
+ )
314
+ print("process_color_in_frame completed successfully")
315
+ except Exception as e:
316
+ print(f"ERROR in process_color_in_frame: {e}")
317
+ import traceback
318
+ traceback.print_exc()
319
+ curr_frame_color = {}
270
320
 
271
- curr_frame_color = self.detector.process_color_in_frame(color_processed_data,input_bytes,config.zone_config)
272
-
273
- # Step 8: Update color tracking state
321
+ self.update_vehicle_stats(curr_frame_color)
274
322
  self._update_color_tracking_state_from_analysis(color_analysis)
275
-
323
+
276
324
  # Step 9: Calculate summaries
277
325
  color_summary = self._calculate_color_summary(color_analysis, config)
278
326
  totals = self.get_total_color_counts()
@@ -284,14 +332,16 @@ class ColorDetectionUseCase(BaseProcessor):
284
332
  if color and tid is not None:
285
333
  tmp[color].add(tid)
286
334
  totals = {color: len(ids) for color, ids in tmp.items()}
287
- total_category_counts = self.get_total_category_counts()
335
+ total_category_counts = self.get_total_category_counts(color_processed_data)
288
336
  color_summary['total_color_counts'] = totals
289
337
  color_summary['total_category_counts'] = total_category_counts
290
338
 
291
339
  general_summary = self._calculate_general_summary(processed_data, config)
292
-
340
+ new_color_summary = self.merge_color_summary(color_processed_data,curr_frame_color)
341
+
293
342
  # Step 10: Zone analysis
294
-
343
+ self.color_helper(curr_frame_color)
344
+
295
345
  zone_analysis = {}
296
346
  if config.zone_config and config.zone_config['zones']:
297
347
  frame_data = color_processed_data
@@ -302,19 +352,19 @@ class ColorDetectionUseCase(BaseProcessor):
302
352
  zone_analysis[zone_name] = enhanced_data
303
353
 
304
354
 
305
-
355
+
306
356
  # Step 11: Generate alerts, incidents, tracking stats, and summary
307
357
  alerts = self._check_alerts(color_summary, frame_number, config)
308
358
 
309
359
  incidents_list = self._generate_incidents(color_summary, alerts, config, frame_number, stream_info)
310
360
  incidents_list = []
311
361
 
312
- tracking_stats_list = self._generate_tracking_stats(color_summary, alerts, config, frame_number, stream_info, curr_frame_color)
362
+ tracking_stats_list = self._generate_tracking_stats(new_color_summary,color_summary, alerts, config,curr_frame_color, frame_number, stream_info)
313
363
 
314
364
  business_analytics_list = []
315
365
  summary_list = self._generate_summary(color_summary, incidents_list, tracking_stats_list, business_analytics_list, alerts)
316
366
 
317
-
367
+
318
368
  incidents = incidents_list[0] if incidents_list else {}
319
369
  tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
320
370
  business_analytics = business_analytics_list[0] if business_analytics_list else {}
@@ -327,7 +377,7 @@ class ColorDetectionUseCase(BaseProcessor):
327
377
  "zone_analysis": zone_analysis,
328
378
  "human_text": summary}
329
379
  }
330
-
380
+
331
381
  context.mark_completed()
332
382
 
333
383
  # Build result object following the new pattern
@@ -341,23 +391,168 @@ class ColorDetectionUseCase(BaseProcessor):
341
391
  proc_time = time.time() - processing_start
342
392
  processing_latency_ms = proc_time * 1000.0
343
393
  processing_fps = (1.0 / proc_time) if proc_time > 0 else None
344
- # Log the performance metrics using the module-level logger
345
394
  print("latency in ms:",processing_latency_ms,"| Throughput fps:",processing_fps,"| Frame_Number:",self._total_frame_counter)
346
395
  return result
347
-
396
+
348
397
  except Exception as e:
349
398
  self.logger.error(f"Color detection failed: {str(e)}", exc_info=True)
350
399
  if context:
351
400
  context.mark_completed()
352
401
  return self.create_error_result(
353
- str(e),
402
+ str(e),
354
403
  type(e).__name__,
355
404
  usecase=self.name,
356
405
  category=self.category,
357
406
  context=context
358
407
  )
359
-
408
+
409
+ def update_vehicle_stats(self, frame_detections: dict):
410
+ """
411
+ Update global vehicle statistics ensuring uniqueness per track_id and per zone.
412
+ If the same vehicle (track_id) is seen again:
413
+ - Ignore if confidence is lower.
414
+ - Update its color if confidence is higher.
415
+ """
416
+
417
+ # Ensure zone-level data structures exist
418
+ if not hasattr(self, "zone_vehicle_stats"):
419
+ self.zone_vehicle_stats = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
420
+
421
+ for _, det in frame_detections.items():
422
+ track_id = det.get('track_id')
423
+ if track_id is None:
424
+ continue
425
+
426
+ vehicle_type = det.get('object_label', 'unknown').lower()
427
+ color = det.get('color', 'unknown').lower()
428
+ conf = det.get('confidence', 0.0)
429
+ zone = det.get('zone_name', 'Unknown_Zone')
430
+
431
+ # If this track_id is new → add and count
432
+ if track_id not in self.vehicle_tracks:
433
+ self.vehicle_tracks[track_id] = {
434
+ 'object_label': vehicle_type,
435
+ 'color': color,
436
+ 'confidence': conf,
437
+ 'zone': zone
438
+ }
439
+ self.vehicle_stats[vehicle_type][color] += 1
440
+ self.zone_vehicle_stats[zone][vehicle_type][color] += 1
441
+
442
+ else:
443
+ existing = self.vehicle_tracks[track_id]
444
+ if conf > existing['confidence']:
445
+ old_color = existing['color']
446
+ old_zone = existing.get('zone', zone)
447
+ old_type = existing.get('object_label', vehicle_type)
448
+
449
+ # Decrease old counts
450
+ self.vehicle_stats[old_type][old_color] -= 1
451
+ if self.vehicle_stats[old_type][old_color] <= 0:
452
+ del self.vehicle_stats[old_type][old_color]
453
+
454
+ self.zone_vehicle_stats[old_zone][old_type][old_color] -= 1
455
+ if self.zone_vehicle_stats[old_zone][old_type][old_color] <= 0:
456
+ del self.zone_vehicle_stats[old_zone][old_type][old_color]
457
+
458
+ # Update track info
459
+ self.vehicle_tracks[track_id].update({
460
+ 'color': color,
461
+ 'confidence': conf,
462
+ 'zone': zone,
463
+ })
464
+
465
+ # Increase new counts
466
+ self.vehicle_stats[vehicle_type][color] += 1
467
+ self.zone_vehicle_stats[zone][vehicle_type][color] += 1
468
+
469
+
470
+ def merge_color_summary(self,detections_data: List[Dict[str, Any]], curr_frame_color: Dict[int, Dict[str, Any]]) -> Dict[str, Any]:
471
+ """
472
+ Combine base detections with current frame color information and produce a color summary.
473
+ Returns structure similar to _calculate_color_summary().
474
+ """
475
+
476
+ category_colors = defaultdict(lambda: defaultdict(int))
477
+ detections = []
478
+ counts = {}
479
+
480
+ # Merge detections with color info
481
+ for record in detections_data:
482
+ track_id = record.get("track_id")
483
+ category = record.get("category", "unknown")
484
+ conf = record.get("confidence", 0.0)
485
+ bbox = record.get("bounding_box", {})
486
+ frame_id = record.get("frame_id")
487
+ zone_name = record.get("zone_name", "Unknown")
488
+
489
+ # Get color from curr_frame_color
490
+ main_color = "unknown"
491
+ if track_id in curr_frame_color:
492
+ main_color = curr_frame_color[track_id].get("color", "unknown")
493
+
494
+ category_colors[category][main_color] += 1
495
+ counts[category] = counts.get(category, 0) + 1
496
+
497
+ detections.append({
498
+ "bounding_box": bbox,
499
+ "category": category,
500
+ "confidence": conf,
501
+ "track_id": track_id,
502
+ "frame_id": frame_id,
503
+ "main_color": main_color,
504
+ "zone_name": zone_name
505
+ })
506
+
507
+ # Flatten color distribution
508
+ all_colors = defaultdict(int)
509
+ for category_data in category_colors.values():
510
+ for color, count in category_data.items():
511
+ all_colors[color] += count
512
+
513
+ # Find dominant color per category
514
+ dominant_colors = {}
515
+ for category, colors in category_colors.items():
516
+ if colors:
517
+ color, count = max(colors.items(), key=lambda x: x[1])
518
+ dominant_colors[category] = {
519
+ "color": color,
520
+ "count": count,
521
+ "percentage": round((count / sum(colors.values())) * 100, 1)
522
+ }
523
+
524
+ # Final summary dict
525
+ summary = {
526
+ "total_count": sum(counts.values()),
527
+ "per_category_count": counts,
528
+ "detections": detections,
529
+ "color_distribution": dict(all_colors),
530
+ "dominant_colors": dominant_colors
531
+ }
532
+
533
+ return summary
534
+
535
+ def get_vehicle_stats(self):
536
+ """Return the current global vehicle statistics as a normal dictionary."""
537
+ return {vtype: dict(colors) for vtype, colors in self.vehicle_stats.items()}
538
+
539
+ def _is_in_zone_robust(self,detections,zones):
540
+ if not detections:
541
+ return {}
542
+ new_data = []
543
+ for det in detections:
544
+ bbox = det.get('bounding_box')
545
+ cx,cy = get_bbox_bottom25_center(bbox)
546
+ for zone, region in zones.items():
547
+ for reg, poly in region.items():
548
+ if point_in_polygon((cx,cy),poly):
549
+ det['zone_name'] = reg
550
+ new_data.append(det)
551
+ return new_data
552
+
360
553
  def color_helper(self, curr_data):
554
+ if curr_data is None:
555
+ return
361
556
  for tid, data in curr_data.items():
362
557
  if tid not in self.all_color_data:
363
558
  # First time seeing this track
@@ -391,25 +586,16 @@ class ColorDetectionUseCase(BaseProcessor):
391
586
  # update track info
392
587
  self.all_color_data[tid]["color"] = new_color
393
588
  self.all_color_data[tid]["confidence"] = data.get("confidence")
394
-
395
-
396
-
589
+ # return self.all_color_data
397
590
 
398
591
  def _analyze_colors_in_media(
399
- self,
400
- data: Any,
401
- media_bytes: bytes,
592
+ self,
593
+ data: Any,
594
+ media_bytes: bytes,
402
595
  config: ColorDetectionConfig
403
596
  ) -> List[Dict[str, Any]]:
404
597
  """Analyze colors of detected objects in video frames or images."""
405
-
406
- # Determine if input is video or image
407
- is_video = self._is_video_bytes(media_bytes)
408
-
409
- if is_video:
410
- return self._analyze_colors_in_video(data, media_bytes, config)
411
- else:
412
- return self._analyze_colors_in_image(data, media_bytes, config)
598
+ return self._analyze_colors_in_image(data, media_bytes, config)
413
599
 
414
600
  def _update_color_tracking_state_from_analysis(self, color_analysis: List[Dict[str, Any]]) -> None:
415
601
  """Update total tracking store using analyzed color results.
@@ -422,7 +608,7 @@ class ColorDetectionUseCase(BaseProcessor):
422
608
  self._color_total_track_ids = existing_store
423
609
  # Reset current frame tracking for this frame
424
610
  self._color_current_frame_track_ids = defaultdict(set)
425
-
611
+
426
612
  for rec in color_analysis:
427
613
  cat = rec.get('category')
428
614
  color = rec.get('main_color')
@@ -466,7 +652,7 @@ class ColorDetectionUseCase(BaseProcessor):
466
652
  self._color_total_track_ids[key].add(track_id)
467
653
  # Also update current frame tracking
468
654
  self._color_current_frame_track_ids[key].add(track_id)
469
-
655
+
470
656
  def _is_video_bytes(self, media_bytes: bytes) -> bool:
471
657
  """Determine if bytes represent a video file."""
472
658
  # Check common video file signatures
@@ -477,16 +663,16 @@ class ColorDetectionUseCase(BaseProcessor):
477
663
  b'\x1aE\xdf\xa3', # MKV/WebM
478
664
  b'ftyp', # General MP4 family
479
665
  ]
480
-
666
+
481
667
  for signature in video_signatures:
482
668
  if media_bytes.startswith(signature) or signature in media_bytes[:50]:
483
669
  return True
484
670
  return False
485
-
671
+
486
672
  def _analyze_colors_in_video(
487
- self,
488
- data: Any,
489
- video_bytes: bytes,
673
+ self,
674
+ data: Any,
675
+ video_bytes: bytes,
490
676
  config: ColorDetectionConfig
491
677
  ) -> List[Dict[str, Any]]:
492
678
  with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_video:
@@ -574,22 +760,22 @@ class ColorDetectionUseCase(BaseProcessor):
574
760
  os.unlink(video_path)
575
761
 
576
762
  def _analyze_colors_in_image(
577
- self,
578
- data: Any,
579
- image_bytes: bytes,
763
+ self,
764
+ data: Any,
765
+ image_bytes: bytes,
580
766
  config: ColorDetectionConfig
581
767
  ) -> List[Dict[str, Any]]:
582
768
  image_array = np.frombuffer(image_bytes, np.uint8)
583
769
  image = cv2.imdecode(image_array, cv2.IMREAD_COLOR)
584
770
  #image = self.jpeg.decode(image_bytes, pixel_format=TJPF_RGB)
585
-
771
+
586
772
  if image is None:
587
773
  raise RuntimeError("Failed to decode image from bytes")
588
-
774
+
589
775
  rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
590
776
  color_analysis = []
591
777
  detections = self._get_frame_detections(data, "0")
592
-
778
+
593
779
  for detection in detections:
594
780
  if detection.get("confidence", 1.0) < config.confidence_threshold:
595
781
  continue
@@ -610,12 +796,14 @@ class ColorDetectionUseCase(BaseProcessor):
610
796
  if not in_any_zone:
611
797
  continue # Skip detections outside zones
612
798
 
613
- crop = self._crop_bbox(rgb_image, bbox, config.bbox_format)
614
- if crop.size == 0:
615
- continue
799
+ # crop = self._crop_bbox(rgb_image, bbox, config.bbox_format)
800
+ # if crop.size == 0:
801
+ # continue
616
802
 
617
- major_colors = extract_major_colors(crop, k=config.top_k_colors)
618
- main_color = major_colors[0][0] if major_colors else "unknown"
803
+ # major_colors = extract_major_colors(crop, k=config.top_k_colors)
804
+ # main_color = major_colors[0][0] if major_colors else "unknown"
805
+ main_color = "unknown"
806
+ major_colors = []
619
807
 
620
808
  color_record = {
621
809
  "frame_id": "0",
@@ -630,10 +818,10 @@ class ColorDetectionUseCase(BaseProcessor):
630
818
  "zone_name": zone_name
631
819
  }
632
820
  color_analysis.append(color_record)
633
-
821
+
634
822
  return color_analysis
635
-
636
-
823
+
824
+
637
825
  def _get_frame_detections(self, data: Any, frame_key: str) -> List[Dict[str, Any]]:
638
826
  """Extract detections for a specific frame from data."""
639
827
  if isinstance(data, dict):
@@ -644,11 +832,11 @@ class ColorDetectionUseCase(BaseProcessor):
644
832
  return data
645
833
  else:
646
834
  return []
647
-
835
+
648
836
  def _crop_bbox(self, image: np.ndarray, bbox: Dict[str, Any], bbox_format: str) -> np.ndarray:
649
837
  """Crop bounding box region from image."""
650
838
  h, w = image.shape[:2]
651
-
839
+
652
840
  # Auto-detect bbox format
653
841
  if bbox_format == "auto":
654
842
  if "xmin" in bbox:
@@ -657,7 +845,7 @@ class ColorDetectionUseCase(BaseProcessor):
657
845
  bbox_format = "x_y_width_height"
658
846
  else:
659
847
  return np.zeros((0, 0, 3), dtype=np.uint8)
660
-
848
+
661
849
  # Extract coordinates based on format
662
850
  if bbox_format == "xmin_ymin_xmax_ymax":
663
851
  xmin = max(0, int(bbox["xmin"]))
@@ -671,9 +859,9 @@ class ColorDetectionUseCase(BaseProcessor):
671
859
  ymax = min(h, int(bbox["y"] + bbox["height"]))
672
860
  else:
673
861
  return np.zeros((0, 0, 3), dtype=np.uint8)
674
-
862
+
675
863
  return image[ymin:ymax, xmin:xmax]
676
-
864
+
677
865
  def _calculate_color_summary(self, color_analysis: List[Dict], config: ColorDetectionConfig) -> Dict[str, Any]:
678
866
  category_colors = defaultdict(lambda: defaultdict(int))
679
867
  total_detections = len(color_analysis)
@@ -693,7 +881,7 @@ class ColorDetectionUseCase(BaseProcessor):
693
881
  "main_color": record["main_color"]
694
882
  })
695
883
 
696
-
884
+
697
885
  self.logger.debug(f"Valid detections after filtering: {len(detections)}")
698
886
  summary = {
699
887
  "total_count": sum(counts.values()),
@@ -725,14 +913,14 @@ class ColorDetectionUseCase(BaseProcessor):
725
913
 
726
914
 
727
915
  return summary
728
-
916
+
729
917
  def _calculate_general_summary(self, processed_data: Any, config: ColorDetectionConfig) -> Dict[str, Any]:
730
918
  """Calculate general detection summary."""
731
-
919
+
732
920
  # Count objects by category
733
921
  category_counts = defaultdict(int)
734
922
  total_objects = 0
735
-
923
+
736
924
  if isinstance(processed_data, dict):
737
925
  # Frame-based format
738
926
  for frame_data in processed_data.values():
@@ -749,18 +937,18 @@ class ColorDetectionUseCase(BaseProcessor):
749
937
  category = detection.get("category", "unknown")
750
938
  category_counts[category] += 1
751
939
  total_objects += 1
752
-
940
+
753
941
  return {
754
942
  "total_objects": total_objects,
755
943
  "category_counts": dict(category_counts),
756
944
  "categories_detected": list(category_counts.keys())
757
945
  }
758
-
946
+
759
947
  def _calculate_metrics(self, color_analysis: List[Dict], color_summary: Dict, config: ColorDetectionConfig, context: ProcessingContext) -> Dict[str, Any]:
760
948
  """Calculate detailed metrics for analytics."""
761
949
  total_detections = len(color_analysis)
762
950
  unique_colors = len(color_summary.get("color_distribution", {}))
763
-
951
+
764
952
  metrics = {
765
953
  "total_detections": total_detections,
766
954
  "unique_colors": unique_colors,
@@ -772,15 +960,15 @@ class ColorDetectionUseCase(BaseProcessor):
772
960
  "detection_rate": 0.0,
773
961
  "average_colors_per_detection": config.top_k_colors
774
962
  }
775
-
963
+
776
964
  # Calculate color diversity
777
965
  if total_detections > 0:
778
966
  metrics["color_diversity"] = (unique_colors / total_detections) * 100
779
-
967
+
780
968
  # Calculate detection rate
781
969
  if config.time_window_minutes and config.time_window_minutes > 0:
782
970
  metrics["detection_rate"] = (total_detections / config.time_window_minutes) * 60
783
-
971
+
784
972
  # Per-category metrics
785
973
  if color_summary.get("categories"):
786
974
  category_metrics = {}
@@ -792,7 +980,7 @@ class ColorDetectionUseCase(BaseProcessor):
792
980
  "color_diversity": (len(colors) / category_total) * 100 if category_total > 0 else 0
793
981
  }
794
982
  metrics["category_metrics"] = category_metrics
795
-
983
+
796
984
  # Processing settings
797
985
  metrics["processing_settings"] = {
798
986
  "confidence_threshold": config.confidence_threshold,
@@ -801,12 +989,12 @@ class ColorDetectionUseCase(BaseProcessor):
801
989
  "target_categories": config.target_categories,
802
990
  "enable_unique_counting": config.enable_unique_counting
803
991
  }
804
-
992
+
805
993
  return metrics
806
-
994
+
807
995
  def _extract_predictions(self, color_analysis: List[Dict], config: ColorDetectionConfig) -> List[Dict]:
808
996
  """Extract predictions in standard format."""
809
-
997
+
810
998
  predictions = []
811
999
  for record in color_analysis:
812
1000
  prediction = {
@@ -821,9 +1009,9 @@ class ColorDetectionUseCase(BaseProcessor):
821
1009
  if "detection_id" in record:
822
1010
  prediction["id"] = record["detection_id"]
823
1011
  predictions.append(prediction)
824
-
1012
+
825
1013
  return predictions
826
-
1014
+
827
1015
  def _generate_summary(self, summary: dict, incidents: List, tracking_stats: List, business_analytics: List, alerts: List) -> List[str]:
828
1016
  """
829
1017
  Generate a human_text string for the tracking_stat, incident, business analytics and alerts.
@@ -834,7 +1022,7 @@ class ColorDetectionUseCase(BaseProcessor):
834
1022
  if len(incidents) > 0:
835
1023
  lines.append("Incidents: "+f"\n\t{incidents[0].get('human_text', 'No incidents detected')}")
836
1024
  if len(tracking_stats) > 0:
837
- lines.append("Tracking Statistics: "+f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}")
1025
+ lines.append(f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}")
838
1026
  if len(business_analytics) > 0:
839
1027
  lines.append("Business Analytics: "+f"\t{business_analytics[0].get('human_text', 'No business analytics detected')}")
840
1028
 
@@ -842,7 +1030,7 @@ class ColorDetectionUseCase(BaseProcessor):
842
1030
  lines.append("Summary: "+"No Summary Data")
843
1031
 
844
1032
  return ["\n".join(lines)]
845
-
1033
+
846
1034
  def _generate_events(self, color_summary: Dict, alerts: List, config: ColorDetectionConfig, frame_number: Optional[int] = None) -> List[Dict]:
847
1035
  """Generate structured events with frame-based keys."""
848
1036
  frame_key = str(frame_number) if frame_number is not None else "current_frame"
@@ -906,15 +1094,17 @@ class ColorDetectionUseCase(BaseProcessor):
906
1094
  frame_events.append(alert_event)
907
1095
 
908
1096
  return events
909
-
1097
+
910
1098
  def _generate_tracking_stats(
911
1099
  self,
1100
+ new_color_summary: Dict,
912
1101
  counting_summary: Dict,
913
1102
  alerts: Any,
914
1103
  config: ColorDetectionConfig,
1104
+ curr_frame_color: Any,
1105
+ total_color_data: Any,
915
1106
  frame_number: Optional[int] = None,
916
1107
  stream_info: Optional[Dict[str, Any]] = None,
917
- curr_frame_color: Any = None
918
1108
  ) -> List[Dict]:
919
1109
  """Generate structured tracking stats for the output format with frame-based keys, including track_ids_info and detections with masks."""
920
1110
  # frame_key = str(frame_number) if frame_number is not None else "current_frame"
@@ -925,7 +1115,7 @@ class ColorDetectionUseCase(BaseProcessor):
925
1115
  total_detections = counting_summary.get("total_count", 0)
926
1116
  total_color_counts_dict = counting_summary.get("total_color_counts", {})
927
1117
  total_category_counts_dict = counting_summary.get("total_category_counts", {})
928
- cumulative_total = sum(total_color_counts_dict.values()) if total_color_counts_dict else 0
1118
+ # cumulative_total = sum(total_color_counts_dict.values()) if total_color_counts_dict else 0
929
1119
  per_category_count = counting_summary.get("per_category_count", {})
930
1120
 
931
1121
  # Compute current color counts from detections
@@ -939,20 +1129,13 @@ class ColorDetectionUseCase(BaseProcessor):
939
1129
 
940
1130
  current_timestamp = self._get_current_timestamp_str(stream_info, precision=False)
941
1131
  start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
942
-
1132
+
943
1133
  # Create high precision timestamps for input_timestamp and reset_timestamp
944
1134
  high_precision_start_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
945
1135
  high_precision_reset_timestamp = self._get_start_timestamp_str(stream_info, precision=True)
946
1136
 
947
1137
  camera_info = self.get_camera_info_from_stream(stream_info)
948
- total_color_data = self.color_helper(curr_frame_color)
949
- print("========================CURR FRAME=======================")
950
- print(curr_frame_color)
951
- print("========================CURR FRAME=======================")
952
-
953
- print("========================TOTAL=======================")
954
- print(total_color_data)
955
- print("========================TOTAL=======================")
1138
+ # total_color_data = self.color_helper(curr_frame_color)
956
1139
 
957
1140
  human_text_lines = []
958
1141
  color_counts = {}
@@ -963,51 +1146,72 @@ class ColorDetectionUseCase(BaseProcessor):
963
1146
  if color not in color_counts:
964
1147
  color_counts[color] = 0
965
1148
  color_counts[color] += 1
1149
+ zone_frame_data = {}
1150
+ if curr_frame_color:
1151
+ for tid, data in curr_frame_color.items():
1152
+ zone = data.get("zone_name", "Unknown_Zone")
1153
+ color = data.get("color", "unknown")
1154
+ category = data.get("object_label", "unknown")
1155
+
1156
+ if zone not in zone_frame_data:
1157
+ zone_frame_data[zone] = {
1158
+ "color_counts": {},
1159
+ "category_counts": {}
1160
+ }
966
1161
 
967
- # After processing all frames, print the final counts
968
- print("Unique color counts:", color_counts)
1162
+ # Count colors
1163
+ zone_frame_data[zone]["color_counts"][color] = (
1164
+ zone_frame_data[zone]["color_counts"].get(color, 0) + 1
1165
+ )
969
1166
 
1167
+ # Count vehicle types
1168
+ zone_frame_data[zone]["category_counts"][category] = (
1169
+ zone_frame_data[zone]["category_counts"].get(category, 0) + 1
1170
+ )
970
1171
 
971
1172
  # CURRENT FRAME section
972
1173
  human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}:")
973
- if total_detections > 0:
974
- # Vehicle categories (current frame)
975
- category_counts = [f"{count} {cat}" for cat, count in per_category_count.items()]
976
- if len(category_counts) == 1:
977
- detection_text = category_counts[0] + " detected"
978
- elif len(category_counts) == 2:
979
- detection_text = f"{category_counts[0]} and {category_counts[1]} detected"
980
- else:
981
- detection_text = f"{', '.join(category_counts[:-1])}, and {category_counts[-1]} detected"
982
- human_text_lines.append(f"\t- {detection_text}")
983
-
984
- # Colors (current frame)
985
- if color_counts:
986
- color_counts_text = ", ".join([f"{count} {color}" for color, count in color_counts.items()])
987
- human_text_lines.append(f"\t- Colors: {color_counts_text}")
988
- else:
1174
+ if not curr_frame_color or total_detections == 0:
989
1175
  human_text_lines.append(f"\t- No detections")
1176
+ else:
1177
+ for zone_name, stats in zone_frame_data.items():
1178
+ color_counts = stats["color_counts"]
1179
+ per_category_count = stats["category_counts"]
1180
+ if config.zone_config:
1181
+ human_text_lines.append(f"\t{zone_name}:")
1182
+ if per_category_count:
1183
+ category_counts = [f"{count} {cat}" for cat, count in per_category_count.items()]
1184
+ if len(category_counts) == 1:
1185
+ detection_text = category_counts[0] + " detected"
1186
+ elif len(category_counts) == 2:
1187
+ detection_text = f"{category_counts[0]} and {category_counts[1]} detected"
1188
+ else:
1189
+ detection_text = f"{', '.join(category_counts[:-1])}, and {category_counts[-1]} detected"
1190
+ human_text_lines.append(f"\t\t- {detection_text}")
1191
+
1192
+ if color_counts:
1193
+ color_counts_text = ", ".join([f"{count} {color}" for color, count in color_counts.items()])
1194
+ human_text_lines.append(f"\t\t- Colors: {color_counts_text}")
990
1195
 
991
1196
  human_text_lines.append("") # spacing
992
1197
 
1198
+ cumulative_total = sum(self.all_color_counts.values())
1199
+ stats = self.zone_vehicle_stats
1200
+
993
1201
  # TOTAL SINCE section
994
1202
  human_text_lines.append(f"TOTAL SINCE {start_timestamp}:")
995
- human_text_lines.append(f"\t- Total Detected (by color): {cumulative_total}")
996
- # Add category-wise totals
997
-
998
- if total_category_counts_dict:
999
- human_text_lines.append("\t- Categories:")
1000
- for cat, count in total_category_counts_dict.items():
1001
- if count > 0:
1002
- human_text_lines.append(f"\t\t- {cat}: {count}")
1003
- # Add color-wise totals
1004
- if total_color_data:
1005
- human_text_lines.append("\t- Colors:")
1006
- for color, count in total_color_data.items():
1007
- if count > 0:
1008
- human_text_lines.append(f"\t\t- {color}: {count}")
1009
- # Build current_counts array in expected format
1010
- # Build arrays
1203
+ for zone_name, vehicles in stats.items():
1204
+ total_in_zone = sum(sum(colors.values()) for colors in vehicles.values())
1205
+ if config.zone_config:
1206
+ human_text_lines.append(f"\t{zone_name}:")
1207
+ human_text_lines.append(f"\t\t- Total Detected: {total_in_zone}")
1208
+
1209
+ for vehicle_type, colors in vehicles.items():
1210
+ total_type_count = sum(colors.values())
1211
+ human_text_lines.append(f"\t\t- {vehicle_type}: {total_type_count}")
1212
+ for color, count in colors.items():
1213
+ human_text_lines.append(f"\t\t\t- {color}: {count}")
1214
+
1011
1215
  current_counts_categories = []
1012
1216
  for cat, count in per_category_count.items():
1013
1217
  if count > 0 or total_detections > 0:
@@ -1030,9 +1234,9 @@ class ColorDetectionUseCase(BaseProcessor):
1030
1234
  # Include detections with masks from counting_summary
1031
1235
  # Prepare detections without confidence scores (as per eg.json)
1032
1236
  detections = []
1033
- for detection in counting_summary.get("detections", []):
1237
+ for detection in new_color_summary.get("detections", []):
1034
1238
  bbox = detection.get("bounding_box", {})
1035
- category = detection.get("category", "person")
1239
+ category = detection.get("main_color", "No_color")
1036
1240
  # Include segmentation if available (like in eg.json)
1037
1241
  if detection.get("masks"):
1038
1242
  segmentation= detection.get("masks", [])
@@ -1077,68 +1281,16 @@ class ColorDetectionUseCase(BaseProcessor):
1077
1281
  }
1078
1282
  ]
1079
1283
 
1284
+
1080
1285
  # Keep backward-compat: put colors into total_counts and categories into current_counts
1081
1286
  tracking_stat=self.create_tracking_stats(total_counts=total_counts_colors, current_counts=current_counts_categories,
1082
1287
  detections=detections, human_text=human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
1083
1288
  reset_settings=reset_settings, start_time=high_precision_start_timestamp ,
1084
1289
  reset_time=high_precision_reset_timestamp)
1085
1290
 
1086
- # Add explicit breakdowns for consumers who want both types
1087
- # tracking_stat["current_category_counts"] = current_counts_categories
1088
- # tracking_stat["current_color_counts"] = current_counts_colors
1089
- # tracking_stat["total_category_counts"] = total_counts_categories
1090
- # tracking_stat["total_color_counts"] = total_counts_colors
1091
-
1092
1291
  tracking_stats.append(tracking_stat)
1093
1292
  return tracking_stats
1094
-
1095
- def _generate_human_text_for_tracking(self, total_detections: int, color_summary: Dict, insights: List[str], summary: str, config: ColorDetectionConfig) -> str:
1096
- """Generate human-readable text for tracking stats."""
1097
- from datetime import datetime, timezone
1098
-
1099
- text_parts = [
1100
- #f"Tracking Start Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M')}",
1101
- #f"Objects Analyzed: {total_detections}"
1102
- ]
1103
-
1104
- if config.time_window_minutes:
1105
- detection_rate_per_hour = (total_detections / config.time_window_minutes) * 60
1106
- #text_parts.append(f"Detection Rate: {detection_rate_per_hour:.1f} objects per hour")
1107
-
1108
- # Add color statistics
1109
- unique_colors = len(color_summary.get("color_distribution", {}))
1110
- #text_parts.append(f"Unique Colors Detected: {unique_colors}")
1111
-
1112
- if total_detections > 0:
1113
- color_diversity = (unique_colors / total_detections) * 100
1114
- #text_parts.append(f"Color Diversity: {color_diversity:.1f}%")
1115
-
1116
- # Add category breakdown
1117
- categories = color_summary.get("categories", {})
1118
- if categories:
1119
- #text_parts.append(f"Categories Analyzed: {len(categories)}")
1120
- for category, colors in categories.items():
1121
- category_total = sum(colors.values())
1122
- if category_total > 0:
1123
- dominant_color = max(colors.items(), key=lambda x: x[1])[0] if colors else "unknown"
1124
- text_parts.append(f" {category_total} {category.title()} detected, Color: {dominant_color}")
1125
-
1126
- # Add color distribution summary
1127
- color_distribution = color_summary.get("color_distribution", {})
1128
- if color_distribution:
1129
- top_colors = sorted(color_distribution.items(), key=lambda x: x[1], reverse=True)[:3]
1130
- #text_parts.append("Top Colors:")
1131
- for color, count in top_colors:
1132
- percentage = (count / total_detections) * 100
1133
- #text_parts.append(f" {color.title()}: {count} objects ({percentage:.1f}%)")
1134
-
1135
- # Add key insights
1136
- # if insights:
1137
- # text_parts.append("Key Color Insights:")
1138
- # for insight in insights[:3]: # Limit to first 3 insights
1139
- # text_parts.append(f" - {insight}")
1140
-
1141
- return "\n".join(text_parts)
1293
+
1142
1294
 
1143
1295
  def reset_tracker(self) -> None:
1144
1296
  """Reset the advanced tracker instance."""
@@ -1222,7 +1374,7 @@ class ColorDetectionUseCase(BaseProcessor):
1222
1374
  filtered.append(detections[best_idx])
1223
1375
  used[best_idx] = True
1224
1376
  return filtered
1225
-
1377
+
1226
1378
  def get_config_schema(self) -> Dict[str, Any]:
1227
1379
  """Get JSON schema for configuration validation."""
1228
1380
  return {
@@ -1242,7 +1394,7 @@ class ColorDetectionUseCase(BaseProcessor):
1242
1394
  "required": ["confidence_threshold", "top_k_colors"],
1243
1395
  "additionalProperties": False
1244
1396
  }
1245
-
1397
+
1246
1398
  def create_default_config(self, **overrides) -> ColorDetectionConfig:
1247
1399
  """Create default configuration with optional overrides."""
1248
1400
  defaults = {
@@ -1261,7 +1413,7 @@ class ColorDetectionUseCase(BaseProcessor):
1261
1413
  }
1262
1414
  defaults.update(overrides)
1263
1415
  return ColorDetectionConfig(**defaults)
1264
-
1416
+
1265
1417
  def _update_color_tracking_state(self, detections: List[Dict]):
1266
1418
  """Track unique track_ids per category and color for total count."""
1267
1419
  # Ensure storage is a defaultdict(set) to allow safe .add()
@@ -1298,20 +1450,19 @@ class ColorDetectionUseCase(BaseProcessor):
1298
1450
  color_to_ids[color].update(ids)
1299
1451
  return {color: len(ids) for color, ids in color_to_ids.items()}
1300
1452
 
1301
- def get_total_category_counts(self):
1453
+ def get_total_category_counts(self,data):
1302
1454
  """Return total unique track_id count per category (across all colors)."""
1303
- store = getattr(self, '_color_total_track_ids', {})
1304
- if not isinstance(store, dict):
1305
- return {}
1306
- category_to_ids = defaultdict(set)
1307
- for key, id_set in store.items():
1308
- if isinstance(key, str) and ':' in key:
1309
- cat, _ = key.split(':', 1)
1310
- else:
1311
- cat = key
1312
- ids = id_set if isinstance(id_set, set) else set(id_set or [])
1313
- category_to_ids[cat].update(ids)
1314
- return {cat: len(ids) for cat, ids in category_to_ids.items()}
1455
+ for det in data:
1456
+ track_id = det.get("track_id")
1457
+ category = det.get("category")
1458
+ if track_id and category:
1459
+ if category not in self.total_category_count:
1460
+ self.total_category_count[category] = set()
1461
+ self.total_category_count[category].add(track_id)
1462
+
1463
+ # Convert sets to counts
1464
+ return {cat: len(track_ids) for cat, track_ids in self.total_category_count.items()}
1465
+
1315
1466
 
1316
1467
  def _get_track_ids_info(self, detections: List[Dict]) -> Dict[str, Any]:
1317
1468
  """Get detailed information about track IDs for color detections (per frame)."""
@@ -1379,7 +1530,7 @@ class ColorDetectionUseCase(BaseProcessor):
1379
1530
  det.setdefault("masks", ["EMPTY"])
1380
1531
 
1381
1532
  return processed_detections
1382
-
1533
+
1383
1534
  def _generate_incidents(self, counting_summary: Dict, alerts: List, config: ColorDetectionConfig,
1384
1535
  frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
1385
1536
  """Generate structured events for the output format with frame-based keys."""
@@ -1390,7 +1541,7 @@ class ColorDetectionUseCase(BaseProcessor):
1390
1541
  total_detections = counting_summary.get("total_count", 0)
1391
1542
  current_timestamp = self._get_current_timestamp_str(stream_info)
1392
1543
  camera_info = self.get_camera_info_from_stream(stream_info)
1393
-
1544
+
1394
1545
  self._ascending_alert_list = self._ascending_alert_list[-900:] if len(self._ascending_alert_list) > 900 else self._ascending_alert_list
1395
1546
 
1396
1547
  if total_detections > 0:
@@ -1401,11 +1552,11 @@ class ColorDetectionUseCase(BaseProcessor):
1401
1552
  if start_timestamp and self.current_incident_end_timestamp=='N/A':
1402
1553
  self.current_incident_end_timestamp = 'Incident still active'
1403
1554
  elif start_timestamp and self.current_incident_end_timestamp=='Incident still active':
1404
- if len(self._ascending_alert_list) >= 15 and sum(self._ascending_alert_list[-15:]) / 15 < 1.5:
1555
+ if len(self._ascending_alert_list) >= 15 and sum(self._ascending_alert_list[-15:]) / 15 < 1.5:
1405
1556
  self.current_incident_end_timestamp = current_timestamp
1406
1557
  elif self.current_incident_end_timestamp!='Incident still active' and self.current_incident_end_timestamp!='N/A':
1407
1558
  self.current_incident_end_timestamp = 'N/A'
1408
-
1559
+
1409
1560
  if config.alert_config and hasattr(config.alert_config, 'count_thresholds') and config.alert_config.count_thresholds:
1410
1561
  threshold = config.alert_config.count_thresholds.get("all", 15)
1411
1562
  intensity = min(10.0, (total_detections / threshold) * 10)
@@ -1456,7 +1607,7 @@ class ColorDetectionUseCase(BaseProcessor):
1456
1607
  getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
1457
1608
  }
1458
1609
  })
1459
-
1610
+
1460
1611
  event= self.create_incident(incident_id=self.CASE_TYPE+'_'+str(frame_number), incident_type=self.CASE_TYPE,
1461
1612
  severity_level=level, human_text=human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
1462
1613
  start_time=start_timestamp, end_time=self.current_incident_end_timestamp,
@@ -1468,7 +1619,7 @@ class ColorDetectionUseCase(BaseProcessor):
1468
1619
  incidents.append({})
1469
1620
 
1470
1621
  return incidents
1471
-
1622
+
1472
1623
  def _check_alerts(self, summary: dict, frame_number:Any, config: ColorDetectionConfig) -> List[Dict]:
1473
1624
  """
1474
1625
  Check if any alert thresholds are exceeded and return alert dicts.
@@ -1510,7 +1661,7 @@ class ColorDetectionUseCase(BaseProcessor):
1510
1661
  if hasattr(config.alert_config, 'count_thresholds') and config.alert_config.count_thresholds:
1511
1662
 
1512
1663
  for category, threshold in config.alert_config.count_thresholds.items():
1513
- if category == "all" and total > threshold:
1664
+ if category == "all" and total > threshold:
1514
1665
 
1515
1666
  alerts.append({
1516
1667
  "alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
@@ -1520,7 +1671,7 @@ class ColorDetectionUseCase(BaseProcessor):
1520
1671
  "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
1521
1672
  "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
1522
1673
  getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
1523
- }
1674
+ }
1524
1675
  })
1525
1676
  elif category in summary.get("per_category_count", {}):
1526
1677
  count = summary.get("per_category_count", {})[category]
@@ -1533,12 +1684,17 @@ class ColorDetectionUseCase(BaseProcessor):
1533
1684
  "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
1534
1685
  "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
1535
1686
  getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
1536
- }
1687
+ }
1537
1688
  })
1538
1689
  else:
1539
1690
  pass
1540
1691
  return alerts
1541
-
1692
+
1693
+ def _format_timestamp_for_stream(self, timestamp: float) -> str:
1694
+ """Format timestamp for streams (YYYY:MM:DD HH:MM:SS format)."""
1695
+ dt = datetime.fromtimestamp(float(timestamp), tz=timezone.utc)
1696
+ return dt.strftime('%Y:%m:%d %H:%M:%S')
1697
+
1542
1698
  def _format_timestamp_for_video(self, timestamp: float) -> str:
1543
1699
  """Format timestamp for video chunks (HH:MM:SS.ms format)."""
1544
1700
  hours = int(timestamp // 3600)
@@ -1546,11 +1702,6 @@ class ColorDetectionUseCase(BaseProcessor):
1546
1702
  seconds = round(float(timestamp % 60),2)
1547
1703
  return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
1548
1704
 
1549
- def _format_timestamp_for_stream(self, timestamp: float) -> str:
1550
- """Format timestamp for streams (YYYY:MM:DD HH:MM:SS format)."""
1551
- dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
1552
- return dt.strftime('%Y:%m:%d %H:%M:%S')
1553
-
1554
1705
  def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
1555
1706
  """Get formatted current timestamp based on stream type."""
1556
1707
 
@@ -1564,6 +1715,7 @@ class ColorDetectionUseCase(BaseProcessor):
1564
1715
  start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
1565
1716
  stream_time_str = self._format_timestamp_for_video(start_time)
1566
1717
 
1718
+
1567
1719
  return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
1568
1720
  else:
1569
1721
  return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
@@ -1575,8 +1727,7 @@ class ColorDetectionUseCase(BaseProcessor):
1575
1727
  start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
1576
1728
 
1577
1729
  stream_time_str = self._format_timestamp_for_video(start_time)
1578
-
1579
-
1730
+
1580
1731
  return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
1581
1732
  else:
1582
1733
  stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
@@ -1595,22 +1746,22 @@ class ColorDetectionUseCase(BaseProcessor):
1595
1746
  """Get formatted start timestamp for 'TOTAL SINCE' based on stream type."""
1596
1747
  if not stream_info:
1597
1748
  return "00:00:00"
1598
-
1749
+
1599
1750
  if precision:
1600
1751
  if self.start_timer is None:
1601
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
1752
+ self.start_timer = stream_info.get("input_settings", {}).get("stream_time", datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC"))
1602
1753
  return self._format_timestamp(self.start_timer)
1603
1754
  elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
1604
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
1755
+ self.start_timer = stream_info.get("input_settings", {}).get("stream_time", datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC"))
1605
1756
  return self._format_timestamp(self.start_timer)
1606
1757
  else:
1607
1758
  return self._format_timestamp(self.start_timer)
1608
1759
 
1609
1760
  if self.start_timer is None:
1610
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
1761
+ self.start_timer = stream_info.get("input_settings", {}).get("stream_time", datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC"))
1611
1762
  return self._format_timestamp(self.start_timer)
1612
1763
  elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
1613
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
1764
+ self.start_timer = stream_info.get("input_settings", {}).get("stream_time", datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC"))
1614
1765
  return self._format_timestamp(self.start_timer)
1615
1766
 
1616
1767
  else:
@@ -1632,7 +1783,7 @@ class ColorDetectionUseCase(BaseProcessor):
1632
1783
  dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
1633
1784
  dt = dt.replace(minute=0, second=0, microsecond=0)
1634
1785
  return dt.strftime('%Y:%m:%d %H:%M:%S')
1635
-
1786
+
1636
1787
  def _format_timestamp(self, timestamp: Any) -> str:
1637
1788
  """Format a timestamp so that exactly two digits follow the decimal point (milliseconds).
1638
1789
 
@@ -1693,37 +1844,37 @@ class ColorDetectionUseCase(BaseProcessor):
1693
1844
  """Update zone tracking with current frame data."""
1694
1845
  if not zone_analysis or not config.zone_config or not config.zone_config['zones']:
1695
1846
  return {}
1696
-
1847
+
1697
1848
  enhanced_zone_analysis = {}
1698
1849
  zones = config.zone_config['zones']
1699
-
1850
+
1700
1851
  # Initialize current frame zone tracks
1701
1852
  current_frame_zone_tracks = {zone_name: set() for zone_name in zones.keys()}
1702
-
1853
+
1703
1854
  # Initialize zone tracking storage
1704
1855
  for zone_name in zones.keys():
1705
1856
  if zone_name not in self._zone_current_track_ids:
1706
1857
  self._zone_current_track_ids[zone_name] = set()
1707
1858
  if zone_name not in self._zone_total_track_ids:
1708
1859
  self._zone_total_track_ids[zone_name] = set()
1709
-
1860
+
1710
1861
  # Check each detection against each zone
1711
1862
  for detection in detections:
1712
1863
  track_id = detection.get("track_id")
1713
1864
  if track_id is None:
1714
1865
  continue
1715
-
1866
+
1716
1867
  bbox = detection.get("bounding_box", detection.get("bbox"))
1717
1868
  if not bbox:
1718
1869
  continue
1719
-
1870
+
1720
1871
  # Check which zone this detection is in
1721
1872
  for zone_name, zone_polygon in zones.items():
1722
1873
  if self._is_in_zone(bbox, zone_polygon):
1723
1874
  current_frame_zone_tracks[zone_name].add(track_id)
1724
1875
  if track_id not in self.color_det_dict: # Use color_det_dict for consistency
1725
1876
  self.color_det_dict[track_id] = [detection.get("main_color", "unknown"), detection.get("confidence", 0.0)]
1726
-
1877
+
1727
1878
  # Update zone tracking for each zone
1728
1879
  for zone_name, zone_counts in zone_analysis.items():
1729
1880
  current_tracks = current_frame_zone_tracks.get(zone_name, set())
@@ -1731,7 +1882,7 @@ class ColorDetectionUseCase(BaseProcessor):
1731
1882
  self._zone_total_track_ids[zone_name].update(current_tracks)
1732
1883
  self._zone_current_counts[zone_name] = len(current_tracks)
1733
1884
  self._zone_total_counts[zone_name] = len(self._zone_total_track_ids[zone_name])
1734
-
1885
+
1735
1886
  enhanced_zone_analysis[zone_name] = {
1736
1887
  "current_count": self._zone_current_counts[zone_name],
1737
1888
  "total_count": self._zone_total_counts[zone_name],
@@ -1739,7 +1890,7 @@ class ColorDetectionUseCase(BaseProcessor):
1739
1890
  "total_track_ids": list(self._zone_total_track_ids[zone_name]),
1740
1891
  "original_counts": zone_counts
1741
1892
  }
1742
-
1893
+
1743
1894
  return enhanced_zone_analysis
1744
1895
 
1745
1896
  def _compute_iou(self, box1: Any, box2: Any) -> float: