matrice-analytics 0.1.89__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 (24) hide show
  1. matrice_analytics/post_processing/__init__.py +8 -2
  2. matrice_analytics/post_processing/config.py +2 -0
  3. matrice_analytics/post_processing/core/config.py +40 -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 +4 -0
  8. matrice_analytics/post_processing/usecases/__init__.py +4 -1
  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 +1 -0
  16. matrice_analytics/post_processing/usecases/weapon_detection.py +2 -1
  17. matrice_analytics/post_processing/utils/alert_instance_utils.py +94 -26
  18. matrice_analytics/post_processing/utils/business_metrics_manager_utils.py +97 -4
  19. matrice_analytics/post_processing/utils/incident_manager_utils.py +103 -6
  20. {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.96.dist-info}/METADATA +1 -1
  21. {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.96.dist-info}/RECORD +24 -23
  22. {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.96.dist-info}/WHEEL +0 -0
  23. {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.96.dist-info}/licenses/LICENSE.txt +0 -0
  24. {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.96.dist-info}/top_level.txt +0 -0
@@ -84,6 +84,7 @@ from .usecases.field_mapping import FieldMappingConfig, FieldMappingUseCase
84
84
  from .usecases.leaf_disease import LeafDiseaseDetectionConfig, LeafDiseaseDetectionUseCase
85
85
  from .usecases.parking import ParkingConfig
86
86
  from .usecases.abandoned_object_detection import AbandonedObjectConfig
87
+ from .usecases.footfall import FootFallConfig
87
88
 
88
89
 
89
90
  from .usecases.weld_defect_detection import WeldDefectConfig
@@ -128,6 +129,7 @@ from .usecases.pcb_defect_detection import PCBDefectConfig, PCBDefectUseCase
128
129
  from .usecases.underground_pipeline_defect_detection import UndergroundPipelineDefectConfig,UndergroundPipelineDefectUseCase
129
130
  from .usecases.suspicious_activity_detection import SusActivityConfig, SusActivityUseCase
130
131
  from .usecases.natural_disaster import NaturalDisasterConfig, NaturalDisasterUseCase
132
+ from .usecases.footfall import FootFallUseCase
131
133
 
132
134
  #Put all IMAGE based usecases here
133
135
  from .usecases.blood_cancer_detection_img import BloodCancerDetectionConfig, BloodCancerDetectionUseCase
@@ -205,7 +207,7 @@ from .usecases import (
205
207
 
206
208
  SusActivityUseCase,
207
209
  NaturalDisasterUseCase,
208
-
210
+ FootFallUseCase,
209
211
  #Put all IMAGE based usecases here
210
212
  BloodCancerDetectionUseCase,
211
213
  SkinCancerClassificationUseCase,
@@ -286,6 +288,7 @@ _pcb_defect_detection = PCBDefectUseCase()
286
288
  _underground_pipeline_defect = UndergroundPipelineDefectUseCase()
287
289
  _suspicious_activity_detection = SusActivityUseCase()
288
290
  _natural_disaster = NaturalDisasterUseCase()
291
+ _footfall = FootFallUseCase()
289
292
 
290
293
  # Face recognition with embeddings
291
294
  _face_recognition = FaceRecognitionEmbeddingUseCase()
@@ -370,6 +373,7 @@ registry.register_use_case(_pcb_defect_detection.category, _pcb_defect_detection
370
373
  registry.register_use_case(_underground_pipeline_defect.category, _underground_pipeline_defect.name, UndergroundPipelineDefectUseCase)
371
374
  registry.register_use_case(_suspicious_activity_detection.category, _suspicious_activity_detection.name, SusActivityUseCase)
372
375
  registry.register_use_case(_natural_disaster.category, _natural_disaster.name, NaturalDisasterUseCase)
376
+ registry.register_use_case(_footfall.category, _footfall.name, FaceEmotionUseCase)
373
377
 
374
378
  #Put all IMAGE based usecases here
375
379
  registry.register_use_case(_blood_cancer_detection.category, _blood_cancer_detection.name, BloodCancerDetectionUseCase)
@@ -574,7 +578,8 @@ __all__ = [
574
578
  'UndergroundPipelineDefectConfig',
575
579
  'SusActivityConfig',
576
580
  'NaturalDisasterConfig',
577
- 'VehiclePeopleDroneMonitoringConfig'
581
+ 'VehiclePeopleDroneMonitoringConfig',
582
+ 'FootFallConfig',
578
583
  #Put all IMAGE based usecase CONFIGS here
579
584
  'BloodCancerDetectionConfig',
580
585
  'SkinCancerClassificationConfig',
@@ -648,6 +653,7 @@ __all__ = [
648
653
  'UndergroundPipelineDefectUseCase',
649
654
  'SusActivityUseCase',
650
655
  'NaturalDisasterUseCase',
656
+ 'FootFallUseCase',
651
657
 
652
658
  #Put all IMAGE based usecases here
653
659
  'BloodCancerDetectionUseCase',
@@ -65,6 +65,7 @@ APP_NAME_TO_USECASE = {
65
65
  "underground_pipeline_defect" : "underground_pipeline_defect",
66
66
  "suspicious_activity_detection": "suspicious_activity_detection",
67
67
  "natural_disaster_detection": "natural_disaster_detection",
68
+ "Foot Fall": "footfall"
68
69
  }
69
70
 
70
71
  APP_NAME_TO_CATEGORY = {
@@ -135,6 +136,7 @@ APP_NAME_TO_CATEGORY = {
135
136
  "underground_pipeline_defect" : "general",
136
137
  "suspicious_activity_detection": "security",
137
138
  "natural_disaster_detection": "environmental",
139
+ "Foot Fall": "retail"
138
140
  }
139
141
 
140
142
  def get_usecase_from_app_name(app_name: str) -> str:
@@ -352,14 +352,19 @@ class AlertConfig:
352
352
  @dataclass
353
353
  class PeopleCountingConfig(BaseConfig):
354
354
  """Configuration for people counting use case."""
355
-
355
+
356
356
  # Smoothing configuration
357
357
  enable_smoothing: bool = True
358
358
  smoothing_algorithm: str = "observability" # "window" or "observability"
359
359
  smoothing_window_size: int = 20
360
360
  smoothing_cooldown_frames: int = 5
361
361
  smoothing_confidence_range_factor: float = 0.5
362
-
362
+
363
+ # ====== PERFORMANCE: Tracker selection (both disabled by default for max throughput) ======
364
+ enable_advanced_tracker: bool = False # Heavy O(n³) tracker - enable only when tracking quality is critical
365
+ enable_simple_tracker: bool = False # Lightweight O(n) tracker - fast but no cross-frame persistence
366
+ # ====== END PERFORMANCE CONFIG ======
367
+
363
368
  # Zone configuration
364
369
  zone_config: Optional[ZoneConfig] = None
365
370
 
@@ -901,6 +906,7 @@ class ConfigManager:
901
906
  'underground_pipeline_defect' : None,
902
907
  'suspicious_activity_detection': None,
903
908
  'natural_disaster_detection': None,
909
+ 'footfall': None,
904
910
 
905
911
  #Put all image based usecases here::
906
912
  'blood_cancer_detection_img': None,
@@ -1405,7 +1411,15 @@ class ConfigManager:
1405
1411
  return NaturalDisasterConfig
1406
1412
  except ImportError:
1407
1413
  return None
1408
-
1414
+
1415
+ def footfall_detection_config_class(self):
1416
+ """Register a configuration class for a use case."""
1417
+ try:
1418
+ from ..usecases.footfall import FootFallConfig
1419
+ return FootFallConfig
1420
+ except ImportError:
1421
+ return None
1422
+
1409
1423
  #put all image based usecases here::
1410
1424
  def blood_cancer_detection_config_class(self):
1411
1425
  """Register a configuration class for a use case."""
@@ -2684,6 +2698,22 @@ class ConfigManager:
2684
2698
  **kwargs
2685
2699
  )
2686
2700
 
2701
+ elif usecase == "footfall":
2702
+ # Import here to avoid circular import
2703
+ from ..usecases.footfall import FootFallConfig
2704
+
2705
+ # Handle nested configurations
2706
+ alert_config = kwargs.pop("alert_config", None)
2707
+ if alert_config and isinstance(alert_config, dict):
2708
+ alert_config = AlertConfig(**alert_config)
2709
+
2710
+ config = FootFallConfig(
2711
+ category=category or "retail",
2712
+ usecase=usecase,
2713
+ alert_config=alert_config,
2714
+ **kwargs
2715
+ )
2716
+
2687
2717
  #Add IMAGE based usecases here::
2688
2718
  elif usecase == "blood_cancer_detection_img":
2689
2719
  # Import here to avoid circular import
@@ -3234,6 +3264,13 @@ class ConfigManager:
3234
3264
  from ..usecases.natural_disaster import NaturalDisasterConfig
3235
3265
  default_config = NaturalDisasterConfig()
3236
3266
  return default_config.to_dict()
3267
+
3268
+ elif usecase == "footfall":
3269
+ # Import here to avoid circular import
3270
+ from ..usecases.footfall import FootFallConfig
3271
+ default_config = FootFallConfig()
3272
+ return default_config.to_dict()
3273
+
3237
3274
 
3238
3275
  elif usecase == "underground_pipeline_defect":
3239
3276
  # Import here to avoid circular import
@@ -82,6 +82,9 @@ from .face_recognition_client import FacialRecognitionClient
82
82
  from .people_activity_logging import PeopleActivityLogging
83
83
  from .embedding_manager import EmbeddingManager, EmbeddingConfig
84
84
 
85
+ # Cache for location names to avoid repeated API calls
86
+ _location_name_cache: Dict[str, str] = {}
87
+
85
88
 
86
89
  # ---- Lightweight identity tracking and temporal smoothing (adapted from compare_similarity.py) ---- #
87
90
  from collections import deque, defaultdict
@@ -386,8 +389,10 @@ class RedisFaceMatcher:
386
389
  if session is None:
387
390
  return None
388
391
 
389
- response = await asyncio.to_thread(
390
- self._fetch_action_details_sync, session, action_id
392
+ # Use run_in_executor for Python 3.8 compatibility (asyncio.to_thread requires 3.9+)
393
+ loop = asyncio.get_running_loop()
394
+ response = await loop.run_in_executor(
395
+ None, self._fetch_action_details_sync, session, action_id
391
396
  )
392
397
  if not response or not response.get("success", False):
393
398
  self.logger.warning(
@@ -1336,6 +1341,98 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
1336
1341
  self.logger.error(f"Error during use case initialization: {e}", exc_info=True)
1337
1342
  raise RuntimeError(f"Failed to initialize face recognition use case: {e}") from e
1338
1343
 
1344
+ def _extract_camera_info_from_stream(self, stream_info: Optional[Dict[str, Any]]) -> Dict[str, str]:
1345
+ """
1346
+ Extract camera_name, camera_id, and location_id from stream_info.
1347
+
1348
+ Args:
1349
+ stream_info: Stream information dictionary
1350
+
1351
+ Returns:
1352
+ Dict with camera_name, camera_id, location_id
1353
+ """
1354
+ camera_name = ""
1355
+ camera_id = ""
1356
+ location_id = ""
1357
+
1358
+ if not stream_info:
1359
+ return {"camera_name": camera_name, "camera_id": camera_id, "location_id": location_id}
1360
+
1361
+ # Extract camera_name from camera_info
1362
+ camera_info = stream_info.get("camera_info", {})
1363
+ if camera_info:
1364
+ camera_name = camera_info.get("camera_name", "")
1365
+ location_id = camera_info.get("location", "")
1366
+
1367
+ # Extract camera_id from topic (format: {camera_id}_input_topic)
1368
+ topic = stream_info.get("topic", "")
1369
+ if topic and "_input_topic" in topic:
1370
+ camera_id = topic.replace("_input_topic", "")
1371
+
1372
+ self.logger.debug(f"Extracted camera info - camera_name: '{camera_name}', camera_id: '{camera_id}', location_id: '{location_id}'")
1373
+
1374
+ return {"camera_name": camera_name, "camera_id": camera_id, "location_id": location_id}
1375
+
1376
+ async def _fetch_location_name(self, location_id: str) -> str:
1377
+ """
1378
+ Fetch location name from API using location_id.
1379
+
1380
+ Args:
1381
+ location_id: The location ID to look up
1382
+
1383
+ Returns:
1384
+ Location name string, or 'Entry Reception' as default if API fails
1385
+ """
1386
+ global _location_name_cache
1387
+ default_location = "Entry Reception"
1388
+
1389
+ if not location_id:
1390
+ self.logger.debug(f"[LOCATION] No location_id provided, using default: '{default_location}'")
1391
+ return default_location
1392
+
1393
+ # Check cache first
1394
+ if location_id in _location_name_cache:
1395
+ cached_name = _location_name_cache[location_id]
1396
+ self.logger.debug(f"[LOCATION] Using cached location name for '{location_id}': '{cached_name}'")
1397
+ return cached_name
1398
+
1399
+ # Need a session to make API call
1400
+ if not self.face_client or not hasattr(self.face_client, 'session') or not self.face_client.session:
1401
+ self.logger.warning(f"[LOCATION] No session available, using default: '{default_location}'")
1402
+ return default_location
1403
+
1404
+ try:
1405
+ endpoint = f"/v1/inference/get_location/{location_id}"
1406
+ self.logger.info(f"[LOCATION] Fetching location name from API: {endpoint}")
1407
+
1408
+ response = self.face_client.session.rpc.get(endpoint)
1409
+
1410
+ if response and isinstance(response, dict):
1411
+ success = response.get("success", False)
1412
+ if success:
1413
+ data = response.get("data", {})
1414
+ location_name = data.get("locationName", default_location)
1415
+ self.logger.info(f"[LOCATION] ✓ Fetched location name: '{location_name}' for location_id: '{location_id}'")
1416
+
1417
+ # Cache the result
1418
+ _location_name_cache[location_id] = location_name
1419
+ return location_name
1420
+ else:
1421
+ self.logger.warning(
1422
+ f"[LOCATION] API returned success=false for location_id '{location_id}': "
1423
+ f"{response.get('message', 'Unknown error')}"
1424
+ )
1425
+ else:
1426
+ self.logger.warning(f"[LOCATION] Invalid response format from API: {response}")
1427
+
1428
+ except Exception as e:
1429
+ self.logger.error(f"[LOCATION] Error fetching location name for '{location_id}': {e}", exc_info=True)
1430
+
1431
+ # Use default on any failure
1432
+ self.logger.info(f"[LOCATION] Using default location name: '{default_location}'")
1433
+ _location_name_cache[location_id] = default_location
1434
+ return default_location
1435
+
1339
1436
  async def _get_facial_recognition_client(
1340
1437
  self, config: FaceRecognitionEmbeddingConfig
1341
1438
  ) -> FacialRecognitionClient:
@@ -1352,6 +1449,8 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
1352
1449
  secret_key = os.getenv("MATRICE_SECRET_ACCESS_KEY", "")
1353
1450
  project_id = os.getenv("MATRICE_PROJECT_ID", "")
1354
1451
 
1452
+ self.logger.info(f"[PROJECT_ID] Initial project_id from env: '{project_id}'")
1453
+
1355
1454
  self.session1 = Session(
1356
1455
  account_number=account_number,
1357
1456
  access_key=access_key_id,
@@ -1362,6 +1461,19 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
1362
1461
  server_id=config.facial_recognition_server_id, session=self.session1
1363
1462
  )
1364
1463
  self.logger.info("Face recognition client initialized")
1464
+
1465
+ # After FacialRecognitionClient initialization, it may have fetched project_id from action details
1466
+ # and updated MATRICE_PROJECT_ID env var. Update session1 with the correct project_id.
1467
+ updated_project_id = self.face_client.project_id or os.getenv("MATRICE_PROJECT_ID", "")
1468
+ if updated_project_id and updated_project_id != project_id:
1469
+ self.logger.info(f"[PROJECT_ID] Project ID updated by FacialRecognitionClient: '{updated_project_id}'")
1470
+ try:
1471
+ self.session1.update(updated_project_id)
1472
+ self.logger.info(f"[PROJECT_ID] Updated session1 with project_id: '{updated_project_id}'")
1473
+ except Exception as e:
1474
+ self.logger.warning(f"[PROJECT_ID] Failed to update session1 with project_id: {e}")
1475
+ elif updated_project_id:
1476
+ self.logger.info(f"[PROJECT_ID] Using project_id: '{updated_project_id}'")
1365
1477
 
1366
1478
  # Call update_deployment if deployment_id is provided
1367
1479
  if config.deployment_id:
@@ -1415,6 +1527,9 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
1415
1527
  """
1416
1528
  processing_start = time.time()
1417
1529
  # Ensure config is correct type
1530
+ self.logger.info(f"[CONFIG-PRINT]-------------------------- {config} --------------------------")
1531
+ self.logger.info(f"[STREAM-PRINT]-------------------------- {stream_info} --------------------------")
1532
+
1418
1533
  if not isinstance(config, FaceRecognitionEmbeddingConfig):
1419
1534
  return self.create_error_result(
1420
1535
  "Invalid config type",
@@ -1546,6 +1661,16 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
1546
1661
  current_frame_staff_details = {}
1547
1662
 
1548
1663
 
1664
+ # Extract camera info and fetch location name
1665
+ camera_info_extracted = self._extract_camera_info_from_stream(stream_info)
1666
+ camera_name = camera_info_extracted.get("camera_name", "")
1667
+ camera_id = camera_info_extracted.get("camera_id", "")
1668
+ location_id = camera_info_extracted.get("location_id", "")
1669
+
1670
+ # Fetch actual location name from API
1671
+ location_name = await self._fetch_location_name(location_id)
1672
+ self.logger.debug(f"Using location_name: '{location_name}', camera_name: '{camera_name}', camera_id: '{camera_id}'")
1673
+
1549
1674
  # Process face recognition for each detection (if enabled)
1550
1675
  if config.enable_face_recognition:
1551
1676
  # Additional safety check: verify embeddings are still loaded and ready
@@ -1562,7 +1687,8 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
1562
1687
  # )
1563
1688
 
1564
1689
  face_recognition_result = await self._process_face_recognition(
1565
- processed_data, config, stream_info, input_bytes
1690
+ processed_data, config, stream_info, input_bytes,
1691
+ camera_name=camera_name, camera_id=camera_id, location_name=location_name
1566
1692
  )
1567
1693
  processed_data, current_recognized_count, current_unknown_count, recognized_persons, current_frame_staff_details = face_recognition_result
1568
1694
  else:
@@ -1753,6 +1879,9 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
1753
1879
  config: FaceRecognitionEmbeddingConfig,
1754
1880
  stream_info: Optional[Dict[str, Any]] = None,
1755
1881
  input_bytes: Optional[bytes] = None,
1882
+ camera_name: str = "",
1883
+ camera_id: str = "",
1884
+ location_name: str = "",
1756
1885
  ) -> List[Dict]:
1757
1886
  """Process face recognition for each detection with embeddings"""
1758
1887
 
@@ -1789,10 +1918,8 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
1789
1918
  )
1790
1919
  self._frame_warning_logged = True
1791
1920
 
1792
- # Get location from stream_info
1793
- location = (
1794
- stream_info.get("camera_location", "unknown") if stream_info else "unknown"
1795
- )
1921
+ # Use the location_name passed from process() (fetched from API)
1922
+ location = location_name if location_name else "Entry Reception"
1796
1923
 
1797
1924
  # Generate current timestamp
1798
1925
  current_timestamp = datetime.now(timezone.utc).isoformat()
@@ -1806,7 +1933,8 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
1806
1933
  processed_detection = await self._process_face(
1807
1934
  detection, current_frame, location, current_timestamp, config,
1808
1935
  current_recognized_count, current_unknown_count,
1809
- recognized_persons, current_frame_staff_details
1936
+ recognized_persons, current_frame_staff_details,
1937
+ camera_name=camera_name, camera_id=camera_id
1810
1938
  )
1811
1939
  # print("------------------WHOLE FACE RECOG PROCESSING DETECTION----------------------------")
1812
1940
  # print("LATENCY:",(time.time() - st1)*1000,"| Throughput fps:",(1.0 / (time.time() - st1)) if (time.time() - st1) > 0 else None)
@@ -1838,6 +1966,8 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
1838
1966
  current_unknown_count: int = 0,
1839
1967
  recognized_persons: Dict = None,
1840
1968
  current_frame_staff_details: Dict = None,
1969
+ camera_name: str = "",
1970
+ camera_id: str = "",
1841
1971
  ) -> Dict:
1842
1972
 
1843
1973
  # Extract and validate embedding using EmbeddingManager
@@ -2011,6 +2141,8 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
2011
2141
  detection=detection,
2012
2142
  current_frame=current_frame,
2013
2143
  location=location,
2144
+ camera_name=camera_name,
2145
+ camera_id=camera_id,
2014
2146
  )
2015
2147
  # print("------------------FACE RECOG ENQUEUEING DETECTION FOR ACTIVITY LOGGING----------------------------")
2016
2148
  # print("LATENCY:",(time.time() - st4)*1000,"| Throughput fps:",(1.0 / (time.time() - st4)) if (time.time() - st4) > 0 else None)
@@ -2273,19 +2405,19 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
2273
2405
 
2274
2406
  # Build current_counts array in expected format
2275
2407
  current_counts = []
2276
- for cat, count in per_category_count.items():
2277
- if count > 0 or total_detections > 0:
2278
- current_counts.append({"category": cat, "count": count})
2408
+ # for cat, count in per_category_count.items():
2409
+ # if count > 0 or total_detections > 0:
2410
+ # current_counts.append({"category": cat, "count": count})
2279
2411
 
2280
2412
  # Add face recognition specific current counts
2281
2413
  current_frame = face_summary.get("current_frame", {})
2282
2414
  current_counts.extend(
2283
2415
  [
2284
2416
  {
2285
- "category": "recognized_faces",
2417
+ "category": "Recognized Faces",
2286
2418
  "count": current_frame.get("recognized", 0),
2287
2419
  },
2288
- {"category": "unknown_faces", "count": current_frame.get("unknown", 0)},
2420
+ {"category": "Unknown Faces", "count": current_frame.get("unknown", 0)},
2289
2421
  ]
2290
2422
  )
2291
2423
 
@@ -2408,7 +2540,7 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
2408
2540
  start_time=high_precision_start_timestamp,
2409
2541
  reset_time=high_precision_reset_timestamp,
2410
2542
  )
2411
-
2543
+ tracking_stat['target_categories'] = ['Recognized Faces', 'Unknown Faces']
2412
2544
  tracking_stats.append(tracking_stat)
2413
2545
  return tracking_stats
2414
2546
 
@@ -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):