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.
Files changed (26) hide show
  1. matrice_analytics/post_processing/__init__.py +21 -2
  2. matrice_analytics/post_processing/config.py +6 -0
  3. matrice_analytics/post_processing/core/config.py +102 -3
  4. matrice_analytics/post_processing/face_reg/face_recognition.py +146 -14
  5. matrice_analytics/post_processing/face_reg/face_recognition_client.py +116 -4
  6. matrice_analytics/post_processing/face_reg/people_activity_logging.py +19 -0
  7. matrice_analytics/post_processing/post_processor.py +12 -0
  8. matrice_analytics/post_processing/usecases/__init__.py +9 -0
  9. matrice_analytics/post_processing/usecases/advanced_customer_service.py +5 -2
  10. matrice_analytics/post_processing/usecases/color_detection.py +1 -0
  11. matrice_analytics/post_processing/usecases/fire_detection.py +94 -14
  12. matrice_analytics/post_processing/usecases/footfall.py +750 -0
  13. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +91 -1
  14. matrice_analytics/post_processing/usecases/people_counting.py +55 -22
  15. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +15 -32
  16. matrice_analytics/post_processing/usecases/vehicle_monitoring_drone_view.py +1007 -0
  17. matrice_analytics/post_processing/usecases/vehicle_monitoring_parking_lot.py +1011 -0
  18. matrice_analytics/post_processing/usecases/weapon_detection.py +2 -1
  19. matrice_analytics/post_processing/utils/alert_instance_utils.py +94 -26
  20. matrice_analytics/post_processing/utils/business_metrics_manager_utils.py +97 -4
  21. matrice_analytics/post_processing/utils/incident_manager_utils.py +103 -6
  22. {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.97.dist-info}/METADATA +1 -1
  23. {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.97.dist-info}/RECORD +26 -23
  24. {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.97.dist-info}/WHEEL +0 -0
  25. {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.97.dist-info}/licenses/LICENSE.txt +0 -0
  26. {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
- self.session.update(self.server_info.get('projectID', ''))
97
- self.logger.info(f"Updated Matrice session with project ID: {self.server_info.get('projectID', '')}")
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": "customer", "count": queue_analytics.get("active_customers", 0)}
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 summary.get("per_category_count", {}):
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
- total_counts.append({
695
- "category": 'Fire/Smoke', #TODO: Discuss and fix what to do with this
696
- "count": 1
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
- if total > 0: # Include even if 0 when there are detections
702
- current_counts.append({
703
- "category": 'Fire/Smoke', #TODO: Discuss and fix what to do with this
704
- "count": 1
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: