matrice-analytics 0.1.2__py3-none-any.whl → 0.1.31__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 (59) 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/face_reg/compare_similarity.py +5 -5
  4. matrice_analytics/post_processing/face_reg/embedding_manager.py +14 -7
  5. matrice_analytics/post_processing/face_reg/face_recognition.py +123 -34
  6. matrice_analytics/post_processing/face_reg/face_recognition_client.py +332 -82
  7. matrice_analytics/post_processing/face_reg/people_activity_logging.py +29 -22
  8. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
  9. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
  10. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
  11. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
  12. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
  13. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
  14. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
  15. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
  16. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
  17. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
  18. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
  19. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
  20. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
  21. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
  22. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
  23. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
  24. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
  25. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
  26. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
  27. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
  28. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
  29. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
  30. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
  31. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
  32. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
  33. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
  34. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
  35. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
  36. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
  37. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
  38. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
  39. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
  40. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
  41. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
  42. matrice_analytics/post_processing/ocr/postprocessing.py +0 -1
  43. matrice_analytics/post_processing/post_processor.py +19 -5
  44. matrice_analytics/post_processing/usecases/color/clip.py +292 -132
  45. matrice_analytics/post_processing/usecases/color/color_mapper.py +2 -2
  46. matrice_analytics/post_processing/usecases/color_detection.py +429 -355
  47. matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +41 -386
  48. matrice_analytics/post_processing/usecases/flare_analysis.py +1 -56
  49. matrice_analytics/post_processing/usecases/license_plate_detection.py +476 -202
  50. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +252 -11
  51. matrice_analytics/post_processing/usecases/people_counting.py +408 -1431
  52. matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
  53. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +39 -10
  54. matrice_analytics/post_processing/utils/__init__.py +8 -8
  55. {matrice_analytics-0.1.2.dist-info → matrice_analytics-0.1.31.dist-info}/METADATA +1 -1
  56. {matrice_analytics-0.1.2.dist-info → matrice_analytics-0.1.31.dist-info}/RECORD +59 -24
  57. {matrice_analytics-0.1.2.dist-info → matrice_analytics-0.1.31.dist-info}/WHEEL +0 -0
  58. {matrice_analytics-0.1.2.dist-info → matrice_analytics-0.1.31.dist-info}/licenses/LICENSE.txt +0 -0
  59. {matrice_analytics-0.1.2.dist-info → matrice_analytics-0.1.31.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict, List, Optional, Tuple
1
+ from typing import Any, Dict, List, Optional
2
2
  from dataclasses import asdict
3
3
  import time
4
4
  from datetime import datetime, timezone
@@ -18,7 +18,6 @@ from ..utils import (
18
18
  )
19
19
  from dataclasses import dataclass, field
20
20
  from ..core.config import BaseConfig, AlertConfig, ZoneConfig
21
- from ..utils.geometry_utils import get_bbox_center, point_in_polygon, get_bbox_bottom25_center
22
21
 
23
22
  @dataclass
24
23
  class VehiclePeopleDroneMonitoringConfig(BaseConfig):
@@ -29,15 +28,6 @@ class VehiclePeopleDroneMonitoringConfig(BaseConfig):
29
28
  smoothing_cooldown_frames: int = 5
30
29
  smoothing_confidence_range_factor: float = 0.5
31
30
  confidence_threshold: float = 0.6
32
-
33
- #JBK_720_GATE POLYGON = [[86, 328], [844, 317], [1277, 520], [1273, 707], [125, 713]]
34
- zone_config: Optional[Dict[str, Dict[str, List[List[float]]]]] = None #field(
35
- # default_factory=lambda: {
36
- # "zones": {
37
- # "Interest_Region": [[86, 328], [844, 317], [1277, 520], [1273, 707], [125, 713]],
38
- # }
39
- # }
40
- # )
41
31
  usecase_categories: List[str] = field(
42
32
  default_factory=lambda: [
43
33
  "pedestrian", "people", "bicycle", "car", "van", "truck", "tricycle", "awning-tricycle", "bus", "motor"
@@ -57,8 +47,7 @@ class VehiclePeopleDroneMonitoringConfig(BaseConfig):
57
47
 
58
48
  class DroneTrafficMonitoringUsecase(BaseProcessor):
59
49
  CATEGORY_DISPLAY = {
60
- # Focus on vehicle-related COCO classes
61
- "pedestrian": "Pesestrian",
50
+ "pedestrian": "Pedestrian",
62
51
  "people": "People",
63
52
  "bicycle": "Bicycle",
64
53
  "car": "Car",
@@ -87,54 +76,29 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
87
76
  self._track_merge_time_window: float = 7.0
88
77
  self._ascending_alert_list: List[int] = []
89
78
  self.current_incident_end_timestamp: str = "N/A"
90
- self.start_timer = None
91
-
92
- # Track ID storage for total count calculation
93
- self._total_track_ids = set() # Store all unique track IDs seen across calls
94
- self._current_frame_track_ids = set() # Store track IDs from current frame
95
- self._total_count = 0 # Cached total count
96
- self._last_update_time = time.time() # Track when last updated
97
- self._total_count_list = []
98
-
99
- # Zone-based tracking storage
100
- self._zone_current_track_ids = {} # zone_name -> set of current track IDs in zone
101
- self._zone_total_track_ids = {} # zone_name -> set of all track IDs that have been in zone
102
- self._zone_current_counts = {} # zone_name -> current count in zone
103
- self._zone_total_counts = {} # zone_name -> total count that have been in zone
104
79
 
105
80
  def process(self, data: Any, config: ConfigProtocol, context: Optional[ProcessingContext] = None,
106
81
  stream_info: Optional[Dict[str, Any]] = None) -> ProcessingResult:
107
- processing_start = time.time()
82
+ start_time = time.time()
108
83
  if not isinstance(config, VehiclePeopleDroneMonitoringConfig):
109
84
  return self.create_error_result("Invalid config type", usecase=self.name, category=self.category, context=context)
110
85
  if context is None:
111
86
  context = ProcessingContext()
112
-
113
- # Normalize typical YOLO outputs (COCO pretrained) to internal schema
114
- data = self._normalize_yolo_results(data, getattr(config, 'index_to_category', None))
115
-
116
87
  input_format = match_results_structure(data)
117
88
  context.input_format = input_format
118
89
  context.confidence_threshold = config.confidence_threshold
119
- config.confidence_threshold = 0.25
120
-
121
90
  if config.confidence_threshold is not None:
122
91
  processed_data = filter_by_confidence(data, config.confidence_threshold)
123
92
  self.logger.debug(f"Applied confidence filtering with threshold {config.confidence_threshold}")
124
93
  else:
125
94
  processed_data = data
126
95
  self.logger.debug("Did not apply confidence filtering since no threshold provided")
127
-
128
96
  if config.index_to_category:
129
97
  processed_data = apply_category_mapping(processed_data, config.index_to_category)
130
98
  self.logger.debug("Applied category mapping")
131
-
132
- processed_data = [d for d in processed_data if d.get('category') in self.target_categories]
133
99
  if config.target_categories:
134
100
  processed_data = [d for d in processed_data if d.get('category') in self.target_categories]
135
101
  self.logger.debug("Applied category filtering")
136
-
137
-
138
102
  if config.enable_smoothing:
139
103
  if self.smoothing_tracker is None:
140
104
  smoothing_config = BBoxSmoothingConfig(
@@ -147,7 +111,6 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
147
111
  )
148
112
  self.smoothing_tracker = BBoxSmoothingTracker(smoothing_config)
149
113
  processed_data = bbox_smoothing(processed_data, self.smoothing_tracker.config, self.smoothing_tracker)
150
-
151
114
  try:
152
115
  from ..advanced_tracker import AdvancedTracker
153
116
  from ..advanced_tracker.config import TrackerConfig
@@ -158,10 +121,8 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
158
121
  processed_data = self.tracker.update(processed_data)
159
122
  except Exception as e:
160
123
  self.logger.warning(f"AdvancedTracker failed: {e}")
161
-
162
124
  self._update_tracking_state(processed_data)
163
125
  self._total_frame_counter += 1
164
-
165
126
  frame_number = None
166
127
  if stream_info:
167
128
  input_settings = stream_info.get("input_settings", {})
@@ -169,39 +130,16 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
169
130
  end_frame = input_settings.get("end_frame")
170
131
  if start_frame is not None and end_frame is not None and start_frame == end_frame:
171
132
  frame_number = start_frame
172
-
173
133
  general_counting_summary = calculate_counting_summary(data)
174
134
  counting_summary = self._count_categories(processed_data, config)
175
135
  total_counts = self.get_total_counts()
176
136
  counting_summary['total_counts'] = total_counts
177
- counting_summary['categories'] = {}
178
- for detection in processed_data:
179
- category = detection.get("category", "unknown")
180
- counting_summary["categories"][category] = counting_summary["categories"].get(category, 0) + 1
181
-
182
- zone_analysis = {}
183
- if config.zone_config and config.zone_config['zones']:
184
- # Convert single frame to format expected by count_objects_in_zones
185
- frame_data = processed_data #[frame_detections]
186
- zone_analysis = count_objects_in_zones(frame_data, config.zone_config['zones'], stream_info)
187
-
188
- if zone_analysis:
189
- enhanced_zone_analysis = self._update_zone_tracking(zone_analysis, processed_data, config)
190
- # Merge enhanced zone analysis with original zone analysis
191
- for zone_name, enhanced_data in enhanced_zone_analysis.items():
192
- zone_analysis[zone_name] = enhanced_data
193
-
194
-
195
- alerts = self._check_alerts(counting_summary,zone_analysis, frame_number, config)
137
+ alerts = self._check_alerts(counting_summary, frame_number, config)
196
138
  predictions = self._extract_predictions(processed_data)
197
-
198
- incidents_list = self._generate_incidents(counting_summary,zone_analysis, alerts, config, frame_number, stream_info)
199
- incidents_list = []
200
- tracking_stats_list = self._generate_tracking_stats(counting_summary,zone_analysis, alerts, config, frame_number, stream_info)
201
-
202
- business_analytics_list = self._generate_business_analytics(counting_summary,zone_analysis, alerts, config, stream_info, is_empty=True)
203
- summary_list = self._generate_summary(counting_summary,zone_analysis, incidents_list, tracking_stats_list, business_analytics_list, alerts)
204
-
139
+ incidents_list = self._generate_incidents(counting_summary, alerts, config, frame_number, stream_info)
140
+ tracking_stats_list = self._generate_tracking_stats(counting_summary, alerts, config, frame_number, stream_info)
141
+ business_analytics_list = self._generate_business_analytics(counting_summary, alerts, config, stream_info, is_empty=True)
142
+ summary_list = self._generate_summary(counting_summary, incidents_list, tracking_stats_list, business_analytics_list, alerts)
205
143
  incidents = incidents_list[0] if incidents_list else {}
206
144
  tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
207
145
  business_analytics = business_analytics_list[0] if business_analytics_list else {}
@@ -211,10 +149,8 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
211
149
  "tracking_stats": tracking_stats,
212
150
  "business_analytics": business_analytics,
213
151
  "alerts": alerts,
214
- "zone_analysis": zone_analysis,
215
152
  "human_text": summary}
216
153
  }
217
-
218
154
  context.mark_completed()
219
155
  result = self.create_result(
220
156
  data={"agg_summary": agg_summary},
@@ -222,167 +158,9 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
222
158
  category=self.category,
223
159
  context=context
224
160
  )
225
- proc_time = time.time() - processing_start
226
- processing_latency_ms = proc_time * 1000.0
227
- processing_fps = (1.0 / proc_time) if proc_time > 0 else None
228
- # Log the performance metrics using the module-level logger
229
- print("latency in ms:",processing_latency_ms,"| Throughput fps:",processing_fps,"| Frame_Number:",self._total_frame_counter)
230
161
  return result
231
162
 
232
- def _update_zone_tracking(self, zone_analysis: Dict[str, Dict[str, int]], detections: List[Dict], config: VehiclePeopleDroneMonitoringConfig) -> Dict[str, Dict[str, Any]]:
233
- """
234
- Update zone tracking with current frame data.
235
-
236
- Args:
237
- zone_analysis: Current zone analysis results
238
- detections: List of detections with track IDs
239
-
240
- Returns:
241
- Enhanced zone analysis with tracking information
242
- """
243
- if not zone_analysis or not config.zone_config or not config.zone_config['zones']:
244
- return {}
245
-
246
- enhanced_zone_analysis = {}
247
- zones = config.zone_config['zones']
248
-
249
- # Get current frame track IDs in each zone
250
- current_frame_zone_tracks = {}
251
-
252
- # Initialize zone tracking for all zones
253
- for zone_name in zones.keys():
254
- current_frame_zone_tracks[zone_name] = set()
255
- if zone_name not in self._zone_current_track_ids:
256
- self._zone_current_track_ids[zone_name] = set()
257
- if zone_name not in self._zone_total_track_ids:
258
- self._zone_total_track_ids[zone_name] = set()
259
-
260
- # Check each detection against each zone
261
- for detection in detections:
262
- track_id = detection.get("track_id")
263
- if track_id is None:
264
- continue
265
-
266
- # Get detection bbox
267
- bbox = detection.get("bounding_box", detection.get("bbox"))
268
- if not bbox:
269
- continue
270
-
271
- # Get detection center point
272
- center_point = get_bbox_bottom25_center(bbox) #get_bbox_center(bbox)
273
-
274
- # Check which zone this detection is in using actual zone polygons
275
- for zone_name, zone_polygon in zones.items():
276
- # Convert polygon points to tuples for point_in_polygon function
277
- # zone_polygon format: [[x1, y1], [x2, y2], [x3, y3], ...]
278
- polygon_points = [(point[0], point[1]) for point in zone_polygon]
279
-
280
- # Check if detection center is inside the zone polygon using ray casting algorithm
281
- if point_in_polygon(center_point, polygon_points):
282
- current_frame_zone_tracks[zone_name].add(track_id)
283
- if track_id not in self._total_count_list:
284
- self._total_count_list.append(track_id)
285
-
286
- # Update zone tracking for each zone
287
- for zone_name, zone_counts in zone_analysis.items():
288
- # Get current frame tracks for this zone
289
- current_tracks = current_frame_zone_tracks.get(zone_name, set())
290
-
291
- # Update current zone tracks
292
- self._zone_current_track_ids[zone_name] = current_tracks
293
-
294
- # Update total zone tracks (accumulate all track IDs that have been in this zone)
295
- self._zone_total_track_ids[zone_name].update(current_tracks)
296
-
297
- # Update counts
298
- self._zone_current_counts[zone_name] = len(current_tracks)
299
- self._zone_total_counts[zone_name] = len(self._zone_total_track_ids[zone_name])
300
-
301
- # Create enhanced zone analysis
302
- enhanced_zone_analysis[zone_name] = {
303
- "current_count": self._zone_current_counts[zone_name],
304
- "total_count": self._zone_total_counts[zone_name],
305
- "current_track_ids": list(current_tracks),
306
- "total_track_ids": list(self._zone_total_track_ids[zone_name]),
307
- "original_counts": zone_counts # Preserve original zone counts
308
- }
309
-
310
- return enhanced_zone_analysis
311
-
312
- def _normalize_yolo_results(self, data: Any, index_to_category: Optional[Dict[int, str]] = None) -> Any:
313
- """
314
- Normalize YOLO-style outputs to internal detection schema:
315
- - category/category_id: prefer string label using COCO mapping if available
316
- - confidence: map from 'conf'/'score' to 'confidence'
317
- - bounding_box: ensure dict with keys (x1,y1,x2,y2) or (xmin,ymin,xmax,ymax)
318
- - supports list of detections and frame_id -> detections dict
319
- """
320
- def to_bbox_dict(d: Dict[str, Any]) -> Dict[str, Any]:
321
- if "bounding_box" in d and isinstance(d["bounding_box"], dict):
322
- return d["bounding_box"]
323
- if "bbox" in d:
324
- bbox = d["bbox"]
325
- if isinstance(bbox, dict):
326
- return bbox
327
- if isinstance(bbox, (list, tuple)) and len(bbox) >= 4:
328
- x1, y1, x2, y2 = bbox[0], bbox[1], bbox[2], bbox[3]
329
- return {"x1": x1, "y1": y1, "x2": x2, "y2": y2}
330
- if "xyxy" in d and isinstance(d["xyxy"], (list, tuple)) and len(d["xyxy"]) >= 4:
331
- x1, y1, x2, y2 = d["xyxy"][0], d["xyxy"][1], d["xyxy"][2], d["xyxy"][3]
332
- return {"x1": x1, "y1": y1, "x2": x2, "y2": y2}
333
- if "xywh" in d and isinstance(d["xywh"], (list, tuple)) and len(d["xywh"]) >= 4:
334
- cx, cy, w, h = d["xywh"][0], d["xywh"][1], d["xywh"][2], d["xywh"][3]
335
- x1, y1, x2, y2 = cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2
336
- return {"x1": x1, "y1": y1, "x2": x2, "y2": y2}
337
- return {}
338
-
339
- def resolve_category(d: Dict[str, Any]) -> Tuple[str, Optional[int]]:
340
- raw_cls = d.get("category", d.get("category_id", d.get("class", d.get("cls"))))
341
- label_name = d.get("name")
342
- if isinstance(raw_cls, int):
343
- if index_to_category and raw_cls in index_to_category:
344
- return index_to_category[raw_cls], raw_cls
345
- return str(raw_cls), raw_cls
346
- if isinstance(raw_cls, str):
347
- # Some YOLO exports provide string labels directly
348
- return raw_cls, None
349
- if label_name:
350
- return str(label_name), None
351
- return "unknown", None
352
-
353
- def normalize_det(det: Dict[str, Any]) -> Dict[str, Any]:
354
- category_name, category_id = resolve_category(det)
355
- confidence = det.get("confidence", det.get("conf", det.get("score", 0.0)))
356
- bbox = to_bbox_dict(det)
357
- normalized = {
358
- "category": category_name,
359
- "confidence": confidence,
360
- "bounding_box": bbox,
361
- }
362
- if category_id is not None:
363
- normalized["category_id"] = category_id
364
- # Preserve optional fields
365
- for key in ("track_id", "frame_id", "masks", "segmentation"):
366
- if key in det:
367
- normalized[key] = det[key]
368
- return normalized
369
-
370
- if isinstance(data, list):
371
- return [normalize_det(d) if isinstance(d, dict) else d for d in data]
372
- if isinstance(data, dict):
373
- # Detect tracking style dict: frame_id -> list of detections
374
- normalized_dict: Dict[str, Any] = {}
375
- for k, v in data.items():
376
- if isinstance(v, list):
377
- normalized_dict[k] = [normalize_det(d) if isinstance(d, dict) else d for d in v]
378
- elif isinstance(v, dict):
379
- normalized_dict[k] = normalize_det(v)
380
- else:
381
- normalized_dict[k] = v
382
- return normalized_dict
383
- return data
384
-
385
- def _check_alerts(self, summary: dict, zone_analysis: Dict, frame_number: Any, config: VehiclePeopleDroneMonitoringConfig) -> List[Dict]:
163
+ def _check_alerts(self, summary: dict, frame_number: Any, config: VehiclePeopleDroneMonitoringConfig) -> List[Dict]:
386
164
  def get_trend(data, lookback=900, threshold=0.6):
387
165
  window = data[-lookback:] if len(data) >= lookback else data
388
166
  if len(window) < 2:
@@ -395,16 +173,13 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
395
173
  total += 1
396
174
  ratio = increasing / total
397
175
  return ratio >= threshold
398
-
399
176
  frame_key = str(frame_number) if frame_number is not None else "current_frame"
400
177
  alerts = []
401
178
  total_detections = summary.get("total_count", 0)
402
179
  total_counts_dict = summary.get("total_counts", {})
403
180
  per_category_count = summary.get("per_category_count", {})
404
-
405
181
  if not config.alert_config:
406
182
  return alerts
407
-
408
183
  if hasattr(config.alert_config, 'count_thresholds') and config.alert_config.count_thresholds:
409
184
  for category, threshold in config.alert_config.count_thresholds.items():
410
185
  if category == "all" and total_detections > threshold:
@@ -415,7 +190,7 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
415
190
  "threshold_level": threshold,
416
191
  "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
417
192
  "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
418
- getattr(config.alert_config, 'alert_value', ['JSON']))}
193
+ getattr(config.alert_config, 'alert_value', ['JSON']))}
419
194
  })
420
195
  elif category in per_category_count and per_category_count[category] > threshold:
421
196
  alerts.append({
@@ -425,19 +200,17 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
425
200
  "threshold_level": threshold,
426
201
  "ascending": get_trend(self._ascending_alert_list, lookback=900, threshold=0.8),
427
202
  "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
428
- getattr(config.alert_config, 'alert_value', ['JSON']))}
203
+ getattr(config.alert_config, 'alert_value', ['JSON']))}
429
204
  })
430
205
  return alerts
431
206
 
432
- def _generate_incidents(self, counting_summary: Dict, zone_analysis: Dict, alerts: List, config: VehiclePeopleDroneMonitoringConfig,
433
- frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
207
+ def _generate_incidents(self, counting_summary: Dict, alerts: List, config: VehiclePeopleDroneMonitoringConfig,
208
+ frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
434
209
  incidents = []
435
210
  total_detections = counting_summary.get("total_count", 0)
436
211
  current_timestamp = self._get_current_timestamp_str(stream_info)
437
212
  camera_info = self.get_camera_info_from_stream(stream_info)
438
-
439
213
  self._ascending_alert_list = self._ascending_alert_list[-900:] if len(self._ascending_alert_list) > 900 else self._ascending_alert_list
440
-
441
214
  if total_detections > 0:
442
215
  level = "low"
443
216
  intensity = 5.0
@@ -449,8 +222,7 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
449
222
  self.current_incident_end_timestamp = current_timestamp
450
223
  elif self.current_incident_end_timestamp != 'Incident still active' and self.current_incident_end_timestamp != 'N/A':
451
224
  self.current_incident_end_timestamp = 'N/A'
452
-
453
- if config.alert_config and hasattr(config.alert_config, 'count_thresholds') and config.alert_config.count_thresholds:
225
+ if config.alert_config and config.alert_config.count_thresholds:
454
226
  threshold = config.alert_config.count_thresholds.get("all", 15)
455
227
  intensity = min(10.0, (total_detections / threshold) * 10)
456
228
  if intensity >= 9:
@@ -482,11 +254,9 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
482
254
  level = "low"
483
255
  intensity = min(10.0, total_detections / 3.0)
484
256
  self._ascending_alert_list.append(0)
485
-
486
257
  human_text_lines = [f"VEHICLE INCIDENTS DETECTED @ {current_timestamp}:"]
487
258
  human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE, level)}")
488
259
  human_text = "\n".join(human_text_lines)
489
-
490
260
  alert_settings = []
491
261
  if config.alert_config and hasattr(config.alert_config, 'alert_type'):
492
262
  alert_settings.append({
@@ -497,7 +267,6 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
497
267
  "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
498
268
  getattr(config.alert_config, 'alert_value', ['JSON']))}
499
269
  })
500
-
501
270
  event = self.create_incident(
502
271
  incident_id=f"{self.CASE_TYPE}_{frame_number}",
503
272
  incident_type=self.CASE_TYPE,
@@ -516,7 +285,7 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
516
285
  incidents.append({})
517
286
  return incidents
518
287
 
519
- def _generate_tracking_stats(self, counting_summary: Dict, zone_analysis: Dict, alerts: List, config: VehiclePeopleDroneMonitoringConfig,
288
+ def _generate_tracking_stats(self, counting_summary: Dict, alerts: List, config: VehiclePeopleDroneMonitoringConfig,
520
289
  frame_number: Optional[int] = None, stream_info: Optional[Dict[str, Any]] = None) -> List[Dict]:
521
290
  camera_info = self.get_camera_info_from_stream(stream_info)
522
291
  tracking_stats = []
@@ -527,10 +296,8 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
527
296
  start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
528
297
  high_precision_start_timestamp = self._get_current_timestamp_str(stream_info, precision=True)
529
298
  high_precision_reset_timestamp = self._get_start_timestamp_str(stream_info, precision=True)
530
-
531
299
  total_counts = [{"category": cat, "count": count} for cat, count in total_counts_dict.items() if count > 0]
532
300
  current_counts = [{"category": cat, "count": count} for cat, count in per_category_count.items() if count > 0 or total_detections > 0]
533
-
534
301
  detections = []
535
302
  for detection in counting_summary.get("detections", []):
536
303
  bbox = detection.get("bounding_box", {})
@@ -547,7 +314,6 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
547
314
  else:
548
315
  detection_obj = self.create_detection_object(category, bbox)
549
316
  detections.append(detection_obj)
550
-
551
317
  alert_settings = []
552
318
  if config.alert_config and hasattr(config.alert_config, 'alert_type'):
553
319
  alert_settings.append({
@@ -556,58 +322,22 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
556
322
  "threshold_level": config.alert_config.count_thresholds if hasattr(config.alert_config, 'count_thresholds') else {},
557
323
  "ascending": True,
558
324
  "settings": {t: v for t, v in zip(getattr(config.alert_config, 'alert_type', ['Default']),
559
- getattr(config.alert_config, 'alert_value', ['JSON']))}
325
+ getattr(config.alert_config, 'alert_value', ['JSON']))}
560
326
  })
561
-
562
327
  human_text_lines = [f"Tracking Statistics:"]
563
328
  human_text_lines.append(f"CURRENT FRAME @ {current_timestamp}")
564
- # Append zone-wise current counts if available
565
- if zone_analysis:
566
- human_text_lines.append("\tZones (current):")
567
- for zone_name, zone_data in zone_analysis.items():
568
- current_count = 0
569
- if isinstance(zone_data, dict):
570
- if "current_count" in zone_data:
571
- current_count = zone_data.get("current_count", 0)
572
- else:
573
- counts_dict = zone_data.get("original_counts") if isinstance(zone_data.get("original_counts"), dict) else zone_data
574
- current_count = counts_dict.get(
575
- "total",
576
- sum(v for v in counts_dict.values() if isinstance(v, (int, float)))
577
- )
578
- human_text_lines.append(f"\t{zone_name}: {int(current_count)}")
579
- else:
580
- for cat, count in per_category_count.items():
581
- human_text_lines.append(f"\t{cat}: {count}")
329
+ for cat, count in per_category_count.items():
330
+ human_text_lines.append(f"\t{cat}: {count}")
582
331
  human_text_lines.append(f"TOTAL SINCE {start_timestamp}")
583
- # Append zone-wise total counts if available
584
- if zone_analysis:
585
- human_text_lines.append("\tZones (total):")
586
- for zone_name, zone_data in zone_analysis.items():
587
- total_count = 0
588
- if isinstance(zone_data, dict):
589
- # Prefer the numeric cumulative total if available
590
- if "total_count" in zone_data and isinstance(zone_data.get("total_count"), (int, float)):
591
- total_count = zone_data.get("total_count", 0)
592
- # Fallback: compute from list of total_track_ids if present
593
- elif "total_track_ids" in zone_data and isinstance(zone_data.get("total_track_ids"), list):
594
- total_count = len(zone_data.get("total_track_ids", []))
595
- else:
596
- # Last resort: try to sum numeric values present
597
- counts_dict = zone_data if isinstance(zone_data, dict) else {}
598
- total_count = sum(v for v in counts_dict.values() if isinstance(v, (int, float)))
599
- human_text_lines.append(f"\t{zone_name}: {int(total_count)}")
600
- else:
601
- for cat, count in total_counts_dict.items():
602
- if count > 0:
603
- human_text_lines.append(f"\t{cat}: {count}")
332
+ for cat, count in total_counts_dict.items():
333
+ if count > 0:
334
+ human_text_lines.append(f"\t{cat}: {count}")
604
335
  if alerts:
605
336
  for alert in alerts:
606
337
  human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
607
338
  else:
608
339
  human_text_lines.append("Alerts: None")
609
340
  human_text = "\n".join(human_text_lines)
610
-
611
341
  reset_settings = [{"interval_type": "daily", "reset_time": {"value": 9, "time_unit": "hour"}}]
612
342
  tracking_stat = self.create_tracking_stats(
613
343
  total_counts=total_counts,
@@ -624,29 +354,24 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
624
354
  tracking_stats.append(tracking_stat)
625
355
  return tracking_stats
626
356
 
627
- def _generate_business_analytics(self, counting_summary: Dict, zone_analysis: Dict, alerts: Any, config: VehiclePeopleDroneMonitoringConfig,
357
+ def _generate_business_analytics(self, counting_summary: Dict, alerts: Any, config: VehiclePeopleDroneMonitoringConfig,
628
358
  stream_info: Optional[Dict[str, Any]] = None, is_empty=False) -> List[Dict]:
629
359
  if is_empty:
630
360
  return []
631
361
 
632
- def _generate_summary(self, summary: dict, zone_analysis: Dict, incidents: List, tracking_stats: List, business_analytics: List, alerts: List) -> List[str]:
633
- """
634
- Generate a human_text string for the tracking_stat, incident, business analytics and alerts.
635
- """
636
- lines = []
637
- lines.append("Application Name: "+self.CASE_TYPE)
638
- lines.append("Application Version: "+self.CASE_VERSION)
362
+ def _generate_summary(self, summary: dict, incidents: List, tracking_stats: List, business_analytics: List, alerts: List) -> List[str]:
363
+ lines = {}
364
+ lines["Application Name"] = self.CASE_TYPE
365
+ lines["Application Version"] = self.CASE_VERSION
639
366
  if len(incidents) > 0:
640
- lines.append("Incidents: "+f"\n\t{incidents[0].get('human_text', 'No incidents detected')}")
367
+ lines["Incidents:"] = f"\n\t{incidents[0].get('human_text', 'No incidents detected')}\n"
641
368
  if len(tracking_stats) > 0:
642
- lines.append("Tracking Statistics: "+f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}")
369
+ lines["Tracking Statistics:"] = f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}\n"
643
370
  if len(business_analytics) > 0:
644
- lines.append("Business Analytics: "+f"\t{business_analytics[0].get('human_text', 'No business analytics detected')}")
645
-
371
+ lines["Business Analytics:"] = f"\t{business_analytics[0].get('human_text', 'No business analytics detected')}\n"
646
372
  if len(incidents) == 0 and len(tracking_stats) == 0 and len(business_analytics) == 0:
647
- lines.append("Summary: "+"No Summary Data")
648
-
649
- return ["\n".join(lines)]
373
+ lines["Summary"] = "No Summary Data"
374
+ return [lines]
650
375
 
651
376
  def _get_track_ids_info(self, detections: list) -> Dict[str, Any]:
652
377
  frame_track_ids = set()
@@ -670,7 +395,6 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
670
395
  if not hasattr(self, "_per_category_total_track_ids"):
671
396
  self._per_category_total_track_ids = {cat: set() for cat in self.target_categories}
672
397
  self._current_frame_track_ids = {cat: set() for cat in self.target_categories}
673
-
674
398
  for det in detections:
675
399
  cat = det.get("category")
676
400
  raw_track_id = det.get("track_id")
@@ -696,32 +420,23 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
696
420
  return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
697
421
 
698
422
  def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
699
- """Get formatted current timestamp based on stream type."""
700
423
  if not stream_info:
701
424
  return "00:00:00.00"
702
-
703
425
  if precision:
704
426
  if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
705
427
  if frame_id:
706
428
  start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
707
429
  else:
708
430
  start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
709
- stream_time_str = self._format_timestamp_for_video(start_time)
710
-
711
-
712
- return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
431
+ return self._format_timestamp_for_video(start_time)
713
432
  else:
714
433
  return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
715
-
716
434
  if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
717
435
  if frame_id:
718
436
  start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
719
437
  else:
720
438
  start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
721
-
722
- stream_time_str = self._format_timestamp_for_video(start_time)
723
-
724
- return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
439
+ return self._format_timestamp_for_video(start_time)
725
440
  else:
726
441
  stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
727
442
  if stream_time_str:
@@ -736,31 +451,16 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
736
451
  return self._format_timestamp_for_stream(time.time())
737
452
 
738
453
  def _get_start_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False) -> str:
739
- """Get formatted start timestamp for 'TOTAL SINCE' based on stream type."""
740
454
  if not stream_info:
741
455
  return "00:00:00"
742
-
743
456
  if precision:
744
- if self.start_timer is None:
745
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
746
- return self._format_timestamp(self.start_timer)
747
- elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
748
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
749
- return self._format_timestamp(self.start_timer)
457
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
458
+ return "00:00:00"
750
459
  else:
751
- return self._format_timestamp(self.start_timer)
752
-
753
- if self.start_timer is None:
754
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
755
- return self._format_timestamp(self.start_timer)
756
- elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
757
- self.start_timer = stream_info.get("input_settings", {}).get("stream_time", "NA")
758
- return self._format_timestamp(self.start_timer)
759
-
460
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
461
+ if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
462
+ return "00:00:00"
760
463
  else:
761
- if self.start_timer is not None:
762
- return self._format_timestamp(self.start_timer)
763
-
764
464
  if self._tracking_start_time is None:
765
465
  stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
766
466
  if stream_time_str:
@@ -772,57 +472,10 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
772
472
  self._tracking_start_time = time.time()
773
473
  else:
774
474
  self._tracking_start_time = time.time()
775
-
776
475
  dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
777
476
  dt = dt.replace(minute=0, second=0, microsecond=0)
778
477
  return dt.strftime('%Y:%m:%d %H:%M:%S')
779
478
 
780
- def _format_timestamp(self, timestamp: Any) -> str:
781
- """Format a timestamp so that exactly two digits follow the decimal point (milliseconds).
782
-
783
- The input can be either:
784
- 1. A numeric Unix timestamp (``float`` / ``int``) – it will first be converted to a
785
- string in the format ``YYYY-MM-DD-HH:MM:SS.ffffff UTC``.
786
- 2. A string already following the same layout.
787
-
788
- The returned value preserves the overall format of the input but truncates or pads
789
- the fractional seconds portion to **exactly two digits**.
790
-
791
- Example
792
- -------
793
- >>> self._format_timestamp("2025-08-19-04:22:47.187574 UTC")
794
- '2025-08-19-04:22:47.18 UTC'
795
- """
796
-
797
- # Convert numeric timestamps to the expected string representation first
798
- if isinstance(timestamp, (int, float)):
799
- timestamp = datetime.fromtimestamp(timestamp, timezone.utc).strftime(
800
- '%Y-%m-%d-%H:%M:%S.%f UTC'
801
- )
802
-
803
- # Ensure we are working with a string from here on
804
- if not isinstance(timestamp, str):
805
- return str(timestamp)
806
-
807
- # If there is no fractional component, simply return the original string
808
- if '.' not in timestamp:
809
- return timestamp
810
-
811
- # Split out the main portion (up to the decimal point)
812
- main_part, fractional_and_suffix = timestamp.split('.', 1)
813
-
814
- # Separate fractional digits from the suffix (typically ' UTC')
815
- if ' ' in fractional_and_suffix:
816
- fractional_part, suffix = fractional_and_suffix.split(' ', 1)
817
- suffix = ' ' + suffix # Re-attach the space removed by split
818
- else:
819
- fractional_part, suffix = fractional_and_suffix, ''
820
-
821
- # Guarantee exactly two digits for the fractional part
822
- fractional_part = (fractional_part + '00')[:2]
823
-
824
- return f"{main_part}.{fractional_part}{suffix}"
825
-
826
479
  def _count_categories(self, detections: list, config: VehiclePeopleDroneMonitoringConfig) -> dict:
827
480
  counts = {}
828
481
  for det in detections:
@@ -867,7 +520,6 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
867
520
  values = [v for v in bbox.values() if isinstance(v, (int, float))]
868
521
  return values[:4] if len(values) >= 4 else []
869
522
  return []
870
-
871
523
  l1 = _bbox_to_list(box1)
872
524
  l2 = _bbox_to_list(box2)
873
525
  if len(l1) < 4 or len(l2) < 4:
@@ -921,6 +573,9 @@ class DroneTrafficMonitoringUsecase(BaseProcessor):
921
573
  }
922
574
  return canonical_id
923
575
 
576
+ def _format_timestamp(self, timestamp: float) -> str:
577
+ return datetime.fromtimestamp(timestamp, timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
578
+
924
579
  def _get_tracking_start_time(self) -> str:
925
580
  if self._tracking_start_time is None:
926
581
  return "N/A"