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,623 @@
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 LaneDetectionConfig(BaseConfig):
25
+ """Configuration for lane detection use case in road monitoring."""
26
+ enable_smoothing: bool = True
27
+ smoothing_algorithm: str = "observability"
28
+ smoothing_window_size: int = 20
29
+ smoothing_cooldown_frames: int = 5
30
+ smoothing_confidence_range_factor: float = 0.5
31
+ confidence_threshold: float = 0.6
32
+ usecase_categories: List[str] = field(
33
+ default_factory=lambda: ['divider-line', 'dotted-line', 'double-line', 'random-line', 'road-sign-line', 'solid-line']
34
+ )
35
+ target_categories: List[str] = field(
36
+ default_factory=lambda: ['divider-line', 'dotted-line', 'double-line', 'random-line', 'road-sign-line', 'solid-line']
37
+ )
38
+ alert_config: Optional[AlertConfig] = None
39
+ index_to_category: Optional[Dict[int, str]] = field(
40
+ default_factory=lambda: {
41
+ 0: "divider-line",
42
+ 1: "dotted-line",
43
+ 2: "double-line",
44
+ 3: "random-line",
45
+ 4: "road-sign-line",
46
+ 5: "solid-line"
47
+ }
48
+ )
49
+
50
+
51
+ class LaneDetectionUseCase(BaseProcessor):
52
+ CATEGORY_DISPLAY = {
53
+ "divider-line": "Divider Line",
54
+ "dotted-line": "Dotted Line",
55
+ "double-line": "Double Line",
56
+ "random-line": "Random Line",
57
+ "road-sign-line": "Road Sign Line",
58
+ "solid-line": "Solid Line"
59
+ }
60
+ def __init__(self):
61
+ super().__init__("lane_detection")
62
+ self.category = "traffic"
63
+ self.CASE_TYPE: Optional[str] = 'lane_detection'
64
+ self.CASE_VERSION: Optional[str] = '1.0'
65
+ self.target_categories = ['divider-line', 'dotted-line', 'double-line', 'random-line', 'road-sign-line', 'solid-line']
66
+ self.smoothing_tracker = None
67
+ self.tracker = None
68
+ self._total_frame_counter = 0
69
+ self._global_frame_offset = 0
70
+ self._tracking_start_time = None
71
+ self._track_aliases: Dict[Any, Any] = {}
72
+ self._canonical_tracks: Dict[Any, Dict[str, Any]] = {}
73
+ self._track_merge_iou_threshold: float = 0.05
74
+ self._track_merge_time_window: float = 7.0
75
+ self._ascending_alert_list: List[int] = []
76
+ self.current_incident_end_timestamp: str = "N/A"
77
+
78
+ def process(self, data: Any, config: ConfigProtocol, context: Optional[ProcessingContext] = None,
79
+ stream_info: Optional[Dict[str, Any]] = None) -> ProcessingResult:
80
+ start_time = time.time()
81
+ if not isinstance(config, LaneDetectionConfig):
82
+ return self.create_error_result("Invalid config type", usecase=self.name, category=self.category, context=context)
83
+ if context is None:
84
+ context = ProcessingContext()
85
+ input_format = match_results_structure(data)
86
+ context.input_format = input_format
87
+ context.confidence_threshold = config.confidence_threshold
88
+
89
+ if config.confidence_threshold is not None:
90
+ processed_data = filter_by_confidence(data, config.confidence_threshold)
91
+ self.logger.debug(f"Applied confidence filtering with threshold {config.confidence_threshold}")
92
+ else:
93
+ processed_data = data
94
+ self.logger.debug("Did not apply confidence filtering")
95
+
96
+ if config.index_to_category:
97
+ processed_data = apply_category_mapping(processed_data, config.index_to_category)
98
+ self.logger.debug("Applied category mapping")
99
+
100
+ if config.target_categories:
101
+ processed_data = [d for d in processed_data if d.get('category') in self.target_categories]
102
+ self.logger.debug("Applied category filtering")
103
+
104
+ if config.enable_smoothing:
105
+ if self.smoothing_tracker is None:
106
+ smoothing_config = BBoxSmoothingConfig(
107
+ smoothing_algorithm=config.smoothing_algorithm,
108
+ window_size=config.smoothing_window_size,
109
+ cooldown_frames=config.smoothing_cooldown_frames,
110
+ confidence_threshold=config.confidence_threshold,
111
+ confidence_range_factor=config.smoothing_confidence_range_factor,
112
+ enable_smoothing=True
113
+ )
114
+ self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
115
+ processed_data = bbox_smoothing(processed_data, self.smoothing_tracker.config, self.smoothing_tracker)
116
+
117
+ try:
118
+ from ..advanced_tracker import AdvancedTracker
119
+ from ..advanced_tracker.config import TrackerConfig
120
+ if self.tracker is None:
121
+ tracker_config = TrackerConfig()
122
+ self.tracker = AdvancedTracker(tracker_config)
123
+ self.logger.info("Initialized AdvancedTracker for Lane Detection")
124
+ processed_data = self.tracker.update(processed_data)
125
+ except Exception as e:
126
+ self.logger.warning(f"AdvancedTracker failed: {e}")
127
+
128
+ self._update_tracking_state(processed_data)
129
+ self._total_frame_counter += 1
130
+
131
+ frame_number = None
132
+ if stream_info:
133
+ input_settings = stream_info.get("input_settings", {})
134
+ start_frame = input_settings.get("start_frame")
135
+ end_frame = input_settings.get("end_frame")
136
+ if start_frame is not None and end_frame is not None and start_frame == end_frame:
137
+ frame_number = start_frame
138
+
139
+ general_counting_summary = calculate_counting_summary(data)
140
+ counting_summary = self._count_categories(processed_data, config)
141
+ total_counts = self.get_total_counts()
142
+ counting_summary['total_counts'] = total_counts
143
+ alerts = self._check_alerts(counting_summary, frame_number, config)
144
+ predictions = self._extract_predictions(processed_data)
145
+
146
+ incidents_list = self._generate_incidents(counting_summary, alerts, config, frame_number, stream_info)
147
+ tracking_stats_list = self._generate_tracking_stats(counting_summary, alerts, config, frame_number, stream_info)
148
+ business_analytics_list = self._generate_business_analytics(counting_summary, alerts, config, stream_info, is_empty=True)
149
+ summary_list = self._generate_summary(counting_summary, incidents_list, tracking_stats_list, business_analytics_list, alerts)
150
+
151
+ incidents = incidents_list[0] if incidents_list else {}
152
+ tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
153
+ business_analytics = business_analytics_list[0] if business_analytics_list else {}
154
+ summary = summary_list[0] if summary_list else {}
155
+ agg_summary = {str(frame_number): {
156
+ "incidents": incidents,
157
+ "tracking_stats": tracking_stats,
158
+ "business_analytics": business_analytics,
159
+ "alerts": alerts,
160
+ "human_text": summary}
161
+ }
162
+
163
+ context.mark_completed()
164
+ result = self.create_result(
165
+ data={"agg_summary": agg_summary},
166
+ usecase=self.name,
167
+ category=self.category,
168
+ context=context
169
+ )
170
+ return result
171
+
172
+ def _check_alerts(self, summary: dict, frame_number: Any, config: LaneDetectionConfig) -> List[Dict]:
173
+ def get_trend(data, lookback=900, threshold=0.6):
174
+ window = data[-lookback:] if len(data) >= lookback else data
175
+ if len(window) < 2:
176
+ return True
177
+ increasing = 0
178
+ total = 0
179
+ for i in range(1, len(window)):
180
+ if window[i] >= window[i - 1]:
181
+ increasing += 1
182
+ total += 1
183
+ ratio = increasing / total
184
+ return ratio >= threshold
185
+
186
+ frame_key = str(frame_number) if frame_number is not None else "current_frame"
187
+ alerts = []
188
+ total_detections = summary.get("total_count", 0)
189
+ total_counts_dict = summary.get("total_counts", {})
190
+ per_category_count = summary.get("per_category_count", {})
191
+
192
+ if not config.alert_config:
193
+ return alerts
194
+
195
+ total = summary.get("total_count", 0)
196
+ if hasattr(config.alert_config, 'count_thresholds') and config.alert_config.count_thresholds:
197
+ for category, threshold in config.alert_config.count_thresholds.items():
198
+ if category == "all" and total > threshold:
199
+ alerts.append({
200
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
201
+ "alert_id": "alert_" + category + '_' + frame_key,
202
+ "incident_category": self.CASE_TYPE,
203
+ "threshold_level": threshold,
204
+ "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
205
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
206
+ getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])}
207
+ })
208
+ elif category in per_category_count:
209
+ count = per_category_count[category]
210
+ if count > threshold:
211
+ alerts.append({
212
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
213
+ "alert_id": "alert_" + category + '_' + frame_key,
214
+ "incident_category": self.CASE_TYPE,
215
+ "threshold_level": threshold,
216
+ "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
217
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
218
+ getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])}
219
+ })
220
+ return alerts
221
+
222
+ def _generate_incidents(self, counting_summary: Dict, alerts: List, config: LaneDetectionConfig,
223
+ frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
224
+ incidents = []
225
+ total_detections = counting_summary.get("total_count", 0)
226
+ current_timestamp = self._get_current_timestamp_str(stream_info)
227
+ camera_info = self.get_camera_info_from_stream(stream_info)
228
+
229
+ self._ascending_alert_list = self._ascending_alert_list[-900:] if len(self._ascending_alert_list) > 900 else self._ascending_alert_list
230
+
231
+ if total_detections > 0:
232
+ level = "low"
233
+ intensity = 5.0
234
+ start_timestamp = self._get_start_timestamp_str(stream_info)
235
+ if start_timestamp and self.current_incident_end_timestamp == 'N/A':
236
+ self.current_incident_end_timestamp = 'Incident still active'
237
+ elif start_timestamp and self.current_incident_end_timestamp == 'Incident still active':
238
+ if len(self._ascending_alert_list) >= 15 and sum(self._ascending_alert_list[-15:]) / 15 < 1.5:
239
+ self.current_incident_end_timestamp = current_timestamp
240
+ elif self.current_incident_end_timestamp != 'Incident still active' and self.current_incident_end_timestamp != 'N/A':
241
+ self.current_incident_end_timestamp = 'N/A'
242
+
243
+ if config.alert_config and config.alert_config.count_thresholds:
244
+ threshold = config.alert_config.count_thresholds.get("all", 15)
245
+ intensity = min(10.0, (total_detections / threshold) * 10)
246
+ if intensity >= 9:
247
+ level = "critical"
248
+ self._ascending_alert_list.append(3)
249
+ elif intensity >= 7:
250
+ level = "significant"
251
+ self._ascending_alert_list.append(2)
252
+ elif intensity >= 5:
253
+ level = "medium"
254
+ self._ascending_alert_list.append(1)
255
+ else:
256
+ level = "low"
257
+ self._ascending_alert_list.append(0)
258
+ else:
259
+ if total_detections > 30:
260
+ level = "critical"
261
+ intensity = 10.0
262
+ self._ascending_alert_list.append(3)
263
+ elif total_detections > 25:
264
+ level = " significant"
265
+ intensity = 9.0
266
+ self._ascending_alert_list.append(2)
267
+ elif total_detections > 15:
268
+ level = "medium"
269
+ intensity = 7.0
270
+ self._ascending_alert_list.append(1)
271
+ else:
272
+ level = "low"
273
+ intensity = min(10.0, total_detections / 3.0)
274
+ self._ascending_alert_list.append(0)
275
+
276
+ human_text_lines = [f"INCIDENTS DETECTED @ {current_timestamp}:"]
277
+ human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE, level)}")
278
+ human_text = "\n".join(human_text_lines)
279
+
280
+ alert_settings = []
281
+ if config.alert_config and hasattr(config.alert_config, 'alert_type'):
282
+ alert_settings.append({
283
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
284
+ "incident_category": self.CASE_TYPE,
285
+ "threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
286
+ "ascending": True,
287
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
288
+ getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])}
289
+ })
290
+
291
+ event = self.create_incident(
292
+ incident_id=self.CASE_TYPE + '_' + str(frame_number),
293
+ incident_type=self.CASE_TYPE,
294
+ severity_level=level,
295
+ human_text=human_text,
296
+ camera_info=camera_info,
297
+ alerts=alerts,
298
+ alert_settings=alert_settings,
299
+ start_time=start_timestamp,
300
+ end_time=self.current_incident_end_timestamp,
301
+ level_settings={"low": 1, "medium": 3, "significant": 4, "critical": 7}
302
+ )
303
+ incidents.append(event)
304
+ else:
305
+ self._ascending_alert_list.append(0)
306
+ incidents.append({})
307
+ return incidents
308
+
309
+ def _generate_tracking_stats(self, counting_summary: Dict, alerts: List, config: LaneDetectionConfig,
310
+ frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
311
+ camera_info = self.get_camera_info_from_stream(stream_info)
312
+ tracking_stats = []
313
+ total_detections = counting_summary.get("total_count", 0)
314
+ total_counts_dict = counting_summary.get("total_counts", {})
315
+ per_category_count = counting_summary.get("per_category_count", {})
316
+ current_timestamp = self._get_current_timestamp_str(stream_info, precision=False)
317
+ start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
318
+ high_precision_start_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
319
+ high_precision_reset_timestamp = self._get_start_timestamp_str(stream_info, precision=True)
320
+
321
+ total_counts = [{"category": cat, "count": count} for cat, count in total_counts_dict.items() if count > 0]
322
+ current_counts = [{"category": cat, "count": count} for cat, count in per_category_count.items() if count > 0 or total_detections > 0]
323
+
324
+ detections = []
325
+ for detection in counting_summary.get("detections", []):
326
+ bbox = detection.get("bounding_box", {})
327
+ category = detection.get("category", "lane")
328
+ if detection.get("masks"):
329
+ segmentation = detection.get("masks", [])
330
+ detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
331
+ elif detection.get("segmentation"):
332
+ segmentation = detection.get("segmentation")
333
+ detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
334
+ elif detection.get("mask"):
335
+ segmentation = detection.get("mask")
336
+ detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
337
+ else:
338
+ detection_obj = self.create_detection_object(category, bbox)
339
+ detections.append(detection_obj)
340
+
341
+ alert_settings = []
342
+ if config.alert_config and hasattr(config.alert_config, 'alert_type'):
343
+ alert_settings.append({
344
+ "alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
345
+ "incident_category": self.CASE_TYPE,
346
+ "threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
347
+ "ascending": True,
348
+ "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
349
+ getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])}
350
+ })
351
+
352
+ human_text_lines = [f"Tracking Statistics:"]
353
+ human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}")
354
+ for cat, count in per_category_count.items():
355
+ human_text_lines.append(f"\t{cat}: {count}")
356
+ human_text_lines.append(f"TOTAL SINCE {start_timestamp}")
357
+ for cat, count in total_counts_dict.items():
358
+ if count > 0:
359
+ human_text_lines.append(f"\t{cat}: {count}")
360
+ if alerts:
361
+ for alert in alerts:
362
+ human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
363
+ else:
364
+ human_text_lines.append("Alerts: None")
365
+ human_text = "\n".join(human_text_lines)
366
+
367
+ reset_settings = [{"interval_type": "daily", "reset_time": {"value": 9, "time_unit": "hour"}}]
368
+
369
+ tracking_stat = self.create_tracking_stats(
370
+ total_counts=total_counts,
371
+ current_counts=current_counts,
372
+ detections=detections,
373
+ human_text=human_text,
374
+ camera_info=camera_info,
375
+ alerts=alerts,
376
+ alert_settings=alert_settings,
377
+ reset_settings=reset_settings,
378
+ start_time=high_precision_start_timestamp,
379
+ reset_time=high_precision_reset_timestamp
380
+ )
381
+ tracking_stats.append(tracking_stat)
382
+ return tracking_stats
383
+
384
+ def _generate_business_analytics(self, counting_summary: Dict, alerts: Any, config: LaneDetectionConfig,
385
+ stream_info: Optional[Dict[str, Any]] = None, is_empty=False) -> List[Dict]:
386
+ if is_empty:
387
+ return []
388
+
389
+ def _generate_summary(self, summary: dict, incidents: List, tracking_stats: List, business_analytics: List, alerts: List) -> List[str]:
390
+ lines = {}
391
+ lines["Application Name"] = self.CASE_TYPE
392
+ lines["Application Version"] = self.CASE_VERSION
393
+ if len(incidents) > 0:
394
+ lines["Incidents:"] = f"\n\t{incidents[0].get('human_text', 'No incidents detected')}\n"
395
+ if len(tracking_stats) > 0:
396
+ lines["Tracking Statistics:"] = f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}\n"
397
+ if len(business_analytics) > 0:
398
+ lines["Business Analytics:"] = f"\t{business_analytics[0].get('human_text', 'No business analytics detected')}\n"
399
+ if len(incidents) == 0 and len(tracking_stats) == 0 and len(business_analytics) == 0:
400
+ lines["Summary"] = "No Summary Data"
401
+ return [lines]
402
+
403
+ def _get_track_ids_info(self, detections: list) -> Dict[str, Any]:
404
+ frame_track_ids = set()
405
+ for det in detections:
406
+ tid = det.get('track_id')
407
+ if tid is not None:
408
+ frame_track_ids.add(tid)
409
+ total_track_ids = set()
410
+ for s in getattr(self, '_per_category_total_track_ids', {}).values():
411
+ total_track_ids.update(s)
412
+ return {
413
+ "total_count": len(total_track_ids),
414
+ "current_frame_count": len(frame_track_ids),
415
+ "total_unique_track_ids": len(total_track_ids),
416
+ "current_frame_track_ids": list(frame_track_ids),
417
+ "last_update_time": time.time(),
418
+ "total_frames_processed": getattr(self, '_total_frame_counter', 0)
419
+ }
420
+
421
+ def _update_tracking_state(self, detections: list):
422
+ if not hasattr(self, "_per_category_total_track_ids"):
423
+ self._per_category_total_track_ids = {cat: set() for cat in self.target_categories}
424
+ self._current_frame_track_ids = {cat: set() for cat in self.target_categories}
425
+
426
+ for det in detections:
427
+ cat = det.get("category")
428
+ raw_track_id = det.get("track_id")
429
+ if cat not in self.target_categories or raw_track_id is None:
430
+ continue
431
+ bbox = det.get("bounding_box", det.get("bbox"))
432
+ canonical_id = self._merge_or_register_track(raw_track_id, bbox)
433
+ det["track_id"] = canonical_id
434
+ self._per_category_total_track_ids.setdefault(cat, set()).add(canonical_id)
435
+ self._current_frame_track_ids[cat].add(canonical_id)
436
+
437
+ def get_total_counts(self):
438
+ return {cat: len(ids) for cat, ids in getattr(self, '_per_category_total_track_ids', {}).items()}
439
+
440
+ def _format_timestamp_for_stream(self, timestamp: float) -> str:
441
+ dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
442
+ return dt.strftime('%Y:%m:%d %H:%M:%S')
443
+
444
+ def _format_timestamp_for_video(self, timestamp: float) -> str:
445
+ hours = int(timestamp // 3600)
446
+ minutes = int((timestamp % 3600) // 60)
447
+ seconds = round(float(timestamp % 60), 2)
448
+ return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
449
+
450
+ def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
451
+ """Get formatted current timestamp based on stream type."""
452
+ if not stream_info:
453
+ return "00:00:00.00"
454
+ # is_video_chunk = stream_info.get("input_settings", {}).get("is_video_chunk", False)
455
+ if precision:
456
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
457
+ if frame_id:
458
+ start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
459
+ else:
460
+ start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
461
+ stream_time_str = self._format_timestamp_for_video(start_time)
462
+ return stream_time_str
463
+ else:
464
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
465
+
466
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
467
+ if frame_id:
468
+ start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
469
+ else:
470
+ start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
471
+ stream_time_str = self._format_timestamp_for_video(start_time)
472
+ return stream_time_str
473
+ else:
474
+ # For streams, use stream_time from stream_info
475
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
476
+ if stream_time_str:
477
+ # Parse the high precision timestamp string to get timestamp
478
+ try:
479
+ # Remove " UTC" suffix and parse
480
+ timestamp_str = stream_time_str.replace(" UTC", "")
481
+ dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
482
+ timestamp = dt.replace(tzinfo=timezone.utc).timestamp()
483
+ return self._format_timestamp_for_stream(timestamp)
484
+ except:
485
+ # Fallback to current time if parsing fails
486
+ return self._format_timestamp_for_stream(time.time())
487
+ else:
488
+ return self._format_timestamp_for_stream(time.time())
489
+
490
+ def _get_start_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False) -> str:
491
+ if not stream_info:
492
+ return "00:00:00"
493
+ if precision:
494
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
495
+ return "00:00:00"
496
+ else:
497
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
498
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
499
+ return "00:00:00"
500
+ else:
501
+ if self._tracking_start_time is None:
502
+ stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
503
+ if stream_time_str:
504
+ try:
505
+ timestamp_str = stream_time_str.replace(" UTC", "")
506
+ dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
507
+ self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
508
+ except:
509
+ self._tracking_start_time = time.time()
510
+ else:
511
+ self._tracking_start_time = time.time()
512
+ dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
513
+ dt = dt.replace(minute=0, second=0, microsecond=0)
514
+ return dt.strftime('%Y:%m:%d %H:%M:%S')
515
+
516
+ def _count_categories(self, detections: list, config: LaneDetectionConfig) -> dict:
517
+ counts = {}
518
+ for det in detections:
519
+ cat = det.get('category', 'unknown')
520
+ counts[cat] = counts.get(cat, 0) + 1
521
+ return {
522
+ "total_count": sum(counts.values()),
523
+ "per_category_count": counts,
524
+ "detections": [
525
+ {
526
+ "bounding_box": det.get("bounding_box"),
527
+ "category": det.get("category"),
528
+ "confidence": det.get("confidence"),
529
+ "track_id": det.get("track_id"),
530
+ "frame_id": det.get("frame_id")
531
+ }
532
+ for det in detections
533
+ ]
534
+ }
535
+
536
+ def _extract_predictions(self, detections: list) -> List[Dict[str, Any]]:
537
+ return [
538
+ {
539
+ "category": det.get("category", "unknown"),
540
+ "confidence": det.get("confidence", 0.0),
541
+ "bounding_box": det.get("bounding_box", {})
542
+ }
543
+ for det in detections
544
+ ]
545
+
546
+ def _compute_iou(self, box1: Any, box2: Any) -> float:
547
+ def _bbox_to_list(bbox):
548
+ if bbox is None:
549
+ return []
550
+ if isinstance(bbox, list):
551
+ return bbox[:4] if len(bbox) >= 4 else []
552
+ if isinstance(bbox, dict):
553
+ if "xmin" in bbox:
554
+ return [bbox["xmin"], bbox["ymin"], bbox["xmax"], bbox["ymax"]]
555
+ if "x1" in bbox:
556
+ return [bbox["x1"], bbox["y1"], bbox["x2"], bbox["y2"]]
557
+ values = [v for v in bbox.values() if isinstance(v, (int, float))]
558
+ return values[:4] if len(values) >= 4 else []
559
+ return []
560
+
561
+ l1 = _bbox_to_list(box1)
562
+ l2 = _bbox_to_list(box2)
563
+ if len(l1) < 4 or len(l2) < 4:
564
+ return 0.0
565
+ x1_min, y1_min, x1_max, y1_max = l1
566
+ x2_min, y2_min, x2_max, y2_max = l2
567
+ x1_min, x1_max = min(x1_min, x1_max), max(x1_min, x1_max)
568
+ y1_min, y1_max = min(y1_min, y1_max), max(y1_min, y1_max)
569
+ x2_min, x2_max = min(x2_min, x2_max), max(x2_min, x2_max)
570
+ y2_min, y2_max = min(y2_min, y2_max), max(y2_min, y2_max)
571
+ inter_x_min = max(x1_min, x2_min)
572
+ inter_y_min = max(y1_min, y2_min)
573
+ inter_x_max = min(x1_max, x2_max)
574
+ inter_y_max = min(y1_max, y2_max)
575
+ inter_w = max(0.0, inter_x_max - inter_x_min)
576
+ inter_h = max(0.0, inter_y_max - inter_y_min)
577
+ inter_area = inter_w * inter_h
578
+ area1 = (x1_max - x1_min) * (y1_max - y1_min)
579
+ area2 = (x2_max - x2_min) * (y2_max - y2_min)
580
+ union_area = area1 + area2 - inter_area
581
+ return (inter_area / union_area) if union_area > 0 else 0.0
582
+
583
+ def _merge_or_register_track(self, raw_id: Any, bbox: Any) -> Any:
584
+ if raw_id is None or bbox is None:
585
+ return raw_id
586
+ now = time.time()
587
+ if raw_id in self._track_aliases:
588
+ canonical_id = self._track_aliases[raw_id]
589
+ track_info = self._canonical_tracks.get(canonical_id)
590
+ if track_info is not None:
591
+ track_info["last_bbox"] = bbox
592
+ track_info["last_update"] = now
593
+ track_info["raw_ids"].add(raw_id)
594
+ return canonical_id
595
+ for canonical_id, info in self._canonical_tracks.items():
596
+ if now - info["last_update"] > self._track_merge_time_window:
597
+ continue
598
+ iou = self._compute_iou(bbox, info["last_bbox"])
599
+ if iou >= self._track_merge_iou_threshold:
600
+ self._track_aliases[raw_id] = canonical_id
601
+ info["last_bbox"] = bbox
602
+ info["last_update"] = now
603
+ info["raw_ids"].add(raw_id)
604
+ return canonical_id
605
+ canonical_id = raw_id
606
+ self._track_aliases[raw_id] = canonical_id
607
+ self._canonical_tracks[canonical_id] = {
608
+ "last_bbox": bbox,
609
+ "last_update": now,
610
+ "raw_ids": {raw_id},
611
+ }
612
+ return canonical_id
613
+
614
+ def _format_timestamp(self, timestamp: float) -> str:
615
+ return datetime.fromtimestamp(timestamp, timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
616
+
617
+ def _get_tracking_start_time(self) -> str:
618
+ if self._tracking_start_time is None:
619
+ return "N/A"
620
+ return self._format_timestamp(self._tracking_start_time)
621
+
622
+ def _set_tracking_start_time(self) -> None:
623
+ self._tracking_start_time = time.time()