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,717 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from dataclasses import asdict
3
+ import time
4
+ from datetime import datetime, timezone
5
+
6
+ from ..core.base import BaseProcessor, ProcessingContext, ProcessingResult, ConfigProtocol
7
+ from ..utils import (
8
+ filter_by_confidence,
9
+ filter_by_categories,
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
+ )
19
+ from dataclasses import dataclass, field
20
+ from ..core.config import BaseConfig, AlertConfig, ZoneConfig
21
+
22
+
23
+ @dataclass
24
+ class LitterDetectionConfig(BaseConfig):
25
+ """Configuration for litter detection use case in litter monitoring."""
26
+ # Smoothing configuration
27
+ enable_smoothing: bool = True
28
+ smoothing_algorithm: str = "observability"
29
+ smoothing_window_size: int = 20
30
+ smoothing_cooldown_frames: int = 5
31
+ smoothing_confidence_range_factor: float = 0.5
32
+
33
+ # Confidence thresholds
34
+ confidence_threshold: float = 0.6
35
+
36
+ usecase_categories: List[str] = field(
37
+ default_factory=lambda: [
38
+ 'Aluminium foil', 'Bottle cap', 'Bottle', 'Broken glass', 'Can', 'Carton',
39
+ 'Cigarette', 'Cup', 'Lid', 'Other litter', 'Other plastic', 'Paper',
40
+ 'Plastic bag - wrapper', 'Plastic container', 'Pop tab', 'Straw', 'Styrofoam piece', 'Unlabeled litter'
41
+ ]
42
+ )
43
+
44
+ target_categories: List[str] = field(
45
+ default_factory=lambda: [
46
+ 'Aluminium foil', 'Bottle cap', 'Bottle', 'Broken glass', 'Can', 'Carton',
47
+ 'Cigarette', 'Cup', 'Lid', 'Other litter', 'Other plastic', 'Paper',
48
+ 'Plastic bag - wrapper', 'Plastic container', 'Pop tab', 'Straw', 'Styrofoam piece', 'Unlabeled litter'
49
+ ]
50
+ )
51
+
52
+ alert_config: Optional[AlertConfig] = None
53
+
54
+ index_to_category: Optional[Dict[int, str]] = field(
55
+ default_factory=lambda: {
56
+ 0: "Aluminium foil",
57
+ 1: "Bottle cap",
58
+ 2: "Bottle",
59
+ 3: "Broken glass",
60
+ 4: "Can",
61
+ 5: "Carton",
62
+ 6: "Cigarette",
63
+ 7: "Cup",
64
+ 8: "Lid",
65
+ 9: "Other litter",
66
+ 10: "Other plastic",
67
+ 11: "Paper",
68
+ 12: "Plastic bag - wrapper",
69
+ 13: "Plastic container",
70
+ 14: "Pop tab",
71
+ 15: "Straw",
72
+ 16: "Styrofoam piece",
73
+ 17: "Unlabeled litter"
74
+ }
75
+ )
76
+
77
+
78
+ class LitterDetectionUseCase(BaseProcessor):
79
+ # Human-friendly display names for categories
80
+ CATEGORY_DISPLAY = {
81
+ "Aluminium foil" : "Aluminum foil",
82
+ "Bottle cap": "Bottle Cap",
83
+ "Bottle": "Bottle",
84
+ "Broken glass": "Broken Glass",
85
+ "Can": "Can",
86
+ "Carton": "Carton",
87
+ "Cigarette": "Cigarette",
88
+ "Cup": "Cup",
89
+ "Lid": "Lid",
90
+ "Other litter": "Other Litter",
91
+ "Other plastic": "Other Plastic",
92
+ "Paper": "Paper",
93
+ "Plastic bag - wrapper": "Plastic Bag/Wrapper",
94
+ "Plastic container": "Plastic Container",
95
+ "Pop tab": "Pop Tab",
96
+ "Straw": "Straw",
97
+ "Styrofoam piece": "Styrofoam Piece",
98
+ "Unlabeled litter": "Unlabeled Litter"
99
+ }
100
+
101
+ def __init__(self):
102
+ super().__init__("litter_detection")
103
+ self.category = "litter_detection"
104
+
105
+ self.CASE_TYPE: Optional[str] = 'litter_detection'
106
+ self.CASE_VERSION: Optional[str] = '1.0'
107
+
108
+ # List of categories to track
109
+ self.target_categories = [
110
+ 'Aluminium foil', 'Bottle cap', 'Bottle', 'Broken glass', 'Can', 'Carton',
111
+ 'Cigarette', 'Cup', 'Lid', 'Other litter', 'Other plastic', 'Paper',
112
+ 'Plastic bag - wrapper', 'Plastic container', 'Pop tab', 'Straw', 'Styrofoam piece', 'Unlabeled litter'
113
+ ]
114
+
115
+ # Initialize smoothing tracker
116
+ self.smoothing_tracker = None
117
+
118
+ # Initialize advanced tracker
119
+ self.tracker = None
120
+
121
+ # Initialize tracking state variables
122
+ self._total_frame_counter = 0
123
+ self._global_frame_offset = 0
124
+
125
+ # Track start time for "TOTAL SINCE" calculation
126
+ self._tracking_start_time = None
127
+
128
+ self._track_aliases: Dict[Any, Any] = {}
129
+ self._canonical_tracks: Dict[Any, Dict[str, Any]] = {}
130
+ self._track_merge_iou_threshold: float = 0.05
131
+ self._track_merge_time_window: float = 7.0
132
+
133
+ self._ascending_alert_list: List[int] = []
134
+ self.current_incident_end_timestamp: str = "N/A"
135
+
136
+ def process(self, data: Any, config: ConfigProtocol, context: Optional[ProcessingContext] = None,
137
+ stream_info: Optional[Dict[str, Any]] = None) -> ProcessingResult:
138
+ """
139
+ Main entry point for litter post-processing.
140
+ Applies category mapping, smoothing, counting, alerting, and summary generation.
141
+ """
142
+ start_time = time.time()
143
+ if not isinstance(config, LitterDetectionConfig):
144
+ return self.create_error_result("Invalid config type", usecase=self.name, category=self.category,
145
+ context=context)
146
+ if context is None:
147
+ context = ProcessingContext()
148
+
149
+ input_format = match_results_structure(data)
150
+ context.input_format = input_format
151
+ context.confidence_threshold = config.confidence_threshold
152
+
153
+ if config.confidence_threshold is not None:
154
+ processed_data = filter_by_confidence(data, config.confidence_threshold)
155
+ self.logger.debug(f"Applied confidence filtering with threshold {config.confidence_threshold}")
156
+ else:
157
+ processed_data = data
158
+ self.logger.debug("Did not apply confidence filtering")
159
+
160
+ if config.index_to_category:
161
+ processed_data = apply_category_mapping(processed_data, config.index_to_category)
162
+ self.logger.debug("Applied category mapping")
163
+
164
+ if config.target_categories:
165
+ processed_data = [d for d in processed_data if d.get('category') in self.target_categories]
166
+ self.logger.debug("Applied category filtering")
167
+
168
+ if config.enable_smoothing:
169
+ if self.smoothing_tracker is None:
170
+ smoothing_config = BBoxSmoothingConfig(
171
+ smoothing_algorithm=config.smoothing_algorithm,
172
+ window_size=config.smoothing_window_size,
173
+ cooldown_frames=config.smoothing_cooldown_frames,
174
+ confidence_threshold=config.confidence_threshold,
175
+ confidence_range_factor=config.smoothing_confidence_range_factor,
176
+ enable_smoothing=True
177
+ )
178
+ self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
179
+ processed_data = bbox_smoothing(processed_data, self.smoothing_tracker.config, self.smoothing_tracker)
180
+
181
+ try:
182
+ from ..advanced_tracker import AdvancedTracker
183
+ from ..advanced_tracker.config import TrackerConfig
184
+
185
+ if self.tracker is None:
186
+ tracker_config = TrackerConfig()
187
+ self.tracker = AdvancedTracker(tracker_config)
188
+ self.logger.info("Initialized AdvancedTracker for Litter Monitoring")
189
+
190
+ processed_data = self.tracker.update(processed_data)
191
+
192
+ except Exception as e:
193
+ self.logger.warning(f"AdvancedTracker failed: {e}")
194
+
195
+ self._update_tracking_state(processed_data)
196
+ self._total_frame_counter += 1
197
+
198
+ frame_number = None
199
+ if stream_info:
200
+ input_settings = stream_info.get("input_settings", {})
201
+ start_frame = input_settings.get("start_frame")
202
+ end_frame = input_settings.get("end_frame")
203
+ if start_frame is not None and end_frame is not None and start_frame == end_frame:
204
+ frame_number = start_frame
205
+
206
+ general_counting_summary = calculate_counting_summary(data)
207
+ counting_summary = self._count_categories(processed_data, config)
208
+ total_counts = self.get_total_counts()
209
+ counting_summary['total_counts'] = total_counts
210
+
211
+ alerts = self._check_alerts(counting_summary, frame_number, config)
212
+ predictions = self._extract_predictions(processed_data)
213
+
214
+ incidents_list = self._generate_incidents(counting_summary, alerts, config, frame_number, stream_info)
215
+ tracking_stats_list = self._generate_tracking_stats(counting_summary, alerts, config, frame_number, stream_info)
216
+ business_analytics_list = self._generate_business_analytics(counting_summary, alerts, config, stream_info, is_empty=True)
217
+ summary_list = self._generate_summary(counting_summary, incidents_list, tracking_stats_list, business_analytics_list, alerts)
218
+
219
+ incidents = incidents_list[0] if incidents_list else {}
220
+ tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
221
+ business_analytics = business_analytics_list[0] if business_analytics_list else {}
222
+ summary = summary_list[0] if summary_list else {}
223
+ agg_summary = {str(frame_number): {
224
+ "incidents": incidents,
225
+ "tracking_stats": tracking_stats,
226
+ "business_analytics": business_analytics,
227
+ "alerts": alerts,
228
+ "human_text": summary}
229
+ }
230
+
231
+ context.mark_completed()
232
+
233
+ result = self.create_result(
234
+ data={"agg_summary": agg_summary},
235
+ usecase=self.name,
236
+ category=self.category,
237
+ context=context
238
+ )
239
+
240
+ return result
241
+
242
+ def _check_alerts(self, summary: dict, frame_number: Any, config: LitterDetectionConfig) -> List[Dict]:
243
+ """
244
+ Check if any alert thresholds are exceeded and return alert dicts.
245
+ """
246
+ def get_trend(data, lookback=900, threshold=0.6):
247
+ window = data[-lookback:] if len(data) >= lookback else data
248
+ if len(window) < 2:
249
+ return True
250
+ increasing = 0
251
+ total = 0
252
+ for i in range(1, len(window)):
253
+ if window[i] >= window[i - 1]:
254
+ increasing += 1
255
+ total += 1
256
+ ratio = increasing / total
257
+ return ratio >= threshold
258
+
259
+ frame_key = str(frame_number) if frame_number is not None else "current_frame"
260
+ alerts = []
261
+ total_detections = summary.get("total_count", 0)
262
+ total_counts_dict = summary.get("total_counts", {})
263
+ per_category_count = summary.get("per_category_count", {})
264
+
265
+ if not config.alert_config:
266
+ return alerts
267
+
268
+ if hasattr(config.alert_config, 'count_thresholds') and config.alert_config.count_thresholds:
269
+ for category, threshold in config.alert_config.count_thresholds.items():
270
+ if category == "all" and total_detections > threshold:
271
+ alerts.append({
272
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
273
+ "alert_id": f"alert_{category}_{frame_key}",
274
+ "incident_category": self.CASE_TYPE,
275
+ "threshold_level": threshold,
276
+ "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
277
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
278
+ getattr(config.alert_config, 'alert_value', ['JSON']))}
279
+ })
280
+ elif category in per_category_count:
281
+ count = per_category_count[category]
282
+ if count > threshold:
283
+ alerts.append({
284
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
285
+ "alert_id": f"alert_{category}_{frame_key}",
286
+ "incident_category": self.CASE_TYPE,
287
+ "threshold_level": threshold,
288
+ "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
289
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
290
+ getattr(config.alert_config, 'alert_value', ['JSON']))}
291
+ })
292
+ return alerts
293
+
294
+ def _generate_incidents(self, counting_summary: Dict, alerts: List, config: LitterDetectionConfig,
295
+ frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
296
+ """Generate structured incidents for the output format."""
297
+ incidents = []
298
+ total_detections = counting_summary.get("total_count", 0)
299
+ current_timestamp = self._get_current_timestamp_str(stream_info)
300
+ camera_info = self.get_camera_info_from_stream(stream_info)
301
+
302
+ self._ascending_alert_list = self._ascending_alert_list[-900:] if len(self._ascending_alert_list) > 900 else self._ascending_alert_list
303
+
304
+ if total_detections > 0:
305
+ level = "low"
306
+ intensity = 5.0
307
+ start_timestamp = self._get_start_timestamp_str(stream_info)
308
+ if start_timestamp and self.current_incident_end_timestamp == 'N/A':
309
+ self.current_incident_end_timestamp = 'Incident still active'
310
+ elif start_timestamp and self.current_incident_end_timestamp == 'Incident still active':
311
+ if len(self._ascending_alert_list) >= 15 and sum(self._ascending_alert_list[-15:]) / 15 < 1.5:
312
+ self.current_incident_end_timestamp = current_timestamp
313
+ elif self.current_incident_end_timestamp != 'Incident still active' and self.current_incident_end_timestamp != 'N/A':
314
+ self.current_incident_end_timestamp = 'N/A'
315
+
316
+ if config.alert_config and config.alert_config.count_thresholds:
317
+ threshold = config.alert_config.count_thresholds.get("all", 15)
318
+ intensity = min(10.0, (total_detections / threshold) * 10)
319
+ if intensity >= 9:
320
+ level = "critical"
321
+ self._ascending_alert_list.append(3)
322
+ elif intensity >= 7:
323
+ level = "significant"
324
+ self._ascending_alert_list.append(2)
325
+ elif intensity >= 5:
326
+ level = "medium"
327
+ self._ascending_alert_list.append(1)
328
+ else:
329
+ level = "low"
330
+ self._ascending_alert_list.append(0)
331
+ else:
332
+ if total_detections > 30:
333
+ level = "critical"
334
+ intensity = 10.0
335
+ self._ascending_alert_list.append(3)
336
+ elif total_detections > 25:
337
+ level = "significant"
338
+ intensity = 9.0
339
+ self._ascending_alert_list.append(2)
340
+ elif total_detections > 15:
341
+ level = "medium"
342
+ intensity = 7.0
343
+ self._ascending_alert_list.append(1)
344
+ else:
345
+ level = "low"
346
+ intensity = min(10.0, total_detections / 3.0)
347
+ self._ascending_alert_list.append(0)
348
+
349
+ human_text_lines = [f"INCIDENTS DETECTED @ {current_timestamp}:"]
350
+ human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE, level)}")
351
+ human_text = "\n".join(human_text_lines)
352
+
353
+ alert_settings = []
354
+ if config.alert_config and hasattr(config.alert_config, 'alert_type'):
355
+ alert_settings.append({
356
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
357
+ "incident_category": self.CASE_TYPE,
358
+ "threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
359
+ "ascending": True,
360
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
361
+ getattr(config.alert_config, 'alert_value', ['JSON']))}
362
+ })
363
+
364
+ event = self.create_incident(
365
+ incident_id=f"{self.CASE_TYPE}_{str(frame_number)}",
366
+ incident_type=self.CASE_TYPE,
367
+ severity_level=level,
368
+ human_text=human_text,
369
+ camera_info=camera_info,
370
+ alerts=alerts,
371
+ alert_settings=alert_settings,
372
+ start_time=start_timestamp,
373
+ end_time=self.current_incident_end_timestamp,
374
+ level_settings={"low": 1, "medium": 3, "significant": 4, "critical": 7}
375
+ )
376
+ incidents.append(event)
377
+ else:
378
+ self._ascending_alert_list.append(0)
379
+ incidents.append({})
380
+
381
+ return incidents
382
+
383
+ def _generate_tracking_stats(self, counting_summary: Dict, alerts: List, config: LitterDetectionConfig,
384
+ frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
385
+ """Generate structured tracking stats."""
386
+ camera_info = self.get_camera_info_from_stream(stream_info)
387
+ tracking_stats = []
388
+
389
+ total_detections = counting_summary.get("total_count", 0)
390
+ total_counts_dict = counting_summary.get("total_counts", {})
391
+ per_category_count = counting_summary.get("per_category_count", {})
392
+
393
+ current_timestamp = self._get_current_timestamp_str(stream_info, precision=False)
394
+ start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
395
+ high_precision_start_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
396
+ high_precision_reset_timestamp = self._get_start_timestamp_str(stream_info, precision=True)
397
+
398
+ total_counts = [{"category": cat, "count": count} for cat, count in total_counts_dict.items() if count > 0]
399
+ current_counts = [{"category": cat, "count": count} for cat, count in per_category_count.items() if count > 0 or total_detections > 0]
400
+
401
+ detections = []
402
+ for detection in counting_summary.get("detections", []):
403
+ bbox = detection.get("bounding_box", {})
404
+ category = detection.get("category", "unknown")
405
+ if detection.get("masks"):
406
+ detection_obj = self.create_detection_object(category, bbox, segmentation=detection.get("masks"))
407
+ elif detection.get("segmentation"):
408
+ detection_obj = self.create_detection_object(category, bbox, segmentation=detection.get("segmentation"))
409
+ elif detection.get("mask"):
410
+ detection_obj = self.create_detection_object(category, bbox, segmentation=detection.get("mask"))
411
+ else:
412
+ detection_obj = self.create_detection_object(category, bbox)
413
+ detections.append(detection_obj)
414
+
415
+ alert_settings = []
416
+ if config.alert_config and hasattr(config.alert_config, 'alert_type'):
417
+ alert_settings.append({
418
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']),
419
+ "incident_category": self.CASE_TYPE,
420
+ "threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
421
+ "ascending": True,
422
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
423
+ getattr(config.alert_config, 'alert_value', ['JSON']))}
424
+ })
425
+
426
+ human_text_lines = [f"Tracking Statistics:"]
427
+ human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}")
428
+ for cat, count in per_category_count.items():
429
+ human_text_lines.append(f"\t{cat}: {count}")
430
+ human_text_lines.append(f"TOTAL SINCE {start_timestamp}")
431
+ for cat, count in total_counts_dict.items():
432
+ if count > 0:
433
+ human_text_lines.append(f"\t{cat}: {count}")
434
+ if alerts:
435
+ for alert in alerts:
436
+ human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
437
+ else:
438
+ human_text_lines.append("Alerts: None")
439
+ human_text = "\n".join(human_text_lines)
440
+
441
+ reset_settings = [{"interval_type": "daily", "reset_time": {"value": 9, "time_unit": "hour"}}]
442
+
443
+ tracking_stat = self.create_tracking_stats(
444
+ total_counts=total_counts,
445
+ current_counts=current_counts,
446
+ detections=detections,
447
+ human_text=human_text,
448
+ camera_info=camera_info,
449
+ alerts=alerts,
450
+ alert_settings=alert_settings,
451
+ reset_settings=reset_settings,
452
+ start_time=high_precision_start_timestamp,
453
+ reset_time=high_precision_reset_timestamp
454
+ )
455
+ tracking_stats.append(tracking_stat)
456
+ return tracking_stats
457
+
458
+ def _generate_business_analytics(self, counting_summary: Dict, alerts: Any, config: LitterDetectionConfig,
459
+ stream_info: Optional[Dict[str, Any]] = None, is_empty=False) -> List[Dict]:
460
+ """Generate standardized business analytics."""
461
+ if is_empty:
462
+ return []
463
+ # Implement business analytics if needed
464
+ return []
465
+
466
+ def _generate_summary(self, summary: dict, incidents: List, tracking_stats: List, business_analytics: List, alerts: List) -> List[str]:
467
+ """Generate a human_text string for the summary."""
468
+ lines = {}
469
+ lines["Application Name"] = self.CASE_TYPE
470
+ lines["Application Version"] = self.CASE_VERSION
471
+ if len(incidents) > 0:
472
+ lines["Incidents:"] = f"\n\t{incidents[0].get('human_text', 'No incidents detected')}\n"
473
+ if len(tracking_stats) > 0:
474
+ lines["Tracking Statistics:"] = f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}\n"
475
+ if len(business_analytics) > 0:
476
+ lines["Business Analytics:"] = f"\t{business_analytics[0].get('human_text', 'No business analytics detected')}\n"
477
+ if len(incidents) == 0 and len(tracking_stats) == 0 and len(business_analytics) == 0:
478
+ lines["Summary"] = "No Summary Data"
479
+ return [lines]
480
+
481
+ def _get_track_ids_info(self, detections: list) -> Dict[str, Any]:
482
+ """Get detailed information about track IDs."""
483
+ frame_track_ids = set()
484
+ for det in detections:
485
+ tid = det.get('track_id')
486
+ if tid is not None:
487
+ frame_track_ids.add(tid)
488
+ total_track_ids = set()
489
+ for s in getattr(self, '_per_category_total_track_ids', {}).values():
490
+ total_track_ids.update(s)
491
+ return {
492
+ "total_count": len(total_track_ids),
493
+ "current_frame_count": len(frame_track_ids),
494
+ "total_unique_track_ids": len(total_track_ids),
495
+ "current_frame_track_ids": list(frame_track_ids),
496
+ "last_update_time": time.time(),
497
+ "total_frames_processed": getattr(self, '_total_frame_counter', 0)
498
+ }
499
+
500
+ def _update_tracking_state(self, detections: list):
501
+ """Track unique categories track_ids per category."""
502
+ if not hasattr(self, "_per_category_total_track_ids"):
503
+ self._per_category_total_track_ids = {cat: set() for cat in self.target_categories}
504
+ self._current_frame_track_ids = {cat: set() for cat in self.target_categories}
505
+
506
+ for det in detections:
507
+ cat = det.get("category")
508
+ raw_track_id = det.get("track_id")
509
+ if cat not in self.target_categories or raw_track_id is None:
510
+ continue
511
+ bbox = det.get("bounding_box", det.get("bbox"))
512
+ canonical_id = self._merge_or_register_track(raw_track_id, bbox)
513
+ det["track_id"] = canonical_id
514
+ self._per_category_total_track_ids.setdefault(cat, set()).add(canonical_id)
515
+ self._current_frame_track_ids[cat].add(canonical_id)
516
+
517
+ def get_total_counts(self):
518
+ """Return total unique track_id count for each category."""
519
+ return {cat: len(ids) for cat, ids in getattr(self, '_per_category_total_track_ids', {}).items()}
520
+
521
+ def _format_timestamp_for_stream(self, timestamp: float) -> str:
522
+ """Format timestamp for streams."""
523
+ dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
524
+ return dt.strftime('%Y:%m:%d %H:%M:%S')
525
+
526
+ def _format_timestamp_for_video(self, timestamp: float) -> str:
527
+ """Format timestamp for video chunks."""
528
+ hours = int(timestamp // 3600)
529
+ minutes = int((timestamp % 3600) // 60)
530
+ seconds = round(float(timestamp % 60), 2)
531
+ return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
532
+
533
+ def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
534
+ """Get formatted current timestamp."""
535
+ if not stream_info:
536
+ return "00:00:00.00"
537
+ if precision:
538
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
539
+ if frame_id:
540
+ start_time = int(frame_id) / stream_info.get("input_settings", {}).get("original_fps", 30)
541
+ else:
542
+ start_time = stream_info.get("input_settings", {}).get("start_frame", 30) / stream_info.get("input_settings", {}).get("original_fps", 30)
543
+ return self._format_timestamp_for_video(start_time)
544
+ else:
545
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
546
+
547
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
548
+ if frame_id:
549
+ start_time = int(frame_id) / stream_info.get("input_settings", {}).get("original_fps", 30)
550
+ else:
551
+ start_time = stream_info.get("input_settings", {}).get("start_frame", 30) / stream_info.get("input_settings", {}).get("original_fps", 30)
552
+ return self._format_timestamp_for_video(start_time)
553
+ else:
554
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
555
+ if stream_time_str:
556
+ try:
557
+ timestamp_str = stream_time_str.replace(" UTC", "")
558
+ dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
559
+ timestamp = dt.replace(tzinfo=timezone.utc).timestamp()
560
+ return self._format_timestamp_for_stream(timestamp)
561
+ except:
562
+ return self._format_timestamp_for_stream(time.time())
563
+ else:
564
+ return self._format_timestamp_for_stream(time.time())
565
+
566
+ def _get_start_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False) -> str:
567
+ """Get formatted start timestamp."""
568
+ if not stream_info:
569
+ return "00:00:00"
570
+ if precision:
571
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
572
+ return "00:00:00"
573
+ else:
574
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
575
+
576
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
577
+ return "00:00:00"
578
+ else:
579
+ if self._tracking_start_time is None:
580
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
581
+ if stream_time_str:
582
+ try:
583
+ timestamp_str = stream_time_str.replace(" UTC", "")
584
+ dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
585
+ self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
586
+ except:
587
+ self._tracking_start_time = time.time()
588
+ else:
589
+ self._tracking_start_time = time.time()
590
+
591
+ dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
592
+ dt = dt.replace(minute=0, second=0, microsecond=0)
593
+ return dt.strftime('%Y:%m:%d %H:%M:%S')
594
+
595
+ def _count_categories(self, detections: list, config: LitterDetectionConfig) -> dict:
596
+ """Count detections per category."""
597
+ counts = {}
598
+ for det in detections:
599
+ cat = det.get('category', 'unknown')
600
+ counts[cat] = counts.get(cat, 0) + 1
601
+ return {
602
+ "total_count": sum(counts.values()),
603
+ "per_category_count": counts,
604
+ "detections": [
605
+ {
606
+ "bounding_box": det.get("bounding_box"),
607
+ "category": det.get("category"),
608
+ "confidence": det.get("confidence"),
609
+ "track_id": det.get("track_id"),
610
+ "frame_id": det.get("frame_id")
611
+ }
612
+ for det in detections
613
+ ]
614
+ }
615
+
616
+ def _extract_predictions(self, detections: list) -> List[Dict[str, Any]]:
617
+ """Extract prediction details."""
618
+ return [
619
+ {
620
+ "category": det.get("category", "unknown"),
621
+ "confidence": det.get("confidence", 0.0),
622
+ "bounding_box": det.get("bounding_box", {})
623
+ }
624
+ for det in detections
625
+ ]
626
+
627
+ def _compute_iou(self, box1: Any, box2: Any) -> float:
628
+ """Compute IoU between two bounding boxes."""
629
+ def _bbox_to_list(bbox):
630
+ if bbox is None:
631
+ return []
632
+ if isinstance(bbox, list):
633
+ return bbox[:4] if len(bbox) >= 4 else []
634
+ if isinstance(bbox, dict):
635
+ if "xmin" in bbox:
636
+ return [bbox["xmin"], bbox["ymin"], bbox["xmax"], bbox["ymax"]]
637
+ if "x1" in bbox:
638
+ return [bbox["x1"], bbox["y1"], bbox["x2"], bbox["y2"]]
639
+ values = [v for v in bbox.values() if isinstance(v, (int, float))]
640
+ return values[:4] if len(values) >= 4 else []
641
+ return []
642
+
643
+ l1 = _bbox_to_list(box1)
644
+ l2 = _bbox_to_list(box2)
645
+ if len(l1) < 4 or len(l2) < 4:
646
+ return 0.0
647
+ x1_min, y1_min, x1_max, y1_max = l1
648
+ x2_min, y2_min, x2_max, y2_max = l2
649
+
650
+ x1_min, x1_max = min(x1_min, x1_max), max(x1_min, x1_max)
651
+ y1_min, y1_max = min(y1_min, y1_max), max(y1_min, y1_max)
652
+ x2_min, x2_max = min(x2_min, x2_max), max(x2_min, x2_max)
653
+ y2_min, y2_max = min(y2_min, y2_max), max(y2_min, y2_max)
654
+
655
+ inter_x_min = max(x1_min, x2_min)
656
+ inter_y_min = max(y1_min, y2_min)
657
+ inter_x_max = min(x1_max, x2_max)
658
+ inter_y_max = min(y1_max, y2_max)
659
+
660
+ inter_w = max(0.0, inter_x_max - inter_x_min)
661
+ inter_h = max(0.0, inter_y_max - inter_y_min)
662
+ inter_area = inter_w * inter_h
663
+
664
+ area1 = (x1_max - x1_min) * (y1_max - y1_min)
665
+ area2 = (x2_max - x2_min) * (y2_max - y2_min)
666
+ union_area = area1 + area2 - inter_area
667
+
668
+ return (inter_area / union_area) if union_area > 0 else 0.0
669
+
670
+ def _merge_or_register_track(self, raw_id: Any, bbox: Any) -> Any:
671
+ """Return a stable canonical ID for a raw tracker ID."""
672
+ if raw_id is None or bbox is None:
673
+ return raw_id
674
+
675
+ now = time.time()
676
+ if raw_id in self._track_aliases:
677
+ canonical_id = self._track_aliases[raw_id]
678
+ track_info = self._canonical_tracks.get(canonical_id)
679
+ if track_info is not None:
680
+ track_info["last_bbox"] = bbox
681
+ track_info["last_update"] = now
682
+ track_info["raw_ids"].add(raw_id)
683
+ return canonical_id
684
+
685
+ for canonical_id, info in self._canonical_tracks.items():
686
+ if now - info["last_update"] > self._track_merge_time_window:
687
+ continue
688
+ iou = self._compute_iou(bbox, info["last_bbox"])
689
+ if iou >= self._track_merge_iou_threshold:
690
+ self._track_aliases[raw_id] = canonical_id
691
+ info["last_bbox"] = bbox
692
+ info["last_update"] = now
693
+ info["raw_ids"].add(raw_id)
694
+ return canonical_id
695
+
696
+ canonical_id = raw_id
697
+ self._track_aliases[raw_id] = canonical_id
698
+ self._canonical_tracks[canonical_id] = {
699
+ "last_bbox": bbox,
700
+ "last_update": now,
701
+ "raw_ids": {raw_id},
702
+ }
703
+ return canonical_id
704
+
705
+ def _format_timestamp(self, timestamp: float) -> str:
706
+ """Format a timestamp for human-readable output."""
707
+ return datetime.fromtimestamp(timestamp, timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
708
+
709
+ def _get_tracking_start_time(self) -> str:
710
+ """Get the tracking start time."""
711
+ if self._tracking_start_time is None:
712
+ return "N/A"
713
+ return self._format_timestamp(self._tracking_start_time)
714
+
715
+ def _set_tracking_start_time(self) -> None:
716
+ """Set the tracking start time."""
717
+ self._tracking_start_time = time.time()