matrice-analytics 0.1.3__py3-none-any.whl → 0.1.32__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.

Potentially problematic release.


This version of matrice-analytics might be problematic. Click here for more details.

Files changed (61) hide show
  1. matrice_analytics/post_processing/advanced_tracker/matching.py +3 -3
  2. matrice_analytics/post_processing/advanced_tracker/strack.py +1 -1
  3. matrice_analytics/post_processing/config.py +4 -0
  4. matrice_analytics/post_processing/core/config.py +115 -12
  5. matrice_analytics/post_processing/face_reg/compare_similarity.py +5 -5
  6. matrice_analytics/post_processing/face_reg/embedding_manager.py +109 -8
  7. matrice_analytics/post_processing/face_reg/face_recognition.py +157 -61
  8. matrice_analytics/post_processing/face_reg/face_recognition_client.py +339 -88
  9. matrice_analytics/post_processing/face_reg/people_activity_logging.py +67 -29
  10. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
  11. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
  12. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
  13. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
  14. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
  15. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
  16. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
  17. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
  18. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
  19. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
  20. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
  21. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
  22. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
  23. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
  24. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
  25. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
  26. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
  27. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
  28. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
  29. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
  30. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
  31. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
  32. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
  33. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
  34. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
  35. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
  36. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
  37. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
  38. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
  39. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
  40. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
  41. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
  42. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
  43. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
  44. matrice_analytics/post_processing/ocr/postprocessing.py +0 -1
  45. matrice_analytics/post_processing/post_processor.py +32 -11
  46. matrice_analytics/post_processing/usecases/color/clip.py +42 -8
  47. matrice_analytics/post_processing/usecases/color/color_mapper.py +2 -2
  48. matrice_analytics/post_processing/usecases/color_detection.py +50 -129
  49. matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +41 -386
  50. matrice_analytics/post_processing/usecases/flare_analysis.py +1 -56
  51. matrice_analytics/post_processing/usecases/license_plate_detection.py +476 -202
  52. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +351 -26
  53. matrice_analytics/post_processing/usecases/people_counting.py +408 -1431
  54. matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
  55. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +39 -10
  56. matrice_analytics/post_processing/utils/__init__.py +8 -8
  57. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/METADATA +1 -1
  58. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/RECORD +61 -26
  59. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/WHEEL +0 -0
  60. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/licenses/LICENSE.txt +0 -0
  61. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,15 @@
1
- from typing import Any, Dict, List, Optional
2
1
  from dataclasses import asdict
3
2
  import time
4
3
  from datetime import datetime, timezone
5
4
  import copy # Added for deep copying detections to preserve original masks
6
-
7
- from ..core.base import BaseProcessor, ProcessingContext, ProcessingResult, ConfigProtocol, ResultFormat
5
+ from typing import Any, Dict, List, Optional
6
+ from ..core.base import (
7
+ BaseProcessor,
8
+ ProcessingContext,
9
+ ProcessingResult,
10
+ ConfigProtocol,
11
+ ResultFormat,
12
+ )
8
13
  from ..utils import (
9
14
  filter_by_confidence,
10
15
  filter_by_categories,
@@ -15,7 +20,7 @@ from ..utils import (
15
20
  match_results_structure,
16
21
  bbox_smoothing,
17
22
  BBoxSmoothingConfig,
18
- BBoxSmoothingTracker
23
+ BBoxSmoothingTracker,
19
24
  )
20
25
  from dataclasses import dataclass, field
21
26
  from ..core.config import BaseConfig, AlertConfig, ZoneConfig
@@ -24,6 +29,7 @@ from ..core.config import BaseConfig, AlertConfig, ZoneConfig
24
29
  @dataclass
25
30
  class LicensePlateConfig(BaseConfig):
26
31
  """Configuration for License plate detection use case in License plate monitoring."""
32
+
27
33
  # Smoothing configuration
28
34
  enable_smoothing: bool = True
29
35
  smoothing_algorithm: str = "observability" # "window" or "observability"
@@ -31,16 +37,12 @@ class LicensePlateConfig(BaseConfig):
31
37
  smoothing_cooldown_frames: int = 5
32
38
  smoothing_confidence_range_factor: float = 0.5
33
39
 
34
- #confidence thresholds
40
+ # confidence thresholds
35
41
  confidence_threshold: float = 0.6
36
42
 
37
- usecase_categories: List[str] = field(
38
- default_factory=lambda: ['license_plate']
39
- )
43
+ usecase_categories: List[str] = field(default_factory=lambda: ["license_plate"])
40
44
 
41
- target_categories: List[str] = field(
42
- default_factory=lambda: ['license_plate']
43
- )
45
+ target_categories: List[str] = field(default_factory=lambda: ["license_plate"])
44
46
 
45
47
  alert_config: Optional[AlertConfig] = None
46
48
 
@@ -57,16 +59,16 @@ class LicensePlateUseCase(BaseProcessor):
57
59
  CATEGORY_DISPLAY = {
58
60
  "license_plate": "license_plate",
59
61
  }
60
-
62
+
61
63
  def __init__(self):
62
64
  super().__init__("license_plate_detection")
63
65
  self.category = "license_plate"
64
66
 
65
67
  # List of categories to track
66
- self.target_categories = ['license_plate']
68
+ self.target_categories = ["license_plate"]
67
69
 
68
- self.CASE_TYPE: Optional[str] = 'license_plate_detection'
69
- self.CASE_VERSION: Optional[str] = '1.3'
70
+ self.CASE_TYPE: Optional[str] = "license_plate_detection"
71
+ self.CASE_VERSION: Optional[str] = "1.3"
70
72
 
71
73
  # Initialize smoothing tracker
72
74
  self.smoothing_tracker = None
@@ -97,8 +99,13 @@ class LicensePlateUseCase(BaseProcessor):
97
99
  self._ascending_alert_list: List[int] = []
98
100
  self.current_incident_end_timestamp: str = "N/A"
99
101
 
100
- def process(self, data: Any, config: ConfigProtocol, context: Optional[ProcessingContext] = None,
101
- stream_info: Optional[Dict[str, Any]] = None) -> ProcessingResult:
102
+ def process(
103
+ self,
104
+ data: Any,
105
+ config: ConfigProtocol,
106
+ context: Optional[ProcessingContext] = None,
107
+ stream_info: Optional[Dict[str, Any]] = None,
108
+ ) -> ProcessingResult:
102
109
  """
103
110
  Main entry point for post-processing.
104
111
  Applies category mapping, smoothing, counting, alerting, and summary generation.
@@ -107,8 +114,12 @@ class LicensePlateUseCase(BaseProcessor):
107
114
  start_time = time.time()
108
115
  # Ensure config is correct type
109
116
  if not isinstance(config, LicensePlateConfig):
110
- return self.create_error_result("Invalid config type", usecase=self.name, category=self.category,
111
- context=context)
117
+ return self.create_error_result(
118
+ "Invalid config type",
119
+ usecase=self.name,
120
+ category=self.category,
121
+ context=context,
122
+ )
112
123
  if context is None:
113
124
  context = ProcessingContext()
114
125
 
@@ -116,21 +127,27 @@ class LicensePlateUseCase(BaseProcessor):
116
127
  input_format = match_results_structure(data)
117
128
  context.input_format = input_format
118
129
  context.confidence_threshold = config.confidence_threshold
119
-
130
+
120
131
  # Step 1: Confidence filtering
121
132
  if config.confidence_threshold is not None:
122
133
  processed_data = filter_by_confidence(data, config.confidence_threshold)
123
134
  else:
124
135
  processed_data = data
125
- self.logger.debug(f"Did not apply confidence filtering with threshold since nothing was provided")
136
+ self.logger.debug(
137
+ f"Did not apply confidence filtering with threshold since nothing was provided"
138
+ )
126
139
 
127
140
  # Step 2: Apply category mapping if provided
128
141
  if config.index_to_category:
129
- processed_data = apply_category_mapping(processed_data, config.index_to_category)
142
+ processed_data = apply_category_mapping(
143
+ processed_data, config.index_to_category
144
+ )
130
145
 
131
146
  # Step 3: Category filtering
132
147
  if config.target_categories:
133
- processed_data = [d for d in processed_data if d.get('category') in self.target_categories]
148
+ processed_data = [
149
+ d for d in processed_data if d.get("category") in self.target_categories
150
+ ]
134
151
 
135
152
  # Step 4: Apply bbox smoothing if enabled
136
153
  # Deep-copy detections so that we preserve the original masks before any
@@ -144,18 +161,20 @@ class LicensePlateUseCase(BaseProcessor):
144
161
  cooldown_frames=config.smoothing_cooldown_frames,
145
162
  confidence_threshold=config.confidence_threshold,
146
163
  confidence_range_factor=config.smoothing_confidence_range_factor,
147
- enable_smoothing=True
164
+ enable_smoothing=True,
148
165
  )
149
166
  self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
150
-
151
- processed_data = bbox_smoothing(processed_data, self.smoothing_tracker.config, self.smoothing_tracker)
167
+
168
+ processed_data = bbox_smoothing(
169
+ processed_data, self.smoothing_tracker.config, self.smoothing_tracker
170
+ )
152
171
  # Restore masks after smoothing
153
172
 
154
173
  # Step 5: Advanced tracking (BYTETracker-like)
155
174
  try:
156
175
  from ..advanced_tracker import AdvancedTracker
157
176
  from ..advanced_tracker.config import TrackerConfig
158
-
177
+
159
178
  if self.tracker is None:
160
179
  # Configure tracker thresholds based on the use-case confidence threshold so that
161
180
  # low-confidence detections (e.g. < 0.7) can still be initialised as tracks when
@@ -164,8 +183,10 @@ class LicensePlateUseCase(BaseProcessor):
164
183
  tracker_config = TrackerConfig(
165
184
  track_high_thresh=float(config.confidence_threshold),
166
185
  # Allow even lower detections to participate in secondary association
167
- track_low_thresh=max(0.05, float(config.confidence_threshold) / 2),
168
- new_track_thresh=float(config.confidence_threshold)
186
+ track_low_thresh=max(
187
+ 0.05, float(config.confidence_threshold) / 2
188
+ ),
189
+ new_track_thresh=float(config.confidence_threshold),
169
190
  )
170
191
  else:
171
192
  tracker_config = TrackerConfig()
@@ -193,7 +214,9 @@ class LicensePlateUseCase(BaseProcessor):
193
214
  # processed detection back to the raw detection with the highest IoU
194
215
  # and copy over its "masks" field (if available).
195
216
  # ------------------------------------------------------------------ #
196
- processed_data = self._attach_masks_to_detections(processed_data, raw_processed_data)
217
+ processed_data = self._attach_masks_to_detections(
218
+ processed_data, raw_processed_data
219
+ )
197
220
 
198
221
  # Update frame counter
199
222
  self._total_frame_counter += 1
@@ -205,7 +228,11 @@ class LicensePlateUseCase(BaseProcessor):
205
228
  start_frame = input_settings.get("start_frame")
206
229
  end_frame = input_settings.get("end_frame")
207
230
  # If start and end frame are the same, it's a single frame
208
- if start_frame is not None and end_frame is not None and start_frame == end_frame:
231
+ if (
232
+ start_frame is not None
233
+ and end_frame is not None
234
+ and start_frame == end_frame
235
+ ):
209
236
  frame_number = start_frame
210
237
 
211
238
  # Compute summaries and alerts
@@ -213,31 +240,45 @@ class LicensePlateUseCase(BaseProcessor):
213
240
  counting_summary = self._count_categories(processed_data, config)
214
241
  # Add total unique counts after tracking using only local state
215
242
  total_counts = self.get_total_counts()
216
- counting_summary['total_counts'] = total_counts
243
+ counting_summary["total_counts"] = total_counts
217
244
 
218
245
  alerts = self._check_alerts(counting_summary, frame_number, config)
219
246
  predictions = self._extract_predictions(processed_data)
220
-
247
+
221
248
  # Step: Generate structured events and tracking stats with frame-based keys
222
- incidents_list = self._generate_incidents(counting_summary, alerts, config, frame_number, stream_info)
223
- tracking_stats_list = self._generate_tracking_stats(counting_summary, alerts, config, frame_number,stream_info)
249
+ incidents_list = self._generate_incidents(
250
+ counting_summary, alerts, config, frame_number, stream_info
251
+ )
252
+ tracking_stats_list = self._generate_tracking_stats(
253
+ counting_summary, alerts, config, frame_number, stream_info
254
+ )
224
255
  # business_analytics_list = self._generate_business_analytics(counting_summary, alerts, config, frame_number, stream_info, is_empty=False)
225
256
  business_analytics_list = []
226
- summary_list = self._generate_summary(counting_summary, incidents_list, tracking_stats_list, business_analytics_list, alerts)
257
+ summary_list = self._generate_summary(
258
+ counting_summary,
259
+ incidents_list,
260
+ tracking_stats_list,
261
+ business_analytics_list,
262
+ alerts,
263
+ )
227
264
 
228
265
  # Extract frame-based dictionaries from the lists
229
266
  incidents = incidents_list[0] if incidents_list else {}
230
267
  tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
231
- business_analytics = business_analytics_list[0] if business_analytics_list else {}
268
+ business_analytics = (
269
+ business_analytics_list[0] if business_analytics_list else {}
270
+ )
232
271
  summary = summary_list[0] if summary_list else {}
233
- agg_summary = {str(frame_number): {
234
- "incidents": incidents,
235
- "tracking_stats": tracking_stats,
236
- "business_analytics": business_analytics,
237
- "alerts": alerts,
238
- "human_text": summary}
239
- }
240
-
272
+ agg_summary = {
273
+ str(frame_number): {
274
+ "incidents": incidents,
275
+ "tracking_stats": tracking_stats,
276
+ "business_analytics": business_analytics,
277
+ "alerts": alerts,
278
+ "human_text": summary,
279
+ }
280
+ }
281
+
241
282
  context.mark_completed()
242
283
 
243
284
  # Build result object following the new pattern
@@ -246,20 +287,23 @@ class LicensePlateUseCase(BaseProcessor):
246
287
  data={"agg_summary": agg_summary},
247
288
  usecase=self.name,
248
289
  category=self.category,
249
- context=context
290
+ context=context,
250
291
  )
251
-
292
+
252
293
  return result
253
294
 
254
- def _check_alerts(self, summary: dict, frame_number:Any, config: LicensePlateConfig) -> List[Dict]:
295
+ def _check_alerts(
296
+ self, summary: dict, frame_number: Any, config: LicensePlateConfig
297
+ ) -> List[Dict]:
255
298
  """
256
299
  Check if any alert thresholds are exceeded and return alert dicts.
257
300
  """
301
+
258
302
  def get_trend(data, lookback=900, threshold=0.6):
259
- '''
303
+ """
260
304
  Determine if the trend is ascending or descending based on actual value progression.
261
305
  Now works with values 0,1,2,3 (not just binary).
262
- '''
306
+ """
263
307
  window = data[-lookback:] if len(data) >= lookback else data
264
308
  if len(window) < 2:
265
309
  return True # not enough data to determine trend
@@ -277,75 +321,166 @@ class LicensePlateUseCase(BaseProcessor):
277
321
 
278
322
  frame_key = str(frame_number) if frame_number is not None else "current_frame"
279
323
  alerts = []
280
- total_detections = summary.get("total_count", 0) #CURRENT combined total count of all classes
281
- total_counts_dict = summary.get("total_counts", {}) #TOTAL cumulative counts per class
282
- cumulative_total = sum(total_counts_dict.values()) if total_counts_dict else 0 #TOTAL combined cumulative count
283
- per_category_count = summary.get("per_category_count", {}) #CURRENT count per class
324
+ total_detections = summary.get(
325
+ "total_count", 0
326
+ ) # CURRENT combined total count of all classes
327
+ total_counts_dict = summary.get(
328
+ "total_counts", {}
329
+ ) # TOTAL cumulative counts per class
330
+ cumulative_total = (
331
+ sum(total_counts_dict.values()) if total_counts_dict else 0
332
+ ) # TOTAL combined cumulative count
333
+ per_category_count = summary.get(
334
+ "per_category_count", {}
335
+ ) # CURRENT count per class
284
336
 
285
337
  if not config.alert_config:
286
338
  return alerts
287
339
 
288
340
  total = summary.get("total_count", 0)
289
- #self._ascending_alert_list
290
- if hasattr(config.alert_config, 'count_thresholds') and config.alert_config.count_thresholds:
341
+ # self._ascending_alert_list
342
+ if (
343
+ hasattr(config.alert_config, "count_thresholds")
344
+ and config.alert_config.count_thresholds
345
+ ):
291
346
 
292
347
  for category, threshold in config.alert_config.count_thresholds.items():
293
- if category == "all" and total > threshold:
294
-
295
- alerts.append({
296
- "alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
297
- "alert_id": "alert_"+category+'_'+frame_key,
298
- "incident_category": self.CASE_TYPE,
299
- "threshold_level": threshold,
300
- "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
301
- "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
302
- getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
303
- }
304
- })
348
+ if category == "all" and total > threshold:
349
+
350
+ alerts.append(
351
+ {
352
+ "alert_type": (
353
+ getattr(config.alert_config, "alert_type", ["Default"])
354
+ if hasattr(config.alert_config, "alert_type")
355
+ else ["Default"]
356
+ ),
357
+ "alert_id": "alert_" + category + "_" + frame_key,
358
+ "incident_category": self.CASE_TYPE,
359
+ "threshold_level": threshold,
360
+ "ascending": get_trend(
361
+ self._ascending_alert_list, lookback=900, threshold=0.8
362
+ ),
363
+ "settings": {
364
+ t: v
365
+ for t, v in zip(
366
+ (
367
+ getattr(
368
+ config.alert_config,
369
+ "alert_type",
370
+ ["Default"],
371
+ )
372
+ if hasattr(config.alert_config, "alert_type")
373
+ else ["Default"]
374
+ ),
375
+ (
376
+ getattr(
377
+ config.alert_config, "alert_value", ["JSON"]
378
+ )
379
+ if hasattr(config.alert_config, "alert_value")
380
+ else ["JSON"]
381
+ ),
382
+ )
383
+ },
384
+ }
385
+ )
305
386
  elif category in summary.get("per_category_count", {}):
306
387
  count = summary.get("per_category_count", {})[category]
307
388
  if count > threshold: # Fixed logic: alert when EXCEEDING threshold
308
- alerts.append({
309
- "alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
310
- "alert_id": "alert_"+category+'_'+frame_key,
311
- "incident_category": self.CASE_TYPE,
312
- "threshold_level": threshold,
313
- "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
314
- "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
315
- getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
316
- }
317
- })
389
+ alerts.append(
390
+ {
391
+ "alert_type": (
392
+ getattr(
393
+ config.alert_config, "alert_type", ["Default"]
394
+ )
395
+ if hasattr(config.alert_config, "alert_type")
396
+ else ["Default"]
397
+ ),
398
+ "alert_id": "alert_" + category + "_" + frame_key,
399
+ "incident_category": self.CASE_TYPE,
400
+ "threshold_level": threshold,
401
+ "ascending": get_trend(
402
+ self._ascending_alert_list,
403
+ lookback=900,
404
+ threshold=0.8,
405
+ ),
406
+ "settings": {
407
+ t: v
408
+ for t, v in zip(
409
+ (
410
+ getattr(
411
+ config.alert_config,
412
+ "alert_type",
413
+ ["Default"],
414
+ )
415
+ if hasattr(
416
+ config.alert_config, "alert_type"
417
+ )
418
+ else ["Default"]
419
+ ),
420
+ (
421
+ getattr(
422
+ config.alert_config,
423
+ "alert_value",
424
+ ["JSON"],
425
+ )
426
+ if hasattr(
427
+ config.alert_config, "alert_value"
428
+ )
429
+ else ["JSON"]
430
+ ),
431
+ )
432
+ },
433
+ }
434
+ )
318
435
  else:
319
436
  pass
320
437
  return alerts
321
438
 
322
- def _generate_incidents(self, counting_summary: Dict, alerts: List, config: LicensePlateConfig,
323
- frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[
324
- Dict]:
439
+ def _generate_incidents(
440
+ self,
441
+ counting_summary: Dict,
442
+ alerts: List,
443
+ config: LicensePlateConfig,
444
+ frame_number: Optional[int] = None,
445
+ stream_info: Optional[Dict[str, Any]] = None,
446
+ ) -> List[Dict]:
325
447
  """Generate structured events for the output format with frame-based keys."""
326
448
 
327
449
  # Use frame number as key, fallback to 'current_frame' if not available
328
450
  frame_key = str(frame_number) if frame_number is not None else "current_frame"
329
- incidents=[]
451
+ incidents = []
330
452
  total_detections = counting_summary.get("total_count", 0)
331
453
  current_timestamp = self._get_current_timestamp_str(stream_info)
332
454
  camera_info = self.get_camera_info_from_stream(stream_info)
333
-
334
- self._ascending_alert_list = self._ascending_alert_list[-900:] if len(self._ascending_alert_list) > 900 else self._ascending_alert_list
455
+
456
+ self._ascending_alert_list = (
457
+ self._ascending_alert_list[-900:]
458
+ if len(self._ascending_alert_list) > 900
459
+ else self._ascending_alert_list
460
+ )
335
461
 
336
462
  if total_detections > 0:
337
463
  # Determine event level based on thresholds
338
464
  level = "low"
339
465
  intensity = 5.0
340
466
  start_timestamp = self._get_start_timestamp_str(stream_info)
341
- if start_timestamp and self.current_incident_end_timestamp=='N/A':
342
- self.current_incident_end_timestamp = 'Incident still active'
343
- elif start_timestamp and self.current_incident_end_timestamp=='Incident still active':
344
- if len(self._ascending_alert_list) >= 15 and sum(self._ascending_alert_list[-15:]) / 15 < 1.5:
467
+ if start_timestamp and self.current_incident_end_timestamp == "N/A":
468
+ self.current_incident_end_timestamp = "Incident still active"
469
+ elif (
470
+ start_timestamp
471
+ and self.current_incident_end_timestamp == "Incident still active"
472
+ ):
473
+ if (
474
+ len(self._ascending_alert_list) >= 15
475
+ and sum(self._ascending_alert_list[-15:]) / 15 < 1.5
476
+ ):
345
477
  self.current_incident_end_timestamp = current_timestamp
346
- elif self.current_incident_end_timestamp!='Incident still active' and self.current_incident_end_timestamp!='N/A':
347
- self.current_incident_end_timestamp = 'N/A'
348
-
478
+ elif (
479
+ self.current_incident_end_timestamp != "Incident still active"
480
+ and self.current_incident_end_timestamp != "N/A"
481
+ ):
482
+ self.current_incident_end_timestamp = "N/A"
483
+
349
484
  if config.alert_config and config.alert_config.count_thresholds:
350
485
  threshold = config.alert_config.count_thresholds.get("all", 15)
351
486
  intensity = min(10.0, (total_detections / threshold) * 10)
@@ -380,27 +515,61 @@ class LicensePlateUseCase(BaseProcessor):
380
515
  intensity = min(10.0, total_detections / 3.0)
381
516
  self._ascending_alert_list.append(0)
382
517
 
383
- # Generate human text in new format
518
+ # Generate human text in new format
384
519
  human_text_lines = [f"INCIDENTS DETECTED @ {current_timestamp}:"]
385
520
  human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE,level)}")
386
521
  human_text = "\n".join(human_text_lines)
387
522
 
388
- alert_settings=[]
389
- if config.alert_config and hasattr(config.alert_config, 'alert_type'):
390
- alert_settings.append({
391
- "alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
392
- "incident_category": self.CASE_TYPE,
393
- "threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
394
- "ascending": True,
395
- "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
396
- getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
397
- }
398
- })
399
-
400
- event= self.create_incident(incident_id=self.CASE_TYPE+'_'+str(frame_number), incident_type=self.CASE_TYPE,
401
- severity_level=level, human_text=human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
402
- start_time=start_timestamp, end_time=self.current_incident_end_timestamp,
403
- level_settings= {"low": 1, "medium": 3, "significant":4, "critical": 7})
523
+ alert_settings = []
524
+ if config.alert_config and hasattr(config.alert_config, "alert_type"):
525
+ alert_settings.append(
526
+ {
527
+ "alert_type": (
528
+ getattr(config.alert_config, "alert_type", ["Default"])
529
+ if hasattr(config.alert_config, "alert_type")
530
+ else ["Default"]
531
+ ),
532
+ "incident_category": self.CASE_TYPE,
533
+ "threshold_level": (
534
+ config.alert_config.count_thresholds
535
+ if hasattr(config.alert_config, "count_thresholds")
536
+ else {}
537
+ ),
538
+ "ascending": True,
539
+ "settings": {
540
+ t: v
541
+ for t, v in zip(
542
+ (
543
+ getattr(
544
+ config.alert_config, "alert_type", ["Default"]
545
+ )
546
+ if hasattr(config.alert_config, "alert_type")
547
+ else ["Default"]
548
+ ),
549
+ (
550
+ getattr(
551
+ config.alert_config, "alert_value", ["JSON"]
552
+ )
553
+ if hasattr(config.alert_config, "alert_value")
554
+ else ["JSON"]
555
+ ),
556
+ )
557
+ },
558
+ }
559
+ )
560
+
561
+ event = self.create_incident(
562
+ incident_id=self.CASE_TYPE + "_" + str(frame_number),
563
+ incident_type=self.CASE_TYPE,
564
+ severity_level=level,
565
+ human_text=human_text,
566
+ camera_info=camera_info,
567
+ alerts=alerts,
568
+ alert_settings=alert_settings,
569
+ start_time=start_timestamp,
570
+ end_time=self.current_incident_end_timestamp,
571
+ level_settings={"low": 1, "medium": 3, "significant": 4, "critical": 7},
572
+ )
404
573
  incidents.append(event)
405
574
 
406
575
  else:
@@ -410,12 +579,12 @@ class LicensePlateUseCase(BaseProcessor):
410
579
  return incidents
411
580
 
412
581
  def _generate_tracking_stats(
413
- self,
414
- counting_summary: Dict,
415
- alerts: Any,
416
- config: LicensePlateConfig,
417
- frame_number: Optional[int] = None,
418
- stream_info: Optional[Dict[str, Any]] = None
582
+ self,
583
+ counting_summary: Dict,
584
+ alerts: Any,
585
+ config: LicensePlateConfig,
586
+ frame_number: Optional[int] = None,
587
+ stream_info: Optional[Dict[str, Any]] = None,
419
588
  ) -> List[Dict]:
420
589
  """Generate structured tracking stats for the output format with frame-based keys, including track_ids_info and detections with masks."""
421
590
  # frame_key = str(frame_number) if frame_number is not None else "current_frame"
@@ -428,14 +597,22 @@ class LicensePlateUseCase(BaseProcessor):
428
597
  cumulative_total = sum(total_counts.values()) if total_counts else 0
429
598
  per_category_count = counting_summary.get("per_category_count", {})
430
599
 
431
- track_ids_info = self._get_track_ids_info(counting_summary.get("detections", []))
600
+ track_ids_info = self._get_track_ids_info(
601
+ counting_summary.get("detections", [])
602
+ )
432
603
 
433
- current_timestamp = self._get_current_timestamp_str(stream_info, precision=False)
604
+ current_timestamp = self._get_current_timestamp_str(
605
+ stream_info, precision=False
606
+ )
434
607
  start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
435
-
608
+
436
609
  # Create high precision timestamps for input_timestamp and reset_timestamp
437
- high_precision_start_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
438
- high_precision_reset_timestamp = self._get_start_timestamp_str(stream_info, precision=True)
610
+ high_precision_start_timestamp = self._get_current_timestamp_str(
611
+ stream_info, precision=True
612
+ )
613
+ high_precision_reset_timestamp = self._get_start_timestamp_str(
614
+ stream_info, precision=True
615
+ )
439
616
 
440
617
  camera_info = self.get_camera_info_from_stream(stream_info)
441
618
  human_text_lines = []
@@ -443,11 +620,15 @@ class LicensePlateUseCase(BaseProcessor):
443
620
  # CURRENT FRAME section
444
621
  human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}:")
445
622
  if total_detections > 0:
446
- category_counts = [f"{count} {cat}" for cat, count in per_category_count.items()]
623
+ category_counts = [
624
+ f"{count} {cat}" for cat, count in per_category_count.items()
625
+ ]
447
626
  if len(category_counts) == 1:
448
627
  detection_text = category_counts[0] + " detected"
449
628
  elif len(category_counts) == 2:
450
- detection_text = f"{category_counts[0]} and {category_counts[1]} detected"
629
+ detection_text = (
630
+ f"{category_counts[0]} and {category_counts[1]} detected"
631
+ )
451
632
  else:
452
633
  detection_text = f"{', '.join(category_counts[:-1])}, and {category_counts[-1]} detected"
453
634
  human_text_lines.append(f"\t- {detection_text}")
@@ -464,14 +645,13 @@ class LicensePlateUseCase(BaseProcessor):
464
645
  for cat, count in total_counts.items():
465
646
  if count > 0: # Only include categories with non-zero counts
466
647
  human_text_lines.append(f"\t- {cat}: {count}")
467
- # Build current_counts array in expected format
648
+ # Build current_counts array in expected format
468
649
  current_counts = []
469
650
  for cat, count in per_category_count.items():
470
- if count > 0 or total_detections > 0: # Include even if 0 when there are detections
471
- current_counts.append({
472
- "category": cat,
473
- "count": count
474
- })
651
+ if (
652
+ count > 0 or total_detections > 0
653
+ ): # Include even if 0 when there are detections
654
+ current_counts.append({"category": cat, "count": count})
475
655
 
476
656
  human_text = "\n".join(human_text_lines)
477
657
 
@@ -483,69 +663,115 @@ class LicensePlateUseCase(BaseProcessor):
483
663
  category = detection.get("category", "person")
484
664
  # Include segmentation if available (like in eg.json)
485
665
  if detection.get("masks"):
486
- segmentation= detection.get("masks", [])
487
- detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
666
+ segmentation = detection.get("masks", [])
667
+ detection_obj = self.create_detection_object(
668
+ category, bbox, segmentation=segmentation
669
+ )
488
670
  elif detection.get("segmentation"):
489
- segmentation= detection.get("segmentation")
490
- detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
671
+ segmentation = detection.get("segmentation")
672
+ detection_obj = self.create_detection_object(
673
+ category, bbox, segmentation=segmentation
674
+ )
491
675
  elif detection.get("mask"):
492
- segmentation= detection.get("mask")
493
- detection_obj = self.create_detection_object(category, bbox, segmentation=segmentation)
676
+ segmentation = detection.get("mask")
677
+ detection_obj = self.create_detection_object(
678
+ category, bbox, segmentation=segmentation
679
+ )
494
680
  else:
495
681
  detection_obj = self.create_detection_object(category, bbox)
496
682
  detections.append(detection_obj)
497
683
 
498
684
  # Build alert_settings array in expected format
499
685
  alert_settings = []
500
- if config.alert_config and hasattr(config.alert_config, 'alert_type'):
501
- alert_settings.append({
502
- "alert_type": getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
503
- "incident_category": self.CASE_TYPE,
504
- "threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
505
- "ascending": True,
506
- "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']) if hasattr(config.alert_config, 'alert_type') else ['Default'],
507
- getattr(config.alert_config, 'alert_value', ['JSON']) if hasattr(config.alert_config, 'alert_value') else ['JSON'])
508
- }
509
- })
686
+ if config.alert_config and hasattr(config.alert_config, "alert_type"):
687
+ alert_settings.append(
688
+ {
689
+ "alert_type": (
690
+ getattr(config.alert_config, "alert_type", ["Default"])
691
+ if hasattr(config.alert_config, "alert_type")
692
+ else ["Default"]
693
+ ),
694
+ "incident_category": self.CASE_TYPE,
695
+ "threshold_level": (
696
+ config.alert_config.count_thresholds
697
+ if hasattr(config.alert_config, "count_thresholds")
698
+ else {}
699
+ ),
700
+ "ascending": True,
701
+ "settings": {
702
+ t: v
703
+ for t, v in zip(
704
+ (
705
+ getattr(config.alert_config, "alert_type", ["Default"])
706
+ if hasattr(config.alert_config, "alert_type")
707
+ else ["Default"]
708
+ ),
709
+ (
710
+ getattr(config.alert_config, "alert_value", ["JSON"])
711
+ if hasattr(config.alert_config, "alert_value")
712
+ else ["JSON"]
713
+ ),
714
+ )
715
+ },
716
+ }
717
+ )
510
718
 
511
719
  if alerts:
512
720
  for alert in alerts:
513
- human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
721
+ human_text_lines.append(
722
+ f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}"
723
+ )
514
724
  else:
515
725
  human_text_lines.append("Alerts: None")
516
726
 
517
727
  human_text = "\n".join(human_text_lines)
518
728
  reset_settings = [
519
- {
520
- "interval_type": "daily",
521
- "reset_time": {
522
- "value": 9,
523
- "time_unit": "hour"
524
- }
525
- }
526
- ]
729
+ {"interval_type": "daily", "reset_time": {"value": 9, "time_unit": "hour"}}
730
+ ]
527
731
 
528
- tracking_stat=self.create_tracking_stats(total_counts=total_counts, current_counts=current_counts,
529
- detections=detections, human_text=human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
530
- reset_settings=reset_settings, start_time=high_precision_start_timestamp ,
531
- reset_time=high_precision_reset_timestamp)
732
+ tracking_stat = self.create_tracking_stats(
733
+ total_counts=total_counts,
734
+ current_counts=current_counts,
735
+ detections=detections,
736
+ human_text=human_text,
737
+ camera_info=camera_info,
738
+ alerts=alerts,
739
+ alert_settings=alert_settings,
740
+ reset_settings=reset_settings,
741
+ start_time=high_precision_start_timestamp,
742
+ reset_time=high_precision_reset_timestamp,
743
+ )
532
744
 
533
745
  tracking_stats.append(tracking_stat)
534
746
  return tracking_stats
535
747
 
536
- def _generate_business_analytics(self, counting_summary: Dict, zone_analysis: Dict, config: LicensePlateConfig, stream_info: Optional[Dict[str, Any]] = None, is_empty=False) -> List[Dict]:
748
+ def _generate_business_analytics(
749
+ self,
750
+ counting_summary: Dict,
751
+ zone_analysis: Dict,
752
+ config: LicensePlateConfig,
753
+ stream_info: Optional[Dict[str, Any]] = None,
754
+ is_empty=False,
755
+ ) -> List[Dict]:
537
756
  """Generate standardized business analytics for the agg_summary structure."""
538
757
  if is_empty:
539
758
  return []
540
759
 
541
- #-----IF YOUR USECASE NEEDS BUSINESS ANALYTICS, YOU CAN USE THIS FUNCTION------#
542
- #camera_info = self.get_camera_info_from_stream(stream_info)
760
+ # -----IF YOUR USECASE NEEDS BUSINESS ANALYTICS, YOU CAN USE THIS FUNCTION------#
761
+ # camera_info = self.get_camera_info_from_stream(stream_info)
543
762
  # business_analytics = self.create_business_analytics(nalysis_name, statistics,
544
763
  # human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
545
764
  # reset_settings)
546
765
  # return business_analytics
547
766
 
548
- def _generate_summary(self, summary: dict, incidents: List, tracking_stats: List, business_analytics: List, alerts: List) -> List[str]:
767
+ def _generate_summary(
768
+ self,
769
+ summary: dict,
770
+ incidents: List,
771
+ tracking_stats: List,
772
+ business_analytics: List,
773
+ alerts: List,
774
+ ) -> List[str]:
549
775
  """
550
776
  Generate a human_text string for the tracking_stat, incident, business analytics and alerts.
551
777
  """
@@ -553,18 +779,27 @@ class LicensePlateUseCase(BaseProcessor):
553
779
  lines["Application Name"] = self.CASE_TYPE
554
780
  lines["Application Version"] = self.CASE_VERSION
555
781
  if len(incidents) > 0:
556
- lines["Incidents:"]=f"\n\t{incidents[0].get('human_text', 'No incidents detected')}\n"
782
+ lines["Incidents:"] = (
783
+ f"\n\t{incidents[0].get('human_text', 'No incidents detected')}\n"
784
+ )
557
785
  if len(tracking_stats) > 0:
558
- lines["Tracking Statistics:"]=f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}\n"
786
+ lines["Tracking Statistics:"] = (
787
+ f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}\n"
788
+ )
559
789
  if len(business_analytics) > 0:
560
- lines["Business Analytics:"]=f"\t{business_analytics[0].get('human_text', 'No business analytics detected')}\n"
561
-
562
- if len(incidents) == 0 and len(tracking_stats) == 0 and len(business_analytics) == 0:
790
+ lines["Business Analytics:"] = (
791
+ f"\t{business_analytics[0].get('human_text', 'No business analytics detected')}\n"
792
+ )
793
+
794
+ if (
795
+ len(incidents) == 0
796
+ and len(tracking_stats) == 0
797
+ and len(business_analytics) == 0
798
+ ):
563
799
  lines["Summary"] = "No Summary Data"
564
800
 
565
801
  return [lines]
566
802
 
567
-
568
803
  def _count_categories(self, detections: list, config: LicensePlateConfig) -> dict:
569
804
  """
570
805
  Count the number of detections per category and return a summary dict.
@@ -574,24 +809,30 @@ class LicensePlateUseCase(BaseProcessor):
574
809
  counts = {}
575
810
  valid_detections = []
576
811
  for det in detections:
577
- cat = det.get('category', 'unknown')
578
- if not all(k in det for k in ['category', 'confidence', 'bounding_box']): # Validate required fields
812
+ cat = det.get("category", "unknown")
813
+ if not all(
814
+ k in det for k in ["category", "confidence", "bounding_box"]
815
+ ): # Validate required fields
579
816
  self.logger.warning(f"Skipping invalid detection: {det}")
580
817
  continue
581
818
  counts[cat] = counts.get(cat, 0) + 1
582
- valid_detections.append({
583
- "bounding_box": det.get("bounding_box"),
584
- "category": det.get("category"),
585
- "confidence": det.get("confidence"),
586
- "track_id": det.get("track_id"),
587
- "frame_id": det.get("frame_id"),
588
- "masks": det.get("masks", det.get("mask", [])) # Include masks, fallback to empty list
589
- })
819
+ valid_detections.append(
820
+ {
821
+ "bounding_box": det.get("bounding_box"),
822
+ "category": det.get("category"),
823
+ "confidence": det.get("confidence"),
824
+ "track_id": det.get("track_id"),
825
+ "frame_id": det.get("frame_id"),
826
+ "masks": det.get(
827
+ "masks", det.get("mask", [])
828
+ ), # Include masks, fallback to empty list
829
+ }
830
+ )
590
831
  self.logger.debug(f"Valid detections after filtering: {len(valid_detections)}")
591
832
  return {
592
833
  "total_count": sum(counts.values()),
593
834
  "per_category_count": counts,
594
- "detections": valid_detections
835
+ "detections": valid_detections,
595
836
  }
596
837
 
597
838
  def _get_track_ids_info(self, detections: list) -> Dict[str, Any]:
@@ -601,12 +842,12 @@ class LicensePlateUseCase(BaseProcessor):
601
842
  # Collect all track_ids in this frame
602
843
  frame_track_ids = set()
603
844
  for det in detections:
604
- tid = det.get('track_id')
845
+ tid = det.get("track_id")
605
846
  if tid is not None:
606
847
  frame_track_ids.add(tid)
607
848
  # Use persistent total set for unique counting
608
849
  total_track_ids = set()
609
- for s in getattr(self, '_per_category_total_track_ids', {}).values():
850
+ for s in getattr(self, "_per_category_total_track_ids", {}).values():
610
851
  total_track_ids.update(s)
611
852
  return {
612
853
  "total_count": len(total_track_ids),
@@ -614,7 +855,7 @@ class LicensePlateUseCase(BaseProcessor):
614
855
  "total_unique_track_ids": len(total_track_ids),
615
856
  "current_frame_track_ids": list(frame_track_ids),
616
857
  "last_update_time": time.time(),
617
- "total_frames_processed": getattr(self, '_total_frame_counter', 0)
858
+ "total_frames_processed": getattr(self, "_total_frame_counter", 0),
618
859
  }
619
860
 
620
861
  def _update_tracking_state(self, detections: list):
@@ -625,7 +866,9 @@ class LicensePlateUseCase(BaseProcessor):
625
866
  """
626
867
  # Lazily initialise storage dicts
627
868
  if not hasattr(self, "_per_category_total_track_ids"):
628
- self._per_category_total_track_ids = {cat: set() for cat in self.target_categories}
869
+ self._per_category_total_track_ids = {
870
+ cat: set() for cat in self.target_categories
871
+ }
629
872
  self._current_frame_track_ids = {cat: set() for cat in self.target_categories}
630
873
 
631
874
  for det in detections:
@@ -645,21 +888,29 @@ class LicensePlateUseCase(BaseProcessor):
645
888
  """
646
889
  Return total unique track_id count for each category.
647
890
  """
648
- return {cat: len(ids) for cat, ids in getattr(self, '_per_category_total_track_ids', {}).items()}
891
+ return {
892
+ cat: len(ids)
893
+ for cat, ids in getattr(self, "_per_category_total_track_ids", {}).items()
894
+ }
649
895
 
650
896
  def _format_timestamp_for_video(self, timestamp: float) -> str:
651
897
  """Format timestamp for video chunks (HH:MM:SS.ms format)."""
652
898
  hours = int(timestamp // 3600)
653
899
  minutes = int((timestamp % 3600) // 60)
654
- seconds = round(float(timestamp % 60),2)
900
+ seconds = round(float(timestamp % 60), 2)
655
901
  return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
656
902
 
657
903
  def _format_timestamp_for_stream(self, timestamp: float) -> str:
658
904
  """Format timestamp for streams (YYYY:MM:DD HH:MM:SS format)."""
659
905
  dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
660
- return dt.strftime('%Y:%m:%d %H:%M:%S')
906
+ return dt.strftime("%Y:%m:%d %H:%M:%S")
661
907
 
662
- def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
908
+ def _get_current_timestamp_str(
909
+ self,
910
+ stream_info: Optional[Dict[str, Any]],
911
+ precision=False,
912
+ frame_id: Optional[str] = None,
913
+ ) -> str:
663
914
  """Get formatted current timestamp based on stream type."""
664
915
  if not stream_info:
665
916
  return "00:00:00.00"
@@ -667,24 +918,36 @@ class LicensePlateUseCase(BaseProcessor):
667
918
  if precision:
668
919
  if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
669
920
  if frame_id:
670
- start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
921
+ start_time = int(frame_id) / stream_info.get(
922
+ "input_settings", {}
923
+ ).get("original_fps", 30)
671
924
  else:
672
- start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
925
+ start_time = stream_info.get("input_settings", {}).get(
926
+ "start_frame", 30
927
+ ) / stream_info.get("input_settings", {}).get("original_fps", 30)
673
928
  stream_time_str = self._format_timestamp_for_video(start_time)
674
929
  return stream_time_str
675
930
  else:
676
931
  return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
677
932
 
678
933
  if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
679
- if frame_id:
680
- start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
681
- else:
682
- start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
683
- stream_time_str = self._format_timestamp_for_video(start_time)
684
- return stream_time_str
934
+ if frame_id:
935
+ start_time = int(frame_id) / stream_info.get("input_settings", {}).get(
936
+ "original_fps", 30
937
+ )
938
+ else:
939
+ start_time = stream_info.get("input_settings", {}).get(
940
+ "start_frame", 30
941
+ ) / stream_info.get("input_settings", {}).get("original_fps", 30)
942
+ stream_time_str = self._format_timestamp_for_video(start_time)
943
+ return stream_time_str
685
944
  else:
686
945
  # For streams, use stream_time from stream_info
687
- stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
946
+ stream_time_str = (
947
+ stream_info.get("input_settings", {})
948
+ .get("stream_info", {})
949
+ .get("stream_time", "")
950
+ )
688
951
  if stream_time_str:
689
952
  # Parse the high precision timestamp string to get timestamp
690
953
  try:
@@ -699,7 +962,9 @@ class LicensePlateUseCase(BaseProcessor):
699
962
  else:
700
963
  return self._format_timestamp_for_stream(time.time())
701
964
 
702
- def _get_start_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False) -> str:
965
+ def _get_start_timestamp_str(
966
+ self, stream_info: Optional[Dict[str, Any]], precision=False
967
+ ) -> str:
703
968
  """Get formatted start timestamp for 'TOTAL SINCE' based on stream type."""
704
969
  if not stream_info:
705
970
  return "00:00:00"
@@ -716,13 +981,19 @@ class LicensePlateUseCase(BaseProcessor):
716
981
  # For streams, use tracking start time or current time with minutes/seconds reset
717
982
  if self._tracking_start_time is None:
718
983
  # Try to extract timestamp from stream_time string
719
- stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
984
+ stream_time_str = (
985
+ stream_info.get("input_settings", {})
986
+ .get("stream_info", {})
987
+ .get("stream_time", "")
988
+ )
720
989
  if stream_time_str:
721
990
  try:
722
991
  # Remove " UTC" suffix and parse
723
992
  timestamp_str = stream_time_str.replace(" UTC", "")
724
993
  dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
725
- self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
994
+ self._tracking_start_time = dt.replace(
995
+ tzinfo=timezone.utc
996
+ ).timestamp()
726
997
  except:
727
998
  # Fallback to current time if parsing fails
728
999
  self._tracking_start_time = time.time()
@@ -732,7 +1003,7 @@ class LicensePlateUseCase(BaseProcessor):
732
1003
  dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
733
1004
  # Reset minutes and seconds to 00:00 for "TOTAL SINCE" format
734
1005
  dt = dt.replace(minute=0, second=0, microsecond=0)
735
- return dt.strftime('%Y:%m:%d %H:%M:%S')
1006
+ return dt.strftime("%Y:%m:%d %H:%M:%S")
736
1007
 
737
1008
  # ------------------------------------------------------------------ #
738
1009
  # Helper to merge masks back into detections #
@@ -772,7 +1043,9 @@ class LicensePlateUseCase(BaseProcessor):
772
1043
  if idx in used_raw_indices:
773
1044
  continue
774
1045
 
775
- iou = self._compute_iou(det.get("bounding_box"), raw_det.get("bounding_box"))
1046
+ iou = self._compute_iou(
1047
+ det.get("bounding_box"), raw_det.get("bounding_box")
1048
+ )
776
1049
  if iou > best_iou:
777
1050
  best_iou = iou
778
1051
  best_idx = idx
@@ -798,12 +1071,11 @@ class LicensePlateUseCase(BaseProcessor):
798
1071
  "category": det.get("category", "unknown"),
799
1072
  "confidence": det.get("confidence", 0.0),
800
1073
  "bounding_box": det.get("bounding_box", {}),
801
- "mask": det.get("mask", det.get("masks", None)) # Accept either key
1074
+ "mask": det.get("mask", det.get("masks", None)), # Accept either key
802
1075
  }
803
1076
  for det in detections
804
1077
  ]
805
1078
 
806
-
807
1079
  # ------------------------------------------------------------------ #
808
1080
  # Canonical ID helpers #
809
1081
  # ------------------------------------------------------------------ #
@@ -901,7 +1173,9 @@ class LicensePlateUseCase(BaseProcessor):
901
1173
 
902
1174
  def _format_timestamp(self, timestamp: float) -> str:
903
1175
  """Format a timestamp for human-readable output."""
904
- return datetime.fromtimestamp(timestamp, timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
1176
+ return datetime.fromtimestamp(timestamp, timezone.utc).strftime(
1177
+ "%Y-%m-%d %H:%M:%S UTC"
1178
+ )
905
1179
 
906
1180
  def _get_tracking_start_time(self) -> str:
907
1181
  """Get the tracking start time, formatted as a string."""