matrice-analytics 0.1.70__py3-none-any.whl → 0.1.96__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. matrice_analytics/post_processing/__init__.py +8 -2
  2. matrice_analytics/post_processing/config.py +4 -2
  3. matrice_analytics/post_processing/core/base.py +1 -1
  4. matrice_analytics/post_processing/core/config.py +40 -3
  5. matrice_analytics/post_processing/face_reg/face_recognition.py +1014 -201
  6. matrice_analytics/post_processing/face_reg/face_recognition_client.py +171 -29
  7. matrice_analytics/post_processing/face_reg/people_activity_logging.py +19 -0
  8. matrice_analytics/post_processing/post_processor.py +4 -0
  9. matrice_analytics/post_processing/usecases/__init__.py +4 -1
  10. matrice_analytics/post_processing/usecases/advanced_customer_service.py +913 -500
  11. matrice_analytics/post_processing/usecases/color_detection.py +19 -18
  12. matrice_analytics/post_processing/usecases/customer_service.py +356 -9
  13. matrice_analytics/post_processing/usecases/fire_detection.py +241 -23
  14. matrice_analytics/post_processing/usecases/footfall.py +750 -0
  15. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +638 -40
  16. matrice_analytics/post_processing/usecases/people_counting.py +66 -33
  17. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +35 -34
  18. matrice_analytics/post_processing/usecases/weapon_detection.py +2 -1
  19. matrice_analytics/post_processing/utils/alert_instance_utils.py +1018 -0
  20. matrice_analytics/post_processing/utils/business_metrics_manager_utils.py +1338 -0
  21. matrice_analytics/post_processing/utils/incident_manager_utils.py +1754 -0
  22. {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.96.dist-info}/METADATA +1 -1
  23. {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.96.dist-info}/RECORD +26 -22
  24. {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.96.dist-info}/WHEEL +0 -0
  25. {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.96.dist-info}/licenses/LICENSE.txt +0 -0
  26. {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.96.dist-info}/top_level.txt +0 -0
@@ -1114,18 +1114,18 @@ class ColorDetectionUseCase(BaseProcessor):
1114
1114
  stats = self.zone_vehicle_stats
1115
1115
 
1116
1116
  # TOTAL SINCE section
1117
- human_text_lines.append(f"TOTAL SINCE {start_timestamp}:")
1118
- for zone_name, vehicles in stats.items():
1119
- total_in_zone = sum(sum(colors.values()) for colors in vehicles.values())
1120
- if config.zone_config:
1121
- human_text_lines.append(f"\t{zone_name}:")
1122
- human_text_lines.append(f"\t\t- Total Detected: {total_in_zone}")
1123
-
1124
- for vehicle_type, colors in vehicles.items():
1125
- total_type_count = sum(colors.values())
1126
- human_text_lines.append(f"\t\t- {vehicle_type}: {total_type_count}")
1127
- for color, count in colors.items():
1128
- human_text_lines.append(f"\t\t\t- {color}: {count}")
1117
+ # human_text_lines.append(f"TOTAL SINCE {start_timestamp}:")
1118
+ # for zone_name, vehicles in stats.items():
1119
+ # total_in_zone = sum(sum(colors.values()) for colors in vehicles.values())
1120
+ # if config.zone_config:
1121
+ # human_text_lines.append(f"\t{zone_name}:")
1122
+ # human_text_lines.append(f"\t\t- Total Detected: {total_in_zone}")
1123
+
1124
+ # for vehicle_type, colors in vehicles.items():
1125
+ # total_type_count = sum(colors.values())
1126
+ # human_text_lines.append(f"\t\t- {vehicle_type}: {total_type_count}")
1127
+ # for color, count in colors.items():
1128
+ # human_text_lines.append(f"\t\t\t- {color}: {count}")
1129
1129
 
1130
1130
  current_counts_categories = []
1131
1131
  for cat, count in per_category_count.items():
@@ -1179,13 +1179,13 @@ class ColorDetectionUseCase(BaseProcessor):
1179
1179
  }
1180
1180
  })
1181
1181
 
1182
- if alerts:
1183
- for alert in alerts:
1184
- human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
1185
- else:
1186
- human_text_lines.append("Alerts: None")
1182
+ # if alerts:
1183
+ # for alert in alerts:
1184
+ # human_text_lines.append(f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}")
1185
+ # else:
1186
+ # human_text_lines.append("Alerts: None")
1187
1187
 
1188
- human_text = "\n".join(human_text_lines)
1188
+ # human_text = "\n".join(human_text_lines)
1189
1189
  reset_settings = [
1190
1190
  {
1191
1191
  "interval_type": "daily",
@@ -1202,6 +1202,7 @@ class ColorDetectionUseCase(BaseProcessor):
1202
1202
  detections=detections, human_text=human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
1203
1203
  reset_settings=reset_settings, start_time=high_precision_start_timestamp ,
1204
1204
  reset_time=high_precision_reset_timestamp)
1205
+ tracking_stat['target_categories'] = self.target_categories
1205
1206
 
1206
1207
  tracking_stats.append(tracking_stat)
1207
1208
  return tracking_stats
@@ -3,6 +3,9 @@ Customer service use case implementation.
3
3
 
4
4
  This module provides comprehensive customer service analytics including staff utilization,
5
5
  service interactions, area occupancy analysis, and business intelligence metrics.
6
+
7
+ Now includes integrated tracking support for plain YOLOv8 frame-wise predictions
8
+ (no external ByteTrack dependency).
6
9
  """
7
10
 
8
11
  from typing import Any, Dict, List, Optional, Tuple
@@ -21,6 +24,11 @@ from ..utils import (
21
24
  calculate_distance,
22
25
  match_results_structure
23
26
  )
27
+ # Import business metrics manager for publishing aggregated metrics
28
+ from ..utils.business_metrics_manager_utils import (
29
+ BUSINESS_METRICS_MANAGER,
30
+ BusinessMetricsManagerFactory
31
+ )
24
32
 
25
33
 
26
34
  def assign_person_by_area(detections, customer_areas, staff_areas):
@@ -68,7 +76,327 @@ class CustomerServiceUseCase(BaseProcessor):
68
76
  # --- Persistent sets for global unique counting across chunks ---
69
77
  self._global_customer_ids = set()
70
78
  self._global_staff_ids = set()
79
+
80
+ # Business metrics manager for publishing aggregated metrics every 5 minutes
81
+ self._business_metrics_manager_factory: Optional[BusinessMetricsManagerFactory] = None
82
+ self._business_metrics_manager: Optional[BUSINESS_METRICS_MANAGER] = None
83
+ self._business_metrics_manager_initialized: bool = False
84
+
85
+ # --- Tracker and tracking state (for YOLOv8 frame-wise predictions) ---
86
+ self.tracker = None
87
+ self._total_frame_counter: int = 0
88
+ self._tracking_start_time: Optional[float] = None
89
+
90
+ # Track ID merging/aliasing for consistent tracking across frames
91
+ self._track_aliases: Dict[Any, Any] = {}
92
+ self._canonical_tracks: Dict[Any, Dict[str, Any]] = {}
93
+ self._track_merge_iou_threshold: float = 0.05
94
+ self._track_merge_time_window: float = 7.0
95
+
96
+ # Per-category tracking for staff and customers
97
+ self._per_category_total_track_ids: Dict[str, set] = {}
98
+ self._current_frame_track_ids: Dict[str, set] = {}
71
99
 
100
+ def _initialize_business_metrics_manager_once(self, config: CustomerServiceConfig) -> None:
101
+ """
102
+ Initialize business metrics manager ONCE with Redis OR Kafka clients (Environment based).
103
+ Called from process() on first invocation.
104
+ Uses config.session (existing session from pipeline) or creates from environment.
105
+ """
106
+ if self._business_metrics_manager_initialized:
107
+ return
108
+
109
+ try:
110
+ self.logger.info("[BUSINESS_METRICS_MANAGER] Starting business metrics manager initialization for customer service...")
111
+
112
+ # Create factory if not exists
113
+ if self._business_metrics_manager_factory is None:
114
+ self._business_metrics_manager_factory = BusinessMetricsManagerFactory(logger=self.logger)
115
+
116
+ # Initialize using factory (handles session creation, Redis/Kafka setup)
117
+ # Aggregation interval: 300 seconds (5 minutes)
118
+ self._business_metrics_manager = self._business_metrics_manager_factory.initialize(
119
+ config,
120
+ aggregation_interval=300 # 5 minutes
121
+ )
122
+
123
+ if self._business_metrics_manager:
124
+ self.logger.info("[BUSINESS_METRICS_MANAGER] ✓ Business metrics manager initialized successfully for customer service")
125
+ else:
126
+ self.logger.warning("[BUSINESS_METRICS_MANAGER] Business metrics manager not available, metrics won't be published")
127
+
128
+ except Exception as e:
129
+ self.logger.error(f"[BUSINESS_METRICS_MANAGER] Business metrics manager initialization failed: {e}", exc_info=True)
130
+ finally:
131
+ self._business_metrics_manager_initialized = True # Mark as initialized (don't retry every frame)
132
+
133
+ def _apply_tracker(self, data: Any) -> Any:
134
+ """
135
+ Initialize and apply AdvancedTracker for YOLOv8 frame-wise predictions.
136
+ This adds track_id to detections for consistent tracking across frames.
137
+
138
+ Args:
139
+ data: Processed detection data (list of detections or dict)
140
+
141
+ Returns:
142
+ Data with track_ids assigned to detections
143
+ """
144
+ try:
145
+ from ..advanced_tracker import AdvancedTracker
146
+ from ..advanced_tracker.config import TrackerConfig
147
+
148
+ # Initialize tracker once
149
+ if self.tracker is None:
150
+ tracker_config = TrackerConfig(
151
+ track_high_thresh=0.4,
152
+ track_low_thresh=0.05,
153
+ new_track_thresh=0.3,
154
+ match_thresh=0.8
155
+ )
156
+ self.tracker = AdvancedTracker(tracker_config)
157
+ self.logger.info("Initialized AdvancedTracker for Customer Service")
158
+
159
+ # Apply tracker to get track_ids
160
+ tracked_data = self.tracker.update(data)
161
+
162
+ # Update tracking state for consistent ID management
163
+ self._update_tracking_state(tracked_data)
164
+ self._total_frame_counter += 1
165
+
166
+ return tracked_data
167
+
168
+ except ImportError as e:
169
+ self.logger.warning(f"AdvancedTracker not available: {e}. Proceeding without tracking.")
170
+ return data
171
+ except Exception as e:
172
+ self.logger.warning(f"AdvancedTracker failed: {e}. Proceeding without tracking.")
173
+ return data
174
+
175
+ def _update_tracking_state(self, detections: Any) -> None:
176
+ """
177
+ Update tracking state with current frame detections.
178
+ Handles track ID aliasing and per-category tracking.
179
+
180
+ Args:
181
+ detections: List of detections or dict with detections
182
+ """
183
+ # Extract detection list from various formats
184
+ detection_list = []
185
+ if isinstance(detections, list):
186
+ detection_list = [d for d in detections if isinstance(d, dict)]
187
+ elif isinstance(detections, dict):
188
+ for key, value in detections.items():
189
+ if isinstance(value, list):
190
+ detection_list.extend([d for d in value if isinstance(d, dict)])
191
+
192
+ # Initialize per-category tracking if not exists
193
+ target_categories = ['person', 'staff', 'customer', 'employee']
194
+ if not self._per_category_total_track_ids:
195
+ self._per_category_total_track_ids = {cat: set() for cat in target_categories}
196
+
197
+ self._current_frame_track_ids = {cat: set() for cat in target_categories}
198
+
199
+ for det in detection_list:
200
+ cat = det.get("category", det.get("class", ""))
201
+ raw_track_id = det.get("track_id")
202
+
203
+ if raw_track_id is None:
204
+ continue
205
+
206
+ bbox = det.get("bounding_box", det.get("bbox"))
207
+ canonical_id = self._merge_or_register_track(raw_track_id, bbox)
208
+ det["track_id"] = canonical_id
209
+
210
+ # Track by category
211
+ if cat in target_categories:
212
+ self._per_category_total_track_ids.setdefault(cat, set()).add(canonical_id)
213
+ self._current_frame_track_ids.setdefault(cat, set()).add(canonical_id)
214
+ else:
215
+ # Default to 'person' category for unknown categories
216
+ self._per_category_total_track_ids.setdefault('person', set()).add(canonical_id)
217
+ self._current_frame_track_ids.setdefault('person', set()).add(canonical_id)
218
+
219
+ def _merge_or_register_track(self, raw_id: Any, bbox: Any) -> Any:
220
+ """
221
+ Merge or register a track ID to maintain consistent tracking.
222
+ Uses IoU-based matching to merge tracks that likely represent the same object.
223
+
224
+ Args:
225
+ raw_id: Raw track ID from tracker
226
+ bbox: Bounding box of the detection
227
+
228
+ Returns:
229
+ Canonical track ID (either merged or new)
230
+ """
231
+ if raw_id is None or bbox is None:
232
+ return raw_id
233
+
234
+ now = time.time()
235
+
236
+ # Check if this raw_id is already aliased
237
+ if raw_id in self._track_aliases:
238
+ canonical_id = self._track_aliases[raw_id]
239
+ track_info = self._canonical_tracks.get(canonical_id)
240
+ if track_info is not None:
241
+ track_info["last_bbox"] = bbox
242
+ track_info["last_update"] = now
243
+ track_info["raw_ids"].add(raw_id)
244
+ return canonical_id
245
+
246
+ # Try to find a matching canonical track by IoU
247
+ for canonical_id, info in self._canonical_tracks.items():
248
+ if now - info["last_update"] > self._track_merge_time_window:
249
+ continue
250
+ iou = self._compute_iou(bbox, info["last_bbox"])
251
+ if iou >= self._track_merge_iou_threshold:
252
+ self._track_aliases[raw_id] = canonical_id
253
+ info["last_bbox"] = bbox
254
+ info["last_update"] = now
255
+ info["raw_ids"].add(raw_id)
256
+ return canonical_id
257
+
258
+ # No match found, register as new canonical track
259
+ canonical_id = raw_id
260
+ self._track_aliases[raw_id] = canonical_id
261
+ self._canonical_tracks[canonical_id] = {
262
+ "last_bbox": bbox,
263
+ "last_update": now,
264
+ "raw_ids": {raw_id},
265
+ }
266
+ return canonical_id
267
+
268
+ def _compute_iou(self, box1: Any, box2: Any) -> float:
269
+ """
270
+ Compute Intersection over Union (IoU) between two bounding boxes.
271
+ Handles various bbox formats (list, dict with xmin/ymin/xmax/ymax or x1/y1/x2/y2).
272
+
273
+ Args:
274
+ box1: First bounding box
275
+ box2: Second bounding box
276
+
277
+ Returns:
278
+ IoU value between 0.0 and 1.0
279
+ """
280
+ def _bbox_to_list(bbox):
281
+ if bbox is None:
282
+ return []
283
+ if isinstance(bbox, list):
284
+ return bbox[:4] if len(bbox) >= 4 else []
285
+ if isinstance(bbox, dict):
286
+ if "xmin" in bbox:
287
+ return [bbox.get("xmin", 0), bbox.get("ymin", 0),
288
+ bbox.get("xmax", 0), bbox.get("ymax", 0)]
289
+ if "x1" in bbox:
290
+ return [bbox.get("x1", 0), bbox.get("y1", 0),
291
+ bbox.get("x2", 0), bbox.get("y2", 0)]
292
+ # Try to extract values from dict
293
+ values = [v for v in bbox.values() if isinstance(v, (int, float))]
294
+ return values[:4] if len(values) >= 4 else []
295
+ return []
296
+
297
+ l1 = _bbox_to_list(box1)
298
+ l2 = _bbox_to_list(box2)
299
+
300
+ if len(l1) < 4 or len(l2) < 4:
301
+ return 0.0
302
+
303
+ x1_min, y1_min, x1_max, y1_max = l1
304
+ x2_min, y2_min, x2_max, y2_max = l2
305
+
306
+ # Ensure proper ordering
307
+ x1_min, x1_max = min(x1_min, x1_max), max(x1_min, x1_max)
308
+ y1_min, y1_max = min(y1_min, y1_max), max(y1_min, y1_max)
309
+ x2_min, x2_max = min(x2_min, x2_max), max(x2_min, x2_max)
310
+ y2_min, y2_max = min(y2_min, y2_max), max(y2_min, y2_max)
311
+
312
+ # Calculate intersection
313
+ inter_x_min = max(x1_min, x2_min)
314
+ inter_y_min = max(y1_min, y2_min)
315
+ inter_x_max = min(x1_max, x2_max)
316
+ inter_y_max = min(y1_max, y2_max)
317
+
318
+ inter_w = max(0.0, inter_x_max - inter_x_min)
319
+ inter_h = max(0.0, inter_y_max - inter_y_min)
320
+ inter_area = inter_w * inter_h
321
+
322
+ # Calculate union
323
+ area1 = (x1_max - x1_min) * (y1_max - y1_min)
324
+ area2 = (x2_max - x2_min) * (y2_max - y2_min)
325
+ union_area = area1 + area2 - inter_area
326
+
327
+ return (inter_area / union_area) if union_area > 0 else 0.0
328
+
329
+ def get_total_counts(self) -> Dict[str, int]:
330
+ """
331
+ Get total unique counts per category across all processed frames.
332
+
333
+ Returns:
334
+ Dictionary mapping category to unique count
335
+ """
336
+ return {cat: len(ids) for cat, ids in self._per_category_total_track_ids.items()}
337
+
338
+ def reset_tracking_state(self) -> None:
339
+ """
340
+ Reset all tracking state. Useful for starting a new session.
341
+ """
342
+ self.tracker = None
343
+ self._total_frame_counter = 0
344
+ self._tracking_start_time = None
345
+ self._track_aliases = {}
346
+ self._canonical_tracks = {}
347
+ self._per_category_total_track_ids = {}
348
+ self._current_frame_track_ids = {}
349
+ self._global_customer_ids = set()
350
+ self._global_staff_ids = set()
351
+ self.logger.info("Tracking state reset for Customer Service")
352
+
353
+ def _send_metrics_to_manager(
354
+ self,
355
+ business_metrics: Dict[str, Any],
356
+ stream_info: Optional[Any] = None
357
+ ) -> None:
358
+ """
359
+ Send business metrics to the business metrics manager for aggregation and publishing.
360
+
361
+ The business metrics manager will:
362
+ 1. Aggregate metrics for 5 minutes (300 seconds)
363
+ 2. Publish aggregated metrics (mean/min/max/sum) to output topic
364
+ 3. Reset all values after publishing
365
+
366
+ Args:
367
+ business_metrics: Business metrics dictionary from _calculate_business_metrics
368
+ stream_info: Stream metadata containing camera info
369
+ """
370
+ if not self._business_metrics_manager:
371
+ self.logger.debug("[BUSINESS_METRICS_MANAGER] No business metrics manager available, skipping")
372
+ return
373
+
374
+ # Extract camera_id from stream_info
375
+ camera_id = ""
376
+ if stream_info:
377
+ camera_info = stream_info.get("camera_info", {}) if isinstance(stream_info, dict) else {}
378
+ camera_id = camera_info.get("camera_id", "")
379
+ if not camera_id:
380
+ camera_id = stream_info.get("camera_id", "") if isinstance(stream_info, dict) else ""
381
+
382
+ if not camera_id:
383
+ # Fallback to a default identifier
384
+ camera_id = "default_camera"
385
+ self.logger.debug(f"[BUSINESS_METRICS_MANAGER] No camera_id found, using default: {camera_id}")
386
+
387
+ try:
388
+ # Process the metrics through the manager
389
+ published = self._business_metrics_manager.process_metrics(
390
+ camera_id=camera_id,
391
+ metrics_data=business_metrics,
392
+ stream_info=stream_info
393
+ )
394
+
395
+ if published:
396
+ self.logger.info(f"[BUSINESS_METRICS_MANAGER] Metrics published for camera: {camera_id}")
397
+ except Exception as e:
398
+ self.logger.error(f"[BUSINESS_METRICS_MANAGER] Error sending metrics to manager: {e}", exc_info=True)
399
+
72
400
  def get_config_schema(self) -> Dict[str, Any]:
73
401
  """Get configuration schema for customer service."""
74
402
  return {
@@ -210,6 +538,10 @@ class CustomerServiceUseCase(BaseProcessor):
210
538
  # Initialize processing context if not provided
211
539
  if context is None:
212
540
  context = ProcessingContext()
541
+ config.enable_tracking=True
542
+ # Initialize business metrics manager once (for publishing aggregated metrics)
543
+ if not self._business_metrics_manager_initialized:
544
+ self._initialize_business_metrics_manager_once(config)
213
545
 
214
546
  # Detect input format
215
547
  input_format = match_results_structure(data)
@@ -230,6 +562,14 @@ class CustomerServiceUseCase(BaseProcessor):
230
562
  processed_data = apply_category_mapping(processed_data, config.index_to_category)
231
563
  self.logger.debug("Applied category mapping")
232
564
 
565
+ # Step 2.5: Initialize and apply tracker for YOLOv8 frame-wise predictions
566
+ # Only apply tracker if tracking is enabled in config
567
+
568
+ if config.enable_tracking:
569
+ processed_data = self._apply_tracker(processed_data)
570
+ else:
571
+ self.logger.debug("Tracking disabled in config, skipping tracker application")
572
+
233
573
  # Step 3: Extract detections and assign 'person' by area if needed
234
574
  detections = self._extract_detections(processed_data)
235
575
  assign_person_by_area(
@@ -268,6 +608,11 @@ class CustomerServiceUseCase(BaseProcessor):
268
608
  area_analysis, service_interactions, customer_analytics,
269
609
  staff_analytics, config
270
610
  )
611
+
612
+ # Step 8.5: Send business metrics to manager for aggregation and publishing
613
+ # The manager aggregates for 5 minutes and publishes mean/min/max/sum
614
+ if business_metrics:
615
+ self._send_metrics_to_manager(business_metrics, stream_info)
271
616
 
272
617
  # Step 9: Generate insights and alerts
273
618
  insights = self._generate_insights(
@@ -447,23 +792,25 @@ class CustomerServiceUseCase(BaseProcessor):
447
792
  def _count_people_in_area(self, detections: List[Dict], polygon: List[List[float]]) -> int:
448
793
  """
449
794
  Count unique people (by track_id) in a specific area defined by polygon.
450
- Previous logic (per-frame count) is commented out below.
795
+ Falls back to raw count if no track_ids are available.
451
796
  """
452
- # count = 0
453
- # for detection in detections:
454
- # center = get_bbox_center(detection.get('bbox', detection.get('bounding_box', {})))
455
- # if center and point_in_polygon(center, polygon):
456
- # count += 1
457
- # return count
458
-
459
797
  track_ids = set()
798
+ count_without_track = 0
799
+
460
800
  for detection in detections:
461
801
  center = get_bbox_center(detection.get('bbox', detection.get('bounding_box', {})))
462
802
  if center and point_in_polygon(center, polygon):
463
803
  track_id = detection.get('track_id')
464
804
  if track_id is not None:
465
805
  track_ids.add(track_id)
466
- return len(track_ids)
806
+ else:
807
+ # Fallback: count detections without track_id
808
+ count_without_track += 1
809
+
810
+ # If we have track_ids, use unique count; otherwise fall back to raw count
811
+ if track_ids:
812
+ return len(track_ids)
813
+ return count_without_track
467
814
 
468
815
  def _analyze_service_interactions(self, staff_detections: List[Dict], customer_detections: List[Dict],
469
816
  config: CustomerServiceConfig) -> List[Dict]: