matrice-analytics 0.1.31__py3-none-any.whl → 0.1.32__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.
Potentially problematic release.
This version of matrice-analytics might be problematic. Click here for more details.
- matrice_analytics/post_processing/config.py +4 -0
- matrice_analytics/post_processing/core/config.py +115 -12
- matrice_analytics/post_processing/face_reg/embedding_manager.py +95 -1
- matrice_analytics/post_processing/face_reg/face_recognition.py +52 -45
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +13 -12
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +40 -9
- matrice_analytics/post_processing/post_processor.py +14 -7
- matrice_analytics/post_processing/usecases/color_detection.py +38 -40
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +128 -44
- {matrice_analytics-0.1.31.dist-info → matrice_analytics-0.1.32.dist-info}/METADATA +1 -1
- {matrice_analytics-0.1.31.dist-info → matrice_analytics-0.1.32.dist-info}/RECORD +14 -14
- {matrice_analytics-0.1.31.dist-info → matrice_analytics-0.1.32.dist-info}/WHEEL +0 -0
- {matrice_analytics-0.1.31.dist-info → matrice_analytics-0.1.32.dist-info}/licenses/LICENSE.txt +0 -0
- {matrice_analytics-0.1.31.dist-info → matrice_analytics-0.1.32.dist-info}/top_level.txt +0 -0
|
@@ -53,7 +53,9 @@ APP_NAME_TO_USECASE = {
|
|
|
53
53
|
"abandoned_object_detection" : "abandoned_object_detection",
|
|
54
54
|
"gas_leak_detection": "gas_leak_detection",
|
|
55
55
|
"color_detection": "color_detection",
|
|
56
|
+
"Color Detection": "color_detection",
|
|
56
57
|
"License Plate Recognition" : "license_plate_monitor",
|
|
58
|
+
"License Plate Monitoring" : "license_plate_monitor",
|
|
57
59
|
"cell_microscopy_segmentation": "cell_microscopy_segmentation",
|
|
58
60
|
"Dwell Detection": "dwell",
|
|
59
61
|
"age_gender_detection": "age_gender_detection",
|
|
@@ -120,7 +122,9 @@ APP_NAME_TO_CATEGORY = {
|
|
|
120
122
|
"abandoned_object_detection" : "security",
|
|
121
123
|
"gas_leak_detection": "oil_gas",
|
|
122
124
|
"color_detection": "visual_appearance",
|
|
125
|
+
"Color Detection": "visual_appearance",
|
|
123
126
|
"License Plate Recognition" : "license_plate_monitor",
|
|
127
|
+
"License Plate Monitoring" : "license_plate_monitor",
|
|
124
128
|
"cell_microscopy_segmentation" : "healthcare",
|
|
125
129
|
"Dwell Detection": "general",
|
|
126
130
|
"age_gender_detection": "age_gender_detection",
|
|
@@ -10,10 +10,13 @@ from typing import Any, Dict, List, Optional, Union, get_type_hints
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
import json
|
|
12
12
|
import yaml
|
|
13
|
+
import logging
|
|
13
14
|
from abc import ABC, abstractmethod
|
|
14
15
|
|
|
15
16
|
from .base import ConfigProtocol
|
|
16
17
|
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
class ConfigValidationError(Exception):
|
|
19
22
|
"""Raised when configuration validation fails."""
|
|
@@ -786,6 +789,43 @@ class PeopleTrackingConfig:
|
|
|
786
789
|
return errors
|
|
787
790
|
|
|
788
791
|
|
|
792
|
+
def filter_config_kwargs(config_class: type, kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
793
|
+
"""
|
|
794
|
+
Filter kwargs to only include parameters that are valid for the config class.
|
|
795
|
+
|
|
796
|
+
Args:
|
|
797
|
+
config_class: The config class to create
|
|
798
|
+
kwargs: Dictionary of parameters to filter
|
|
799
|
+
|
|
800
|
+
Returns:
|
|
801
|
+
Dict[str, Any]: Filtered kwargs containing only valid parameters
|
|
802
|
+
"""
|
|
803
|
+
if not hasattr(config_class, '__dataclass_fields__'):
|
|
804
|
+
# Not a dataclass, return kwargs as-is
|
|
805
|
+
return kwargs
|
|
806
|
+
|
|
807
|
+
# Get valid field names from the dataclass
|
|
808
|
+
valid_fields = set(config_class.__dataclass_fields__.keys())
|
|
809
|
+
|
|
810
|
+
# Filter kwargs to only include valid fields
|
|
811
|
+
filtered_kwargs = {}
|
|
812
|
+
ignored_params = []
|
|
813
|
+
|
|
814
|
+
for key, value in kwargs.items():
|
|
815
|
+
if key in valid_fields:
|
|
816
|
+
filtered_kwargs[key] = value
|
|
817
|
+
else:
|
|
818
|
+
ignored_params.append(key)
|
|
819
|
+
|
|
820
|
+
# Log ignored parameters for debugging
|
|
821
|
+
if ignored_params:
|
|
822
|
+
logger.debug(
|
|
823
|
+
f"Ignoring non-config parameters for {config_class.__name__}: {ignored_params}"
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
return filtered_kwargs
|
|
827
|
+
|
|
828
|
+
|
|
789
829
|
class ConfigManager:
|
|
790
830
|
"""Centralized configuration management for post-processing operations."""
|
|
791
831
|
|
|
@@ -1423,6 +1463,19 @@ class ConfigManager:
|
|
|
1423
1463
|
except ImportError:
|
|
1424
1464
|
return None
|
|
1425
1465
|
|
|
1466
|
+
def _filter_kwargs_for_config(self, config_class: type, kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
1467
|
+
"""
|
|
1468
|
+
Filter kwargs to only include valid parameters for the config class.
|
|
1469
|
+
|
|
1470
|
+
Args:
|
|
1471
|
+
config_class: The config class
|
|
1472
|
+
kwargs: Dictionary of parameters
|
|
1473
|
+
|
|
1474
|
+
Returns:
|
|
1475
|
+
Filtered kwargs
|
|
1476
|
+
"""
|
|
1477
|
+
return filter_config_kwargs(config_class, kwargs)
|
|
1478
|
+
|
|
1426
1479
|
def create_config(self, usecase: str, category: Optional[str] = None, **kwargs) -> BaseConfig:
|
|
1427
1480
|
"""
|
|
1428
1481
|
Create configuration for a specific use case.
|
|
@@ -1438,6 +1491,17 @@ class ConfigManager:
|
|
|
1438
1491
|
Raises:
|
|
1439
1492
|
ConfigValidationError: If configuration is invalid
|
|
1440
1493
|
"""
|
|
1494
|
+
# Filter out common non-config parameters that should never be passed to configs
|
|
1495
|
+
common_non_config_params = [
|
|
1496
|
+
'deployment_id', 'stream_key', 'stream_id', 'camera_id', 'server_id',
|
|
1497
|
+
'inference_id', 'timestamp', 'frame_id', 'frame_number', 'request_id',
|
|
1498
|
+
'user_id', 'tenant_id', 'organization_id', 'app_name', 'app_id'
|
|
1499
|
+
]
|
|
1500
|
+
for param in common_non_config_params:
|
|
1501
|
+
if param in kwargs:
|
|
1502
|
+
logger.debug(f"Removing non-config parameter '{param}' from config creation")
|
|
1503
|
+
kwargs.pop(param, None)
|
|
1504
|
+
|
|
1441
1505
|
if usecase == "people_counting":
|
|
1442
1506
|
# Handle nested configurations
|
|
1443
1507
|
zone_config = kwargs.pop("zone_config", None)
|
|
@@ -1448,12 +1512,15 @@ class ConfigManager:
|
|
|
1448
1512
|
if alert_config and isinstance(alert_config, dict):
|
|
1449
1513
|
alert_config = AlertConfig(**alert_config)
|
|
1450
1514
|
|
|
1515
|
+
# Filter kwargs to only include valid parameters
|
|
1516
|
+
filtered_kwargs = self._filter_kwargs_for_config(PeopleCountingConfig, kwargs)
|
|
1517
|
+
|
|
1451
1518
|
config = PeopleCountingConfig(
|
|
1452
1519
|
category=category or "general",
|
|
1453
1520
|
usecase=usecase,
|
|
1454
1521
|
zone_config=zone_config,
|
|
1455
1522
|
alert_config=alert_config,
|
|
1456
|
-
**
|
|
1523
|
+
**filtered_kwargs
|
|
1457
1524
|
)
|
|
1458
1525
|
|
|
1459
1526
|
|
|
@@ -1467,12 +1534,15 @@ class ConfigManager:
|
|
|
1467
1534
|
if alert_config and isinstance(alert_config, dict):
|
|
1468
1535
|
alert_config = AlertConfig(**alert_config)
|
|
1469
1536
|
|
|
1537
|
+
# Filter kwargs to only include valid parameters
|
|
1538
|
+
filtered_kwargs = self._filter_kwargs_for_config(PeopleTrackingConfig, kwargs)
|
|
1539
|
+
|
|
1470
1540
|
config = PeopleTrackingConfig(
|
|
1471
1541
|
category=category or "general",
|
|
1472
1542
|
usecase=usecase,
|
|
1473
1543
|
zone_config=zone_config,
|
|
1474
1544
|
alert_config=alert_config,
|
|
1475
|
-
**
|
|
1545
|
+
**filtered_kwargs
|
|
1476
1546
|
)
|
|
1477
1547
|
|
|
1478
1548
|
elif usecase == "intrusion_detection":
|
|
@@ -1485,12 +1555,15 @@ class ConfigManager:
|
|
|
1485
1555
|
if alert_config and isinstance(alert_config, dict):
|
|
1486
1556
|
alert_config = AlertConfig(**alert_config)
|
|
1487
1557
|
|
|
1558
|
+
# Filter kwargs to only include valid parameters
|
|
1559
|
+
filtered_kwargs = self._filter_kwargs_for_config(IntrusionConfig, kwargs)
|
|
1560
|
+
|
|
1488
1561
|
config = IntrusionConfig(
|
|
1489
1562
|
category=category or "security",
|
|
1490
1563
|
usecase=usecase,
|
|
1491
1564
|
zone_config=zone_config,
|
|
1492
1565
|
alert_config=alert_config,
|
|
1493
|
-
**
|
|
1566
|
+
**filtered_kwargs
|
|
1494
1567
|
)
|
|
1495
1568
|
|
|
1496
1569
|
elif usecase == "proximity_detection":
|
|
@@ -1503,12 +1576,15 @@ class ConfigManager:
|
|
|
1503
1576
|
if alert_config and isinstance(alert_config, dict):
|
|
1504
1577
|
alert_config = AlertConfig(**alert_config)
|
|
1505
1578
|
|
|
1579
|
+
# Filter kwargs to only include valid parameters
|
|
1580
|
+
filtered_kwargs = self._filter_kwargs_for_config(ProximityConfig, kwargs)
|
|
1581
|
+
|
|
1506
1582
|
config = ProximityConfig(
|
|
1507
1583
|
category=category or "security",
|
|
1508
1584
|
usecase=usecase,
|
|
1509
1585
|
zone_config=zone_config,
|
|
1510
1586
|
alert_config=alert_config,
|
|
1511
|
-
**
|
|
1587
|
+
**filtered_kwargs
|
|
1512
1588
|
)
|
|
1513
1589
|
|
|
1514
1590
|
elif usecase in ["customer_service", "advanced_customer_service"]:
|
|
@@ -1521,12 +1597,15 @@ class ConfigManager:
|
|
|
1521
1597
|
if alert_config and isinstance(alert_config, dict):
|
|
1522
1598
|
alert_config = AlertConfig(**alert_config)
|
|
1523
1599
|
|
|
1600
|
+
# Filter kwargs to only include valid parameters
|
|
1601
|
+
filtered_kwargs = self._filter_kwargs_for_config(CustomerServiceConfig, kwargs)
|
|
1602
|
+
|
|
1524
1603
|
config = CustomerServiceConfig(
|
|
1525
1604
|
category=category or "sales",
|
|
1526
1605
|
usecase=usecase,
|
|
1527
1606
|
tracking_config=tracking_config,
|
|
1528
1607
|
alert_config=alert_config,
|
|
1529
|
-
**
|
|
1608
|
+
**filtered_kwargs
|
|
1530
1609
|
)
|
|
1531
1610
|
elif usecase == "basic_counting_tracking":
|
|
1532
1611
|
# Import here to avoid circular import
|
|
@@ -1552,6 +1631,9 @@ class ConfigManager:
|
|
|
1552
1631
|
alert_cooldown = kwargs.pop("alert_cooldown", 60.0)
|
|
1553
1632
|
enable_unique_counting = kwargs.pop("enable_unique_counting", True)
|
|
1554
1633
|
|
|
1634
|
+
# Filter kwargs to only include valid parameters
|
|
1635
|
+
filtered_kwargs = self._filter_kwargs_for_config(BasicCountingTrackingConfig, kwargs)
|
|
1636
|
+
|
|
1555
1637
|
config = BasicCountingTrackingConfig(
|
|
1556
1638
|
category=category or "general",
|
|
1557
1639
|
usecase=usecase,
|
|
@@ -1564,7 +1646,7 @@ class ConfigManager:
|
|
|
1564
1646
|
zone_thresholds=zone_thresholds,
|
|
1565
1647
|
alert_cooldown=alert_cooldown,
|
|
1566
1648
|
enable_unique_counting=enable_unique_counting,
|
|
1567
|
-
**
|
|
1649
|
+
**filtered_kwargs
|
|
1568
1650
|
)
|
|
1569
1651
|
elif usecase == "license_plate_detection":
|
|
1570
1652
|
# Import here to avoid circular import
|
|
@@ -1575,11 +1657,14 @@ class ConfigManager:
|
|
|
1575
1657
|
if alert_config and isinstance(alert_config, dict):
|
|
1576
1658
|
alert_config = AlertConfig(**alert_config)
|
|
1577
1659
|
|
|
1660
|
+
# Filter kwargs to only include valid parameters
|
|
1661
|
+
filtered_kwargs = self._filter_kwargs_for_config(LicensePlateConfig, kwargs)
|
|
1662
|
+
|
|
1578
1663
|
config = LicensePlateConfig(
|
|
1579
1664
|
category=category or "vehicle",
|
|
1580
1665
|
usecase=usecase,
|
|
1581
1666
|
alert_config=alert_config,
|
|
1582
|
-
**
|
|
1667
|
+
**filtered_kwargs
|
|
1583
1668
|
)
|
|
1584
1669
|
elif usecase == "parking_space_detection":
|
|
1585
1670
|
# Import here to avoid circular import
|
|
@@ -1590,11 +1675,14 @@ class ConfigManager:
|
|
|
1590
1675
|
if alert_config and isinstance(alert_config, dict):
|
|
1591
1676
|
alert_config = AlertConfig(**alert_config)
|
|
1592
1677
|
|
|
1678
|
+
# Filter kwargs to only include valid parameters
|
|
1679
|
+
filtered_kwargs = self._filter_kwargs_for_config(ParkingSpaceConfig, kwargs)
|
|
1680
|
+
|
|
1593
1681
|
config = ParkingSpaceConfig(
|
|
1594
1682
|
category=category or "parking_space",
|
|
1595
1683
|
usecase=usecase,
|
|
1596
1684
|
alert_config=alert_config,
|
|
1597
|
-
**
|
|
1685
|
+
**filtered_kwargs
|
|
1598
1686
|
)
|
|
1599
1687
|
elif usecase == "field_mapping":
|
|
1600
1688
|
# Import here to avoid circular import
|
|
@@ -1605,11 +1693,14 @@ class ConfigManager:
|
|
|
1605
1693
|
if alert_config and isinstance(alert_config, dict):
|
|
1606
1694
|
alert_config = AlertConfig(**alert_config)
|
|
1607
1695
|
|
|
1696
|
+
# Filter kwargs to only include valid parameters
|
|
1697
|
+
filtered_kwargs = self._filter_kwargs_for_config(FieldMappingConfig, kwargs)
|
|
1698
|
+
|
|
1608
1699
|
config = FieldMappingConfig(
|
|
1609
1700
|
category=category or "infrastructure",
|
|
1610
1701
|
usecase=usecase,
|
|
1611
1702
|
alert_config=alert_config,
|
|
1612
|
-
**
|
|
1703
|
+
**filtered_kwargs
|
|
1613
1704
|
)
|
|
1614
1705
|
|
|
1615
1706
|
elif usecase == "leaf_disease_detection":
|
|
@@ -1621,6 +1712,9 @@ class ConfigManager:
|
|
|
1621
1712
|
if alert_config and isinstance(alert_config, dict):
|
|
1622
1713
|
alert_config = AlertConfig(**alert_config)
|
|
1623
1714
|
|
|
1715
|
+
# Filter kwargs to only include valid parameters
|
|
1716
|
+
filtered_kwargs = self._filter_kwargs_for_config(LeafDiseaseDetectionConfig, kwargs)
|
|
1717
|
+
|
|
1624
1718
|
config = LeafDiseaseDetectionConfig(
|
|
1625
1719
|
category=category or "agriculture",
|
|
1626
1720
|
usecase=usecase,
|
|
@@ -1964,11 +2058,14 @@ class ConfigManager:
|
|
|
1964
2058
|
if alert_config and isinstance(alert_config, dict):
|
|
1965
2059
|
alert_config = AlertConfig(**alert_config)
|
|
1966
2060
|
|
|
2061
|
+
# Filter kwargs to only include valid parameters
|
|
2062
|
+
filtered_kwargs = self._filter_kwargs_for_config(ColorDetectionConfig, kwargs)
|
|
2063
|
+
|
|
1967
2064
|
config = ColorDetectionConfig(
|
|
1968
2065
|
category=category or "visual_appearance",
|
|
1969
2066
|
usecase=usecase,
|
|
1970
2067
|
alert_config=alert_config,
|
|
1971
|
-
**
|
|
2068
|
+
**filtered_kwargs
|
|
1972
2069
|
)
|
|
1973
2070
|
elif usecase == "video_color_classification":
|
|
1974
2071
|
# Alias for color_detection - Import here to avoid circular import
|
|
@@ -1979,11 +2076,14 @@ class ConfigManager:
|
|
|
1979
2076
|
if alert_config and isinstance(alert_config, dict):
|
|
1980
2077
|
alert_config = AlertConfig(**alert_config)
|
|
1981
2078
|
|
|
2079
|
+
# Filter kwargs to only include valid parameters
|
|
2080
|
+
filtered_kwargs = self._filter_kwargs_for_config(ColorDetectionConfig, kwargs)
|
|
2081
|
+
|
|
1982
2082
|
config = ColorDetectionConfig(
|
|
1983
2083
|
category=category or "visual_appearance",
|
|
1984
2084
|
usecase="color_detection", # Use canonical name internally
|
|
1985
2085
|
alert_config=alert_config,
|
|
1986
|
-
**
|
|
2086
|
+
**filtered_kwargs
|
|
1987
2087
|
)
|
|
1988
2088
|
elif usecase == "ppe_compliance_detection":
|
|
1989
2089
|
# Import here to avoid circular import
|
|
@@ -2478,11 +2578,14 @@ class ConfigManager:
|
|
|
2478
2578
|
if alert_config and isinstance(alert_config, dict):
|
|
2479
2579
|
alert_config = AlertConfig(**alert_config)
|
|
2480
2580
|
|
|
2581
|
+
# Filter kwargs to only include valid parameters
|
|
2582
|
+
filtered_kwargs = self._filter_kwargs_for_config(LicensePlateMonitorConfig, kwargs)
|
|
2583
|
+
|
|
2481
2584
|
config = LicensePlateMonitorConfig(
|
|
2482
2585
|
category=category or "license_plate_monitor",
|
|
2483
2586
|
usecase=usecase,
|
|
2484
2587
|
alert_config=alert_config,
|
|
2485
|
-
**
|
|
2588
|
+
**filtered_kwargs
|
|
2486
2589
|
)
|
|
2487
2590
|
|
|
2488
2591
|
elif usecase == "dwell":
|
|
@@ -46,6 +46,10 @@ class EmbeddingConfig:
|
|
|
46
46
|
# Search settings
|
|
47
47
|
search_limit: int = 5
|
|
48
48
|
search_collection: str = "staff_enrollment"
|
|
49
|
+
|
|
50
|
+
# Background embedding refresh settings
|
|
51
|
+
enable_background_refresh: bool = True
|
|
52
|
+
background_refresh_interval: int = 600 # Refresh embeddings every 10 minutes (600 seconds)
|
|
49
53
|
|
|
50
54
|
|
|
51
55
|
class EmbeddingManager:
|
|
@@ -76,10 +80,93 @@ class EmbeddingManager:
|
|
|
76
80
|
self._cache_lock = threading.Lock()
|
|
77
81
|
self._embeddings_lock = threading.Lock()
|
|
78
82
|
|
|
83
|
+
# Background refresh thread
|
|
84
|
+
self._refresh_thread = None
|
|
85
|
+
self._is_running = False
|
|
86
|
+
self._stop_event = threading.Event()
|
|
87
|
+
|
|
88
|
+
# Start background refresh if enabled
|
|
89
|
+
if self.config.enable_background_refresh and self.face_client:
|
|
90
|
+
self.start_background_refresh()
|
|
91
|
+
self.logger.info(f"Background embedding refresh enabled - interval: {self.config.background_refresh_interval}s")
|
|
92
|
+
|
|
79
93
|
def set_face_client(self, face_client: FacialRecognitionClient):
|
|
80
94
|
"""Set the face recognition client."""
|
|
81
95
|
self.face_client = face_client
|
|
82
96
|
|
|
97
|
+
# Start background refresh if it wasn't started yet
|
|
98
|
+
if self.config.enable_background_refresh and not self._is_running:
|
|
99
|
+
self.start_background_refresh()
|
|
100
|
+
self.logger.info("Background embedding refresh started after setting face client")
|
|
101
|
+
|
|
102
|
+
def start_background_refresh(self):
|
|
103
|
+
"""Start the background embedding refresh thread"""
|
|
104
|
+
if not self._is_running and self.face_client:
|
|
105
|
+
self._is_running = True
|
|
106
|
+
self._stop_event.clear()
|
|
107
|
+
self._refresh_thread = threading.Thread(
|
|
108
|
+
target=self._run_refresh_loop, daemon=True, name="EmbeddingRefreshThread"
|
|
109
|
+
)
|
|
110
|
+
self._refresh_thread.start()
|
|
111
|
+
self.logger.info("Started background embedding refresh thread")
|
|
112
|
+
|
|
113
|
+
def stop_background_refresh(self):
|
|
114
|
+
"""Stop the background embedding refresh thread"""
|
|
115
|
+
if self._is_running:
|
|
116
|
+
self.logger.info("Stopping background embedding refresh thread...")
|
|
117
|
+
self._is_running = False
|
|
118
|
+
self._stop_event.set()
|
|
119
|
+
if self._refresh_thread:
|
|
120
|
+
self._refresh_thread.join(timeout=10.0)
|
|
121
|
+
self.logger.info("Background embedding refresh thread stopped")
|
|
122
|
+
|
|
123
|
+
def _run_refresh_loop(self):
|
|
124
|
+
"""Run the embedding refresh loop in background thread"""
|
|
125
|
+
import asyncio
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
# Create new event loop for this thread
|
|
129
|
+
loop = asyncio.new_event_loop()
|
|
130
|
+
asyncio.set_event_loop(loop)
|
|
131
|
+
|
|
132
|
+
# Run initial load
|
|
133
|
+
self.logger.info("Loading initial staff embeddings in background thread...")
|
|
134
|
+
loop.run_until_complete(self._load_staff_embeddings())
|
|
135
|
+
|
|
136
|
+
# Periodic refresh loop
|
|
137
|
+
while self._is_running and not self._stop_event.is_set():
|
|
138
|
+
try:
|
|
139
|
+
# Wait for refresh interval with ability to stop
|
|
140
|
+
if self._stop_event.wait(timeout=self.config.background_refresh_interval):
|
|
141
|
+
# Stop event was set
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
if not self._is_running:
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
# Refresh embeddings
|
|
148
|
+
self.logger.info("Refreshing staff embeddings from server...")
|
|
149
|
+
success = loop.run_until_complete(self._load_staff_embeddings())
|
|
150
|
+
|
|
151
|
+
if success:
|
|
152
|
+
self.logger.info("Successfully refreshed staff embeddings in background")
|
|
153
|
+
else:
|
|
154
|
+
self.logger.warning("Failed to refresh staff embeddings in background")
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
self.logger.error(f"Error in background embedding refresh loop: {e}", exc_info=True)
|
|
158
|
+
# Continue loop even on error
|
|
159
|
+
time.sleep(60) # Wait 1 minute before retry on error
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
self.logger.error(f"Fatal error in background refresh thread: {e}", exc_info=True)
|
|
163
|
+
finally:
|
|
164
|
+
try:
|
|
165
|
+
loop.close()
|
|
166
|
+
except:
|
|
167
|
+
pass
|
|
168
|
+
self.logger.info("Background embedding refresh loop ended")
|
|
169
|
+
|
|
83
170
|
async def _load_staff_embeddings(self) -> bool:
|
|
84
171
|
"""Load all staff embeddings from API and cache them."""
|
|
85
172
|
if not self.face_client:
|
|
@@ -685,4 +772,11 @@ class EmbeddingManager:
|
|
|
685
772
|
self.logger.warning(f"Unknown detection type: {search_result.detection_type}")
|
|
686
773
|
return None
|
|
687
774
|
|
|
688
|
-
return detection
|
|
775
|
+
return detection
|
|
776
|
+
|
|
777
|
+
def __del__(self):
|
|
778
|
+
"""Cleanup when object is destroyed"""
|
|
779
|
+
try:
|
|
780
|
+
self.stop_background_refresh()
|
|
781
|
+
except:
|
|
782
|
+
pass
|
|
@@ -15,6 +15,7 @@ Configuration options:
|
|
|
15
15
|
- cache_ttl: Cache time-to-live in seconds (default: 3600)
|
|
16
16
|
"""
|
|
17
17
|
import subprocess
|
|
18
|
+
import logging
|
|
18
19
|
import asyncio
|
|
19
20
|
import os
|
|
20
21
|
log_file = open("pip_jetson_btii.log", "w")
|
|
@@ -93,6 +94,7 @@ class TemporalIdentityManager:
|
|
|
93
94
|
switch_patience: int = 5,
|
|
94
95
|
fallback_margin: float = 0.05,
|
|
95
96
|
) -> None:
|
|
97
|
+
self.logger = logging.getLogger(__name__)
|
|
96
98
|
self.face_client = face_client
|
|
97
99
|
self.threshold = float(recognition_threshold)
|
|
98
100
|
self.history_size = int(history_size)
|
|
@@ -436,12 +438,14 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
436
438
|
self._default_config = config
|
|
437
439
|
self._initialized = False
|
|
438
440
|
|
|
439
|
-
asyncio.run(
|
|
441
|
+
# Don't call asyncio.run() in __init__ - it will fail if called from async context
|
|
442
|
+
# Initialization must be done by calling await initialize(config) after instantiation
|
|
443
|
+
# This is handled in PostProcessor._get_use_case_instance()
|
|
440
444
|
|
|
441
445
|
async def initialize(self, config: Optional[FaceRecognitionEmbeddingConfig] = None) -> None:
|
|
442
446
|
"""
|
|
443
|
-
Async initialization method to set up face client and
|
|
444
|
-
|
|
447
|
+
Async initialization method to set up face client and all components.
|
|
448
|
+
Must be called after __init__ before process() can be called.
|
|
445
449
|
|
|
446
450
|
Args:
|
|
447
451
|
config: Optional config to use. If not provided, uses config from __init__.
|
|
@@ -454,25 +458,53 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
454
458
|
init_config = config or self._default_config
|
|
455
459
|
|
|
456
460
|
if not init_config:
|
|
457
|
-
|
|
458
|
-
return
|
|
461
|
+
raise ValueError("No config provided for initialization - config is required")
|
|
459
462
|
|
|
460
463
|
# Validate config type
|
|
461
464
|
if not isinstance(init_config, FaceRecognitionEmbeddingConfig):
|
|
462
|
-
|
|
463
|
-
return
|
|
465
|
+
raise TypeError(f"Invalid config type for initialization: {type(init_config)}, expected FaceRecognitionEmbeddingConfig")
|
|
464
466
|
|
|
465
467
|
self.logger.info("Initializing face recognition use case with provided config")
|
|
466
468
|
|
|
467
469
|
# Initialize face client (includes deployment update)
|
|
468
470
|
try:
|
|
469
471
|
self.face_client = await self._get_facial_recognition_client(init_config)
|
|
472
|
+
|
|
473
|
+
# Initialize People activity logging if enabled
|
|
474
|
+
if init_config.enable_people_activity_logging:
|
|
475
|
+
self.people_activity_logging = PeopleActivityLogging(self.face_client)
|
|
476
|
+
self.people_activity_logging.start_background_processing()
|
|
477
|
+
self.logger.info("People activity logging enabled and started")
|
|
478
|
+
|
|
479
|
+
# Initialize EmbeddingManager
|
|
480
|
+
if not init_config.embedding_config:
|
|
481
|
+
init_config.embedding_config = EmbeddingConfig(
|
|
482
|
+
similarity_threshold=init_config.similarity_threshold,
|
|
483
|
+
confidence_threshold=init_config.confidence_threshold,
|
|
484
|
+
enable_track_id_cache=init_config.enable_track_id_cache,
|
|
485
|
+
cache_max_size=init_config.cache_max_size,
|
|
486
|
+
cache_ttl=3600
|
|
487
|
+
)
|
|
488
|
+
self.embedding_manager = EmbeddingManager(init_config.embedding_config, self.face_client)
|
|
489
|
+
self.logger.info("Embedding manager initialized")
|
|
490
|
+
|
|
491
|
+
# Initialize TemporalIdentityManager
|
|
492
|
+
self.temporal_identity_manager = TemporalIdentityManager(
|
|
493
|
+
face_client=self.face_client,
|
|
494
|
+
recognition_threshold=float(init_config.similarity_threshold),
|
|
495
|
+
history_size=20,
|
|
496
|
+
unknown_patience=7,
|
|
497
|
+
switch_patience=5,
|
|
498
|
+
fallback_margin=0.05,
|
|
499
|
+
)
|
|
500
|
+
self.logger.info("Temporal identity manager initialized")
|
|
501
|
+
|
|
470
502
|
self._initialized = True
|
|
471
|
-
self.logger.info("Face recognition use case initialized
|
|
503
|
+
self.logger.info("Face recognition use case fully initialized")
|
|
472
504
|
|
|
473
505
|
except Exception as e:
|
|
474
506
|
self.logger.error(f"Error during use case initialization: {e}", exc_info=True)
|
|
475
|
-
|
|
507
|
+
raise RuntimeError(f"Failed to initialize face recognition use case: {e}") from e
|
|
476
508
|
|
|
477
509
|
async def _get_facial_recognition_client(
|
|
478
510
|
self, config: FaceRecognitionEmbeddingConfig
|
|
@@ -540,47 +572,16 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
540
572
|
)
|
|
541
573
|
context = ProcessingContext()
|
|
542
574
|
|
|
543
|
-
# Ensure
|
|
575
|
+
# Ensure use case is initialized (should be done in _get_use_case_instance, not lazy loaded)
|
|
544
576
|
if not self._initialized:
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
self.face_client = await self._get_facial_recognition_client(config)
|
|
577
|
+
raise RuntimeError(
|
|
578
|
+
"Face recognition use case not initialized. "
|
|
579
|
+
"This should be initialized eagerly in PostProcessor._get_use_case_instance()"
|
|
580
|
+
)
|
|
550
581
|
|
|
551
582
|
# Ensure confidence threshold is set
|
|
552
583
|
if not config.confidence_threshold:
|
|
553
584
|
config.confidence_threshold = 0.35
|
|
554
|
-
|
|
555
|
-
# Ensure People activity logging is initialized if enabled
|
|
556
|
-
if config.enable_people_activity_logging and not self.people_activity_logging:
|
|
557
|
-
self.people_activity_logging = PeopleActivityLogging(self.face_client)
|
|
558
|
-
self.people_activity_logging.start_background_processing()
|
|
559
|
-
self.logger.info("People activity logging enabled and started")
|
|
560
|
-
|
|
561
|
-
# Ensure EmbeddingManager is initialized
|
|
562
|
-
if not self.embedding_manager:
|
|
563
|
-
# Create default embedding config if not provided
|
|
564
|
-
if not config.embedding_config:
|
|
565
|
-
config.embedding_config = EmbeddingConfig(
|
|
566
|
-
similarity_threshold=config.similarity_threshold,
|
|
567
|
-
confidence_threshold=config.confidence_threshold,
|
|
568
|
-
enable_track_id_cache=config.enable_track_id_cache,
|
|
569
|
-
cache_max_size=config.cache_max_size,
|
|
570
|
-
cache_ttl=3600
|
|
571
|
-
)
|
|
572
|
-
self.embedding_manager = EmbeddingManager(config.embedding_config, self.face_client)
|
|
573
|
-
|
|
574
|
-
# Ensure TemporalIdentityManager is initialized (top-1 via API with smoothing)
|
|
575
|
-
if not self.temporal_identity_manager:
|
|
576
|
-
self.temporal_identity_manager = TemporalIdentityManager(
|
|
577
|
-
face_client=self.face_client,
|
|
578
|
-
recognition_threshold=float(config.similarity_threshold),
|
|
579
|
-
history_size=20,
|
|
580
|
-
unknown_patience=7,
|
|
581
|
-
switch_patience=5,
|
|
582
|
-
fallback_margin=0.05,
|
|
583
|
-
)
|
|
584
585
|
|
|
585
586
|
|
|
586
587
|
# Detect input format and store in context
|
|
@@ -1957,3 +1958,9 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
1957
1958
|
self.people_activity_logging.stop_background_processing()
|
|
1958
1959
|
except:
|
|
1959
1960
|
pass
|
|
1961
|
+
|
|
1962
|
+
try:
|
|
1963
|
+
if hasattr(self, "embedding_manager") and self.embedding_manager:
|
|
1964
|
+
self.embedding_manager.stop_background_refresh()
|
|
1965
|
+
except:
|
|
1966
|
+
pass
|
|
@@ -284,9 +284,10 @@ class FacialRecognitionClient:
|
|
|
284
284
|
location: str,
|
|
285
285
|
employee_id: Optional[str] = None,
|
|
286
286
|
timestamp: str = datetime.now(timezone.utc).isoformat(),
|
|
287
|
-
|
|
287
|
+
image_data: Optional[str] = None,
|
|
288
|
+
) -> Dict[str, Any]:
|
|
288
289
|
"""
|
|
289
|
-
Store people activity data
|
|
290
|
+
Store people activity data with optional image data
|
|
290
291
|
|
|
291
292
|
API: POST /v1/facial_recognition/store_people_activity?projectId={projectId}&serverID={serverID}
|
|
292
293
|
|
|
@@ -297,10 +298,10 @@ class FacialRecognitionClient:
|
|
|
297
298
|
location: Location identifier
|
|
298
299
|
employee_id: Employee ID (for unknown faces, this will be generated)
|
|
299
300
|
timestamp: Timestamp in ISO format
|
|
301
|
+
image_data: Base64-encoded JPEG image data (optional)
|
|
300
302
|
|
|
301
303
|
Returns:
|
|
302
|
-
Dict containing response data
|
|
303
|
-
or None if the request failed
|
|
304
|
+
Dict containing response data with success status
|
|
304
305
|
"""
|
|
305
306
|
activity_request = {
|
|
306
307
|
"staff_id": staff_id,
|
|
@@ -316,7 +317,11 @@ class FacialRecognitionClient:
|
|
|
316
317
|
elif detection_type == "known" and employee_id:
|
|
317
318
|
activity_request["employee_id"] = employee_id
|
|
318
319
|
|
|
319
|
-
|
|
320
|
+
# Add image data if provided
|
|
321
|
+
if image_data:
|
|
322
|
+
activity_request["imageData"] = image_data
|
|
323
|
+
|
|
324
|
+
self.logger.info(f"API REQUEST: Storing people activity - type={detection_type}, staff_id={staff_id}, location={location}, has_image={bool(image_data)}")
|
|
320
325
|
self.logger.debug(f"Activity request payload: bbox={bbox}, employee_id={employee_id}")
|
|
321
326
|
|
|
322
327
|
try:
|
|
@@ -329,18 +334,14 @@ class FacialRecognitionClient:
|
|
|
329
334
|
handled_response = self._handle_response(response)
|
|
330
335
|
|
|
331
336
|
if handled_response.get("success", False):
|
|
332
|
-
data = handled_response.get("data", {})
|
|
333
337
|
self.logger.info(f"API RESPONSE: Successfully stored {detection_type} activity for staff_id={staff_id}")
|
|
334
|
-
|
|
335
|
-
self.logger.warning(f"No data returned from store people activity for staff_id={staff_id}")
|
|
336
|
-
return None
|
|
337
|
-
return data
|
|
338
|
+
return handled_response
|
|
338
339
|
else:
|
|
339
340
|
self.logger.warning(f"Failed to store {detection_type} activity: {handled_response.get('error', 'Unknown error')}")
|
|
340
|
-
return
|
|
341
|
+
return handled_response
|
|
341
342
|
except Exception as e:
|
|
342
343
|
self.logger.error(f"API ERROR: Store people activity request failed - type={detection_type}, staff_id={staff_id} - {e}", exc_info=True)
|
|
343
|
-
return
|
|
344
|
+
return {"success": False, "error": str(e)}
|
|
344
345
|
|
|
345
346
|
async def update_staff_images(self, image_url: str, employee_id: str) -> Dict[str, Any]:
|
|
346
347
|
"""Update staff images with uploaded image URL
|