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.
- matrice_analytics/__init__.py +28 -0
- matrice_analytics/boundary_drawing_internal/README.md +305 -0
- matrice_analytics/boundary_drawing_internal/__init__.py +45 -0
- matrice_analytics/boundary_drawing_internal/boundary_drawing_internal.py +1207 -0
- matrice_analytics/boundary_drawing_internal/boundary_drawing_tool.py +429 -0
- matrice_analytics/boundary_drawing_internal/boundary_tool_template.html +1036 -0
- matrice_analytics/boundary_drawing_internal/data/.gitignore +12 -0
- matrice_analytics/boundary_drawing_internal/example_usage.py +206 -0
- matrice_analytics/boundary_drawing_internal/usage/README.md +110 -0
- matrice_analytics/boundary_drawing_internal/usage/boundary_drawer_launcher.py +102 -0
- matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py +107 -0
- matrice_analytics/post_processing/README.md +455 -0
- matrice_analytics/post_processing/__init__.py +732 -0
- matrice_analytics/post_processing/advanced_tracker/README.md +650 -0
- matrice_analytics/post_processing/advanced_tracker/__init__.py +17 -0
- matrice_analytics/post_processing/advanced_tracker/base.py +99 -0
- matrice_analytics/post_processing/advanced_tracker/config.py +77 -0
- matrice_analytics/post_processing/advanced_tracker/kalman_filter.py +370 -0
- matrice_analytics/post_processing/advanced_tracker/matching.py +195 -0
- matrice_analytics/post_processing/advanced_tracker/strack.py +230 -0
- matrice_analytics/post_processing/advanced_tracker/tracker.py +367 -0
- matrice_analytics/post_processing/config.py +142 -0
- matrice_analytics/post_processing/core/__init__.py +63 -0
- matrice_analytics/post_processing/core/base.py +704 -0
- matrice_analytics/post_processing/core/config.py +3188 -0
- matrice_analytics/post_processing/core/config_utils.py +925 -0
- matrice_analytics/post_processing/face_reg/__init__.py +43 -0
- matrice_analytics/post_processing/face_reg/compare_similarity.py +556 -0
- matrice_analytics/post_processing/face_reg/embedding_manager.py +681 -0
- matrice_analytics/post_processing/face_reg/face_recognition.py +1870 -0
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +339 -0
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +283 -0
- matrice_analytics/post_processing/ocr/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/easyocr_extractor.py +248 -0
- matrice_analytics/post_processing/ocr/postprocessing.py +271 -0
- matrice_analytics/post_processing/ocr/preprocessing.py +52 -0
- matrice_analytics/post_processing/post_processor.py +1153 -0
- matrice_analytics/post_processing/test_cases/__init__.py +1 -0
- matrice_analytics/post_processing/test_cases/run_tests.py +143 -0
- matrice_analytics/post_processing/test_cases/test_advanced_customer_service.py +841 -0
- matrice_analytics/post_processing/test_cases/test_basic_counting_tracking.py +523 -0
- matrice_analytics/post_processing/test_cases/test_comprehensive.py +531 -0
- matrice_analytics/post_processing/test_cases/test_config.py +852 -0
- matrice_analytics/post_processing/test_cases/test_customer_service.py +585 -0
- matrice_analytics/post_processing/test_cases/test_data_generators.py +583 -0
- matrice_analytics/post_processing/test_cases/test_people_counting.py +510 -0
- matrice_analytics/post_processing/test_cases/test_processor.py +524 -0
- matrice_analytics/post_processing/test_cases/test_utilities.py +356 -0
- matrice_analytics/post_processing/test_cases/test_utils.py +743 -0
- matrice_analytics/post_processing/usecases/Histopathological_Cancer_Detection_img.py +604 -0
- matrice_analytics/post_processing/usecases/__init__.py +267 -0
- matrice_analytics/post_processing/usecases/abandoned_object_detection.py +797 -0
- matrice_analytics/post_processing/usecases/advanced_customer_service.py +1601 -0
- matrice_analytics/post_processing/usecases/age_detection.py +842 -0
- matrice_analytics/post_processing/usecases/age_gender_detection.py +1043 -0
- matrice_analytics/post_processing/usecases/anti_spoofing_detection.py +656 -0
- matrice_analytics/post_processing/usecases/assembly_line_detection.py +841 -0
- matrice_analytics/post_processing/usecases/banana_defect_detection.py +624 -0
- matrice_analytics/post_processing/usecases/basic_counting_tracking.py +667 -0
- matrice_analytics/post_processing/usecases/blood_cancer_detection_img.py +881 -0
- matrice_analytics/post_processing/usecases/car_damage_detection.py +834 -0
- matrice_analytics/post_processing/usecases/car_part_segmentation.py +946 -0
- matrice_analytics/post_processing/usecases/car_service.py +1601 -0
- matrice_analytics/post_processing/usecases/cardiomegaly_classification.py +864 -0
- matrice_analytics/post_processing/usecases/cell_microscopy_segmentation.py +897 -0
- matrice_analytics/post_processing/usecases/chicken_pose_detection.py +648 -0
- matrice_analytics/post_processing/usecases/child_monitoring.py +814 -0
- matrice_analytics/post_processing/usecases/color/clip.py +232 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/merges.txt +48895 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/preprocessor_config.json +28 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/special_tokens_map.json +30 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer.json +245079 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer_config.json +32 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/vocab.json +1 -0
- matrice_analytics/post_processing/usecases/color/color_map_utils.py +70 -0
- matrice_analytics/post_processing/usecases/color/color_mapper.py +468 -0
- matrice_analytics/post_processing/usecases/color_detection.py +1835 -0
- matrice_analytics/post_processing/usecases/color_map_utils.py +70 -0
- matrice_analytics/post_processing/usecases/concrete_crack_detection.py +827 -0
- matrice_analytics/post_processing/usecases/crop_weed_detection.py +781 -0
- matrice_analytics/post_processing/usecases/customer_service.py +1008 -0
- matrice_analytics/post_processing/usecases/defect_detection_products.py +936 -0
- matrice_analytics/post_processing/usecases/distracted_driver_detection.py +822 -0
- matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +930 -0
- matrice_analytics/post_processing/usecases/drowsy_driver_detection.py +829 -0
- matrice_analytics/post_processing/usecases/dwell_detection.py +829 -0
- matrice_analytics/post_processing/usecases/emergency_vehicle_detection.py +827 -0
- matrice_analytics/post_processing/usecases/face_emotion.py +813 -0
- matrice_analytics/post_processing/usecases/face_recognition.py +827 -0
- matrice_analytics/post_processing/usecases/fashion_detection.py +835 -0
- matrice_analytics/post_processing/usecases/field_mapping.py +902 -0
- matrice_analytics/post_processing/usecases/fire_detection.py +1112 -0
- matrice_analytics/post_processing/usecases/flare_analysis.py +891 -0
- matrice_analytics/post_processing/usecases/flower_segmentation.py +1006 -0
- matrice_analytics/post_processing/usecases/gas_leak_detection.py +837 -0
- matrice_analytics/post_processing/usecases/gender_detection.py +832 -0
- matrice_analytics/post_processing/usecases/human_activity_recognition.py +871 -0
- matrice_analytics/post_processing/usecases/intrusion_detection.py +1672 -0
- matrice_analytics/post_processing/usecases/leaf.py +821 -0
- matrice_analytics/post_processing/usecases/leaf_disease.py +840 -0
- matrice_analytics/post_processing/usecases/leak_detection.py +837 -0
- matrice_analytics/post_processing/usecases/license_plate_detection.py +914 -0
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +1194 -0
- matrice_analytics/post_processing/usecases/litter_monitoring.py +717 -0
- matrice_analytics/post_processing/usecases/mask_detection.py +869 -0
- matrice_analytics/post_processing/usecases/natural_disaster.py +907 -0
- matrice_analytics/post_processing/usecases/parking.py +787 -0
- matrice_analytics/post_processing/usecases/parking_space_detection.py +822 -0
- matrice_analytics/post_processing/usecases/pcb_defect_detection.py +888 -0
- matrice_analytics/post_processing/usecases/pedestrian_detection.py +808 -0
- matrice_analytics/post_processing/usecases/people_counting.py +1728 -0
- matrice_analytics/post_processing/usecases/people_tracking.py +1842 -0
- matrice_analytics/post_processing/usecases/pipeline_detection.py +605 -0
- matrice_analytics/post_processing/usecases/plaque_segmentation_img.py +874 -0
- matrice_analytics/post_processing/usecases/pothole_segmentation.py +915 -0
- matrice_analytics/post_processing/usecases/ppe_compliance.py +645 -0
- matrice_analytics/post_processing/usecases/price_tag_detection.py +822 -0
- matrice_analytics/post_processing/usecases/proximity_detection.py +1901 -0
- matrice_analytics/post_processing/usecases/road_lane_detection.py +623 -0
- matrice_analytics/post_processing/usecases/road_traffic_density.py +832 -0
- matrice_analytics/post_processing/usecases/road_view_segmentation.py +915 -0
- matrice_analytics/post_processing/usecases/shelf_inventory_detection.py +583 -0
- matrice_analytics/post_processing/usecases/shoplifting_detection.py +822 -0
- matrice_analytics/post_processing/usecases/shopping_cart_analysis.py +899 -0
- matrice_analytics/post_processing/usecases/skin_cancer_classification_img.py +864 -0
- matrice_analytics/post_processing/usecases/smoker_detection.py +833 -0
- matrice_analytics/post_processing/usecases/solar_panel.py +810 -0
- matrice_analytics/post_processing/usecases/suspicious_activity_detection.py +1030 -0
- matrice_analytics/post_processing/usecases/template_usecase.py +380 -0
- matrice_analytics/post_processing/usecases/theft_detection.py +648 -0
- matrice_analytics/post_processing/usecases/traffic_sign_monitoring.py +724 -0
- matrice_analytics/post_processing/usecases/underground_pipeline_defect_detection.py +775 -0
- matrice_analytics/post_processing/usecases/underwater_pollution_detection.py +842 -0
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +950 -0
- matrice_analytics/post_processing/usecases/warehouse_object_segmentation.py +899 -0
- matrice_analytics/post_processing/usecases/waterbody_segmentation.py +923 -0
- matrice_analytics/post_processing/usecases/weapon_detection.py +771 -0
- matrice_analytics/post_processing/usecases/weld_defect_detection.py +615 -0
- matrice_analytics/post_processing/usecases/wildlife_monitoring.py +898 -0
- matrice_analytics/post_processing/usecases/windmill_maintenance.py +834 -0
- matrice_analytics/post_processing/usecases/wound_segmentation.py +856 -0
- matrice_analytics/post_processing/utils/__init__.py +150 -0
- matrice_analytics/post_processing/utils/advanced_counting_utils.py +400 -0
- matrice_analytics/post_processing/utils/advanced_helper_utils.py +317 -0
- matrice_analytics/post_processing/utils/advanced_tracking_utils.py +461 -0
- matrice_analytics/post_processing/utils/alerting_utils.py +213 -0
- matrice_analytics/post_processing/utils/category_mapping_utils.py +94 -0
- matrice_analytics/post_processing/utils/color_utils.py +592 -0
- matrice_analytics/post_processing/utils/counting_utils.py +182 -0
- matrice_analytics/post_processing/utils/filter_utils.py +261 -0
- matrice_analytics/post_processing/utils/format_utils.py +293 -0
- matrice_analytics/post_processing/utils/geometry_utils.py +300 -0
- matrice_analytics/post_processing/utils/smoothing_utils.py +358 -0
- matrice_analytics/post_processing/utils/tracking_utils.py +234 -0
- matrice_analytics/py.typed +0 -0
- matrice_analytics-0.1.2.dist-info/METADATA +481 -0
- matrice_analytics-0.1.2.dist-info/RECORD +160 -0
- matrice_analytics-0.1.2.dist-info/WHEEL +5 -0
- matrice_analytics-0.1.2.dist-info/licenses/LICENSE.txt +21 -0
- 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
|