matrice-analytics 0.1.2__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 (160) hide show
  1. matrice_analytics/__init__.py +28 -0
  2. matrice_analytics/boundary_drawing_internal/README.md +305 -0
  3. matrice_analytics/boundary_drawing_internal/__init__.py +45 -0
  4. matrice_analytics/boundary_drawing_internal/boundary_drawing_internal.py +1207 -0
  5. matrice_analytics/boundary_drawing_internal/boundary_drawing_tool.py +429 -0
  6. matrice_analytics/boundary_drawing_internal/boundary_tool_template.html +1036 -0
  7. matrice_analytics/boundary_drawing_internal/data/.gitignore +12 -0
  8. matrice_analytics/boundary_drawing_internal/example_usage.py +206 -0
  9. matrice_analytics/boundary_drawing_internal/usage/README.md +110 -0
  10. matrice_analytics/boundary_drawing_internal/usage/boundary_drawer_launcher.py +102 -0
  11. matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py +107 -0
  12. matrice_analytics/post_processing/README.md +455 -0
  13. matrice_analytics/post_processing/__init__.py +732 -0
  14. matrice_analytics/post_processing/advanced_tracker/README.md +650 -0
  15. matrice_analytics/post_processing/advanced_tracker/__init__.py +17 -0
  16. matrice_analytics/post_processing/advanced_tracker/base.py +99 -0
  17. matrice_analytics/post_processing/advanced_tracker/config.py +77 -0
  18. matrice_analytics/post_processing/advanced_tracker/kalman_filter.py +370 -0
  19. matrice_analytics/post_processing/advanced_tracker/matching.py +195 -0
  20. matrice_analytics/post_processing/advanced_tracker/strack.py +230 -0
  21. matrice_analytics/post_processing/advanced_tracker/tracker.py +367 -0
  22. matrice_analytics/post_processing/config.py +142 -0
  23. matrice_analytics/post_processing/core/__init__.py +63 -0
  24. matrice_analytics/post_processing/core/base.py +704 -0
  25. matrice_analytics/post_processing/core/config.py +3188 -0
  26. matrice_analytics/post_processing/core/config_utils.py +925 -0
  27. matrice_analytics/post_processing/face_reg/__init__.py +43 -0
  28. matrice_analytics/post_processing/face_reg/compare_similarity.py +556 -0
  29. matrice_analytics/post_processing/face_reg/embedding_manager.py +681 -0
  30. matrice_analytics/post_processing/face_reg/face_recognition.py +1870 -0
  31. matrice_analytics/post_processing/face_reg/face_recognition_client.py +339 -0
  32. matrice_analytics/post_processing/face_reg/people_activity_logging.py +283 -0
  33. matrice_analytics/post_processing/ocr/__init__.py +0 -0
  34. matrice_analytics/post_processing/ocr/easyocr_extractor.py +248 -0
  35. matrice_analytics/post_processing/ocr/postprocessing.py +271 -0
  36. matrice_analytics/post_processing/ocr/preprocessing.py +52 -0
  37. matrice_analytics/post_processing/post_processor.py +1153 -0
  38. matrice_analytics/post_processing/test_cases/__init__.py +1 -0
  39. matrice_analytics/post_processing/test_cases/run_tests.py +143 -0
  40. matrice_analytics/post_processing/test_cases/test_advanced_customer_service.py +841 -0
  41. matrice_analytics/post_processing/test_cases/test_basic_counting_tracking.py +523 -0
  42. matrice_analytics/post_processing/test_cases/test_comprehensive.py +531 -0
  43. matrice_analytics/post_processing/test_cases/test_config.py +852 -0
  44. matrice_analytics/post_processing/test_cases/test_customer_service.py +585 -0
  45. matrice_analytics/post_processing/test_cases/test_data_generators.py +583 -0
  46. matrice_analytics/post_processing/test_cases/test_people_counting.py +510 -0
  47. matrice_analytics/post_processing/test_cases/test_processor.py +524 -0
  48. matrice_analytics/post_processing/test_cases/test_utilities.py +356 -0
  49. matrice_analytics/post_processing/test_cases/test_utils.py +743 -0
  50. matrice_analytics/post_processing/usecases/Histopathological_Cancer_Detection_img.py +604 -0
  51. matrice_analytics/post_processing/usecases/__init__.py +267 -0
  52. matrice_analytics/post_processing/usecases/abandoned_object_detection.py +797 -0
  53. matrice_analytics/post_processing/usecases/advanced_customer_service.py +1601 -0
  54. matrice_analytics/post_processing/usecases/age_detection.py +842 -0
  55. matrice_analytics/post_processing/usecases/age_gender_detection.py +1043 -0
  56. matrice_analytics/post_processing/usecases/anti_spoofing_detection.py +656 -0
  57. matrice_analytics/post_processing/usecases/assembly_line_detection.py +841 -0
  58. matrice_analytics/post_processing/usecases/banana_defect_detection.py +624 -0
  59. matrice_analytics/post_processing/usecases/basic_counting_tracking.py +667 -0
  60. matrice_analytics/post_processing/usecases/blood_cancer_detection_img.py +881 -0
  61. matrice_analytics/post_processing/usecases/car_damage_detection.py +834 -0
  62. matrice_analytics/post_processing/usecases/car_part_segmentation.py +946 -0
  63. matrice_analytics/post_processing/usecases/car_service.py +1601 -0
  64. matrice_analytics/post_processing/usecases/cardiomegaly_classification.py +864 -0
  65. matrice_analytics/post_processing/usecases/cell_microscopy_segmentation.py +897 -0
  66. matrice_analytics/post_processing/usecases/chicken_pose_detection.py +648 -0
  67. matrice_analytics/post_processing/usecases/child_monitoring.py +814 -0
  68. matrice_analytics/post_processing/usecases/color/clip.py +232 -0
  69. matrice_analytics/post_processing/usecases/color/clip_processor/merges.txt +48895 -0
  70. matrice_analytics/post_processing/usecases/color/clip_processor/preprocessor_config.json +28 -0
  71. matrice_analytics/post_processing/usecases/color/clip_processor/special_tokens_map.json +30 -0
  72. matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer.json +245079 -0
  73. matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer_config.json +32 -0
  74. matrice_analytics/post_processing/usecases/color/clip_processor/vocab.json +1 -0
  75. matrice_analytics/post_processing/usecases/color/color_map_utils.py +70 -0
  76. matrice_analytics/post_processing/usecases/color/color_mapper.py +468 -0
  77. matrice_analytics/post_processing/usecases/color_detection.py +1835 -0
  78. matrice_analytics/post_processing/usecases/color_map_utils.py +70 -0
  79. matrice_analytics/post_processing/usecases/concrete_crack_detection.py +827 -0
  80. matrice_analytics/post_processing/usecases/crop_weed_detection.py +781 -0
  81. matrice_analytics/post_processing/usecases/customer_service.py +1008 -0
  82. matrice_analytics/post_processing/usecases/defect_detection_products.py +936 -0
  83. matrice_analytics/post_processing/usecases/distracted_driver_detection.py +822 -0
  84. matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +930 -0
  85. matrice_analytics/post_processing/usecases/drowsy_driver_detection.py +829 -0
  86. matrice_analytics/post_processing/usecases/dwell_detection.py +829 -0
  87. matrice_analytics/post_processing/usecases/emergency_vehicle_detection.py +827 -0
  88. matrice_analytics/post_processing/usecases/face_emotion.py +813 -0
  89. matrice_analytics/post_processing/usecases/face_recognition.py +827 -0
  90. matrice_analytics/post_processing/usecases/fashion_detection.py +835 -0
  91. matrice_analytics/post_processing/usecases/field_mapping.py +902 -0
  92. matrice_analytics/post_processing/usecases/fire_detection.py +1112 -0
  93. matrice_analytics/post_processing/usecases/flare_analysis.py +891 -0
  94. matrice_analytics/post_processing/usecases/flower_segmentation.py +1006 -0
  95. matrice_analytics/post_processing/usecases/gas_leak_detection.py +837 -0
  96. matrice_analytics/post_processing/usecases/gender_detection.py +832 -0
  97. matrice_analytics/post_processing/usecases/human_activity_recognition.py +871 -0
  98. matrice_analytics/post_processing/usecases/intrusion_detection.py +1672 -0
  99. matrice_analytics/post_processing/usecases/leaf.py +821 -0
  100. matrice_analytics/post_processing/usecases/leaf_disease.py +840 -0
  101. matrice_analytics/post_processing/usecases/leak_detection.py +837 -0
  102. matrice_analytics/post_processing/usecases/license_plate_detection.py +914 -0
  103. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +1194 -0
  104. matrice_analytics/post_processing/usecases/litter_monitoring.py +717 -0
  105. matrice_analytics/post_processing/usecases/mask_detection.py +869 -0
  106. matrice_analytics/post_processing/usecases/natural_disaster.py +907 -0
  107. matrice_analytics/post_processing/usecases/parking.py +787 -0
  108. matrice_analytics/post_processing/usecases/parking_space_detection.py +822 -0
  109. matrice_analytics/post_processing/usecases/pcb_defect_detection.py +888 -0
  110. matrice_analytics/post_processing/usecases/pedestrian_detection.py +808 -0
  111. matrice_analytics/post_processing/usecases/people_counting.py +1728 -0
  112. matrice_analytics/post_processing/usecases/people_tracking.py +1842 -0
  113. matrice_analytics/post_processing/usecases/pipeline_detection.py +605 -0
  114. matrice_analytics/post_processing/usecases/plaque_segmentation_img.py +874 -0
  115. matrice_analytics/post_processing/usecases/pothole_segmentation.py +915 -0
  116. matrice_analytics/post_processing/usecases/ppe_compliance.py +645 -0
  117. matrice_analytics/post_processing/usecases/price_tag_detection.py +822 -0
  118. matrice_analytics/post_processing/usecases/proximity_detection.py +1901 -0
  119. matrice_analytics/post_processing/usecases/road_lane_detection.py +623 -0
  120. matrice_analytics/post_processing/usecases/road_traffic_density.py +832 -0
  121. matrice_analytics/post_processing/usecases/road_view_segmentation.py +915 -0
  122. matrice_analytics/post_processing/usecases/shelf_inventory_detection.py +583 -0
  123. matrice_analytics/post_processing/usecases/shoplifting_detection.py +822 -0
  124. matrice_analytics/post_processing/usecases/shopping_cart_analysis.py +899 -0
  125. matrice_analytics/post_processing/usecases/skin_cancer_classification_img.py +864 -0
  126. matrice_analytics/post_processing/usecases/smoker_detection.py +833 -0
  127. matrice_analytics/post_processing/usecases/solar_panel.py +810 -0
  128. matrice_analytics/post_processing/usecases/suspicious_activity_detection.py +1030 -0
  129. matrice_analytics/post_processing/usecases/template_usecase.py +380 -0
  130. matrice_analytics/post_processing/usecases/theft_detection.py +648 -0
  131. matrice_analytics/post_processing/usecases/traffic_sign_monitoring.py +724 -0
  132. matrice_analytics/post_processing/usecases/underground_pipeline_defect_detection.py +775 -0
  133. matrice_analytics/post_processing/usecases/underwater_pollution_detection.py +842 -0
  134. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +950 -0
  135. matrice_analytics/post_processing/usecases/warehouse_object_segmentation.py +899 -0
  136. matrice_analytics/post_processing/usecases/waterbody_segmentation.py +923 -0
  137. matrice_analytics/post_processing/usecases/weapon_detection.py +771 -0
  138. matrice_analytics/post_processing/usecases/weld_defect_detection.py +615 -0
  139. matrice_analytics/post_processing/usecases/wildlife_monitoring.py +898 -0
  140. matrice_analytics/post_processing/usecases/windmill_maintenance.py +834 -0
  141. matrice_analytics/post_processing/usecases/wound_segmentation.py +856 -0
  142. matrice_analytics/post_processing/utils/__init__.py +150 -0
  143. matrice_analytics/post_processing/utils/advanced_counting_utils.py +400 -0
  144. matrice_analytics/post_processing/utils/advanced_helper_utils.py +317 -0
  145. matrice_analytics/post_processing/utils/advanced_tracking_utils.py +461 -0
  146. matrice_analytics/post_processing/utils/alerting_utils.py +213 -0
  147. matrice_analytics/post_processing/utils/category_mapping_utils.py +94 -0
  148. matrice_analytics/post_processing/utils/color_utils.py +592 -0
  149. matrice_analytics/post_processing/utils/counting_utils.py +182 -0
  150. matrice_analytics/post_processing/utils/filter_utils.py +261 -0
  151. matrice_analytics/post_processing/utils/format_utils.py +293 -0
  152. matrice_analytics/post_processing/utils/geometry_utils.py +300 -0
  153. matrice_analytics/post_processing/utils/smoothing_utils.py +358 -0
  154. matrice_analytics/post_processing/utils/tracking_utils.py +234 -0
  155. matrice_analytics/py.typed +0 -0
  156. matrice_analytics-0.1.2.dist-info/METADATA +481 -0
  157. matrice_analytics-0.1.2.dist-info/RECORD +160 -0
  158. matrice_analytics-0.1.2.dist-info/WHEEL +5 -0
  159. matrice_analytics-0.1.2.dist-info/licenses/LICENSE.txt +21 -0
  160. matrice_analytics-0.1.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,681 @@
1
+
2
+ from typing import Any, Dict, List, Optional, Tuple, NamedTuple
3
+ import time
4
+ import logging
5
+ import threading
6
+ import numpy as np
7
+ from datetime import datetime, timezone
8
+ from dataclasses import dataclass, field
9
+
10
+ from .face_recognition_client import FacialRecognitionClient
11
+
12
+
13
+ class SearchResult(NamedTuple):
14
+ """Search result containing staff information as separate variables."""
15
+ employee_id: str
16
+ staff_id: str
17
+ detection_type: str # "known" or "unknown"
18
+ staff_details: Dict[str, Any]
19
+ person_name: str
20
+ similarity_score: float
21
+
22
+
23
+ class StaffEmbedding(NamedTuple):
24
+ """Staff embedding data structure."""
25
+ embedding_id: str
26
+ staff_id: str
27
+ embedding: List[float]
28
+ employee_id: str
29
+ staff_details: Dict[str, Any]
30
+ is_active: bool
31
+
32
+
33
+ @dataclass
34
+ class EmbeddingConfig:
35
+ """Configuration for embedding processing and search."""
36
+
37
+ # Similarity and confidence thresholds
38
+ similarity_threshold: float = 0.35
39
+ confidence_threshold: float = 0.6
40
+
41
+ # Track ID cache optimization settings
42
+ enable_track_id_cache: bool = True
43
+ cache_max_size: int = 3000
44
+ cache_ttl: int = 3600 # Cache time-to-live in seconds (1 hour)
45
+
46
+ # Search settings
47
+ search_limit: int = 5
48
+ search_collection: str = "staff_enrollment"
49
+
50
+
51
+ class EmbeddingManager:
52
+ """Manages face embeddings, search operations, and caching."""
53
+
54
+ def __init__(self, config: EmbeddingConfig, face_client: FacialRecognitionClient = None):
55
+ self.config = config
56
+ self.face_client = face_client
57
+ self.logger = logging.getLogger(__name__)
58
+
59
+ # Track ID cache for optimization - cache track IDs and their best results
60
+ # Format: {track_id: {"result": search_result, "similarity_score": float, "timestamp": timestamp}}
61
+ self.track_id_cache = {}
62
+
63
+ # Staff embeddings cache for local similarity search
64
+ self.staff_embeddings: List[StaffEmbedding] = []
65
+ self.staff_embeddings_last_update = 0
66
+ self.staff_embeddings_cache_ttl = 3600 # 1 hour
67
+
68
+ # Numpy arrays for fast similarity computation
69
+ self.embeddings_matrix = None
70
+ self.embedding_metadata = [] # List of StaffEmbedding objects corresponding to matrix rows
71
+
72
+ # Unknown faces cache - storing unknown embeddings locally
73
+ self.unknown_faces_counter = 0
74
+
75
+ # Thread safety
76
+ self._cache_lock = threading.Lock()
77
+ self._embeddings_lock = threading.Lock()
78
+
79
+ def set_face_client(self, face_client: FacialRecognitionClient):
80
+ """Set the face recognition client."""
81
+ self.face_client = face_client
82
+
83
+ async def _load_staff_embeddings(self) -> bool:
84
+ """Load all staff embeddings from API and cache them."""
85
+ if not self.face_client:
86
+ self.logger.error("Face client not available for loading staff embeddings")
87
+ return False
88
+
89
+ try:
90
+ response = await self.face_client.get_all_staff_embeddings()
91
+
92
+ if not response.get("success", False):
93
+ self.logger.error(f"Failed to get staff embeddings: {response.get('error', 'Unknown error')}")
94
+ return False
95
+
96
+ # The API response has the format: {"success": True, "data": [embedding_items]}
97
+ # Each item has the structure shown in the sample response
98
+ embeddings_data = response.get("data", [])
99
+
100
+ self.staff_embeddings = []
101
+ embeddings_list = []
102
+
103
+ for item in embeddings_data:
104
+ # if not item.get("isActive", True): # TODO: Check what is isActive
105
+ # continue
106
+
107
+ staff_embedding = StaffEmbedding(
108
+ embedding_id=item.get("embeddingId", ""),
109
+ staff_id=item.get("staffId", ""),
110
+ embedding=item.get("embedding", []),
111
+ employee_id=str(item.get("employeeId", "")),
112
+ staff_details=item.get("staffDetails", {}),
113
+ is_active=item.get("isActive", True)
114
+ )
115
+
116
+ if staff_embedding.embedding: # Only add if embedding exists
117
+ self.staff_embeddings.append(staff_embedding)
118
+ embeddings_list.append(staff_embedding.embedding)
119
+
120
+ # Create numpy matrix for fast similarity computation (thread-safe)
121
+ with self._embeddings_lock:
122
+ if embeddings_list:
123
+ self.embeddings_matrix = np.array(embeddings_list, dtype=np.float32)
124
+ # Normalize embeddings for cosine similarity
125
+ norms = np.linalg.norm(self.embeddings_matrix, axis=1, keepdims=True)
126
+ norms[norms == 0] = 1 # Avoid division by zero
127
+ self.embeddings_matrix = self.embeddings_matrix / norms
128
+
129
+ self.embedding_metadata = self.staff_embeddings.copy()
130
+ self.staff_embeddings_last_update = time.time()
131
+ self.logger.info(f"Loaded {len(self.staff_embeddings)} staff embeddings")
132
+ return True
133
+ else:
134
+ self.logger.warning("No active staff embeddings found")
135
+ return False
136
+
137
+ except Exception as e:
138
+ self.logger.error(f"Error loading staff embeddings: {e}", exc_info=True)
139
+ return False
140
+
141
+ def _should_refresh_embeddings(self) -> bool:
142
+ """Check if staff embeddings should be refreshed."""
143
+ current_time = time.time()
144
+ return (current_time - self.staff_embeddings_last_update) > self.staff_embeddings_cache_ttl
145
+
146
+ def _add_embedding_to_local_cache(self, staff_embedding: StaffEmbedding):
147
+ """Add a new embedding to the local cache and update the matrix."""
148
+ try:
149
+ if not staff_embedding.embedding:
150
+ return
151
+
152
+ # Add to staff_embeddings list
153
+ self.staff_embeddings.append(staff_embedding)
154
+ self.embedding_metadata.append(staff_embedding)
155
+
156
+ # Update the embeddings matrix
157
+ new_embedding = np.array([staff_embedding.embedding], dtype=np.float32)
158
+ # Normalize the new embedding
159
+ norm = np.linalg.norm(new_embedding)
160
+ if norm > 0:
161
+ new_embedding = new_embedding / norm
162
+
163
+ if self.embeddings_matrix is None:
164
+ self.embeddings_matrix = new_embedding
165
+ else:
166
+ self.embeddings_matrix = np.vstack([self.embeddings_matrix, new_embedding])
167
+
168
+ self.logger.debug(f"Added embedding for {staff_embedding.staff_id} to local cache")
169
+
170
+ except Exception as e:
171
+ self.logger.error(f"Error adding embedding to local cache: {e}", exc_info=True)
172
+
173
+ def _find_best_local_match(self, query_embedding: List[float]) -> Optional[Tuple[StaffEmbedding, float]]:
174
+ """Find best matching staff member using optimized matrix operations (thread-safe)."""
175
+ with self._embeddings_lock:
176
+ if self.embeddings_matrix is None or len(self.embedding_metadata) == 0:
177
+ return None
178
+
179
+ # Create local copies to avoid issues with concurrent modifications
180
+ embeddings_matrix = self.embeddings_matrix.copy() if self.embeddings_matrix is not None else None
181
+ embedding_metadata = self.embedding_metadata.copy()
182
+
183
+ if embeddings_matrix is None:
184
+ return None
185
+
186
+ try:
187
+ query_array = np.array(query_embedding, dtype=np.float32).reshape(1, -1)
188
+
189
+ # Normalize query embedding
190
+ query_norm = np.linalg.norm(query_array)
191
+ if query_norm == 0:
192
+ return None
193
+ query_array = query_array / query_norm
194
+
195
+ # Compute cosine similarities using matrix multiplication (much faster)
196
+ similarities = np.dot(embeddings_matrix, query_array.T).flatten()
197
+
198
+ # Find the best match
199
+ best_idx = np.argmax(similarities)
200
+ best_similarity = similarities[best_idx]
201
+
202
+ # Check if similarity meets threshold
203
+ if best_similarity >= self.config.similarity_threshold:
204
+ best_staff_embedding = embedding_metadata[best_idx]
205
+ return best_staff_embedding, float(best_similarity)
206
+
207
+ return None
208
+
209
+ except Exception as e:
210
+ self.logger.error(f"Error in local similarity search: {e}", exc_info=True)
211
+ return None
212
+
213
+ def extract_embedding_from_detection(self, detection: Dict) -> Tuple[Dict, Optional[List[float]]]:
214
+ """Extract and validate embedding from detection."""
215
+ embedding = detection.get("embedding", [])
216
+
217
+ # Validate embedding format and dimensions
218
+ if not embedding:
219
+ self.logger.warning(
220
+ f"Missing embedding in detection: {detection.get('track_id', 'unknown')}"
221
+ )
222
+ return detection, None
223
+
224
+ if not isinstance(embedding, list):
225
+ self.logger.warning(
226
+ f"Invalid embedding type {type(embedding)} in detection: {detection.get('track_id', 'unknown')}"
227
+ )
228
+ return detection, None
229
+
230
+ if len(embedding) == 0:
231
+ self.logger.warning(
232
+ f"Empty embedding in detection: {detection.get('track_id', 'unknown')}"
233
+ )
234
+ return detection, None
235
+
236
+ # Additional validation for embedding values
237
+ try:
238
+ # Check if all embedding values are numeric
239
+ if not all(isinstance(val, (int, float)) for val in embedding):
240
+ self.logger.warning(
241
+ f"Non-numeric values in embedding for detection: {detection.get('track_id', 'unknown')}"
242
+ )
243
+ return detection, None
244
+ except Exception as e:
245
+ self.logger.warning(
246
+ f"Error validating embedding values for detection {detection.get('track_id', 'unknown')}: {e}"
247
+ )
248
+ return detection, None
249
+
250
+ return detection, embedding
251
+
252
+ # COMMENTED OUT: Track ID caching functionality removed
253
+ # def _check_track_id_cache(self, track_id: str) -> Optional[Dict]:
254
+ # """
255
+ # Check if a track_id exists in cache.
256
+ # Returns cached result if found, None otherwise.
257
+ # """
258
+ # if not self.config.enable_track_id_cache:
259
+ # return None
260
+ #
261
+ # try:
262
+ # current_time = time.time()
263
+ #
264
+ # # Clean expired entries
265
+ # expired_keys = [
266
+ # key for key, data in self.track_id_cache.items()
267
+ # if current_time - data["timestamp"] > self.config.cache_ttl
268
+ # ]
269
+ # for key in expired_keys:
270
+ # del self.track_id_cache[key]
271
+ #
272
+ # # Check for existing track_id
273
+ # if track_id in self.track_id_cache:
274
+ # self.logger.debug(f"Found cached result for track_id: {track_id}")
275
+ # return self.track_id_cache[track_id]["result"]
276
+ #
277
+ # return None
278
+ # except Exception as e:
279
+ # self.logger.warning(f"Error checking track_id cache: {e}")
280
+ # return None
281
+
282
+ def _check_track_id_cache(self, track_id: str) -> Optional[SearchResult]:
283
+ """
284
+ Check if a track_id exists in cache and return the best result.
285
+ Returns cached SearchResult if found, None otherwise.
286
+ """
287
+ if not self.config.enable_track_id_cache or not track_id:
288
+ return None
289
+
290
+ try:
291
+ with self._cache_lock:
292
+ current_time = time.time()
293
+
294
+ # Clean expired entries
295
+ expired_keys = [
296
+ key for key, data in self.track_id_cache.items()
297
+ if current_time - data["timestamp"] > self.config.cache_ttl
298
+ ]
299
+ for key in expired_keys:
300
+ del self.track_id_cache[key]
301
+
302
+ # Check for existing track_id
303
+ if track_id in self.track_id_cache:
304
+ cached_data = self.track_id_cache[track_id]
305
+ self.logger.debug(f"Found cached result for track_id: {track_id} with similarity: {cached_data['similarity_score']:.3f}")
306
+ return cached_data["result"]
307
+
308
+ return None
309
+ except Exception as e:
310
+ self.logger.warning(f"Error checking track_id cache: {e}")
311
+ return None
312
+
313
+ # COMMENTED OUT: Track ID caching functionality removed
314
+ # def _update_track_id_cache(self, track_id: str, result: Dict):
315
+ # """Update track_id cache with new result."""
316
+ # if not self.config.enable_track_id_cache:
317
+ # return
318
+ #
319
+ # try:
320
+ # # Manage cache size
321
+ # if len(self.track_id_cache) >= self.config.cache_max_size:
322
+ # # Remove oldest entries (simple FIFO)
323
+ # oldest_key = min(
324
+ # self.track_id_cache.keys(),
325
+ # key=lambda k: self.track_id_cache[k]["timestamp"]
326
+ # )
327
+ # del self.track_id_cache[oldest_key]
328
+ #
329
+ # # Add new entry
330
+ # self.track_id_cache[track_id] = {
331
+ # "result": result.copy(),
332
+ # "timestamp": time.time()
333
+ # }
334
+ # except Exception as e:
335
+ # self.logger.warning(f"Error updating track_id cache: {e}")
336
+
337
+ def _update_track_id_cache(self, track_id: str, search_result: SearchResult):
338
+ """
339
+ Update track_id cache with new result.
340
+ Note: Similarity comparison is now handled in the search method.
341
+ """
342
+ if not self.config.enable_track_id_cache or not track_id:
343
+ return
344
+
345
+ try:
346
+ with self._cache_lock:
347
+ current_time = time.time()
348
+ similarity_score = search_result.similarity_score
349
+
350
+ # Manage cache size
351
+ if len(self.track_id_cache) >= self.config.cache_max_size:
352
+ # Remove oldest entries (simple FIFO)
353
+ oldest_key = min(
354
+ self.track_id_cache.keys(),
355
+ key=lambda k: self.track_id_cache[k]["timestamp"]
356
+ )
357
+ del self.track_id_cache[oldest_key]
358
+
359
+ # Update cache entry
360
+ self.track_id_cache[track_id] = {
361
+ "result": search_result,
362
+ "similarity_score": similarity_score,
363
+ "timestamp": current_time
364
+ }
365
+
366
+ self.logger.debug(f"Updated cache for track_id {track_id} with similarity {similarity_score:.3f}")
367
+
368
+ except Exception as e:
369
+ self.logger.warning(f"Error updating track_id cache: {e}")
370
+
371
+ # COMMENTED OUT: Unknown face creation functionality removed
372
+ # def _create_unknown_face_local(self, embedding: List[float], track_id: str = None) -> SearchResult:
373
+ # """Create unknown face entry locally without API call."""
374
+ # try:
375
+ # # Generate unique IDs
376
+ # self.unknown_faces_counter += 1
377
+ # employee_id = f"unknown_{int(time.time())}_{self.unknown_faces_counter}"
378
+ # staff_id = track_id if track_id else f"unknown_{self.unknown_faces_counter}"
379
+ #
380
+ # self.logger.info(f"Creating local unknown face with ID: {employee_id}")
381
+ #
382
+ # # Create SearchResult
383
+ # search_result = SearchResult(
384
+ # employee_id=employee_id,
385
+ # staff_id=staff_id,
386
+ # detection_type="unknown",
387
+ # staff_details={"name": f"Unknown {track_id}"},
388
+ # person_name=f"Unknown {track_id}",
389
+ # similarity_score=0.0
390
+ # )
391
+ #
392
+ # # Add the new unknown embedding to local cache
393
+ # unknown_staff_embedding = StaffEmbedding(
394
+ # embedding_id=f"embedding_{employee_id}",
395
+ # staff_id=staff_id,
396
+ # embedding=embedding,
397
+ # employee_id=employee_id,
398
+ # staff_details={"name": f"Unknown {track_id}"},
399
+ # is_active=True
400
+ # )
401
+ # self._add_embedding_to_local_cache(unknown_staff_embedding)
402
+ #
403
+ # # Cache the result for track_id if caching is enabled
404
+ # if self.config.enable_track_id_cache and track_id:
405
+ # api_result = {
406
+ # "_id": employee_id,
407
+ # "staffId": staff_id,
408
+ # "detectionType": "unknown",
409
+ # "staffDetails": {"name": f"Unknown {track_id}"}
410
+ # }
411
+ # self._update_track_id_cache(track_id, api_result)
412
+ #
413
+ # return search_result
414
+ #
415
+ # except Exception as e:
416
+ # self.logger.error(f"Error creating local unknown face: {e}", exc_info=True)
417
+ # return None
418
+
419
+ def _create_unknown_face_local(self, embedding: List[float], track_id: str = None) -> SearchResult:
420
+ """Unknown face creation disabled - returns None"""
421
+ return None
422
+
423
+ async def search_face_embedding(self, embedding: List[float], track_id: str = None,
424
+ location: str = "", timestamp: str = "") -> Optional[SearchResult]:
425
+ """
426
+ Search for similar faces using embedding with local similarity search first, then API fallback.
427
+
428
+ Args:
429
+ embedding: Face embedding vector
430
+ track_id: Track ID for caching optimization
431
+ location: Location identifier for logging
432
+ timestamp: Current timestamp in ISO format
433
+
434
+ Returns:
435
+ SearchResult containing staff information as variables or None if failed
436
+ """
437
+ if not self.face_client:
438
+ self.logger.error("Face client not available for embedding search")
439
+ return None
440
+
441
+ # Refresh staff embeddings if needed
442
+ if self._should_refresh_embeddings() or self.embeddings_matrix is None:
443
+ await self._load_staff_embeddings()
444
+
445
+ # Always perform similarity search first
446
+ local_match = self._find_best_local_match(embedding)
447
+ current_search_result = None
448
+
449
+ if local_match:
450
+ 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}")
452
+
453
+ current_search_result = SearchResult(
454
+ employee_id=staff_embedding.employee_id,
455
+ staff_id=staff_embedding.staff_id,
456
+ detection_type="known",
457
+ staff_details=staff_embedding.staff_details,
458
+ person_name=self._extract_person_name(staff_embedding.staff_details),
459
+ similarity_score=similarity_score
460
+ )
461
+ else:
462
+ # Create unknown face entry (thread-safe counter)
463
+ with self._cache_lock:
464
+ self.unknown_faces_counter += 1
465
+ counter_value = self.unknown_faces_counter
466
+ employee_id = f"unknown_{int(time.time())}_{counter_value}"
467
+ staff_id = track_id if track_id else f"unknown_{counter_value}"
468
+
469
+ current_search_result = SearchResult(
470
+ employee_id=employee_id,
471
+ staff_id=staff_id,
472
+ detection_type="unknown",
473
+ staff_details={"name": f"Unknown {track_id}"},
474
+ person_name=f"Unknown {track_id}",
475
+ similarity_score=0.0
476
+ )
477
+
478
+ # Check cache and compare similarities (if caching enabled and track_id available)
479
+ # BUT: For unknown faces, always re-check to allow for potential identification
480
+ if self.config.enable_track_id_cache and track_id:
481
+ cached_result = self._check_track_id_cache(track_id)
482
+
483
+ # If current result is unknown, always continue checking even if cached
484
+ 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")
486
+ # Still update cache if new result is better, but don't return cached result for unknowns
487
+ if cached_result and current_search_result.similarity_score > cached_result.similarity_score:
488
+ self._update_track_id_cache(track_id, current_search_result)
489
+ elif not cached_result:
490
+ # 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}")
492
+ return current_search_result
493
+
494
+ if cached_result:
495
+ cached_similarity = cached_result.similarity_score
496
+ current_similarity = current_search_result.similarity_score
497
+
498
+ # If cached result was unknown but current is known, always use current (upgrade)
499
+ if cached_result.detection_type == "unknown" and current_search_result.detection_type == "known":
500
+ self.logger.info(f"Upgrading unknown face to known for track_id: {track_id} - similarity: {current_similarity:.3f}")
501
+ self._update_track_id_cache(track_id, current_search_result)
502
+ return current_search_result
503
+ elif current_similarity > cached_similarity:
504
+ # New result is better - update cache and return new result
505
+ self.logger.debug(f"New similarity {current_similarity:.3f} > cached {cached_similarity:.3f} for track_id: {track_id} - updating cache")
506
+ self._update_track_id_cache(track_id, current_search_result)
507
+ return current_search_result
508
+ else:
509
+ # Cached result is better or equal - keep cache and return cached result
510
+ self.logger.debug(f"Cached similarity {cached_similarity:.3f} >= new {current_similarity:.3f} for track_id: {track_id} - using cached result")
511
+ return cached_result
512
+ else:
513
+ # No cached result - add to cache and return current result (only for known faces)
514
+ if current_search_result.detection_type == "known":
515
+ self.logger.debug(f"No cached result for track_id: {track_id} - adding known face to cache")
516
+ self._update_track_id_cache(track_id, current_search_result)
517
+ return current_search_result
518
+
519
+ # If caching is disabled, just return the current result
520
+ return current_search_result
521
+
522
+ # # API calls are commented out for now
523
+ # try:
524
+ # # TODO: Uncomment this when API is ready
525
+ # # search_results = await self.face_client.search_similar_faces(
526
+ # # face_embedding=embedding,
527
+ # # threshold=self.config.similarity_threshold,
528
+ # # limit=self.config.search_limit,
529
+ # # collection=self.config.search_collection,
530
+ # # location=location,
531
+ # # timestamp=timestamp,
532
+ # # )
533
+ #
534
+ # # # Check if API call was successful
535
+ # # if not search_results.get("success", False):
536
+ # # self.logger.error(
537
+ # # f"API call failed: {search_results.get('message', 'Unknown error')}"
538
+ # # )
539
+ # # # If API fails and no local match, create unknown face locally
540
+ # # return self._create_unknown_face_local(embedding, track_id)
541
+
542
+ # # if not search_results.get("data", []):
543
+ # # # No matches found, create unknown face locally
544
+ # # return self._create_unknown_face_local(embedding, track_id)
545
+
546
+ # # response_data = search_results.get("data", [])
547
+ # # result = response_data[0] # Get first result
548
+ #
549
+ # # For now, create unknown face locally instead of API calls
550
+ # return self._create_unknown_face_local(embedding, track_id)
551
+ #
552
+ # except Exception as e:
553
+ # self.logger.error(f"Error in face embedding search: {e}", exc_info=True)
554
+ # # If any error occurs, create unknown face locally
555
+ # return self._create_unknown_face_local(embedding, track_id)
556
+
557
+ def _extract_person_name(self, staff_details: Dict[str, Any]) -> str:
558
+ """Extract person name from staff details."""
559
+ return str(
560
+ staff_details.get(
561
+ "name",
562
+ staff_details.get("firstName", "Unknown")
563
+ + " "
564
+ + staff_details.get("lastName", "Unknown"),
565
+ )
566
+ )
567
+
568
+ def _parse_api_result_to_search_result(self, api_result: Dict) -> SearchResult:
569
+ """Parse API result to SearchResult."""
570
+ employee_id = api_result["_id"]
571
+ staff_id = api_result["staffId"]
572
+ detection_type = api_result["detectionType"]
573
+ staff_details = api_result["staffDetails"]
574
+
575
+ person_name = "Unknown"
576
+ if detection_type == "known":
577
+ person_name = self._extract_person_name(staff_details)
578
+ elif detection_type == "unknown":
579
+ person_name = "Unknown"
580
+
581
+ return SearchResult(
582
+ employee_id=employee_id,
583
+ staff_id=staff_id,
584
+ detection_type=detection_type,
585
+ staff_details=staff_details,
586
+ person_name=person_name,
587
+ similarity_score=api_result.get("score", 0.0)
588
+ )
589
+
590
+ # COMMENTED OUT: Unknown face enrollment functionality removed
591
+ # async def _enroll_unknown_face(self, embedding: List[float], location: str = "", timestamp: str = "", track_id: str = None) -> Optional[SearchResult]:
592
+ # """Enroll unknown face and return SearchResult."""
593
+ # # For now, use local creation instead of API
594
+ # return self._create_unknown_face_local(embedding, track_id)
595
+
596
+ async def _enroll_unknown_face(self, embedding: List[float], location: str = "", timestamp: str = "", track_id: str = None) -> Optional[SearchResult]:
597
+ """Enroll unknown face and return SearchResult."""
598
+ # For now, use local creation instead of API
599
+ # return self._create_unknown_face_local(embedding, track_id)
600
+ return None
601
+
602
+ # TODO: Uncomment when API is ready
603
+ # try:
604
+ # if not timestamp:
605
+ # timestamp = datetime.now(timezone.utc).isoformat()
606
+ #
607
+ # response = await self.face_client.enroll_unknown_person(
608
+ # embedding=embedding,
609
+ # timestamp=timestamp,
610
+ # location=location
611
+ # )
612
+ #
613
+ # if response.get("success", False):
614
+ # data = response.get("data", {})
615
+ # employee_id = data.get("employeeId", "")
616
+ # staff_id = data.get("staffId", "")
617
+ #
618
+ # self.logger.info(f"Successfully enrolled unknown face with ID: {employee_id}")
619
+ #
620
+ # # Create SearchResult
621
+ # search_result = SearchResult(
622
+ # employee_id=employee_id,
623
+ # staff_id=staff_id,
624
+ # detection_type="unknown",
625
+ # staff_details={},
626
+ # person_name="Unknown",
627
+ # similarity_score=0.0
628
+ # )
629
+ #
630
+ # # Add the new unknown embedding to local cache
631
+ # unknown_staff_embedding = StaffEmbedding(
632
+ # embedding_id=data.get("embeddingId", ""),
633
+ # staff_id=staff_id,
634
+ # embedding=embedding,
635
+ # employee_id=employee_id,
636
+ # staff_details={},
637
+ # is_active=True
638
+ # )
639
+ # self._add_embedding_to_local_cache(unknown_staff_embedding)
640
+ #
641
+ # # Cache the result for track_id if caching is enabled
642
+ # if self.config.enable_track_id_cache and track_id:
643
+ # api_result = {
644
+ # "_id": employee_id,
645
+ # "staffId": staff_id,
646
+ # "detectionType": "unknown",
647
+ # "staffDetails": {}
648
+ # }
649
+ # self._update_track_id_cache(track_id, api_result)
650
+ #
651
+ # return search_result
652
+ # else:
653
+ # self.logger.error(f"Failed to enroll unknown face: {response.get('error', 'Unknown error')}")
654
+ # return None
655
+ #
656
+ # except Exception as e:
657
+ # self.logger.error(f"Error enrolling unknown face: {e}", exc_info=True)
658
+ # return None
659
+
660
+ def update_detection_with_search_result(self, search_result: SearchResult, detection: Dict) -> Dict:
661
+ """Update detection object with search result data."""
662
+ detection = detection.copy() # Create a copy to avoid modifying original
663
+
664
+ detection["person_id"] = search_result.staff_id
665
+ detection["person_name"] = search_result.person_name
666
+ detection["recognition_status"] = search_result.detection_type
667
+ detection["employee_id"] = search_result.employee_id
668
+ detection["staff_details"] = search_result.staff_details
669
+ detection["similarity_score"] = search_result.similarity_score
670
+
671
+ if search_result.detection_type == "known":
672
+ detection["enrolled"] = True
673
+ detection["category"] = f"{search_result.person_name.replace(' ', '_')}_{search_result.staff_id}"
674
+ elif search_result.detection_type == "unknown":
675
+ detection["enrolled"] = False
676
+ detection["category"] = "unrecognized"
677
+ else:
678
+ self.logger.warning(f"Unknown detection type: {search_result.detection_type}")
679
+ return None
680
+
681
+ return detection