matrice-analytics 0.1.89__py3-none-any.whl → 0.1.97__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 +21 -2
- matrice_analytics/post_processing/config.py +6 -0
- matrice_analytics/post_processing/core/config.py +102 -3
- matrice_analytics/post_processing/face_reg/face_recognition.py +146 -14
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +116 -4
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +19 -0
- matrice_analytics/post_processing/post_processor.py +12 -0
- matrice_analytics/post_processing/usecases/__init__.py +9 -0
- matrice_analytics/post_processing/usecases/advanced_customer_service.py +5 -2
- matrice_analytics/post_processing/usecases/color_detection.py +1 -0
- matrice_analytics/post_processing/usecases/fire_detection.py +94 -14
- matrice_analytics/post_processing/usecases/footfall.py +750 -0
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +91 -1
- matrice_analytics/post_processing/usecases/people_counting.py +55 -22
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +15 -32
- matrice_analytics/post_processing/usecases/vehicle_monitoring_drone_view.py +1007 -0
- matrice_analytics/post_processing/usecases/vehicle_monitoring_parking_lot.py +1011 -0
- matrice_analytics/post_processing/usecases/weapon_detection.py +2 -1
- matrice_analytics/post_processing/utils/alert_instance_utils.py +94 -26
- matrice_analytics/post_processing/utils/business_metrics_manager_utils.py +97 -4
- matrice_analytics/post_processing/utils/incident_manager_utils.py +103 -6
- {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.97.dist-info}/METADATA +1 -1
- {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.97.dist-info}/RECORD +26 -23
- {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.97.dist-info}/WHEEL +0 -0
- {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.97.dist-info}/licenses/LICENSE.txt +0 -0
- {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.97.dist-info}/top_level.txt +0 -0
|
@@ -7,6 +7,7 @@ in the post-processing pipeline using Matrice Session.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
|
+
import re
|
|
10
11
|
import base64
|
|
11
12
|
import logging
|
|
12
13
|
import httpx
|
|
@@ -14,6 +15,7 @@ import urllib
|
|
|
14
15
|
import urllib.request
|
|
15
16
|
from typing import List, Dict, Any, Optional
|
|
16
17
|
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
17
19
|
|
|
18
20
|
# Import matrice session
|
|
19
21
|
try:
|
|
@@ -29,6 +31,78 @@ class FacialRecognitionClient:
|
|
|
29
31
|
Simplified Face Recognition Client using Matrice Session.
|
|
30
32
|
All API calls are made through the Matrice session RPC interface.
|
|
31
33
|
"""
|
|
34
|
+
|
|
35
|
+
# Pattern for matching action IDs (hex strings of at least 8 characters)
|
|
36
|
+
ACTION_ID_PATTERN = re.compile(r"^[0-9a-f]{8,}$", re.IGNORECASE)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def _discover_action_id(cls) -> Optional[str]:
|
|
40
|
+
"""Discover action_id from current working directory name (and parents)."""
|
|
41
|
+
candidates: List[str] = []
|
|
42
|
+
try:
|
|
43
|
+
cwd = Path.cwd()
|
|
44
|
+
candidates.append(cwd.name)
|
|
45
|
+
for parent in cwd.parents:
|
|
46
|
+
candidates.append(parent.name)
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
usr_src = Path("/usr/src")
|
|
52
|
+
if usr_src.exists():
|
|
53
|
+
for child in usr_src.iterdir():
|
|
54
|
+
if child.is_dir():
|
|
55
|
+
candidates.append(child.name)
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
for candidate in candidates:
|
|
60
|
+
if candidate and len(candidate) >= 8 and cls.ACTION_ID_PATTERN.match(candidate):
|
|
61
|
+
return candidate
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def _fetch_project_id_from_action(self) -> Optional[str]:
|
|
65
|
+
"""
|
|
66
|
+
Fetch project ID from action details using discovered action ID.
|
|
67
|
+
|
|
68
|
+
This method discovers the action ID from the working directory name,
|
|
69
|
+
fetches action details from the API, and extracts the _idProject field.
|
|
70
|
+
If successful, it also updates the MATRICE_PROJECT_ID environment variable.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The project ID string if found, None otherwise.
|
|
74
|
+
"""
|
|
75
|
+
action_id = self._discover_action_id()
|
|
76
|
+
if not action_id:
|
|
77
|
+
self.logger.warning("[PROJECT_ID] Could not discover action_id from folder name")
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
self.logger.info(f"[PROJECT_ID] Discovered action_id from folder: {action_id}")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
url = f"/v1/actions/action/{action_id}/details"
|
|
84
|
+
self.logger.info(f"[PROJECT_ID] Fetching action details from: {url}")
|
|
85
|
+
response = self.session.rpc.get(url)
|
|
86
|
+
|
|
87
|
+
if response and response.get("success", False) and response.get("code") == 200:
|
|
88
|
+
data = response.get("data", {})
|
|
89
|
+
project_id = data.get("_idProject", "")
|
|
90
|
+
|
|
91
|
+
if project_id:
|
|
92
|
+
self.logger.info(f"[PROJECT_ID] Successfully fetched project ID from action details: {project_id}")
|
|
93
|
+
# Update environment variable so other components can use it
|
|
94
|
+
os.environ["MATRICE_PROJECT_ID"] = project_id
|
|
95
|
+
self.logger.info(f"[PROJECT_ID] Updated MATRICE_PROJECT_ID environment variable: {project_id}")
|
|
96
|
+
return project_id
|
|
97
|
+
else:
|
|
98
|
+
self.logger.warning(f"[PROJECT_ID] _idProject not found in action details for action_id={action_id}")
|
|
99
|
+
else:
|
|
100
|
+
error_msg = response.get('message', 'Unknown error') if response else 'Empty response'
|
|
101
|
+
self.logger.warning(f"[PROJECT_ID] Failed to fetch action details: {error_msg}")
|
|
102
|
+
except Exception as e:
|
|
103
|
+
self.logger.error(f"[PROJECT_ID] Error fetching action details for action_id={action_id}: {e}", exc_info=True)
|
|
104
|
+
|
|
105
|
+
return None
|
|
32
106
|
|
|
33
107
|
def __init__(self, account_number: str = "", access_key: str = "", secret_key: str = "",
|
|
34
108
|
project_id: str = "", server_id: str = "", session=None):
|
|
@@ -75,6 +149,23 @@ class FacialRecognitionClient:
|
|
|
75
149
|
except Exception as e:
|
|
76
150
|
self.logger.error(f"Failed to initialize Matrice session: {e}", exc_info=True)
|
|
77
151
|
raise
|
|
152
|
+
|
|
153
|
+
# If project_id is still empty, try to fetch from action details
|
|
154
|
+
if not self.project_id:
|
|
155
|
+
self.logger.info("[PROJECT_ID] Project ID is empty, attempting to fetch from action details...")
|
|
156
|
+
fetched_project_id = self._fetch_project_id_from_action()
|
|
157
|
+
if fetched_project_id:
|
|
158
|
+
self.project_id = fetched_project_id
|
|
159
|
+
self.logger.info(f"[PROJECT_ID] Successfully set project_id from action details: {self.project_id}")
|
|
160
|
+
# Update session with the new project_id if possible
|
|
161
|
+
if hasattr(self.session, 'update'):
|
|
162
|
+
try:
|
|
163
|
+
self.session.update(self.project_id)
|
|
164
|
+
self.logger.info(f"[PROJECT_ID] Updated session with project_id: {self.project_id}")
|
|
165
|
+
except Exception as e:
|
|
166
|
+
self.logger.warning(f"[PROJECT_ID] Failed to update session with project_id: {e}")
|
|
167
|
+
else:
|
|
168
|
+
self.logger.warning("[PROJECT_ID] Could not fetch project_id from action details")
|
|
78
169
|
|
|
79
170
|
# Fetch server connection info if server_id is provided
|
|
80
171
|
if self.server_id:
|
|
@@ -93,12 +184,29 @@ class FacialRecognitionClient:
|
|
|
93
184
|
self.server_base_url = f"http://{server_host}:{server_port}"
|
|
94
185
|
self.logger.warning(f"Facial recognition server base URL: {self.server_base_url}")
|
|
95
186
|
|
|
96
|
-
|
|
97
|
-
|
|
187
|
+
# Update project_id from server_info if available and current project_id is empty
|
|
188
|
+
server_project_id = self.server_info.get('projectID', '')
|
|
189
|
+
if server_project_id:
|
|
190
|
+
if not self.project_id:
|
|
191
|
+
self.project_id = server_project_id
|
|
192
|
+
self.logger.info(f"[PROJECT_ID] Set project_id from server_info: {self.project_id}")
|
|
193
|
+
# Update environment variable
|
|
194
|
+
os.environ["MATRICE_PROJECT_ID"] = self.project_id
|
|
195
|
+
self.logger.info(f"[PROJECT_ID] Updated MATRICE_PROJECT_ID env var from server_info: {self.project_id}")
|
|
196
|
+
self.session.update(server_project_id)
|
|
197
|
+
self.logger.info(f"Updated Matrice session with project ID: {server_project_id}")
|
|
198
|
+
else:
|
|
199
|
+
self.logger.warning("[PROJECT_ID] server_info.projectID is empty")
|
|
98
200
|
else:
|
|
99
201
|
self.logger.warning("Failed to fetch facial recognition server connection info")
|
|
100
202
|
except Exception as e:
|
|
101
203
|
self.logger.error(f"Error fetching facial recognition server connection info: {e}", exc_info=True)
|
|
204
|
+
|
|
205
|
+
# Final check: log the project_id status
|
|
206
|
+
if self.project_id:
|
|
207
|
+
self.logger.info(f"[PROJECT_ID] Final project_id: {self.project_id}")
|
|
208
|
+
else:
|
|
209
|
+
self.logger.error("[PROJECT_ID] WARNING: project_id is still empty after all initialization attempts!")
|
|
102
210
|
|
|
103
211
|
def _get_public_ip(self) -> str:
|
|
104
212
|
"""Get the public IP address of this machine."""
|
|
@@ -286,6 +394,8 @@ class FacialRecognitionClient:
|
|
|
286
394
|
employee_id: Optional[str] = None,
|
|
287
395
|
timestamp: str = datetime.now(timezone.utc).isoformat(),
|
|
288
396
|
image_data: Optional[str] = None,
|
|
397
|
+
camera_name: Optional[str] = None,
|
|
398
|
+
camera_id: Optional[str] = None,
|
|
289
399
|
) -> Dict[str, Any]:
|
|
290
400
|
"""
|
|
291
401
|
Store people activity data with optional image data
|
|
@@ -310,6 +420,8 @@ class FacialRecognitionClient:
|
|
|
310
420
|
"timestamp": timestamp,
|
|
311
421
|
"bbox": bbox,
|
|
312
422
|
"location": location,
|
|
423
|
+
"camera_name": camera_name,
|
|
424
|
+
"camera_id": camera_id,
|
|
313
425
|
}
|
|
314
426
|
|
|
315
427
|
# Add optional fields if provided based on API spec
|
|
@@ -322,8 +434,8 @@ class FacialRecognitionClient:
|
|
|
322
434
|
if image_data:
|
|
323
435
|
activity_request["imageData"] = image_data
|
|
324
436
|
|
|
325
|
-
self.logger.info(f"API REQUEST: Storing people activity - type={detection_type}, staff_id={staff_id}, location={location}, has_image={bool(image_data)}")
|
|
326
|
-
self.logger.debug(f"Activity request payload: bbox={bbox}, employee_id={employee_id}")
|
|
437
|
+
self.logger.info(f"API REQUEST: Storing people activity - type={detection_type}, staff_id={staff_id}, location={location}, camera_name={camera_name}, camera_id={camera_id}, has_image={bool(image_data)}")
|
|
438
|
+
self.logger.debug(f"Activity request payload: bbox={bbox}, employee_id={employee_id}, camera_name={camera_name}, camera_id={camera_id}")
|
|
327
439
|
|
|
328
440
|
try:
|
|
329
441
|
response = await self.session.rpc.async_send_request(
|
|
@@ -4,6 +4,7 @@ import time
|
|
|
4
4
|
import threading
|
|
5
5
|
import queue
|
|
6
6
|
import base64
|
|
7
|
+
import os
|
|
7
8
|
from typing import Dict, Optional, Set
|
|
8
9
|
import numpy as np
|
|
9
10
|
import cv2
|
|
@@ -18,6 +19,15 @@ class PeopleActivityLogging:
|
|
|
18
19
|
self.face_client = face_client
|
|
19
20
|
self.logger = logging.getLogger(__name__)
|
|
20
21
|
|
|
22
|
+
# Log project ID information for observability and debugging
|
|
23
|
+
face_client_project_id = getattr(self.face_client, "project_id", None) if self.face_client else None
|
|
24
|
+
env_project_id = os.getenv("MATRICE_PROJECT_ID", "")
|
|
25
|
+
self.logger.info(
|
|
26
|
+
"[PROJECT_ID] PeopleActivityLogging initialized "
|
|
27
|
+
f"with face_client.project_id='{face_client_project_id}', "
|
|
28
|
+
f"MATRICE_PROJECT_ID env='{env_project_id}'"
|
|
29
|
+
)
|
|
30
|
+
|
|
21
31
|
# Use thread-safe queue for cross-thread communication (Python 3.8 compatibility)
|
|
22
32
|
self.activity_queue = queue.Queue()
|
|
23
33
|
|
|
@@ -95,6 +105,8 @@ class PeopleActivityLogging:
|
|
|
95
105
|
detection: Dict,
|
|
96
106
|
current_frame: Optional[np.ndarray] = None,
|
|
97
107
|
location: str = "",
|
|
108
|
+
camera_name: str = "",
|
|
109
|
+
camera_id: str = "",
|
|
98
110
|
):
|
|
99
111
|
"""Enqueue a detection for background processing"""
|
|
100
112
|
try:
|
|
@@ -103,6 +115,8 @@ class PeopleActivityLogging:
|
|
|
103
115
|
"detection": detection,
|
|
104
116
|
"current_frame": current_frame,
|
|
105
117
|
"location": location,
|
|
118
|
+
"camera_name": camera_name,
|
|
119
|
+
"camera_id": camera_id,
|
|
106
120
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
107
121
|
"employee_id": detection.get("employee_id", None),
|
|
108
122
|
"staff_id": detection.get("person_id")
|
|
@@ -178,7 +192,10 @@ class PeopleActivityLogging:
|
|
|
178
192
|
location = activity_data["location"]
|
|
179
193
|
staff_id = activity_data["staff_id"]
|
|
180
194
|
timestamp = activity_data["timestamp"]
|
|
195
|
+
camera_name = activity_data.get("camera_name", "")
|
|
196
|
+
camera_id = activity_data.get("camera_id", "")
|
|
181
197
|
|
|
198
|
+
self.logger.debug(f"Processing activity - location: '{location}', camera_name: '{camera_name}', camera_id: '{camera_id}'")
|
|
182
199
|
try:
|
|
183
200
|
if not self.face_client:
|
|
184
201
|
self.logger.warning("Face client not available for activity logging")
|
|
@@ -211,6 +228,8 @@ class PeopleActivityLogging:
|
|
|
211
228
|
employee_id=employee_id,
|
|
212
229
|
timestamp=timestamp,
|
|
213
230
|
image_data=image_data,
|
|
231
|
+
camera_name=camera_name,
|
|
232
|
+
camera_id=camera_id,
|
|
214
233
|
)
|
|
215
234
|
|
|
216
235
|
if response and response.get("success", False):
|
|
@@ -99,6 +99,9 @@ from .usecases import (
|
|
|
99
99
|
UndergroundPipelineDefectUseCase,
|
|
100
100
|
SusActivityUseCase,
|
|
101
101
|
NaturalDisasterUseCase,
|
|
102
|
+
FootFallUseCase,
|
|
103
|
+
VehicleMonitoringParkingLotUseCase,
|
|
104
|
+
VehicleMonitoringDroneViewUseCase,
|
|
102
105
|
# Put all IMAGE based usecases here
|
|
103
106
|
BloodCancerDetectionUseCase,
|
|
104
107
|
SkinCancerClassificationUseCase,
|
|
@@ -569,6 +572,15 @@ class PostProcessor:
|
|
|
569
572
|
registry.register_use_case(
|
|
570
573
|
"environmental", "natural_disaster_detection", NaturalDisasterUseCase
|
|
571
574
|
)
|
|
575
|
+
registry.register_use_case(
|
|
576
|
+
"retail", "footfall", FootFallUseCase
|
|
577
|
+
)
|
|
578
|
+
registry.register_use_case(
|
|
579
|
+
"traffic", "vehicle_monitoring_parking_lot", VehicleMonitoringParkingLotUseCase
|
|
580
|
+
)
|
|
581
|
+
registry.register_use_case(
|
|
582
|
+
"traffic", "vehicle_monitoring_drone_view", VehicleMonitoringDroneViewUseCase
|
|
583
|
+
)
|
|
572
584
|
|
|
573
585
|
# Put all IMAGE based usecases here
|
|
574
586
|
registry.register_use_case(
|
|
@@ -85,6 +85,9 @@ from .pcb_defect_detection import PCBDefectConfig, PCBDefectUseCase
|
|
|
85
85
|
from .underground_pipeline_defect_detection import UndergroundPipelineDefectConfig,UndergroundPipelineDefectUseCase
|
|
86
86
|
from .suspicious_activity_detection import SusActivityConfig, SusActivityUseCase
|
|
87
87
|
from .natural_disaster import NaturalDisasterConfig, NaturalDisasterUseCase
|
|
88
|
+
from .footfall import FootFallConfig, FootFallUseCase
|
|
89
|
+
from .vehicle_monitoring_parking_lot import VehicleMonitoringParkingLotUseCase, VehicleMonitoringParkingLotConfig
|
|
90
|
+
from .vehicle_monitoring_drone_view import VehicleMonitoringDroneViewUseCase, VehicleMonitoringDroneViewConfig
|
|
88
91
|
|
|
89
92
|
#Put all IMAGE based usecases here
|
|
90
93
|
from .blood_cancer_detection_img import BloodCancerDetectionConfig, BloodCancerDetectionUseCase
|
|
@@ -172,6 +175,9 @@ __all__ = [
|
|
|
172
175
|
'UndergroundPipelineDefectUseCase',
|
|
173
176
|
'SusActivityUseCase',
|
|
174
177
|
'NaturalDisasterUseCase',
|
|
178
|
+
'FootFallUseCase',
|
|
179
|
+
'VehicleMonitoringParkingLotUseCase',
|
|
180
|
+
'VehicleMonitoringDroneViewUseCase',
|
|
175
181
|
|
|
176
182
|
#Put all IMAGE based usecases here
|
|
177
183
|
'BloodCancerDetectionUseCase',
|
|
@@ -254,6 +260,9 @@ __all__ = [
|
|
|
254
260
|
'PCBDefectConfig',
|
|
255
261
|
'SusActivityConfig',
|
|
256
262
|
'NaturalDisasterConfig',
|
|
263
|
+
'FootFallConfig',
|
|
264
|
+
'VehicleMonitoringParkingLotConfig',
|
|
265
|
+
'VehicleMonitoringDroneViewConfig',
|
|
257
266
|
|
|
258
267
|
#Put all IMAGE based usecase CONFIGS here
|
|
259
268
|
'BloodCancerDetectionConfig',
|
|
@@ -74,6 +74,8 @@ class AdvancedCustomerServiceUseCase(BaseProcessor):
|
|
|
74
74
|
"""Initialize advanced customer service use case."""
|
|
75
75
|
super().__init__("advanced_customer_service")
|
|
76
76
|
self.category = "sales"
|
|
77
|
+
self.CASE_TYPE: Optional[str] = 'advanced_customer_service'
|
|
78
|
+
self.CASE_VERSION: Optional[str] = '1.3'
|
|
77
79
|
|
|
78
80
|
# Advanced tracking structures
|
|
79
81
|
self.customer_occupancy = {}
|
|
@@ -496,7 +498,7 @@ class AdvancedCustomerServiceUseCase(BaseProcessor):
|
|
|
496
498
|
# Calculate current_counts (frame-wise counts)
|
|
497
499
|
current_counts = [
|
|
498
500
|
{"category": "staff", "count": staff_analytics.get("active_staff", 0)},
|
|
499
|
-
{"category": "
|
|
501
|
+
{"category": "Active Customers", "count": queue_analytics.get("active_customers", 0)}
|
|
500
502
|
]
|
|
501
503
|
# Detections: include all detections for this frame
|
|
502
504
|
detection_objs = []
|
|
@@ -529,7 +531,8 @@ class AdvancedCustomerServiceUseCase(BaseProcessor):
|
|
|
529
531
|
"alerts": alerts,
|
|
530
532
|
"alert_settings": alert_settings,
|
|
531
533
|
"reset_settings": reset_settings,
|
|
532
|
-
"human_text": human_text
|
|
534
|
+
"human_text": human_text,
|
|
535
|
+
"target_categories": ['Staff', 'Active Customers']
|
|
533
536
|
}
|
|
534
537
|
# Patch: Build real_time_occupancy with correct service_areas info (not just empty lists)
|
|
535
538
|
real_time_occupancy = analytics_results.get("real_time_occupancy", {}).copy()
|
|
@@ -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
|
|
@@ -107,7 +107,7 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
107
107
|
|
|
108
108
|
self.smoothing_tracker = None # Required for bbox smoothing
|
|
109
109
|
self._fire_smoke_recent_history = []
|
|
110
|
-
self.target_categories=['fire']
|
|
110
|
+
self.target_categories=['fire', 'smoke'] # Lowercase to match filtering logic at line 276
|
|
111
111
|
|
|
112
112
|
self._ascending_alert_list: List[str] = []
|
|
113
113
|
self.current_incident_end_timestamp: str = "N/A"
|
|
@@ -538,9 +538,21 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
538
538
|
count_thresholds = {}
|
|
539
539
|
if config.alert_config and hasattr(config.alert_config, "count_thresholds"):
|
|
540
540
|
count_thresholds = config.alert_config.count_thresholds or {}
|
|
541
|
+
|
|
542
|
+
# CRITICAL FIX: Ensure we have at least one category to process
|
|
543
|
+
# If count_thresholds is empty, use detected categories from per_category_count
|
|
544
|
+
# This ensures incidents are always generated when detections exist
|
|
545
|
+
per_category_count = summary.get("per_category_count", {})
|
|
546
|
+
if not count_thresholds and per_category_count:
|
|
547
|
+
# Create thresholds for all detected categories with threshold=0 (always trigger)
|
|
548
|
+
count_thresholds = {cat: 0 for cat in per_category_count.keys()}
|
|
549
|
+
self.logger.debug(f"[INCIDENT] count_thresholds was empty, using detected categories: {count_thresholds}")
|
|
550
|
+
|
|
551
|
+
# Flag to track if we generated any incident
|
|
552
|
+
incident_generated = False
|
|
541
553
|
|
|
542
554
|
for category, threshold in count_thresholds.items():
|
|
543
|
-
if category in
|
|
555
|
+
if category in per_category_count:
|
|
544
556
|
|
|
545
557
|
#count = summary.get("per_category_count", {})[category]
|
|
546
558
|
start_timestamp = self._get_start_timestamp_str(stream_info)
|
|
@@ -643,6 +655,55 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
643
655
|
event['duration'] = self.get_duration_seconds(start_timestamp, self.current_incident_end_timestamp)
|
|
644
656
|
event['incident_quant'] = intensity_pct
|
|
645
657
|
incidents.append(event)
|
|
658
|
+
incident_generated = True
|
|
659
|
+
|
|
660
|
+
# CRITICAL FALLBACK: If no incident was generated despite having detections,
|
|
661
|
+
# generate a basic incident to ensure the incident manager receives data
|
|
662
|
+
if not incident_generated and total > 0:
|
|
663
|
+
self.logger.warning(f"[INCIDENT] No incident generated despite {total} detections. Generating fallback incident.")
|
|
664
|
+
# Calculate area and intensity for fallback
|
|
665
|
+
for det in detections:
|
|
666
|
+
bbox = det.get("bounding_box") or det.get("bbox")
|
|
667
|
+
if bbox:
|
|
668
|
+
xmin, ymin = bbox.get("xmin"), bbox.get("ymin")
|
|
669
|
+
xmax, ymax = bbox.get("xmax"), bbox.get("ymax")
|
|
670
|
+
if None not in (xmin, ymin, xmax, ymax):
|
|
671
|
+
width, height = xmax - xmin, ymax - ymin
|
|
672
|
+
if width > 0 and height > 0:
|
|
673
|
+
total_area += width * height
|
|
674
|
+
|
|
675
|
+
threshold_area = config.threshold_area or 250200.0
|
|
676
|
+
intensity_pct = min(100.0, (total_area / threshold_area) * 100)
|
|
677
|
+
|
|
678
|
+
# Determine severity level
|
|
679
|
+
if intensity_pct >= 30:
|
|
680
|
+
level = "critical"
|
|
681
|
+
elif intensity_pct >= 13:
|
|
682
|
+
level = "significant"
|
|
683
|
+
elif intensity_pct >= 3:
|
|
684
|
+
level = "medium"
|
|
685
|
+
else:
|
|
686
|
+
level = "low"
|
|
687
|
+
self._ascending_alert_list.append(level)
|
|
688
|
+
|
|
689
|
+
start_timestamp = self._get_start_timestamp_str(stream_info)
|
|
690
|
+
human_text = f"INCIDENTS DETECTED @ {current_timestamp}:\n\tSeverity Level: {(self.CASE_TYPE, level)}"
|
|
691
|
+
|
|
692
|
+
event = self.create_incident(
|
|
693
|
+
incident_id='incident_' + self.CASE_TYPE + '_fallback',
|
|
694
|
+
incident_type=self.CASE_TYPE,
|
|
695
|
+
severity_level=level,
|
|
696
|
+
human_text=human_text,
|
|
697
|
+
camera_info=camera_info,
|
|
698
|
+
alerts=alerts,
|
|
699
|
+
alert_settings=[],
|
|
700
|
+
start_time=start_timestamp,
|
|
701
|
+
end_time='Incident still active',
|
|
702
|
+
level_settings={"low": 3, "medium": 5, "significant": 15, "critical": 30}
|
|
703
|
+
)
|
|
704
|
+
event['incident_quant'] = intensity_pct
|
|
705
|
+
incidents.append(event)
|
|
706
|
+
self.logger.info(f"[INCIDENT] Generated fallback incident with level={level}, intensity={intensity_pct:.2f}%")
|
|
646
707
|
|
|
647
708
|
else:
|
|
648
709
|
#self._ascending_alert_list.append(level)
|
|
@@ -689,20 +750,39 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
689
750
|
|
|
690
751
|
|
|
691
752
|
# Build total_counts array in expected format
|
|
753
|
+
# ALWAYS populate with all target categories to avoid empty arrays downstream
|
|
692
754
|
total_counts = []
|
|
693
755
|
if total > 0:
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
756
|
+
total_counts.append({
|
|
757
|
+
"category": 'Fire/Smoke',
|
|
758
|
+
"count": 1
|
|
759
|
+
})
|
|
760
|
+
else:
|
|
761
|
+
# When no detections, send count=0 for each category to avoid empty array
|
|
762
|
+
total_counts.append({
|
|
763
|
+
"category": 'Fire',
|
|
764
|
+
"count": 0
|
|
765
|
+
})
|
|
766
|
+
total_counts.append({
|
|
767
|
+
"category": 'Smoke',
|
|
768
|
+
"count": 0
|
|
769
|
+
})
|
|
698
770
|
|
|
699
|
-
# Build current_counts array in expected format
|
|
771
|
+
# Build current_counts array in expected format
|
|
772
|
+
# ALWAYS populate with all target categories to avoid empty arrays downstream
|
|
700
773
|
current_counts = []
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
774
|
+
|
|
775
|
+
# Add Fire entry (count=1 if detected, count=0 if not)
|
|
776
|
+
current_counts.append({
|
|
777
|
+
"category": 'Fire',
|
|
778
|
+
"count": 1 if total_fire > 0 else 0
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
# Add Smoke entry (count=1 if detected, count=0 if not)
|
|
782
|
+
current_counts.append({
|
|
783
|
+
"category": 'Smoke',
|
|
784
|
+
"count": 1 if total_smoke > 0 else 0
|
|
785
|
+
})
|
|
706
786
|
|
|
707
787
|
human_lines = [f"CURRENT FRAME @ {current_timestamp}:"]
|
|
708
788
|
if total_fire > 0:
|
|
@@ -773,8 +853,8 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
773
853
|
detections=detections, human_text=human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
|
|
774
854
|
reset_settings=reset_settings, start_time=high_precision_start_timestamp ,
|
|
775
855
|
reset_time=high_precision_reset_timestamp)
|
|
776
|
-
|
|
777
|
-
|
|
856
|
+
|
|
857
|
+
tracking_stat['target_categories'] = self.target_categories
|
|
778
858
|
tracking_stats.append(tracking_stat)
|
|
779
859
|
|
|
780
860
|
if len(self.id_hit_list)==1:
|