matrice-analytics 0.1.3__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.

Files changed (61) hide show
  1. matrice_analytics/post_processing/advanced_tracker/matching.py +3 -3
  2. matrice_analytics/post_processing/advanced_tracker/strack.py +1 -1
  3. matrice_analytics/post_processing/config.py +4 -0
  4. matrice_analytics/post_processing/core/config.py +115 -12
  5. matrice_analytics/post_processing/face_reg/compare_similarity.py +5 -5
  6. matrice_analytics/post_processing/face_reg/embedding_manager.py +109 -8
  7. matrice_analytics/post_processing/face_reg/face_recognition.py +157 -61
  8. matrice_analytics/post_processing/face_reg/face_recognition_client.py +339 -88
  9. matrice_analytics/post_processing/face_reg/people_activity_logging.py +67 -29
  10. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
  11. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
  12. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
  13. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
  14. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
  15. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
  16. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
  17. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
  18. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
  19. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
  20. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
  21. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
  22. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
  23. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
  24. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
  25. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
  26. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
  27. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
  28. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
  29. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
  30. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
  31. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
  32. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
  33. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
  34. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
  35. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
  36. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
  37. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
  38. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
  39. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
  40. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
  41. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
  42. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
  43. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
  44. matrice_analytics/post_processing/ocr/postprocessing.py +0 -1
  45. matrice_analytics/post_processing/post_processor.py +32 -11
  46. matrice_analytics/post_processing/usecases/color/clip.py +42 -8
  47. matrice_analytics/post_processing/usecases/color/color_mapper.py +2 -2
  48. matrice_analytics/post_processing/usecases/color_detection.py +50 -129
  49. matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +41 -386
  50. matrice_analytics/post_processing/usecases/flare_analysis.py +1 -56
  51. matrice_analytics/post_processing/usecases/license_plate_detection.py +476 -202
  52. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +351 -26
  53. matrice_analytics/post_processing/usecases/people_counting.py +408 -1431
  54. matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
  55. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +39 -10
  56. matrice_analytics/post_processing/utils/__init__.py +8 -8
  57. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/METADATA +1 -1
  58. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/RECORD +61 -26
  59. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/WHEEL +0 -0
  60. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/licenses/LICENSE.txt +0 -0
  61. {matrice_analytics-0.1.3.dist-info → matrice_analytics-0.1.32.dist-info}/top_level.txt +0 -0
@@ -93,8 +93,8 @@ def iou_distance(atracks: list, btracks: list) -> np.ndarray:
93
93
  Compute cost based on Intersection over Union (IoU) between tracks.
94
94
 
95
95
  Args:
96
- atracks (List[STrack] | List[np.ndarray]): List of tracks 'a' or bounding boxes.
97
- btracks (List[STrack] | List[np.ndarray]): List of tracks 'b' or bounding boxes.
96
+ atracks (List[STrack] or List[np.ndarray]): List of tracks 'a' or bounding boxes.
97
+ btracks (List[STrack] or List[np.ndarray]): List of tracks 'b' or bounding boxes.
98
98
 
99
99
  Returns:
100
100
  (np.ndarray): Cost matrix computed based on IoU with shape (len(atracks), len(btracks)).
@@ -139,7 +139,7 @@ def embedding_distance(tracks: list, detections: list, metric: str = "cosine") -
139
139
  Compute distance between tracks and detections based on embeddings.
140
140
 
141
141
  Args:
142
- tracks (List[STrack]): List of tracks, where each track contains embedding features.
142
+ tracks (List[STrack] or List[np.ndarray]): List of tracks, where each track contains embedding features.
143
143
  detections (List[BaseTrack]): List of detections, where each detection contains embedding features.
144
144
  metric (str): Metric for distance computation. Supported metrics include 'cosine', 'euclidean', etc.
145
145
 
@@ -46,7 +46,7 @@ class STrack(BaseTrack):
46
46
  idx (int): Index or identifier for the object.
47
47
  frame_id (int): Current frame ID.
48
48
  start_frame (int): Frame where the object was first detected.
49
- angle (float | None): Optional angle information for oriented bounding boxes.
49
+ angle (float or None): Optional angle information for oriented bounding boxes.
50
50
  """
51
51
 
52
52
  shared_kalman = KalmanFilterXYAH()
@@ -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
- **kwargs
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
- **kwargs
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
- **kwargs
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
- **kwargs
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
- **kwargs
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
- **kwargs
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
- **kwargs
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
- **kwargs
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
- **kwargs
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
- **kwargs
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
- **kwargs
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
- **kwargs
2588
+ **filtered_kwargs
2486
2589
  )
2487
2590
 
2488
2591
  elif usecase == "dwell":
@@ -62,11 +62,11 @@ class FaceTracker:
62
62
  self.tracks: Dict[str, Dict[str, object]] = {}
63
63
  self.track_counter: int = 1
64
64
 
65
- def _find_matching_track(self, new_embedding: List[float]) -> str | None:
65
+ def _find_matching_track(self, new_embedding: List[float]) -> Optional[str]:
66
66
  if not new_embedding:
67
67
  return None
68
68
  best_similarity: float = 0.0
69
- best_track_id: str | None = None
69
+ best_track_id: Optional[str] = None
70
70
  for track_id, data in self.tracks.items():
71
71
  stored_embedding = data.get("embedding")
72
72
  if stored_embedding:
@@ -76,7 +76,7 @@ class FaceTracker:
76
76
  best_track_id = track_id
77
77
  return best_track_id
78
78
 
79
- def assign_track_id(self, embedding: List[float], frame_id: int | None = None) -> str:
79
+ def assign_track_id(self, embedding: List[float], frame_id: Optional[int] = None) -> str:
80
80
  match_id = self._find_matching_track(embedding)
81
81
  if match_id is not None and match_id in self.tracks:
82
82
  # Update last seen frame for the matched track
@@ -139,7 +139,7 @@ def compute_pairwise_similarities(embeddings: List[List[float]]) -> Dict[Tuple[i
139
139
  return similarity_dict
140
140
 
141
141
 
142
- def get_embeddings_from_folder(folder_path: str, max_images: int | None = None) -> Tuple[List[List[float]], List[str]]:
142
+ def get_embeddings_from_folder(folder_path: str, max_images: Optional[int] = None) -> Tuple[List[List[float]], List[str]]:
143
143
  image_paths = sorted([p for p in Path(folder_path).iterdir() if p.suffix.lower() in {'.jpg', '.jpeg', '.png'}])
144
144
  if max_images is not None:
145
145
  image_paths = image_paths[:max_images]
@@ -155,7 +155,7 @@ def get_embeddings_from_folder(folder_path: str, max_images: int | None = None)
155
155
  return embeddings, img_names
156
156
 
157
157
 
158
- def get_embeddings_per_person(identity_root: str, max_images_per_person: int | None = None) -> Dict[str, List[List[float]]]:
158
+ def get_embeddings_per_person(identity_root: str, max_images_per_person: Optional[int] = None) -> Dict[str, List[List[float]]]:
159
159
  """Build a mapping: person (subdirectory name) -> list of embeddings from all images inside it."""
160
160
  root = Path(identity_root)
161
161
  if not root.exists():
@@ -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:
@@ -87,10 +174,11 @@ class EmbeddingManager:
87
174
  return False
88
175
 
89
176
  try:
177
+ self.logger.info("Loading staff embeddings from API...")
90
178
  response = await self.face_client.get_all_staff_embeddings()
91
179
 
92
180
  if not response.get("success", False):
93
- self.logger.error(f"Failed to get staff embeddings: {response.get('error', 'Unknown error')}")
181
+ self.logger.error(f"Failed to get staff embeddings from API: {response.get('error', 'Unknown error')}")
94
182
  return False
95
183
 
96
184
  # The API response has the format: {"success": True, "data": [embedding_items]}
@@ -128,10 +216,11 @@ class EmbeddingManager:
128
216
 
129
217
  self.embedding_metadata = self.staff_embeddings.copy()
130
218
  self.staff_embeddings_last_update = time.time()
131
- self.logger.info(f"Loaded {len(self.staff_embeddings)} staff embeddings")
219
+ self.logger.info(f"Successfully loaded and cached {len(self.staff_embeddings)} staff embeddings")
220
+ self.logger.debug(f"Embeddings matrix shape: {self.embeddings_matrix.shape}")
132
221
  return True
133
222
  else:
134
- self.logger.warning("No active staff embeddings found")
223
+ self.logger.warning("No active staff embeddings found in API response")
135
224
  return False
136
225
 
137
226
  except Exception as e:
@@ -440,6 +529,7 @@ class EmbeddingManager:
440
529
 
441
530
  # Refresh staff embeddings if needed
442
531
  if self._should_refresh_embeddings() or self.embeddings_matrix is None:
532
+ self.logger.debug("Staff embeddings cache expired or empty, refreshing...")
443
533
  await self._load_staff_embeddings()
444
534
 
445
535
  # Always perform similarity search first
@@ -448,7 +538,8 @@ class EmbeddingManager:
448
538
 
449
539
  if local_match:
450
540
  staff_embedding, similarity_score = local_match
451
- self.logger.debug(f"Found local match for staff {staff_embedding.staff_id} with similarity {similarity_score:.3f}")
541
+ self.logger.info(f"Local embedding match found - staff_id={staff_embedding.staff_id}, similarity={similarity_score:.3f}, employee_id={staff_embedding.employee_id}")
542
+ self.logger.debug(f"Match details: staff_details={staff_embedding.staff_details}")
452
543
 
453
544
  current_search_result = SearchResult(
454
545
  employee_id=staff_embedding.employee_id,
@@ -466,6 +557,8 @@ class EmbeddingManager:
466
557
  employee_id = f"unknown_{int(time.time())}_{counter_value}"
467
558
  staff_id = track_id if track_id else f"unknown_{counter_value}"
468
559
 
560
+ self.logger.info(f"No local match found - creating unknown face entry: employee_id={employee_id}, track_id={track_id}")
561
+
469
562
  current_search_result = SearchResult(
470
563
  employee_id=employee_id,
471
564
  staff_id=staff_id,
@@ -482,13 +575,14 @@ class EmbeddingManager:
482
575
 
483
576
  # If current result is unknown, always continue checking even if cached
484
577
  if current_search_result.detection_type == "unknown":
485
- self.logger.debug(f"Unknown face with track_id: {track_id} - continuing to re-check for potential identification")
578
+ self.logger.debug(f"Unknown face with track_id={track_id} - not caching, will re-check for potential identification")
486
579
  # Still update cache if new result is better, but don't return cached result for unknowns
487
580
  if cached_result and current_search_result.similarity_score > cached_result.similarity_score:
488
- self._update_track_id_cache(track_id, current_search_result)
581
+ self._update_track_id_cache(track_id, current_search_result) # TODO: check if this is correct
582
+ self.logger.debug(f"Not updating cache for unknown face (track_id={track_id})")
489
583
  elif not cached_result:
490
584
  # Don't cache unknown results - let them be rechecked every time
491
- self.logger.debug(f"Not caching unknown face result for track_id: {track_id}")
585
+ self.logger.debug(f"Not caching unknown face result for track_id={track_id}")
492
586
  return current_search_result
493
587
 
494
588
  if cached_result:
@@ -678,4 +772,11 @@ class EmbeddingManager:
678
772
  self.logger.warning(f"Unknown detection type: {search_result.detection_type}")
679
773
  return None
680
774
 
681
- 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