matrice-analytics 0.1.60__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 (196) hide show
  1. matrice_analytics/__init__.py +28 -0
  2. matrice_analytics/boundary_drawing_internal/README.md +305 -0
  3. matrice_analytics/boundary_drawing_internal/__init__.py +45 -0
  4. matrice_analytics/boundary_drawing_internal/boundary_drawing_internal.py +1207 -0
  5. matrice_analytics/boundary_drawing_internal/boundary_drawing_tool.py +429 -0
  6. matrice_analytics/boundary_drawing_internal/boundary_tool_template.html +1036 -0
  7. matrice_analytics/boundary_drawing_internal/data/.gitignore +12 -0
  8. matrice_analytics/boundary_drawing_internal/example_usage.py +206 -0
  9. matrice_analytics/boundary_drawing_internal/usage/README.md +110 -0
  10. matrice_analytics/boundary_drawing_internal/usage/boundary_drawer_launcher.py +102 -0
  11. matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py +107 -0
  12. matrice_analytics/post_processing/README.md +455 -0
  13. matrice_analytics/post_processing/__init__.py +732 -0
  14. matrice_analytics/post_processing/advanced_tracker/README.md +650 -0
  15. matrice_analytics/post_processing/advanced_tracker/__init__.py +17 -0
  16. matrice_analytics/post_processing/advanced_tracker/base.py +99 -0
  17. matrice_analytics/post_processing/advanced_tracker/config.py +77 -0
  18. matrice_analytics/post_processing/advanced_tracker/kalman_filter.py +370 -0
  19. matrice_analytics/post_processing/advanced_tracker/matching.py +195 -0
  20. matrice_analytics/post_processing/advanced_tracker/strack.py +230 -0
  21. matrice_analytics/post_processing/advanced_tracker/tracker.py +367 -0
  22. matrice_analytics/post_processing/config.py +146 -0
  23. matrice_analytics/post_processing/core/__init__.py +63 -0
  24. matrice_analytics/post_processing/core/base.py +704 -0
  25. matrice_analytics/post_processing/core/config.py +3291 -0
  26. matrice_analytics/post_processing/core/config_utils.py +925 -0
  27. matrice_analytics/post_processing/face_reg/__init__.py +43 -0
  28. matrice_analytics/post_processing/face_reg/compare_similarity.py +556 -0
  29. matrice_analytics/post_processing/face_reg/embedding_manager.py +950 -0
  30. matrice_analytics/post_processing/face_reg/face_recognition.py +2234 -0
  31. matrice_analytics/post_processing/face_reg/face_recognition_client.py +606 -0
  32. matrice_analytics/post_processing/face_reg/people_activity_logging.py +321 -0
  33. matrice_analytics/post_processing/ocr/__init__.py +0 -0
  34. matrice_analytics/post_processing/ocr/easyocr_extractor.py +250 -0
  35. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
  36. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
  37. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
  38. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
  39. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
  40. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
  41. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
  42. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
  43. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
  44. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
  45. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
  46. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
  47. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
  48. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
  49. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
  50. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
  51. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
  52. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
  53. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
  54. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
  55. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
  56. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
  57. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
  58. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
  59. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
  60. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
  61. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
  62. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
  63. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
  64. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
  65. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
  66. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
  67. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
  68. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
  69. matrice_analytics/post_processing/ocr/postprocessing.py +270 -0
  70. matrice_analytics/post_processing/ocr/preprocessing.py +52 -0
  71. matrice_analytics/post_processing/post_processor.py +1175 -0
  72. matrice_analytics/post_processing/test_cases/__init__.py +1 -0
  73. matrice_analytics/post_processing/test_cases/run_tests.py +143 -0
  74. matrice_analytics/post_processing/test_cases/test_advanced_customer_service.py +841 -0
  75. matrice_analytics/post_processing/test_cases/test_basic_counting_tracking.py +523 -0
  76. matrice_analytics/post_processing/test_cases/test_comprehensive.py +531 -0
  77. matrice_analytics/post_processing/test_cases/test_config.py +852 -0
  78. matrice_analytics/post_processing/test_cases/test_customer_service.py +585 -0
  79. matrice_analytics/post_processing/test_cases/test_data_generators.py +583 -0
  80. matrice_analytics/post_processing/test_cases/test_people_counting.py +510 -0
  81. matrice_analytics/post_processing/test_cases/test_processor.py +524 -0
  82. matrice_analytics/post_processing/test_cases/test_usecases.py +165 -0
  83. matrice_analytics/post_processing/test_cases/test_utilities.py +356 -0
  84. matrice_analytics/post_processing/test_cases/test_utils.py +743 -0
  85. matrice_analytics/post_processing/usecases/Histopathological_Cancer_Detection_img.py +604 -0
  86. matrice_analytics/post_processing/usecases/__init__.py +267 -0
  87. matrice_analytics/post_processing/usecases/abandoned_object_detection.py +797 -0
  88. matrice_analytics/post_processing/usecases/advanced_customer_service.py +1601 -0
  89. matrice_analytics/post_processing/usecases/age_detection.py +842 -0
  90. matrice_analytics/post_processing/usecases/age_gender_detection.py +1085 -0
  91. matrice_analytics/post_processing/usecases/anti_spoofing_detection.py +656 -0
  92. matrice_analytics/post_processing/usecases/assembly_line_detection.py +841 -0
  93. matrice_analytics/post_processing/usecases/banana_defect_detection.py +624 -0
  94. matrice_analytics/post_processing/usecases/basic_counting_tracking.py +667 -0
  95. matrice_analytics/post_processing/usecases/blood_cancer_detection_img.py +881 -0
  96. matrice_analytics/post_processing/usecases/car_damage_detection.py +834 -0
  97. matrice_analytics/post_processing/usecases/car_part_segmentation.py +946 -0
  98. matrice_analytics/post_processing/usecases/car_service.py +1601 -0
  99. matrice_analytics/post_processing/usecases/cardiomegaly_classification.py +864 -0
  100. matrice_analytics/post_processing/usecases/cell_microscopy_segmentation.py +897 -0
  101. matrice_analytics/post_processing/usecases/chicken_pose_detection.py +648 -0
  102. matrice_analytics/post_processing/usecases/child_monitoring.py +814 -0
  103. matrice_analytics/post_processing/usecases/color/clip.py +660 -0
  104. matrice_analytics/post_processing/usecases/color/clip_processor/merges.txt +48895 -0
  105. matrice_analytics/post_processing/usecases/color/clip_processor/preprocessor_config.json +28 -0
  106. matrice_analytics/post_processing/usecases/color/clip_processor/special_tokens_map.json +30 -0
  107. matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer.json +245079 -0
  108. matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer_config.json +32 -0
  109. matrice_analytics/post_processing/usecases/color/clip_processor/vocab.json +1 -0
  110. matrice_analytics/post_processing/usecases/color/color_map_utils.py +70 -0
  111. matrice_analytics/post_processing/usecases/color/color_mapper.py +468 -0
  112. matrice_analytics/post_processing/usecases/color_detection.py +1936 -0
  113. matrice_analytics/post_processing/usecases/color_map_utils.py +70 -0
  114. matrice_analytics/post_processing/usecases/concrete_crack_detection.py +827 -0
  115. matrice_analytics/post_processing/usecases/crop_weed_detection.py +781 -0
  116. matrice_analytics/post_processing/usecases/customer_service.py +1008 -0
  117. matrice_analytics/post_processing/usecases/defect_detection_products.py +936 -0
  118. matrice_analytics/post_processing/usecases/distracted_driver_detection.py +822 -0
  119. matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +585 -0
  120. matrice_analytics/post_processing/usecases/drowsy_driver_detection.py +829 -0
  121. matrice_analytics/post_processing/usecases/dwell_detection.py +829 -0
  122. matrice_analytics/post_processing/usecases/emergency_vehicle_detection.py +827 -0
  123. matrice_analytics/post_processing/usecases/face_emotion.py +813 -0
  124. matrice_analytics/post_processing/usecases/face_recognition.py +827 -0
  125. matrice_analytics/post_processing/usecases/fashion_detection.py +835 -0
  126. matrice_analytics/post_processing/usecases/field_mapping.py +902 -0
  127. matrice_analytics/post_processing/usecases/fire_detection.py +1146 -0
  128. matrice_analytics/post_processing/usecases/flare_analysis.py +836 -0
  129. matrice_analytics/post_processing/usecases/flower_segmentation.py +1006 -0
  130. matrice_analytics/post_processing/usecases/gas_leak_detection.py +837 -0
  131. matrice_analytics/post_processing/usecases/gender_detection.py +832 -0
  132. matrice_analytics/post_processing/usecases/human_activity_recognition.py +871 -0
  133. matrice_analytics/post_processing/usecases/intrusion_detection.py +1672 -0
  134. matrice_analytics/post_processing/usecases/leaf.py +821 -0
  135. matrice_analytics/post_processing/usecases/leaf_disease.py +840 -0
  136. matrice_analytics/post_processing/usecases/leak_detection.py +837 -0
  137. matrice_analytics/post_processing/usecases/license_plate_detection.py +1188 -0
  138. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +1781 -0
  139. matrice_analytics/post_processing/usecases/litter_monitoring.py +717 -0
  140. matrice_analytics/post_processing/usecases/mask_detection.py +869 -0
  141. matrice_analytics/post_processing/usecases/natural_disaster.py +907 -0
  142. matrice_analytics/post_processing/usecases/parking.py +787 -0
  143. matrice_analytics/post_processing/usecases/parking_space_detection.py +822 -0
  144. matrice_analytics/post_processing/usecases/pcb_defect_detection.py +888 -0
  145. matrice_analytics/post_processing/usecases/pedestrian_detection.py +808 -0
  146. matrice_analytics/post_processing/usecases/people_counting.py +706 -0
  147. matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
  148. matrice_analytics/post_processing/usecases/people_tracking.py +1842 -0
  149. matrice_analytics/post_processing/usecases/pipeline_detection.py +605 -0
  150. matrice_analytics/post_processing/usecases/plaque_segmentation_img.py +874 -0
  151. matrice_analytics/post_processing/usecases/pothole_segmentation.py +915 -0
  152. matrice_analytics/post_processing/usecases/ppe_compliance.py +645 -0
  153. matrice_analytics/post_processing/usecases/price_tag_detection.py +822 -0
  154. matrice_analytics/post_processing/usecases/proximity_detection.py +1901 -0
  155. matrice_analytics/post_processing/usecases/road_lane_detection.py +623 -0
  156. matrice_analytics/post_processing/usecases/road_traffic_density.py +832 -0
  157. matrice_analytics/post_processing/usecases/road_view_segmentation.py +915 -0
  158. matrice_analytics/post_processing/usecases/shelf_inventory_detection.py +583 -0
  159. matrice_analytics/post_processing/usecases/shoplifting_detection.py +822 -0
  160. matrice_analytics/post_processing/usecases/shopping_cart_analysis.py +899 -0
  161. matrice_analytics/post_processing/usecases/skin_cancer_classification_img.py +864 -0
  162. matrice_analytics/post_processing/usecases/smoker_detection.py +833 -0
  163. matrice_analytics/post_processing/usecases/solar_panel.py +810 -0
  164. matrice_analytics/post_processing/usecases/suspicious_activity_detection.py +1030 -0
  165. matrice_analytics/post_processing/usecases/template_usecase.py +380 -0
  166. matrice_analytics/post_processing/usecases/theft_detection.py +648 -0
  167. matrice_analytics/post_processing/usecases/traffic_sign_monitoring.py +724 -0
  168. matrice_analytics/post_processing/usecases/underground_pipeline_defect_detection.py +775 -0
  169. matrice_analytics/post_processing/usecases/underwater_pollution_detection.py +842 -0
  170. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +1029 -0
  171. matrice_analytics/post_processing/usecases/warehouse_object_segmentation.py +899 -0
  172. matrice_analytics/post_processing/usecases/waterbody_segmentation.py +923 -0
  173. matrice_analytics/post_processing/usecases/weapon_detection.py +771 -0
  174. matrice_analytics/post_processing/usecases/weld_defect_detection.py +615 -0
  175. matrice_analytics/post_processing/usecases/wildlife_monitoring.py +898 -0
  176. matrice_analytics/post_processing/usecases/windmill_maintenance.py +834 -0
  177. matrice_analytics/post_processing/usecases/wound_segmentation.py +856 -0
  178. matrice_analytics/post_processing/utils/__init__.py +150 -0
  179. matrice_analytics/post_processing/utils/advanced_counting_utils.py +400 -0
  180. matrice_analytics/post_processing/utils/advanced_helper_utils.py +317 -0
  181. matrice_analytics/post_processing/utils/advanced_tracking_utils.py +461 -0
  182. matrice_analytics/post_processing/utils/alerting_utils.py +213 -0
  183. matrice_analytics/post_processing/utils/category_mapping_utils.py +94 -0
  184. matrice_analytics/post_processing/utils/color_utils.py +592 -0
  185. matrice_analytics/post_processing/utils/counting_utils.py +182 -0
  186. matrice_analytics/post_processing/utils/filter_utils.py +261 -0
  187. matrice_analytics/post_processing/utils/format_utils.py +293 -0
  188. matrice_analytics/post_processing/utils/geometry_utils.py +300 -0
  189. matrice_analytics/post_processing/utils/smoothing_utils.py +358 -0
  190. matrice_analytics/post_processing/utils/tracking_utils.py +234 -0
  191. matrice_analytics/py.typed +0 -0
  192. matrice_analytics-0.1.60.dist-info/METADATA +481 -0
  193. matrice_analytics-0.1.60.dist-info/RECORD +196 -0
  194. matrice_analytics-0.1.60.dist-info/WHEEL +5 -0
  195. matrice_analytics-0.1.60.dist-info/licenses/LICENSE.txt +21 -0
  196. matrice_analytics-0.1.60.dist-info/top_level.txt +1 -0
@@ -0,0 +1,797 @@
1
+ from typing import Any, Dict, List, Optional, Tuple
2
+ from dataclasses import asdict
3
+ import time
4
+ from datetime import datetime, timezone
5
+ import math
6
+
7
+ from ..core.base import BaseProcessor, ProcessingContext, ProcessingResult, ConfigProtocol
8
+ from ..utils import (
9
+ filter_by_confidence,
10
+ apply_category_mapping,
11
+ count_objects_by_category,
12
+ count_objects_in_zones,
13
+ calculate_counting_summary,
14
+ match_results_structure,
15
+ bbox_smoothing,
16
+ BBoxSmoothingConfig,
17
+ BBoxSmoothingTracker,
18
+ get_bbox_center,
19
+ point_in_polygon
20
+ )
21
+ from dataclasses import dataclass, field
22
+ from ..core.config import BaseConfig, AlertConfig
23
+
24
+ @dataclass
25
+ class AbandonedObjectConfig(BaseConfig):
26
+ """Configuration for abandoned object detection use case."""
27
+ enable_smoothing: bool = True
28
+ centroid_threshold: float = 30.0
29
+ proximity_threshold: float = 50.0 # New: Distance threshold for person proximity
30
+ proximity_hysteresis_frames: int = 5 # New: Frames required for proximity reset
31
+ smoothing_algorithm: str = "observability"
32
+ smoothing_window_size: int = 20
33
+ smoothing_cooldown_frames: int = 5
34
+ smoothing_confidence_range_factor: float = 0.5
35
+ confidence_threshold: float = 0.4
36
+ stationary_threshold_frames: int = 30
37
+ usecase_categories: List[str] = field(default_factory=lambda: ['backpack', 'handbag', 'suitcase'])
38
+ target_categories: List[str] = field(default_factory=lambda: ['handbag'])
39
+ alert_config: Optional[AlertConfig] = None
40
+ zone_config: Optional[Dict[str, Dict[str, List[List[float]]]]] = None
41
+ index_to_category: Optional[Dict[int, str]] = field(
42
+ default_factory=lambda: {24: "backpack", 26: "handbag", 28: "suitcase"}
43
+ )
44
+ person_category: str = "person"
45
+ person_index: int = 0
46
+
47
+ class AbandonedObjectDetectionUseCase(BaseProcessor):
48
+ CATEGORY_DISPLAY = {"backpack": "Backpack", "handbag": "Handbag", "suitcase": "Suitcase"}
49
+
50
+ def __init__(self):
51
+ super().__init__("abandoned_object_detection")
52
+ self.category = "security"
53
+ self.CASE_TYPE: Optional[str] = 'abandoned_object_detection'
54
+ self.CASE_VERSION: Optional[str] = '1.0'
55
+ self.target_categories = ['handbag']
56
+ self.smoothing_tracker = None
57
+ self.tracker = None
58
+ self._total_frame_counter = 0
59
+ self._global_frame_offset = 0
60
+ self._tracking_start_time = None
61
+ self._track_aliases: Dict[Any, Any] = {}
62
+ self._canonical_tracks: Dict[Any, Dict[str, Any]] = {}
63
+ self._track_merge_iou_threshold: float = 0.05
64
+ self._track_merge_time_window: float = 7.0
65
+ self._ascending_alert_list: List[int] = []
66
+ self.current_incident_end_timestamp: str = "N/A"
67
+ self._stationary_tracks: Dict[Any, Dict[str, Any]] = {}
68
+ self._zone_current_track_ids: Dict[str, set] = {}
69
+ self._zone_total_track_ids: Dict[str, set] = {}
70
+ self._zone_current_counts: Dict[str, int] = {}
71
+ self._zone_total_counts: Dict[str, int] = {}
72
+ self.start_timer = None
73
+
74
+ def process(self, data: Any, config: ConfigProtocol, context: Optional[ProcessingContext] = None,
75
+ stream_info: Optional[Dict[str, Any]] = None) -> ProcessingResult:
76
+ start_time = time.time()
77
+ if not isinstance(config, AbandonedObjectConfig):
78
+ return self.create_error_result("Invalid config type", usecase=self.name, category=self.category, context=context)
79
+ if context is None:
80
+ context = ProcessingContext()
81
+
82
+ input_format = match_results_structure(data)
83
+ context.input_format = input_format
84
+ context.confidence_threshold = config.confidence_threshold
85
+
86
+ processed_data = filter_by_confidence(data, config.confidence_threshold)
87
+ if config.index_to_category:
88
+ processed_data = apply_category_mapping(processed_data, config.index_to_category)
89
+
90
+ person_dets = [d for d in processed_data if d.get('category') == config.person_category]
91
+ object_dets = [d for d in processed_data if d.get('category') in self.target_categories]
92
+
93
+ if config.enable_smoothing:
94
+ if self.smoothing_tracker is None:
95
+ smoothing_config = BBoxSmoothingConfig(
96
+ smoothing_algorithm=config.smoothing_algorithm,
97
+ window_size=config.smoothing_window_size,
98
+ cooldown_frames=config.smoothing_cooldown_frames,
99
+ confidence_threshold=config.confidence_threshold,
100
+ confidence_range_factor=config.smoothing_confidence_range_factor,
101
+ enable_smoothing=True
102
+ )
103
+ self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
104
+ processed_data = bbox_smoothing(processed_data, self.smoothing_tracker.config, self.smoothing_tracker)
105
+
106
+ try:
107
+ from ..advanced_tracker import AdvancedTracker
108
+ from ..advanced_tracker.config import TrackerConfig
109
+ if self.tracker is None:
110
+ tracker_config = TrackerConfig()
111
+ self.tracker = AdvancedTracker(tracker_config)
112
+ processed_data = self.tracker.update(processed_data)
113
+ except Exception as e:
114
+ self.logger.warning(f"AdvancedTracker failed: {e}")
115
+
116
+ person_dets = [d for d in processed_data if d.get('category') == config.person_category]
117
+ object_dets = [d for d in processed_data if d.get('category') in self.target_categories]
118
+ abandoned_data = self._check_abandoned_objects(object_dets, person_dets, config)
119
+ self._update_tracking_state(abandoned_data)
120
+ self._total_frame_counter += 1
121
+
122
+ frame_number = None
123
+ if stream_info:
124
+ input_settings = stream_info.get("input_settings", {})
125
+ start_frame = input_settings.get("start_frame")
126
+ end_frame = input_settings.get("end_frame")
127
+ if start_frame is not None and end_frame is not None and start_frame == end_frame:
128
+ frame_number = start_frame
129
+
130
+ counting_summary = self._count_categories(abandoned_data, config)
131
+ total_counts = self.get_total_counts()
132
+ counting_summary['total_counts'] = total_counts
133
+
134
+ zone_analysis = {}
135
+ if config.zone_config and config.zone_config['zones']:
136
+ frame_data = object_dets
137
+ zone_analysis = count_objects_in_zones(frame_data, config.zone_config['zones'])
138
+ if zone_analysis:
139
+ zone_analysis = self._update_zone_tracking(zone_analysis, abandoned_data, config)
140
+
141
+ alerts = self._check_alerts(counting_summary, zone_analysis, frame_number, config)
142
+ predictions = self._extract_predictions(abandoned_data)
143
+
144
+ incidents_list = self._generate_incidents(counting_summary, zone_analysis, alerts, config, frame_number, stream_info)
145
+ tracking_stats_list = self._generate_tracking_stats(counting_summary, zone_analysis, alerts, config, frame_number, stream_info)
146
+ business_analytics_list = self._generate_business_analytics(counting_summary, zone_analysis, alerts, config, stream_info, is_empty=True)
147
+ summary_list = self._generate_summary(counting_summary, zone_analysis, incidents_list, tracking_stats_list, business_analytics_list, alerts)
148
+
149
+ incidents = incidents_list[0] if incidents_list else {}
150
+ tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
151
+ business_analytics = business_analytics_list[0] if business_analytics_list else {}
152
+ summary = summary_list[0] if summary_list else {}
153
+ agg_summary = {str(frame_number): {
154
+ "incidents": incidents,
155
+ "tracking_stats": tracking_stats,
156
+ "business_analytics": business_analytics,
157
+ "alerts": alerts,
158
+ "zone_analysis": zone_analysis,
159
+ "human_text": summary}
160
+ }
161
+
162
+ context.mark_completed()
163
+ result = self.create_result(
164
+ data={"agg_summary": agg_summary},
165
+ usecase=self.name,
166
+ category=self.category,
167
+ context=context
168
+ )
169
+ return result
170
+
171
+ def _check_abandoned_objects(self, object_dets: List[Dict], person_dets: List[Dict], config: AbandonedObjectConfig) -> List[Dict]:
172
+ abandoned_data = []
173
+ current_time = time.time()
174
+
175
+ for det in object_dets:
176
+ track_id = det.get('track_id')
177
+ bbox = det.get('bounding_box')
178
+ if not track_id or not bbox:
179
+ continue
180
+
181
+ centroid = self._calculate_centroid(bbox)
182
+ in_zone = self._is_in_zone(bbox, config.zone_config['zones'] if config.zone_config else None)
183
+ is_near_person = False
184
+
185
+ # Proximity check: Calculate distance to nearest person's centroid
186
+ for person in person_dets:
187
+ person_bbox = person.get('bounding_box')
188
+ if person_bbox:
189
+ person_centroid = self._calculate_centroid(person_bbox)
190
+ distance = math.sqrt((centroid[0] - person_centroid[0])**2 + (centroid[1] - person_centroid[1])**2)
191
+ if distance < config.proximity_threshold:
192
+ is_near_person = True
193
+ break
194
+
195
+ if track_id not in self._stationary_tracks:
196
+ self._stationary_tracks[track_id] = {
197
+ 'centroid': centroid,
198
+ 'frame_count': 1,
199
+ 'start_time': current_time,
200
+ 'bbox': bbox,
201
+ 'in_zone': in_zone,
202
+ 'proximal_frames': 0
203
+ }
204
+ track_info = self._stationary_tracks[track_id]
205
+ if track_info['frame_count'] >= config.stationary_threshold_frames and in_zone:
206
+ det['category'] = 'abandoned_object'
207
+ abandoned_data.append(det)
208
+ self.logger.info(f"Marked as abandoned_object: {det}")
209
+ else:
210
+ track_info = self._stationary_tracks[track_id]
211
+
212
+ if is_near_person:
213
+ track_info['proximal_frames'] = track_info.get('proximal_frames', 0) + 1
214
+ if track_info['proximal_frames'] >= config.proximity_hysteresis_frames:
215
+ track_info['frame_count'] = 1
216
+ track_info['start_time'] = current_time
217
+ continue
218
+
219
+ track_info['proximal_frames'] = 0
220
+ prev_centroid = track_info['centroid']
221
+ track_info['frame_count'] += 1
222
+ track_info['bbox'] = bbox
223
+ track_info['in_zone'] = in_zone
224
+
225
+ if self._is_centroid_stationary(centroid, prev_centroid, config.centroid_threshold):
226
+ if track_info['frame_count'] >= config.stationary_threshold_frames and in_zone:
227
+ det['category'] = 'abandoned_object'
228
+ abandoned_data.append(det)
229
+ self.logger.info(f"Marked as abandoned_object: {det}")
230
+ else:
231
+ track_info['frame_count'] = max(1, track_info['frame_count'] - 5)
232
+ track_info['centroid'] = centroid
233
+ track_info['start_time'] = current_time
234
+
235
+ self.logger.info(f"Object: confidence={det.get('confidence')}, category={det.get('category')}, track_id={track_id}, frame_count={track_info['frame_count']}, in_zone={in_zone}, centroid={centroid}, is_near_person={is_near_person}")
236
+
237
+ return abandoned_data
238
+
239
+ def _calculate_centroid(self, bbox: Dict) -> tuple:
240
+ if 'xmin' in bbox:
241
+ x = (bbox['xmin'] + bbox['xmax']) / 2
242
+ y = (bbox['ymin'] + bbox['ymax']) / 2
243
+ elif 'x1' in bbox:
244
+ x = (bbox['x1'] + bbox['x2']) / 2
245
+ y = (bbox['y1'] + bbox['y2']) / 2
246
+ else:
247
+ return (0, 0)
248
+ return (x, y)
249
+
250
+ def _is_centroid_stationary(self, centroid: tuple, prev_centroid: tuple, threshold: float) -> bool:
251
+ x1, y1 = centroid
252
+ x2, y2 = prev_centroid
253
+ distance = ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5
254
+ return distance < threshold
255
+
256
+ def _is_in_zone(self, bbox: Dict, zones: Optional[Dict[str, List[List[float]]]]) -> bool:
257
+ if not zones:
258
+ return True
259
+ center = get_bbox_center(bbox)
260
+ for zone_name, zone_polygon in zones.items():
261
+ polygon_points = [(point[0], point[1]) for point in zone_polygon]
262
+ if point_in_polygon(center, polygon_points):
263
+ return True
264
+ return False
265
+
266
+ def _update_zone_tracking(self, zone_analysis: Dict[str, Dict[str, int]], detections: List[Dict], config: AbandonedObjectConfig) -> Dict[str, Dict[str, Any]]:
267
+ if not zone_analysis or not config.zone_config or not config.zone_config['zones']:
268
+ return {}
269
+
270
+ enhanced_zone_analysis = {}
271
+ zones = config.zone_config['zones']
272
+ current_frame_zone_tracks = {}
273
+
274
+ for zone_name in zones.keys():
275
+ current_frame_zone_tracks[zone_name] = set()
276
+ if zone_name not in self._zone_current_track_ids:
277
+ self._zone_current_track_ids[zone_name] = set()
278
+ if zone_name not in self._zone_total_track_ids:
279
+ self._zone_total_track_ids[zone_name] = set()
280
+
281
+ for detection in detections:
282
+ track_id = detection.get("track_id")
283
+ if track_id is None:
284
+ continue
285
+ bbox = detection.get("bounding_box")
286
+ if not bbox:
287
+ continue
288
+ center_point = get_bbox_center(bbox)
289
+ for zone_name, zone_polygon in zones.items():
290
+ polygon_points = [(point[0], point[1]) for point in zone_polygon]
291
+ if point_in_polygon(center_point, polygon_points):
292
+ current_frame_zone_tracks[zone_name].add(track_id)
293
+
294
+ for zone_name, zone_counts in zone_analysis.items():
295
+ current_tracks = current_frame_zone_tracks.get(zone_name, set())
296
+ self._zone_current_track_ids[zone_name] = current_tracks
297
+ self._zone_total_track_ids[zone_name].update(current_tracks)
298
+ self._zone_current_counts[zone_name] = len(current_tracks)
299
+ self._zone_total_counts[zone_name] = len(self._zone_total_track_ids[zone_name])
300
+ enhanced_zone_analysis[zone_name] = {
301
+ "current_count": self._zone_current_counts[zone_name],
302
+ "total_count": self._zone_total_counts[zone_name],
303
+ "current_track_ids": list(current_tracks),
304
+ "total_track_ids": list(self._zone_total_track_ids[zone_name]),
305
+ "original_counts": zone_counts
306
+ }
307
+
308
+ return enhanced_zone_analysis
309
+
310
+ def _check_alerts(self, summary: dict, zone_analysis: Dict, frame_number: Any, config: AbandonedObjectConfig) -> List[Dict]:
311
+ def get_trend(data, lookback=900, threshold=0.6):
312
+ window = data[-lookback:] if len(data) >= lookback else data
313
+ if len(window) < 2:
314
+ return True
315
+ increasing = sum(1 for i in range(1, len(window)) if window[i] >= window[i - 1])
316
+ ratio = increasing / (len(window) - 1)
317
+ return ratio >= threshold
318
+
319
+ frame_key = str(frame_number) if frame_number is not None else "current_frame"
320
+ alerts = []
321
+ total_detections = summary.get("total_count", 0)
322
+ per_category_count = summary.get("per_category_count", {})
323
+
324
+ if not config.alert_config:
325
+ return alerts
326
+
327
+ if hasattr(config.alert_config, 'count_thresholds') and config.alert_config.count_thresholds:
328
+ for category, threshold in config.alert_config.count_thresholds.items():
329
+ if category == "all" and total_detections > threshold:
330
+ alerts.append({
331
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
332
+ "alert_id": f"alert_{category}_{frame_key}",
333
+ "incident_category": self.CASE_TYPE,
334
+ "threshold_level": threshold,
335
+ "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
336
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
337
+ getattr(config.alert_config, 'alert_value', ['JSON']))}
338
+ })
339
+ elif category == "abandoned_object" and per_category_count.get(category, 0) > threshold:
340
+ alerts.append({
341
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
342
+ "alert_id": f"alert_{category}_{frame_key}",
343
+ "incident_category": self.CASE_TYPE,
344
+ "threshold_level": threshold,
345
+ "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
346
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
347
+ getattr(config.alert_config, 'alert_value', ['JSON']))}
348
+ })
349
+ return alerts
350
+
351
+ def _generate_incidents(self, counting_summary: Dict, zone_analysis: Dict, alerts: List, config: AbandonedObjectConfig,
352
+ frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
353
+ incidents = []
354
+ total_detections = counting_summary.get("total_count", 0)
355
+ current_timestamp = self._get_current_timestamp_str(stream_info)
356
+ camera_info = self.get_camera_info_from_stream(stream_info)
357
+
358
+ self._ascending_alert_list = self._ascending_alert_list[-900:] if len(self._ascending_alert_list) > 900 else self._ascending_alert_list
359
+
360
+ if total_detections > 0:
361
+ level = "low"
362
+ intensity = 5.0
363
+ start_timestamp = self._get_start_timestamp_str(stream_info)
364
+ if start_timestamp and self.current_incident_end_timestamp == 'N/A':
365
+ self.current_incident_end_timestamp = 'Incident still active'
366
+ elif start_timestamp and self.current_incident_end_timestamp == 'Incident still active':
367
+ if len(self._ascending_alert_list) >= 15 and sum(self._ascending_alert_list[-15:]) / 15 < 1.5:
368
+ self.current_incident_end_timestamp = current_timestamp
369
+ elif self.current_incident_end_timestamp != 'Incident still active' and self.current_incident_end_timestamp != 'N/A':
370
+ self.current_incident_end_timestamp = 'N/A'
371
+
372
+ if config.alert_config and config.alert_config.count_thresholds:
373
+ threshold = config.alert_config.count_thresholds.get("all", 15)
374
+ intensity = min(10.0, (total_detections / threshold) * 10)
375
+ if intensity >= 9:
376
+ level = "critical"
377
+ self._ascending_alert_list.append(3)
378
+ elif intensity >= 7:
379
+ level = "significant"
380
+ self._ascending_alert_list.append(2)
381
+ elif intensity >= 5:
382
+ level = "medium"
383
+ self._ascending_alert_list.append(1)
384
+ else:
385
+ level = "low"
386
+ self._ascending_alert_list.append(0)
387
+ else:
388
+ if total_detections > 30:
389
+ level = "critical"
390
+ intensity = 10.0
391
+ self._ascending_alert_list.append(3)
392
+ elif total_detections > 25:
393
+ level = "significant"
394
+ intensity = 9.0
395
+ self._ascending_alert_list.append(2)
396
+ elif total_detections > 15:
397
+ level = "medium"
398
+ intensity = 7.0
399
+ self._ascending_alert_list.append(1)
400
+ else:
401
+ level = "low"
402
+ intensity = min(10.0, total_detections / 3.0)
403
+ self._ascending_alert_list.append(0)
404
+
405
+ human_text_lines = [f"ABANDONED OBJECT DETECTED @ {current_timestamp}:"]
406
+ human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE, level)}")
407
+ human_text = "\n".join(human_text_lines)
408
+
409
+ alert_settings = []
410
+ if config.alert_config and hasattr(config.alert_config, 'alert_type'):
411
+ alert_settings.append({
412
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
413
+ "incident_category": self.CASE_TYPE,
414
+ "threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
415
+ "ascending": True,
416
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
417
+ getattr(config.alert_config, 'alert_value', ['JSON']))}
418
+ })
419
+
420
+ event = self.create_incident(
421
+ incident_id=self.CASE_TYPE + '_' + str(frame_number),
422
+ incident_type=self.CASE_TYPE,
423
+ severity_level=level,
424
+ human_text=human_text,
425
+ camera_info=camera_info,
426
+ alerts=alerts,
427
+ alert_settings=alert_settings,
428
+ start_time=start_timestamp,
429
+ end_time=self.current_incident_end_timestamp,
430
+ level_settings={"low": 1, "medium": 3, "significant": 4, "critical": 7}
431
+ )
432
+ incidents.append(event)
433
+ else:
434
+ self._ascending_alert_list.append(0)
435
+ incidents.append({})
436
+
437
+ return incidents
438
+
439
+ def _generate_tracking_stats(self, counting_summary: Dict, zone_analysis: Dict, alerts: List, config: AbandonedObjectConfig,
440
+ frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
441
+ camera_info = self.get_camera_info_from_stream(stream_info)
442
+ tracking_stats = []
443
+ total_detections = counting_summary.get("total_count", 0)
444
+ total_counts_dict = counting_summary.get("total_counts", {})
445
+ per_category_count = counting_summary.get("per_category_count", {})
446
+ current_timestamp = self._get_current_timestamp_str(stream_info, precision=False)
447
+ start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
448
+ high_precision_start_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
449
+ high_precision_reset_timestamp = self._get_start_timestamp_str(stream_info, precision=True)
450
+
451
+ total_counts = [{"category": cat, "count": count} for cat, count in total_counts_dict.items() if count > 0]
452
+ current_counts = [{"category": cat, "count": count} for cat, count in per_category_count.items() if count > 0 or total_detections > 0]
453
+
454
+ detections = []
455
+ for detection in counting_summary.get("detections", []):
456
+ bbox = detection.get("bounding_box", {})
457
+ category = detection.get("category", "abandoned_object")
458
+ detection_obj = self.create_detection_object(category, bbox)
459
+ detections.append(detection_obj)
460
+
461
+ alert_settings = []
462
+ if config.alert_config and hasattr(config.alert_config, 'alert_type'):
463
+ alert_settings.append({
464
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
465
+ "incident_category": self.CASE_TYPE,
466
+ "threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
467
+ "ascending": True,
468
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
469
+ getattr(config.alert_config, 'alert_value', ['JSON']))}
470
+ })
471
+
472
+ human_text_lines = [f"Tracking Statistics:"]
473
+ human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}")
474
+ if zone_analysis:
475
+ human_text_lines.append("\tZones (current):")
476
+ for zone_name, zone_data in zone_analysis.items():
477
+ current_count = 0
478
+ if isinstance(zone_data, dict):
479
+ if "current_count" in zone_data:
480
+ current_count = zone_data.get("current_count", 0)
481
+ else:
482
+ counts_dict = zone_data.get("original_counts") if isinstance(zone_data.get("original_counts"), dict) else zone_data
483
+ current_count = counts_dict.get(
484
+ "total",
485
+ sum(v for v in counts_dict.values() if isinstance(v, (int, float)))
486
+ )
487
+ human_text_lines.append(f"\t{zone_name}: {int(current_count)}")
488
+ else:
489
+ if any(count > 0 for count in per_category_count.values()):
490
+ for cat, count in per_category_count.items():
491
+ if count > 0:
492
+ human_text_lines.append(f"\t- {count} Abandoned Objects detected")
493
+ else:
494
+ human_text_lines.append(f"\t- No detections")
495
+ human_text_lines.append(f"TOTAL SINCE {start_timestamp}")
496
+ if zone_analysis:
497
+ human_text_lines.append("\tZones (total):")
498
+ for zone_name, zone_data in zone_analysis.items():
499
+ total_count = 0
500
+ if isinstance(zone_data, dict):
501
+ if "total_count" in zone_data and isinstance(zone_data.get("total_count"), (int, float)):
502
+ total_count = zone_data.get("total_count", 0)
503
+ elif "total_track_ids" in zone_data and isinstance(zone_data.get("total_track_ids"), list):
504
+ total_count = len(zone_data.get("total_track_ids", []))
505
+ else:
506
+ counts_dict = zone_data if isinstance(zone_data, dict) else {}
507
+ total_count = sum(v for v in counts_dict.values() if isinstance(v, (int, float)))
508
+ human_text_lines.append(f"\t{zone_name}: {int(total_count)}")
509
+ else:
510
+ for cat, count in total_counts_dict.items():
511
+ if count > 0:
512
+ human_text_lines.append(f"\t{cat}: {count}")
513
+ if alerts:
514
+ for alert in alerts:
515
+ human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
516
+ else:
517
+ human_text_lines.append("Alerts: None")
518
+ human_text = "\n".join(human_text_lines)
519
+
520
+ reset_settings = [{"interval_type": "daily", "reset_time": {"value": 9, "time_unit": "hour"}}]
521
+ tracking_stat = self.create_tracking_stats(
522
+ total_counts=total_counts,
523
+ current_counts=current_counts,
524
+ detections=detections,
525
+ human_text=human_text,
526
+ camera_info=camera_info,
527
+ alerts=alerts,
528
+ alert_settings=alert_settings,
529
+ reset_settings=reset_settings,
530
+ start_time=high_precision_start_timestamp,
531
+ reset_time=high_precision_reset_timestamp
532
+ )
533
+ tracking_stats.append(tracking_stat)
534
+ return tracking_stats
535
+
536
+ def _generate_business_analytics(self, counting_summary: Dict, zone_analysis: Dict, alerts: Any, config: AbandonedObjectConfig,
537
+ stream_info: Optional[Dict[str, Any]] = None, is_empty=False) -> List[Dict]:
538
+ if is_empty:
539
+ return []
540
+ return []
541
+
542
+ def _generate_summary(self, summary: dict, zone_analysis: Dict, incidents: List, tracking_stats: List, business_analytics: List, alerts: List) -> List[str]:
543
+ lines = {}
544
+ lines["Application Name"] = self.CASE_TYPE
545
+ lines["Application Version"] = self.CASE_VERSION
546
+ if len(incidents) > 0:
547
+ lines["Incidents:"] = f"\n\t{incidents[0].get('human_text', 'No incidents detected')}\n"
548
+ if len(tracking_stats) > 0:
549
+ lines["Tracking Statistics:"] = f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}\n"
550
+ if len(business_analytics) > 0:
551
+ lines["Business Analytics:"] = f"\t{business_analytics[0].get('human_text', 'No business analytics detected')}\n"
552
+ if len(incidents) == 0 and len(tracking_stats) == 0 and len(business_analytics) == 0:
553
+ lines["Summary"] = "No Summary Data"
554
+ return ["\n".join(f"{k}: {v}" for k, v in lines.items())]
555
+
556
+ def _get_track_ids_info(self, detections: list) -> Dict[str, Any]:
557
+ frame_track_ids = {det.get('track_id') for det in detections if det.get('track_id') is not None}
558
+ total_track_ids = set()
559
+ for s in getattr(self, '_per_category_total_track_ids', {}).values():
560
+ total_track_ids.update(s)
561
+ return {
562
+ "total_count": len(total_track_ids),
563
+ "current_frame_count": len(frame_track_ids),
564
+ "total_unique_track_ids": len(total_track_ids),
565
+ "current_frame_track_ids": list(frame_track_ids),
566
+ "last_update_time": time.time(),
567
+ "total_frames_processed": getattr(self, '_total_frame_counter', 0)
568
+ }
569
+
570
+ def _update_tracking_state(self, detections: list):
571
+ if not hasattr(self, "_per_category_total_track_ids"):
572
+ self._per_category_total_track_ids = {cat: set() for cat in self.target_categories + ['abandoned_object']}
573
+ self._current_frame_track_ids = {cat: set() for cat in self.target_categories + ['abandoned_object']}
574
+
575
+ for det in detections:
576
+ cat = det.get("category")
577
+ raw_track_id = det.get("track_id")
578
+ if cat not in self.target_categories + ['abandoned_object'] or raw_track_id is None:
579
+ continue
580
+ bbox = det.get("bounding_box", det.get("bbox"))
581
+ canonical_id = self._merge_or_register_track(raw_track_id, bbox)
582
+ det["track_id"] = canonical_id
583
+ self._per_category_total_track_ids.setdefault(cat, set()).add(canonical_id)
584
+ self._current_frame_track_ids[cat].add(canonical_id)
585
+
586
+ def get_total_counts(self):
587
+ return {cat: len(ids) for cat, ids in getattr(self, '_per_category_total_track_ids', {}).items()}
588
+
589
+ def _format_timestamp_for_stream(self, timestamp: float) -> str:
590
+ dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
591
+ return dt.strftime('%Y:%m:%d %H:%M:%S')
592
+
593
+ def _format_timestamp_for_video(self, timestamp: float) -> str:
594
+ hours = int(timestamp // 3600)
595
+ minutes = int((timestamp % 3600) // 60)
596
+ seconds = round(float(timestamp % 60), 2)
597
+ return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
598
+
599
+ def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
600
+ if not stream_info:
601
+ return "00:00:00.00"
602
+ if precision:
603
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
604
+ if frame_id:
605
+ start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
606
+ else:
607
+ start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
608
+ stream_time_str = self._format_timestamp_for_video(start_time)
609
+ return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
610
+ else:
611
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
612
+
613
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
614
+ if frame_id:
615
+ start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
616
+ else:
617
+ start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
618
+
619
+ stream_time_str = self._format_timestamp_for_video(start_time)
620
+ return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
621
+ else:
622
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
623
+ if stream_time_str:
624
+ try:
625
+ timestamp_str = stream_time_str.replace(" UTC", "")
626
+ dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
627
+ timestamp = dt.replace(tzinfo=timezone.utc).timestamp()
628
+ return self._format_timestamp_for_stream(timestamp)
629
+ except:
630
+ return self._format_timestamp_for_stream(time.time())
631
+ else:
632
+ return self._format_timestamp_for_stream(time.time())
633
+
634
+ def _get_start_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False) -> str:
635
+ if not stream_info:
636
+ return "00:00:00"
637
+
638
+ if precision:
639
+ if self.start_timer is None:
640
+ self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
641
+ return self._format_timestamp(self.start_timer)
642
+ elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
643
+ self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
644
+ return self._format_timestamp(self.start_timer)
645
+ else:
646
+ return self._format_timestamp(self.start_timer)
647
+
648
+ if self.start_timer is None:
649
+ self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
650
+ return self._format_timestamp(self.start_timer)
651
+ elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
652
+ self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
653
+ return self._format_timestamp(self.start_timer)
654
+
655
+ else:
656
+ if self.start_timer is not None:
657
+ return self._format_timestamp(self.start_timer)
658
+
659
+ if self._tracking_start_time is None:
660
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
661
+ if stream_time_str:
662
+ try:
663
+ timestamp_str = stream_time_str.replace(" UTC", "")
664
+ dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
665
+ self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
666
+ except:
667
+ self._tracking_start_time = time.time()
668
+ else:
669
+ self._tracking_start_time = time.time()
670
+
671
+ dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
672
+ dt = dt.replace(minute=0, second=0, microsecond=0)
673
+ return dt.strftime('%Y:%m:%d %H:%M:%S')
674
+
675
+ def _count_categories(self, detections: list, config: AbandonedObjectConfig) -> dict:
676
+ counts = {}
677
+ for det in detections:
678
+ cat = det.get('category', 'unknown')
679
+ counts[cat] = counts.get(cat, 0) + 1
680
+ return {
681
+ "total_count": sum(counts.values()),
682
+ "per_category_count": counts,
683
+ "detections": [
684
+ {
685
+ "bounding_box": det.get("bounding_box"),
686
+ "category": det.get("category"),
687
+ "confidence": det.get("confidence"),
688
+ "track_id": det.get("track_id"),
689
+ "frame_id": det.get("frame_id")
690
+ }
691
+ for det in detections
692
+ ]
693
+ }
694
+
695
+ def _extract_predictions(self, detections: list) -> List[Dict[str, Any]]:
696
+ return [
697
+ {
698
+ "category": det.get("category", "unknown"),
699
+ "confidence": det.get("confidence", 0.0),
700
+ "bounding_box": det.get("bounding_box", {})
701
+ }
702
+ for det in detections
703
+ ]
704
+
705
+ def _compute_iou(self, box1: Any, box2: Any) -> float:
706
+ def _bbox_to_list(bbox):
707
+ if bbox is None:
708
+ return []
709
+ if isinstance(bbox, list):
710
+ return bbox[:4] if len(bbox) >= 4 else []
711
+ if isinstance(bbox, dict):
712
+ if "xmin" in bbox:
713
+ return [bbox["xmin"], bbox["ymin"], bbox["xmax"], bbox["ymax"]]
714
+ if "x1" in bbox:
715
+ return [bbox["x1"], bbox["y1"], bbox["x2"], bbox["y2"]]
716
+ values = [v for v in bbox.values() if isinstance(v, (int, float))]
717
+ return values[:4] if len(values) >= 4 else []
718
+ return []
719
+
720
+ l1 = _bbox_to_list(box1)
721
+ l2 = _bbox_to_list(box2)
722
+ if len(l1) < 4 or len(l2) < 4:
723
+ return 0.0
724
+ x1_min, y1_min, x1_max, y1_max = l1
725
+ x2_min, y2_min, x2_max, y2_max = l2
726
+ x1_min, x1_max = min(x1_min, x1_max), max(x1_min, x1_max)
727
+ y1_min, y1_max = min(y1_min, y1_max), max(y1_min, y1_max)
728
+ x2_min, x2_max = min(x2_min, x2_max), max(x2_min, x2_max)
729
+ y2_min, y2_max = min(y2_min, y2_max), max(y2_min, y2_max)
730
+ inter_x_min = max(x1_min, x2_min)
731
+ inter_y_min = max(y1_min, y2_min)
732
+ inter_x_max = min(x1_max, x2_max)
733
+ inter_y_max = min(y1_max, y2_max)
734
+ inter_w = max(0.0, inter_x_max - inter_x_min)
735
+ inter_h = max(0.0, inter_y_max - inter_y_min)
736
+ inter_area = inter_w * inter_h
737
+ area1 = (x1_max - x1_min) * (y1_max - y1_min)
738
+ area2 = (x2_max - x2_min) * (y2_max - y2_min)
739
+ union_area = area1 + area2 - inter_area
740
+ return (inter_area / union_area) if union_area > 0 else 0.0
741
+
742
+ def _merge_or_register_track(self, raw_id: Any, bbox: Any) -> Any:
743
+ if raw_id is None or bbox is None:
744
+ return raw_id
745
+ now = time.time()
746
+ if raw_id in self._track_aliases:
747
+ canonical_id = self._track_aliases[raw_id]
748
+ track_info = self._canonical_tracks.get(canonical_id)
749
+ if track_info is not None:
750
+ track_info["last_bbox"] = bbox
751
+ track_info["last_update"] = now
752
+ track_info["raw_ids"].add(raw_id)
753
+ return canonical_id
754
+ for canonical_id, info in self._canonical_tracks.items():
755
+ if now - info["last_update"] > self._track_merge_time_window:
756
+ continue
757
+ iou = self._compute_iou(bbox, info["last_bbox"])
758
+ if iou >= self._track_merge_iou_threshold:
759
+ self._track_aliases[raw_id] = canonical_id
760
+ info["last_bbox"] = bbox
761
+ info["last_update"] = now
762
+ info["raw_ids"].add(raw_id)
763
+ return canonical_id
764
+ canonical_id = raw_id
765
+ self._track_aliases[raw_id] = canonical_id
766
+ self._canonical_tracks[canonical_id] = {
767
+ "last_bbox": bbox,
768
+ "last_update": now,
769
+ "raw_ids": {raw_id},
770
+ }
771
+ return canonical_id
772
+
773
+ def _format_timestamp(self, timestamp: Any) -> str:
774
+ if isinstance(timestamp, (int, float)):
775
+ timestamp = datetime.fromtimestamp(timestamp, timezone.utc).strftime(
776
+ '%Y-%m-%d-%H:%M:%S.%f UTC'
777
+ )
778
+ if not isinstance(timestamp, str):
779
+ return str(timestamp)
780
+ if '.' not in timestamp:
781
+ return timestamp
782
+ main_part, fractional_and_suffix = timestamp.split('.', 1)
783
+ if ' ' in fractional_and_suffix:
784
+ fractional_part, suffix = fractional_and_suffix.split(' ', 1)
785
+ suffix = ' ' + suffix
786
+ else:
787
+ fractional_part, suffix = fractional_and_suffix, ''
788
+ fractional_part = (fractional_part + '00')[:2]
789
+ return f"{main_part}.{fractional_part}{suffix}"
790
+
791
+ def _get_tracking_start_time(self) -> str:
792
+ if self._tracking_start_time is None:
793
+ return "N/A"
794
+ return self._format_timestamp(self._tracking_start_time)
795
+
796
+ def _set_tracking_start_time(self) -> None:
797
+ self._tracking_start_time = time.time()