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.
- matrice_analytics/post_processing/__init__.py +8 -2
- matrice_analytics/post_processing/config.py +2 -0
- matrice_analytics/post_processing/core/config.py +40 -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 +4 -0
- matrice_analytics/post_processing/usecases/__init__.py +4 -1
- 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 +1 -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.96.dist-info}/METADATA +1 -1
- {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.96.dist-info}/RECORD +24 -23
- {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.96.dist-info}/WHEEL +0 -0
- {matrice_analytics-0.1.89.dist-info → matrice_analytics-0.1.96.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|
-
|
|
390
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
2278
|
-
|
|
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": "
|
|
2417
|
+
"category": "Recognized Faces",
|
|
2286
2418
|
"count": current_frame.get("recognized", 0),
|
|
2287
2419
|
},
|
|
2288
|
-
{"category": "
|
|
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
|
-
|
|
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):
|