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