matrice-analytics 0.1.60__py3-none-any.whl → 0.1.89__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.
Files changed (21) hide show
  1. matrice_analytics/post_processing/config.py +2 -2
  2. matrice_analytics/post_processing/core/base.py +1 -1
  3. matrice_analytics/post_processing/face_reg/embedding_manager.py +8 -8
  4. matrice_analytics/post_processing/face_reg/face_recognition.py +886 -201
  5. matrice_analytics/post_processing/face_reg/face_recognition_client.py +68 -2
  6. matrice_analytics/post_processing/usecases/advanced_customer_service.py +908 -498
  7. matrice_analytics/post_processing/usecases/color_detection.py +18 -18
  8. matrice_analytics/post_processing/usecases/customer_service.py +356 -9
  9. matrice_analytics/post_processing/usecases/fire_detection.py +149 -11
  10. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +548 -40
  11. matrice_analytics/post_processing/usecases/people_counting.py +11 -11
  12. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +34 -34
  13. matrice_analytics/post_processing/usecases/weapon_detection.py +98 -22
  14. matrice_analytics/post_processing/utils/alert_instance_utils.py +950 -0
  15. matrice_analytics/post_processing/utils/business_metrics_manager_utils.py +1245 -0
  16. matrice_analytics/post_processing/utils/incident_manager_utils.py +1657 -0
  17. {matrice_analytics-0.1.60.dist-info → matrice_analytics-0.1.89.dist-info}/METADATA +1 -1
  18. {matrice_analytics-0.1.60.dist-info → matrice_analytics-0.1.89.dist-info}/RECORD +21 -18
  19. {matrice_analytics-0.1.60.dist-info → matrice_analytics-0.1.89.dist-info}/WHEEL +0 -0
  20. {matrice_analytics-0.1.60.dist-info → matrice_analytics-0.1.89.dist-info}/licenses/LICENSE.txt +0 -0
  21. {matrice_analytics-0.1.60.dist-info → matrice_analytics-0.1.89.dist-info}/top_level.txt +0 -0
@@ -333,20 +333,20 @@ class PeopleCountingUseCase(BaseProcessor):
333
333
  })
334
334
 
335
335
  human_text_lines = []
336
- human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}")
336
+ human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}:")
337
337
  for cat, count in per_category_count.items():
338
338
  human_text_lines.append(f"\t- People Detected: {count}")
339
339
  human_text_lines.append("")
340
- human_text_lines.append(f"TOTAL SINCE {start_timestamp}")
341
- for cat, count in total_counts_dict.items():
342
- if count > 0:
343
- human_text_lines.append("")
344
- human_text_lines.append(f"\t- Total unique people count: {count}")
345
- if alerts:
346
- for alert in alerts:
347
- human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
348
- else:
349
- human_text_lines.append("Alerts: None")
340
+ # human_text_lines.append(f"TOTAL SINCE {start_timestamp}")
341
+ # for cat, count in total_counts_dict.items():
342
+ # if count > 0:
343
+ # human_text_lines.append("")
344
+ # human_text_lines.append(f"\t- Total unique people count: {count}")
345
+ # if alerts:
346
+ # for alert in alerts:
347
+ # human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
348
+ # else:
349
+ # human_text_lines.append("Alerts: None")
350
350
  human_text = "\n".join(human_text_lines)
351
351
 
352
352
  reset_settings = [{"interval_type": "daily", "reset_time": {"value": 9, "time_unit": "hour"}}]
@@ -633,40 +633,40 @@ class VehicleMonitoringUseCase(BaseProcessor):
633
633
  human_text_lines.append(f"\t\t- {cat}: {count}")
634
634
 
635
635
  human_text_lines.append("")
636
- human_text_lines.append(f"TOTAL SINCE @ {start_timestamp}:")
637
-
638
- # Display total counts - zone-wise or category-wise
639
- if zone_analysis:
640
- human_text_lines.append("\t- Total Vehicles by Zone:")
641
- for zone_name, zone_data in zone_analysis.items():
642
- total_count = 0
643
- if isinstance(zone_data, dict):
644
- # Prefer the numeric cumulative total if available
645
- if "total_count" in zone_data and isinstance(zone_data.get("total_count"), (int, float)):
646
- total_count = zone_data.get("total_count", 0)
647
- # Fallback: compute from list of total_track_ids if present
648
- elif "total_track_ids" in zone_data and isinstance(zone_data.get("total_track_ids"), list):
649
- total_count = len(zone_data.get("total_track_ids", []))
650
- else:
651
- # Last resort: try to sum numeric values present
652
- counts_dict = zone_data if isinstance(zone_data, dict) else {}
653
- total_count = sum(v for v in counts_dict.values() if isinstance(v, (int, float)))
654
- human_text_lines.append(f"\t\t- {zone_name}: {int(total_count)}")
655
- else:
656
- if total_counts_dict:
657
- human_text_lines.append("\t- Total Unique Vehicles:")
658
- for cat, count in total_counts_dict.items():
659
- if count > 0:
660
- human_text_lines.append(f"\t\t- {cat}: {count}")
661
-
662
- # Display alerts
663
- if alerts:
664
- human_text_lines.append("")
665
- for alert in alerts:
666
- human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
667
- else:
668
- human_text_lines.append("")
669
- human_text_lines.append("Alerts: None")
636
+ # human_text_lines.append(f"TOTAL SINCE @ {start_timestamp}:")
637
+
638
+ # # Display total counts - zone-wise or category-wise
639
+ # if zone_analysis:
640
+ # human_text_lines.append("\t- Total Vehicles by Zone:")
641
+ # for zone_name, zone_data in zone_analysis.items():
642
+ # total_count = 0
643
+ # if isinstance(zone_data, dict):
644
+ # # Prefer the numeric cumulative total if available
645
+ # if "total_count" in zone_data and isinstance(zone_data.get("total_count"), (int, float)):
646
+ # total_count = zone_data.get("total_count", 0)
647
+ # # Fallback: compute from list of total_track_ids if present
648
+ # elif "total_track_ids" in zone_data and isinstance(zone_data.get("total_track_ids"), list):
649
+ # total_count = len(zone_data.get("total_track_ids", []))
650
+ # else:
651
+ # # Last resort: try to sum numeric values present
652
+ # counts_dict = zone_data if isinstance(zone_data, dict) else {}
653
+ # total_count = sum(v for v in counts_dict.values() if isinstance(v, (int, float)))
654
+ # human_text_lines.append(f"\t\t- {zone_name}: {int(total_count)}")
655
+ # else:
656
+ # if total_counts_dict:
657
+ # human_text_lines.append("\t- Total Unique Vehicles:")
658
+ # for cat, count in total_counts_dict.items():
659
+ # if count > 0:
660
+ # human_text_lines.append(f"\t\t- {cat}: {count}")
661
+
662
+ # # Display alerts
663
+ # if alerts:
664
+ # human_text_lines.append("")
665
+ # for alert in alerts:
666
+ # human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
667
+ # else:
668
+ # human_text_lines.append("")
669
+ # human_text_lines.append("Alerts: None")
670
670
 
671
671
  human_text = "\n".join(human_text_lines)
672
672
 
@@ -585,23 +585,6 @@ class WeaponDetectionUseCase(BaseProcessor):
585
585
  """Return total unique track_id count for each category."""
586
586
  return {cat: len(ids) for cat, ids in getattr(self, '_per_category_total_track_ids', {}).items()}
587
587
 
588
- def _format_timestamp(self, timestamp: Any) -> str:
589
- """Format a timestamp so that exactly two digits follow the decimal point (milliseconds)."""
590
- if isinstance(timestamp, (int, float)):
591
- timestamp = datetime.fromtimestamp(timestamp, timezone.utc).strftime('%Y-%m-%d-%H:%M:%S.%f UTC')
592
- if not isinstance(timestamp, str):
593
- return str(timestamp)
594
- if '.' not in timestamp:
595
- return timestamp
596
- main_part, fractional_and_suffix = timestamp.split('.', 1)
597
- if ' ' in fractional_and_suffix:
598
- fractional_part, suffix = fractional_and_suffix.split(' ', 1)
599
- suffix = ' ' + suffix
600
- else:
601
- fractional_part, suffix = fractional_and_suffix, ''
602
- fractional_part = (fractional_part + '00')[:2]
603
- return f"{main_part}.{fractional_part}{suffix}"
604
-
605
588
  def _format_timestamp_for_stream(self, timestamp: float) -> str:
606
589
  """Format timestamp for streams (YYYY:MM:DD HH:MM:SS format)."""
607
590
  dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
@@ -614,8 +597,56 @@ class WeaponDetectionUseCase(BaseProcessor):
614
597
  seconds = round(float(timestamp % 60), 2)
615
598
  return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
616
599
 
600
+ def _format_timestamp(self, timestamp: Any) -> str:
601
+ """Format a timestamp to match the current timestamp format: YYYY:MM:DD HH:MM:SS.
602
+
603
+ The input can be either:
604
+ 1. A numeric Unix timestamp (``float`` / ``int``) – it will be converted to datetime.
605
+ 2. A string in the format ``YYYY-MM-DD-HH:MM:SS.ffffff UTC``.
606
+
607
+ The returned value will be in the format: YYYY:MM:DD HH:MM:SS (no milliseconds, no UTC suffix).
608
+
609
+ Example
610
+ -------
611
+ >>> self._format_timestamp("2025-10-27-19:31:20.187574 UTC")
612
+ '2025:10:27 19:31:20'
613
+ """
614
+
615
+ # Convert numeric timestamps to datetime first
616
+ if isinstance(timestamp, (int, float)):
617
+ dt = datetime.fromtimestamp(timestamp, timezone.utc)
618
+ return dt.strftime('%Y:%m:%d %H:%M:%S')
619
+
620
+ # Ensure we are working with a string from here on
621
+ if not isinstance(timestamp, str):
622
+ return str(timestamp)
623
+
624
+ # Remove ' UTC' suffix if present
625
+ timestamp_clean = timestamp.replace(' UTC', '').strip()
626
+
627
+ # Remove milliseconds if present (everything after the last dot)
628
+ if '.' in timestamp_clean:
629
+ timestamp_clean = timestamp_clean.split('.')[0]
630
+
631
+ # Parse the timestamp string and convert to desired format
632
+ try:
633
+ # Handle format: YYYY-MM-DD-HH:MM:SS
634
+ if timestamp_clean.count('-') >= 2:
635
+ # Replace first two dashes with colons for date part, third with space
636
+ parts = timestamp_clean.split('-')
637
+ if len(parts) >= 4:
638
+ # parts = ['2025', '10', '27', '19:31:20']
639
+ formatted = f"{parts[0]}:{parts[1]}:{parts[2]} {'-'.join(parts[3:])}"
640
+ return formatted
641
+ except Exception:
642
+ pass
643
+
644
+ # If parsing fails, return the cleaned string as-is
645
+ return timestamp_clean
646
+
617
647
  def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
618
648
  """Get formatted current timestamp based on stream type."""
649
+
619
650
  if not stream_info:
620
651
  return "00:00:00.00"
621
652
  if precision:
@@ -625,15 +656,20 @@ class WeaponDetectionUseCase(BaseProcessor):
625
656
  else:
626
657
  start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
627
658
  stream_time_str = self._format_timestamp_for_video(start_time)
659
+
628
660
  return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
629
661
  else:
630
662
  return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
663
+
631
664
  if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
632
665
  if frame_id:
633
666
  start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
634
667
  else:
635
668
  start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
669
+
636
670
  stream_time_str = self._format_timestamp_for_video(start_time)
671
+
672
+
637
673
  return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
638
674
  else:
639
675
  stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
@@ -652,24 +688,62 @@ class WeaponDetectionUseCase(BaseProcessor):
652
688
  """Get formatted start timestamp for 'TOTAL SINCE' based on stream type."""
653
689
  if not stream_info:
654
690
  return "00:00:00"
691
+
655
692
  if precision:
656
693
  if self.start_timer is None:
657
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
694
+ candidate = stream_info.get("input_settings", {}).get("stream_time")
695
+ if not candidate or candidate == "NA":
696
+ candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
697
+ self.start_timer = candidate
658
698
  return self._format_timestamp(self.start_timer)
659
699
  elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
660
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
700
+ candidate = stream_info.get("input_settings", {}).get("stream_time")
701
+ if not candidate or candidate == "NA":
702
+ candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
703
+ self.start_timer = candidate
661
704
  return self._format_timestamp(self.start_timer)
662
705
  else:
663
706
  return self._format_timestamp(self.start_timer)
707
+
664
708
  if self.start_timer is None:
665
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
709
+ # Prefer direct input_settings.stream_time if available and not NA
710
+ candidate = stream_info.get("input_settings", {}).get("stream_time")
711
+ if not candidate or candidate == "NA":
712
+ # Fallback to nested stream_info.stream_time used by current timestamp path
713
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
714
+ if stream_time_str:
715
+ try:
716
+ timestamp_str = stream_time_str.replace(" UTC", "")
717
+ dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
718
+ self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
719
+ candidate = datetime.fromtimestamp(self._tracking_start_time, timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
720
+ except:
721
+ candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
722
+ else:
723
+ candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
724
+ self.start_timer = candidate
666
725
  return self._format_timestamp(self.start_timer)
667
726
  elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
668
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
727
+ candidate = stream_info.get("input_settings", {}).get("stream_time")
728
+ if not candidate or candidate == "NA":
729
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
730
+ if stream_time_str:
731
+ try:
732
+ timestamp_str = stream_time_str.replace(" UTC", "")
733
+ dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
734
+ ts = dt.replace(tzinfo=timezone.utc).timestamp()
735
+ candidate = datetime.fromtimestamp(ts, timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
736
+ except:
737
+ candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
738
+ else:
739
+ candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
740
+ self.start_timer = candidate
669
741
  return self._format_timestamp(self.start_timer)
742
+
670
743
  else:
671
- if self.start_timer is not None:
744
+ if self.start_timer is not None and self.start_timer != "NA":
672
745
  return self._format_timestamp(self.start_timer)
746
+
673
747
  if self._tracking_start_time is None:
674
748
  stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
675
749
  if stream_time_str:
@@ -681,10 +755,12 @@ class WeaponDetectionUseCase(BaseProcessor):
681
755
  self._tracking_start_time = time.time()
682
756
  else:
683
757
  self._tracking_start_time = time.time()
758
+
684
759
  dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
685
760
  dt = dt.replace(minute=0, second=0, microsecond=0)
686
761
  return dt.strftime('%Y:%m:%d %H:%M:%S')
687
762
 
763
+
688
764
  def _compute_iou(self, box1: Any, box2: Any) -> float:
689
765
  """Compute IoU between two bounding boxes."""
690
766
  def _bbox_to_list(bbox):