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.
- matrice_analytics/post_processing/__init__.py +8 -2
- matrice_analytics/post_processing/config.py +4 -2
- matrice_analytics/post_processing/core/base.py +1 -1
- matrice_analytics/post_processing/core/config.py +40 -3
- matrice_analytics/post_processing/face_reg/face_recognition.py +1014 -201
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +171 -29
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +19 -0
- matrice_analytics/post_processing/post_processor.py +4 -0
- matrice_analytics/post_processing/usecases/__init__.py +4 -1
- matrice_analytics/post_processing/usecases/advanced_customer_service.py +913 -500
- matrice_analytics/post_processing/usecases/color_detection.py +19 -18
- matrice_analytics/post_processing/usecases/customer_service.py +356 -9
- matrice_analytics/post_processing/usecases/fire_detection.py +241 -23
- matrice_analytics/post_processing/usecases/footfall.py +750 -0
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +638 -40
- matrice_analytics/post_processing/usecases/people_counting.py +66 -33
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +35 -34
- matrice_analytics/post_processing/usecases/weapon_detection.py +2 -1
- matrice_analytics/post_processing/utils/alert_instance_utils.py +1018 -0
- matrice_analytics/post_processing/utils/business_metrics_manager_utils.py +1338 -0
- matrice_analytics/post_processing/utils/incident_manager_utils.py +1754 -0
- {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.96.dist-info}/METADATA +1 -1
- {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.96.dist-info}/RECORD +26 -22
- {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.96.dist-info}/WHEEL +0 -0
- {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.96.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|
-
|
|
1184
|
-
|
|
1185
|
-
else:
|
|
1186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]:
|