matrice-analytics 0.1.60__py3-none-any.whl → 0.1.89__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matrice_analytics/post_processing/config.py +2 -2
- matrice_analytics/post_processing/core/base.py +1 -1
- matrice_analytics/post_processing/face_reg/embedding_manager.py +8 -8
- matrice_analytics/post_processing/face_reg/face_recognition.py +886 -201
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +68 -2
- matrice_analytics/post_processing/usecases/advanced_customer_service.py +908 -498
- matrice_analytics/post_processing/usecases/color_detection.py +18 -18
- matrice_analytics/post_processing/usecases/customer_service.py +356 -9
- matrice_analytics/post_processing/usecases/fire_detection.py +149 -11
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +548 -40
- matrice_analytics/post_processing/usecases/people_counting.py +11 -11
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +34 -34
- matrice_analytics/post_processing/usecases/weapon_detection.py +98 -22
- matrice_analytics/post_processing/utils/alert_instance_utils.py +950 -0
- matrice_analytics/post_processing/utils/business_metrics_manager_utils.py +1245 -0
- matrice_analytics/post_processing/utils/incident_manager_utils.py +1657 -0
- {matrice_analytics-0.1.60.dist-info → matrice_analytics-0.1.89.dist-info}/METADATA +1 -1
- {matrice_analytics-0.1.60.dist-info → matrice_analytics-0.1.89.dist-info}/RECORD +21 -18
- {matrice_analytics-0.1.60.dist-info → matrice_analytics-0.1.89.dist-info}/WHEEL +0 -0
- {matrice_analytics-0.1.60.dist-info → matrice_analytics-0.1.89.dist-info}/licenses/LICENSE.txt +0 -0
- {matrice_analytics-0.1.60.dist-info → matrice_analytics-0.1.89.dist-info}/top_level.txt +0 -0
|
@@ -26,7 +26,10 @@ Configuration options:
|
|
|
26
26
|
import subprocess
|
|
27
27
|
import logging
|
|
28
28
|
import asyncio
|
|
29
|
+
import json
|
|
29
30
|
import os
|
|
31
|
+
import re
|
|
32
|
+
from pathlib import Path
|
|
30
33
|
log_file = open("pip_jetson_btii.log", "w")
|
|
31
34
|
cmd = ["pip", "install", "httpx"]
|
|
32
35
|
subprocess.run(
|
|
@@ -37,7 +40,7 @@ subprocess.run(
|
|
|
37
40
|
)
|
|
38
41
|
log_file.close()
|
|
39
42
|
|
|
40
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
43
|
+
from typing import Any, Dict, List, Optional, Tuple, NamedTuple
|
|
41
44
|
import time
|
|
42
45
|
import base64
|
|
43
46
|
import cv2
|
|
@@ -46,6 +49,20 @@ import threading
|
|
|
46
49
|
from datetime import datetime, timezone
|
|
47
50
|
from collections import deque
|
|
48
51
|
|
|
52
|
+
try:
|
|
53
|
+
from matrice_common.session import Session
|
|
54
|
+
HAS_MATRICE_SESSION = True
|
|
55
|
+
except ImportError:
|
|
56
|
+
Session = None
|
|
57
|
+
HAS_MATRICE_SESSION = False
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
import redis.asyncio as aioredis
|
|
61
|
+
HAS_AIREDIS = True
|
|
62
|
+
except ImportError:
|
|
63
|
+
aioredis = None
|
|
64
|
+
HAS_AIREDIS = False
|
|
65
|
+
|
|
49
66
|
from ..core.base import (
|
|
50
67
|
BaseProcessor,
|
|
51
68
|
ProcessingContext,
|
|
@@ -68,6 +85,7 @@ from .embedding_manager import EmbeddingManager, EmbeddingConfig
|
|
|
68
85
|
|
|
69
86
|
# ---- Lightweight identity tracking and temporal smoothing (adapted from compare_similarity.py) ---- #
|
|
70
87
|
from collections import deque, defaultdict
|
|
88
|
+
from matrice_common.session import Session
|
|
71
89
|
|
|
72
90
|
|
|
73
91
|
|
|
@@ -83,6 +101,543 @@ def _normalize_embedding(vec: List[float]) -> List[float]:
|
|
|
83
101
|
return arr.tolist()
|
|
84
102
|
|
|
85
103
|
|
|
104
|
+
class RedisFaceMatchResult(NamedTuple):
|
|
105
|
+
staff_id: Optional[str]
|
|
106
|
+
person_name: str
|
|
107
|
+
confidence: float
|
|
108
|
+
employee_id: Optional[str]
|
|
109
|
+
raw: Dict[str, Any]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class RedisFaceMatcher:
|
|
113
|
+
"""Handles Redis-based face similarity search."""
|
|
114
|
+
|
|
115
|
+
ACTION_ID_PATTERN = re.compile(r"^[0-9a-f]{8,}$", re.IGNORECASE)
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
session=None,
|
|
120
|
+
logger: Optional[logging.Logger] = None,
|
|
121
|
+
redis_url: Optional[str] = None,
|
|
122
|
+
face_client=None,
|
|
123
|
+
) -> None:
|
|
124
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
125
|
+
self._session = session
|
|
126
|
+
self.face_client = face_client
|
|
127
|
+
self.redis_url = (
|
|
128
|
+
redis_url
|
|
129
|
+
or os.getenv("FACE_RECOG_REDIS_URL")
|
|
130
|
+
or os.getenv("REDIS_URL")
|
|
131
|
+
)
|
|
132
|
+
self.stream_name = os.getenv(
|
|
133
|
+
"FACE_RECOG_REDIS_STREAM", "facial_detection_stream"
|
|
134
|
+
)
|
|
135
|
+
self.default_min_confidence = float(
|
|
136
|
+
os.getenv("FACE_RECOG_REDIS_MIN_CONFIDENCE", "0.01")
|
|
137
|
+
)
|
|
138
|
+
self.response_timeout = (
|
|
139
|
+
float(os.getenv("FACE_RECOG_REDIS_RESPONSE_TIMEOUT_MS", "200")) / 1000.0 # Reduced from 600ms to 200ms for faster failure
|
|
140
|
+
)
|
|
141
|
+
self.poll_interval = (
|
|
142
|
+
float(os.getenv("FACE_RECOG_REDIS_POLL_INTERVAL_MS", "5")) / 1000.0 # Reduced from 20ms to 5ms for faster polling
|
|
143
|
+
)
|
|
144
|
+
self.stream_maxlen = int(
|
|
145
|
+
os.getenv("FACE_RECOG_REDIS_STREAM_MAXLEN", "5000")
|
|
146
|
+
)
|
|
147
|
+
self._redis_client = None # type: ignore[assignment]
|
|
148
|
+
self._redis_connection_params: Optional[Dict[str, Any]] = None
|
|
149
|
+
self._app_deployment_id = os.getenv("APP_DEPLOYMENT_ID")
|
|
150
|
+
self._action_id = (
|
|
151
|
+
os.getenv("ACTION_ID")
|
|
152
|
+
or os.getenv("MATRISE_ACTION_ID")
|
|
153
|
+
or self._discover_action_id()
|
|
154
|
+
)
|
|
155
|
+
self._redis_server_id = os.getenv("REDIS_SERVER_ID")
|
|
156
|
+
self._app_dep_lock = asyncio.Lock()
|
|
157
|
+
self._session_lock = asyncio.Lock()
|
|
158
|
+
self._redis_lock = asyncio.Lock()
|
|
159
|
+
self._redis_warning_logged = False
|
|
160
|
+
|
|
161
|
+
def is_available(self) -> bool:
|
|
162
|
+
return HAS_AIREDIS
|
|
163
|
+
|
|
164
|
+
async def match_embedding(
|
|
165
|
+
self,
|
|
166
|
+
embedding: List[float],
|
|
167
|
+
search_id: Optional[str],
|
|
168
|
+
location: str = "",
|
|
169
|
+
min_confidence: Optional[float] = None,
|
|
170
|
+
) -> Optional[RedisFaceMatchResult]:
|
|
171
|
+
"""Send embedding to Redis stream and wait for match result."""
|
|
172
|
+
if not HAS_AIREDIS:
|
|
173
|
+
if not self._redis_warning_logged:
|
|
174
|
+
self.logger.warning(
|
|
175
|
+
"redis.asyncio not available; skipping Redis face matcher flow"
|
|
176
|
+
)
|
|
177
|
+
self._redis_warning_logged = True
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
embedding_list = self._prepare_embedding_list(embedding)
|
|
181
|
+
if not embedding_list:
|
|
182
|
+
self.logger.warning(f"Empty embedding list for search_id={search_id}, cannot send to Redis")
|
|
183
|
+
print(f"WARNING: Empty embedding list for search_id={search_id}, cannot send to Redis")
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
if len(embedding_list) == 0:
|
|
187
|
+
self.logger.warning(f"Embedding list has zero length for search_id={search_id}")
|
|
188
|
+
print(f"WARNING: Embedding list has zero length for search_id={search_id}")
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
app_dep_id = await self._ensure_app_deployment_id()
|
|
192
|
+
if not app_dep_id:
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
redis_client = await self._ensure_redis_client()
|
|
196
|
+
if redis_client is None:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
resolved_search_id = str(search_id or self._generate_search_id())
|
|
200
|
+
payload = {
|
|
201
|
+
"appDepId": app_dep_id,
|
|
202
|
+
"searchId": resolved_search_id,
|
|
203
|
+
"embedding": embedding_list,
|
|
204
|
+
"location": location or "",
|
|
205
|
+
"minConfidence": float(
|
|
206
|
+
min_confidence if min_confidence is not None else self.default_min_confidence
|
|
207
|
+
),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
self.logger.debug(
|
|
212
|
+
f"Sending embedding to Redis stream {self.stream_name} with search_id={resolved_search_id}, "
|
|
213
|
+
f"embedding_len={len(embedding_list)}, minConfidence={payload.get('minConfidence')}"
|
|
214
|
+
)
|
|
215
|
+
await redis_client.xadd(
|
|
216
|
+
self.stream_name,
|
|
217
|
+
{"data": json.dumps(payload, separators=(",", ":"))},
|
|
218
|
+
maxlen=self.stream_maxlen,
|
|
219
|
+
approximate=True,
|
|
220
|
+
)
|
|
221
|
+
self.logger.debug(f"Successfully sent embedding to Redis stream for search_id={resolved_search_id}")
|
|
222
|
+
except Exception as exc:
|
|
223
|
+
self.logger.error(
|
|
224
|
+
"Failed to enqueue face embedding to Redis stream %s: %s",
|
|
225
|
+
self.stream_name,
|
|
226
|
+
exc,
|
|
227
|
+
exc_info=True,
|
|
228
|
+
)
|
|
229
|
+
print(f"ERROR: Failed to send to Redis stream {self.stream_name}: {exc}")
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
result_key = f"{resolved_search_id}_{app_dep_id}"
|
|
233
|
+
deadline = time.monotonic() + self.response_timeout
|
|
234
|
+
poll_count = 0
|
|
235
|
+
start_poll_time = time.monotonic()
|
|
236
|
+
|
|
237
|
+
self.logger.debug(f"Waiting for Redis response with key={result_key}, timeout={self.response_timeout:.3f}s")
|
|
238
|
+
|
|
239
|
+
# Poll loop - check immediately first, then with intervals
|
|
240
|
+
while time.monotonic() < deadline:
|
|
241
|
+
try:
|
|
242
|
+
raw_value = await redis_client.get(result_key)
|
|
243
|
+
poll_count += 1
|
|
244
|
+
except Exception as exc:
|
|
245
|
+
self.logger.error(
|
|
246
|
+
"Failed to read Redis result for key %s: %s",
|
|
247
|
+
result_key,
|
|
248
|
+
exc,
|
|
249
|
+
exc_info=True,
|
|
250
|
+
)
|
|
251
|
+
print(f"ERROR: Failed to read Redis result for key {result_key}: {exc}")
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
if raw_value:
|
|
255
|
+
await redis_client.delete(result_key)
|
|
256
|
+
try:
|
|
257
|
+
parsed = json.loads(raw_value)
|
|
258
|
+
except Exception as exc:
|
|
259
|
+
parsed = json.loads(raw_value)
|
|
260
|
+
self.logger.error(
|
|
261
|
+
"Unable to parse Redis face match response: %s",
|
|
262
|
+
exc,
|
|
263
|
+
exc_info=True,
|
|
264
|
+
)
|
|
265
|
+
print(f"ERROR: Unable to parse Redis face match response: {exc}")
|
|
266
|
+
#return None
|
|
267
|
+
|
|
268
|
+
# Log and print the raw Redis response for debugging
|
|
269
|
+
self.logger.info(f"Redis raw response for search_id={resolved_search_id}: {parsed}")
|
|
270
|
+
print(f"Redis raw response for search_id={resolved_search_id}: {parsed}")
|
|
271
|
+
|
|
272
|
+
match_data = None
|
|
273
|
+
if isinstance(parsed, list) and parsed:
|
|
274
|
+
match_data = parsed[0]
|
|
275
|
+
self.logger.info(f"Redis response is array, extracted first element: {match_data}")
|
|
276
|
+
print(f"Redis response is array, extracted first element: {match_data}")
|
|
277
|
+
elif isinstance(parsed, dict):
|
|
278
|
+
match_data = parsed
|
|
279
|
+
self.logger.info(f"Redis response is dict: {match_data}")
|
|
280
|
+
print(f"Redis response is dict: {match_data}")
|
|
281
|
+
else:
|
|
282
|
+
self.logger.warning(f"Redis response is neither list nor dict: {type(parsed)}, value: {parsed}")
|
|
283
|
+
print(f"WARNING: Redis response is neither list nor dict: {type(parsed)}, value: {parsed}")
|
|
284
|
+
|
|
285
|
+
if not isinstance(match_data, dict):
|
|
286
|
+
self.logger.warning(f"match_data is not a dict after extraction: {type(match_data)}, value: {match_data}")
|
|
287
|
+
print(f"WARNING: match_data is not a dict after extraction: {type(match_data)}, value: {match_data}")
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
staff_id = match_data.get("staffId") or match_data.get("staff_id")
|
|
291
|
+
if not staff_id:
|
|
292
|
+
self.logger.warning(f"No staffId found in match_data: {match_data}")
|
|
293
|
+
print(f"WARNING: No staffId found in match_data: {match_data}")
|
|
294
|
+
return None
|
|
295
|
+
person_name = str(match_data.get("name") or "Unknown")
|
|
296
|
+
confidence = float(match_data.get("conf") or match_data.get("confidence") or 0.0)
|
|
297
|
+
employee_id = match_data.get("employeeId") or match_data.get("embeddingId")
|
|
298
|
+
|
|
299
|
+
# Log the extracted values
|
|
300
|
+
self.logger.info(
|
|
301
|
+
f"Redis match extracted - staff_id={staff_id}, person_name={person_name}, "
|
|
302
|
+
f"confidence={confidence}, employee_id={employee_id}"
|
|
303
|
+
)
|
|
304
|
+
print(
|
|
305
|
+
f"Redis match extracted - staff_id={staff_id}, person_name={person_name}, "
|
|
306
|
+
f"confidence={confidence}, employee_id={employee_id}"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Check confidence threshold before returning
|
|
310
|
+
min_conf = float(min_confidence if min_confidence is not None else self.default_min_confidence)
|
|
311
|
+
if confidence < min_conf:
|
|
312
|
+
self.logger.debug(
|
|
313
|
+
f"Redis match confidence {confidence:.3f} below threshold {min_conf:.3f}, rejecting"
|
|
314
|
+
)
|
|
315
|
+
print(f"Redis match confidence {confidence:.3f} below threshold {min_conf:.3f}, rejecting")
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
result = RedisFaceMatchResult(
|
|
319
|
+
staff_id=str(staff_id),
|
|
320
|
+
person_name=person_name,
|
|
321
|
+
confidence=round(confidence, 3),
|
|
322
|
+
employee_id=str(employee_id) if employee_id else None,
|
|
323
|
+
raw=match_data,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
poll_time = (time.monotonic() - start_poll_time) * 1000.0
|
|
327
|
+
self.logger.info(
|
|
328
|
+
f"Redis match result created (polls={poll_count}, poll_time={poll_time:.2f}ms): "
|
|
329
|
+
f"staff_id={result.staff_id}, name={result.person_name}, conf={result.confidence}"
|
|
330
|
+
)
|
|
331
|
+
print(
|
|
332
|
+
f"Redis match result created (polls={poll_count}, poll_time={poll_time:.2f}ms): "
|
|
333
|
+
f"staff_id={result.staff_id}, name={result.person_name}, conf={result.confidence}"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return result
|
|
337
|
+
|
|
338
|
+
# Use shorter sleep for faster response (already reduced poll_interval to 5ms)
|
|
339
|
+
await asyncio.sleep(self.poll_interval)
|
|
340
|
+
|
|
341
|
+
poll_time = (time.monotonic() - start_poll_time) * 1000.0
|
|
342
|
+
self.logger.warning(
|
|
343
|
+
"Timed out waiting for Redis face match result for key %s (timeout=%.3fs, polls=%d, poll_time=%.2fms)",
|
|
344
|
+
result_key,
|
|
345
|
+
self.response_timeout,
|
|
346
|
+
poll_count,
|
|
347
|
+
poll_time,
|
|
348
|
+
)
|
|
349
|
+
print(
|
|
350
|
+
f"WARNING: Redis timeout for search_id={resolved_search_id} "
|
|
351
|
+
f"(timeout={self.response_timeout:.3f}s, polls={poll_count}, poll_time={poll_time:.2f}ms)"
|
|
352
|
+
)
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
def _prepare_embedding_list(self, embedding: List[float]) -> List[float]:
|
|
356
|
+
if isinstance(embedding, np.ndarray):
|
|
357
|
+
return embedding.astype(np.float32).tolist()
|
|
358
|
+
prepared = []
|
|
359
|
+
try:
|
|
360
|
+
for value in embedding:
|
|
361
|
+
prepared.append(float(value))
|
|
362
|
+
except Exception:
|
|
363
|
+
self.logger.debug("Failed to convert embedding to float list", exc_info=True)
|
|
364
|
+
return []
|
|
365
|
+
return prepared
|
|
366
|
+
|
|
367
|
+
def _generate_search_id(self) -> str:
|
|
368
|
+
return f"face_{int(time.time() * 1000)}"
|
|
369
|
+
|
|
370
|
+
async def _ensure_app_deployment_id(self) -> Optional[str]:
|
|
371
|
+
if self._app_deployment_id:
|
|
372
|
+
return self._app_deployment_id
|
|
373
|
+
|
|
374
|
+
async with self._app_dep_lock:
|
|
375
|
+
if self._app_deployment_id:
|
|
376
|
+
return self._app_deployment_id
|
|
377
|
+
|
|
378
|
+
action_id = self._action_id or self._discover_action_id()
|
|
379
|
+
if not action_id:
|
|
380
|
+
self.logger.warning(
|
|
381
|
+
"Unable to determine action_id for Redis face matcher"
|
|
382
|
+
)
|
|
383
|
+
return None
|
|
384
|
+
|
|
385
|
+
session = await self._ensure_session()
|
|
386
|
+
if session is None:
|
|
387
|
+
return None
|
|
388
|
+
|
|
389
|
+
response = await asyncio.to_thread(
|
|
390
|
+
self._fetch_action_details_sync, session, action_id
|
|
391
|
+
)
|
|
392
|
+
if not response or not response.get("success", False):
|
|
393
|
+
self.logger.warning(
|
|
394
|
+
"Failed to fetch action details for action_id=%s", action_id
|
|
395
|
+
)
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
action_doc = response.get("data", {})
|
|
399
|
+
action_details = action_doc.get("actionDetails", {})
|
|
400
|
+
app_dep_id = (
|
|
401
|
+
action_details.get("app_deployment_id")
|
|
402
|
+
or action_details.get("appDepId")
|
|
403
|
+
)
|
|
404
|
+
redis_server_id = (
|
|
405
|
+
action_details.get("redis_server_id")
|
|
406
|
+
or action_details.get("redisServerId")
|
|
407
|
+
or action_details.get("redis_serverid")
|
|
408
|
+
or action_details.get("redisServerID")
|
|
409
|
+
)
|
|
410
|
+
if not app_dep_id:
|
|
411
|
+
self.logger.warning(
|
|
412
|
+
"app_deployment_id missing in action details for action_id=%s",
|
|
413
|
+
action_id,
|
|
414
|
+
)
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
self._app_deployment_id = str(app_dep_id)
|
|
418
|
+
if redis_server_id:
|
|
419
|
+
self._redis_server_id = str(redis_server_id)
|
|
420
|
+
self.logger.info(
|
|
421
|
+
"Resolved app deployment id %s for action_id=%s",
|
|
422
|
+
self._app_deployment_id,
|
|
423
|
+
action_id,
|
|
424
|
+
)
|
|
425
|
+
return self._app_deployment_id
|
|
426
|
+
|
|
427
|
+
async def _ensure_session(self):
|
|
428
|
+
if self._session or not HAS_MATRICE_SESSION:
|
|
429
|
+
if not self._session and not HAS_MATRICE_SESSION:
|
|
430
|
+
self.logger.warning(
|
|
431
|
+
"matrice_common.session unavailable; cannot create RPC session for Redis matcher"
|
|
432
|
+
)
|
|
433
|
+
return self._session
|
|
434
|
+
|
|
435
|
+
async with self._session_lock:
|
|
436
|
+
if self._session:
|
|
437
|
+
return self._session
|
|
438
|
+
|
|
439
|
+
access_key = os.getenv("MATRICE_ACCESS_KEY_ID")
|
|
440
|
+
secret_key = os.getenv("MATRICE_SECRET_ACCESS_KEY")
|
|
441
|
+
account_number = os.getenv("MATRICE_ACCOUNT_NUMBER", "")
|
|
442
|
+
|
|
443
|
+
if not access_key or not secret_key:
|
|
444
|
+
self.logger.warning(
|
|
445
|
+
"Missing Matrice credentials; cannot initialize session for Redis matcher"
|
|
446
|
+
)
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
self._session = Session(
|
|
451
|
+
account_number=account_number,
|
|
452
|
+
access_key=access_key,
|
|
453
|
+
secret_key=secret_key,
|
|
454
|
+
)
|
|
455
|
+
self.logger.info("Initialized Matrice session for Redis face matcher")
|
|
456
|
+
except Exception as exc:
|
|
457
|
+
self.logger.error(
|
|
458
|
+
"Failed to initialize Matrice session for Redis matcher: %s",
|
|
459
|
+
exc,
|
|
460
|
+
exc_info=True,
|
|
461
|
+
)
|
|
462
|
+
self._session = None
|
|
463
|
+
|
|
464
|
+
return self._session
|
|
465
|
+
|
|
466
|
+
async def _ensure_redis_client(self):
|
|
467
|
+
if self._redis_client:
|
|
468
|
+
return self._redis_client
|
|
469
|
+
|
|
470
|
+
async with self._redis_lock:
|
|
471
|
+
if self._redis_client:
|
|
472
|
+
return self._redis_client
|
|
473
|
+
|
|
474
|
+
if not self.redis_url:
|
|
475
|
+
host = os.getenv("FACE_RECOG_REDIS_HOST")
|
|
476
|
+
port = os.getenv("FACE_RECOG_REDIS_PORT")
|
|
477
|
+
if host and port:
|
|
478
|
+
self.redis_url = f"redis://{host}:{port}/0"
|
|
479
|
+
|
|
480
|
+
if self.redis_url:
|
|
481
|
+
try:
|
|
482
|
+
self._redis_client = aioredis.from_url(
|
|
483
|
+
self.redis_url,
|
|
484
|
+
decode_responses=True,
|
|
485
|
+
health_check_interval=30,
|
|
486
|
+
)
|
|
487
|
+
self.logger.info(
|
|
488
|
+
"Connected Redis face matcher client to %s (stream=%s)",
|
|
489
|
+
self.redis_url,
|
|
490
|
+
self.stream_name,
|
|
491
|
+
)
|
|
492
|
+
return self._redis_client
|
|
493
|
+
except Exception as exc:
|
|
494
|
+
self.logger.error(
|
|
495
|
+
"Failed to connect to Redis at %s: %s",
|
|
496
|
+
self.redis_url,
|
|
497
|
+
exc,
|
|
498
|
+
exc_info=True,
|
|
499
|
+
)
|
|
500
|
+
self._redis_client = None
|
|
501
|
+
|
|
502
|
+
conn_params = await self._ensure_redis_connection_params()
|
|
503
|
+
if not conn_params:
|
|
504
|
+
self.logger.error(
|
|
505
|
+
"Redis connection parameters unavailable. Configure FACE_RECOG_REDIS_URL or ensure redis_server_id is set."
|
|
506
|
+
)
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
try:
|
|
510
|
+
self._redis_client = aioredis.Redis(
|
|
511
|
+
host=conn_params.get("host"),
|
|
512
|
+
port=conn_params.get("port", 6379),
|
|
513
|
+
username=conn_params.get("username"),
|
|
514
|
+
password=conn_params.get("password") or None,
|
|
515
|
+
db=conn_params.get("db", 0),
|
|
516
|
+
ssl=conn_params.get("ssl", False),
|
|
517
|
+
decode_responses=True,
|
|
518
|
+
socket_connect_timeout=conn_params.get("connection_timeout", 120),
|
|
519
|
+
socket_timeout=conn_params.get("socket_timeout", 120),
|
|
520
|
+
retry_on_timeout=True,
|
|
521
|
+
health_check_interval=30,
|
|
522
|
+
)
|
|
523
|
+
self.logger.info(
|
|
524
|
+
"Connected Redis face matcher client to %s:%s (db=%s, stream=%s)",
|
|
525
|
+
conn_params.get("host"),
|
|
526
|
+
conn_params.get("port"),
|
|
527
|
+
conn_params.get("db"),
|
|
528
|
+
self.stream_name,
|
|
529
|
+
)
|
|
530
|
+
except Exception as exc:
|
|
531
|
+
self.logger.error(
|
|
532
|
+
"Failed to create Redis client with fetched parameters: %s",
|
|
533
|
+
exc,
|
|
534
|
+
exc_info=True,
|
|
535
|
+
)
|
|
536
|
+
self._redis_client = None
|
|
537
|
+
|
|
538
|
+
return self._redis_client
|
|
539
|
+
|
|
540
|
+
async def _ensure_redis_connection_params(self) -> Optional[Dict[str, Any]]:
|
|
541
|
+
if self._redis_connection_params:
|
|
542
|
+
return self._redis_connection_params
|
|
543
|
+
|
|
544
|
+
if not self.face_client:
|
|
545
|
+
self.logger.warning(
|
|
546
|
+
"Cannot fetch Redis connection parameters without face_client"
|
|
547
|
+
)
|
|
548
|
+
return None
|
|
549
|
+
|
|
550
|
+
await self._ensure_app_deployment_id()
|
|
551
|
+
|
|
552
|
+
try:
|
|
553
|
+
response = await self.face_client.get_redis_details()
|
|
554
|
+
except Exception as exc:
|
|
555
|
+
self.logger.error(
|
|
556
|
+
"Failed to fetch Redis details from facial recognition server: %s",
|
|
557
|
+
exc,
|
|
558
|
+
exc_info=True,
|
|
559
|
+
)
|
|
560
|
+
return None
|
|
561
|
+
|
|
562
|
+
if not response or not response.get("success", False):
|
|
563
|
+
self.logger.warning(
|
|
564
|
+
"Redis details API returned failure: %s",
|
|
565
|
+
response,
|
|
566
|
+
)
|
|
567
|
+
return None
|
|
568
|
+
|
|
569
|
+
data = response.get("data", {})
|
|
570
|
+
host = data.get("REDIS_IP")
|
|
571
|
+
port = data.get("REDIS_PORT")
|
|
572
|
+
password = data.get("REDIS_PASSWORD")
|
|
573
|
+
|
|
574
|
+
if not host or not port:
|
|
575
|
+
self.logger.warning(
|
|
576
|
+
"Redis details missing REDIS_IP or REDIS_PORT"
|
|
577
|
+
)
|
|
578
|
+
return None
|
|
579
|
+
|
|
580
|
+
try:
|
|
581
|
+
params = {
|
|
582
|
+
"host": host,
|
|
583
|
+
"port": int(port),
|
|
584
|
+
"password": password or None,
|
|
585
|
+
"username": None,
|
|
586
|
+
"db": 0,
|
|
587
|
+
"connection_timeout": 120,
|
|
588
|
+
"socket_timeout": 120,
|
|
589
|
+
"ssl": False,
|
|
590
|
+
}
|
|
591
|
+
except Exception as exc:
|
|
592
|
+
self.logger.error(
|
|
593
|
+
"Invalid Redis connection config: %s",
|
|
594
|
+
exc,
|
|
595
|
+
exc_info=True,
|
|
596
|
+
)
|
|
597
|
+
return None
|
|
598
|
+
|
|
599
|
+
self._redis_connection_params = params
|
|
600
|
+
return self._redis_connection_params
|
|
601
|
+
|
|
602
|
+
@classmethod
|
|
603
|
+
def _discover_action_id(cls) -> Optional[str]:
|
|
604
|
+
candidates: List[str] = []
|
|
605
|
+
try:
|
|
606
|
+
cwd = Path.cwd()
|
|
607
|
+
candidates.append(cwd.name)
|
|
608
|
+
for parent in cwd.parents:
|
|
609
|
+
candidates.append(parent.name)
|
|
610
|
+
except Exception:
|
|
611
|
+
pass
|
|
612
|
+
|
|
613
|
+
try:
|
|
614
|
+
usr_src = Path("/usr/src")
|
|
615
|
+
if usr_src.exists():
|
|
616
|
+
for child in usr_src.iterdir():
|
|
617
|
+
if child.is_dir():
|
|
618
|
+
candidates.append(child.name)
|
|
619
|
+
except Exception:
|
|
620
|
+
pass
|
|
621
|
+
|
|
622
|
+
for candidate in candidates:
|
|
623
|
+
if candidate and len(candidate) >= 8 and cls.ACTION_ID_PATTERN.match(candidate):
|
|
624
|
+
return candidate
|
|
625
|
+
return None
|
|
626
|
+
|
|
627
|
+
def _fetch_action_details_sync(self, session, action_id: str) -> Optional[Dict[str, Any]]:
|
|
628
|
+
url = f"/v1/actions/action/{action_id}/details"
|
|
629
|
+
try:
|
|
630
|
+
return session.rpc.get(url)
|
|
631
|
+
except Exception as exc:
|
|
632
|
+
self.logger.error(
|
|
633
|
+
"Failed to fetch action details for action_id=%s: %s",
|
|
634
|
+
action_id,
|
|
635
|
+
exc,
|
|
636
|
+
exc_info=True,
|
|
637
|
+
)
|
|
638
|
+
return None
|
|
639
|
+
|
|
640
|
+
|
|
86
641
|
## Removed FaceTracker fallback (using AdvancedTracker only)
|
|
87
642
|
|
|
88
643
|
|
|
@@ -97,8 +652,9 @@ class TemporalIdentityManager:
|
|
|
97
652
|
def __init__(
|
|
98
653
|
self,
|
|
99
654
|
face_client: FacialRecognitionClient,
|
|
100
|
-
embedding_manager
|
|
101
|
-
|
|
655
|
+
embedding_manager=None,
|
|
656
|
+
redis_matcher: Optional[RedisFaceMatcher] = None,
|
|
657
|
+
recognition_threshold: float = 0.3,
|
|
102
658
|
history_size: int = 20,
|
|
103
659
|
unknown_patience: int = 7,
|
|
104
660
|
switch_patience: int = 5,
|
|
@@ -107,12 +663,14 @@ class TemporalIdentityManager:
|
|
|
107
663
|
self.logger = logging.getLogger(__name__)
|
|
108
664
|
self.face_client = face_client
|
|
109
665
|
self.embedding_manager = embedding_manager
|
|
666
|
+
self.redis_matcher = redis_matcher
|
|
110
667
|
self.threshold = float(recognition_threshold)
|
|
111
668
|
self.history_size = int(history_size)
|
|
112
669
|
self.unknown_patience = int(unknown_patience)
|
|
113
670
|
self.switch_patience = int(switch_patience)
|
|
114
671
|
self.fallback_margin = float(fallback_margin)
|
|
115
672
|
self.tracks: Dict[Any, Dict[str, object]] = {}
|
|
673
|
+
self.emb_run=False
|
|
116
674
|
|
|
117
675
|
def _ensure_track(self, track_id: Any) -> None:
|
|
118
676
|
if track_id not in self.tracks:
|
|
@@ -128,7 +686,13 @@ class TemporalIdentityManager:
|
|
|
128
686
|
"streaks": defaultdict(int), # staff_id -> consecutive frames
|
|
129
687
|
}
|
|
130
688
|
|
|
131
|
-
async def _compute_best_identity(
|
|
689
|
+
async def _compute_best_identity(
|
|
690
|
+
self,
|
|
691
|
+
emb: List[float],
|
|
692
|
+
location: str = "",
|
|
693
|
+
timestamp: str = "",
|
|
694
|
+
search_id: Optional[str] = None,
|
|
695
|
+
) -> Tuple[Optional[str], str, float, Optional[str], Dict[str, Any], str]:
|
|
132
696
|
"""
|
|
133
697
|
Find best identity match using local similarity search (fast) with optional API fallback.
|
|
134
698
|
Returns (staff_id, person_name, score, employee_id, staff_details, detection_type).
|
|
@@ -138,11 +702,86 @@ class TemporalIdentityManager:
|
|
|
138
702
|
"""
|
|
139
703
|
if not emb or not isinstance(emb, list):
|
|
140
704
|
return None, "Unknown", 0.0, None, {}, "unknown"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
705
|
+
|
|
706
|
+
#-------------- New Redis API Fast Call Start------------------------------------------------------------------------------------------------------------------------------
|
|
707
|
+
# ALWAYS attempt Redis match for every detection (required for every frame)
|
|
708
|
+
if self.redis_matcher:
|
|
709
|
+
try:
|
|
710
|
+
self.logger.debug(f"Attempting Redis match for search_id={search_id}, embedding_len={len(emb) if emb else 0}")
|
|
711
|
+
redis_start_time = time.time()
|
|
712
|
+
redis_match = await self.redis_matcher.match_embedding(
|
|
713
|
+
embedding=emb,
|
|
714
|
+
search_id=search_id,
|
|
715
|
+
location=location or "",
|
|
716
|
+
min_confidence=self.threshold, # Use recognition threshold instead of default_min_confidence
|
|
717
|
+
)
|
|
718
|
+
redis_latency_ms = (time.time() - redis_start_time) * 1000.0
|
|
719
|
+
|
|
720
|
+
if redis_match:
|
|
721
|
+
self.logger.info(
|
|
722
|
+
f"Redis match found in {redis_latency_ms:.2f}ms - staff_id={redis_match.staff_id}, "
|
|
723
|
+
f"person_name={redis_match.person_name}, confidence={redis_match.confidence:.3f}"
|
|
724
|
+
)
|
|
725
|
+
print(
|
|
726
|
+
f"Redis match found in {redis_latency_ms:.2f}ms - staff_id={redis_match.staff_id}, "
|
|
727
|
+
f"person_name={redis_match.person_name}, confidence={redis_match.confidence:.3f}"
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
if redis_match.staff_id:
|
|
731
|
+
staff_details = (
|
|
732
|
+
dict(redis_match.raw) if isinstance(redis_match.raw, dict) else {}
|
|
733
|
+
)
|
|
734
|
+
if redis_match.person_name and not staff_details.get("name"):
|
|
735
|
+
staff_details["name"] = redis_match.person_name
|
|
736
|
+
|
|
737
|
+
# Check if confidence meets threshold
|
|
738
|
+
if float(redis_match.confidence) >= self.threshold:
|
|
739
|
+
self.logger.info(
|
|
740
|
+
"Redis embedding match ACCEPTED - staff_id=%s, person_name=%s, score=%.3f (threshold=%.3f)",
|
|
741
|
+
redis_match.staff_id,
|
|
742
|
+
redis_match.person_name,
|
|
743
|
+
float(redis_match.confidence),
|
|
744
|
+
self.threshold,
|
|
745
|
+
)
|
|
746
|
+
print(
|
|
747
|
+
f"Redis embedding match ACCEPTED - staff_id={redis_match.staff_id}, "
|
|
748
|
+
f"person_name={redis_match.person_name}, score={redis_match.confidence:.3f} "
|
|
749
|
+
f"(threshold={self.threshold:.3f})"
|
|
750
|
+
)
|
|
751
|
+
return (
|
|
752
|
+
str(redis_match.staff_id),
|
|
753
|
+
redis_match.person_name or "Unknown",
|
|
754
|
+
float(redis_match.confidence),
|
|
755
|
+
redis_match.employee_id,
|
|
756
|
+
staff_details,
|
|
757
|
+
"known",
|
|
758
|
+
)
|
|
759
|
+
else:
|
|
760
|
+
self.logger.debug(
|
|
761
|
+
"Redis embedding match REJECTED - confidence %.3f below threshold %.3f",
|
|
762
|
+
float(redis_match.confidence),
|
|
763
|
+
self.threshold,
|
|
764
|
+
)
|
|
765
|
+
print(
|
|
766
|
+
f"Redis embedding match REJECTED - confidence {redis_match.confidence:.3f} "
|
|
767
|
+
f"below threshold {self.threshold:.3f}"
|
|
768
|
+
)
|
|
769
|
+
else:
|
|
770
|
+
self.logger.warning("Redis match returned but staff_id is None/empty")
|
|
771
|
+
print("WARNING: Redis match returned but staff_id is None/empty")
|
|
772
|
+
else:
|
|
773
|
+
self.logger.debug(f"No Redis match found for search_id={search_id} (took {redis_latency_ms:.2f}ms)")
|
|
774
|
+
print(f"No Redis match found for search_id={search_id} (took {redis_latency_ms:.2f}ms)")
|
|
775
|
+
except Exception as exc:
|
|
776
|
+
self.logger.warning(
|
|
777
|
+
"Redis face match flow failed; falling back to local search: %s",
|
|
778
|
+
exc,
|
|
779
|
+
exc_info=True,
|
|
780
|
+
)
|
|
781
|
+
print(f"Redis face match flow failed: {exc}")
|
|
782
|
+
#-------------- New Redis API Fast Call END------------------------------------------------------------------------------------------------------------------------------
|
|
144
783
|
# PRIMARY PATH: Local similarity search using EmbeddingManager (FAST - ~1-5ms)
|
|
145
|
-
if self.embedding_manager:
|
|
784
|
+
if self.embedding_manager and self.emb_run:
|
|
146
785
|
# Defensive check: ensure embeddings are loaded before attempting search
|
|
147
786
|
if not self.embedding_manager.is_ready():
|
|
148
787
|
status = self.embedding_manager.get_status()
|
|
@@ -169,10 +808,6 @@ class TemporalIdentityManager:
|
|
|
169
808
|
elif first_name or last_name:
|
|
170
809
|
person_name = f"{first_name or ''} {last_name or ''}".strip() or "Unknown"
|
|
171
810
|
|
|
172
|
-
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY (LOCAL)----------------------------")
|
|
173
|
-
# print("LATENCY:",(time.time() - st10)*1000,"| Throughput fps:",(1.0 / (time.time() - st10)) if (time.time() - st10) > 0 else None)
|
|
174
|
-
# print(f"LOCAL MATCH: staff_id={staff_embedding.staff_id}, similarity={similarity_score:.3f}")
|
|
175
|
-
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY (LOCAL)----------------------------")
|
|
176
811
|
|
|
177
812
|
self.logger.info(f"Local embedding match - staff_id={staff_embedding.staff_id}, person_name={person_name}, score={similarity_score:.3f}")
|
|
178
813
|
|
|
@@ -192,10 +827,6 @@ class TemporalIdentityManager:
|
|
|
192
827
|
except Exception:
|
|
193
828
|
pass
|
|
194
829
|
self.logger.debug(f"No local match found - best_similarity={best_sim:.3f}, threshold={self.threshold:.3f}")
|
|
195
|
-
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY (LOCAL - NO MATCH)----------------------------")
|
|
196
|
-
# print("LATENCY:",(time.time() - st10)*1000,"| Throughput fps:",(1.0 / (time.time() - st10)) if (time.time() - st10) > 0 else None)
|
|
197
|
-
# print(f"BEST_SIM={best_sim:.3f} THRESH={self.threshold:.3f}")
|
|
198
|
-
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY (LOCAL - NO MATCH)----------------------------")
|
|
199
830
|
|
|
200
831
|
return None, "Unknown", 0.0, None, {}, "unknown"
|
|
201
832
|
|
|
@@ -203,74 +834,83 @@ class TemporalIdentityManager:
|
|
|
203
834
|
self.logger.warning(f"Local similarity search failed, falling back to API: {e}")
|
|
204
835
|
# Fall through to API call below
|
|
205
836
|
|
|
837
|
+
#---------------------------------BACKUP MONGODB API SLOW CALL--------------------------------------------------------------------------------------
|
|
206
838
|
# FALLBACK PATH: API call (SLOW - ~2000ms) - only if embedding manager not available
|
|
207
839
|
# This path should rarely be used in production
|
|
208
|
-
try:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
# print("LATENCY:",(time.time() - st10)*1000,"| Throughput fps:",(1.0 / (time.time() - st10)) if (time.time() - st10) > 0 else None)
|
|
220
|
-
# print("WARNING: Using slow API fallback!")
|
|
221
|
-
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY (API FALLBACK)----------------------------")
|
|
840
|
+
# try:
|
|
841
|
+
# self.logger.warning("Using slow API fallback for identity search - consider checking embedding manager initialization")
|
|
842
|
+
# resp = await self.face_client.search_similar_faces(
|
|
843
|
+
# face_embedding=emb,
|
|
844
|
+
# threshold=0.01, # low threshold to always get top-1
|
|
845
|
+
# limit=1,
|
|
846
|
+
# collection="staff_enrollment",
|
|
847
|
+
# location=location,
|
|
848
|
+
# timestamp=timestamp,
|
|
849
|
+
# )
|
|
850
|
+
|
|
222
851
|
|
|
223
|
-
except Exception as e:
|
|
224
|
-
|
|
225
|
-
|
|
852
|
+
# except Exception as e:
|
|
853
|
+
# self.logger.error(f"API ERROR: Failed to search similar faces in _compute_best_identity: {e}", exc_info=True)
|
|
854
|
+
# return None, "Unknown", 0.0, None, {}, "unknown"
|
|
226
855
|
|
|
227
|
-
try:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
except Exception as e:
|
|
270
|
-
|
|
271
|
-
|
|
856
|
+
# try:
|
|
857
|
+
# results: List[Any] = []
|
|
858
|
+
# self.logger.debug('API Response received for identity search')
|
|
859
|
+
# if isinstance(resp, dict):
|
|
860
|
+
# if isinstance(resp.get("data"), list):
|
|
861
|
+
# results = resp.get("data", [])
|
|
862
|
+
# elif isinstance(resp.get("results"), list):
|
|
863
|
+
# results = resp.get("results", [])
|
|
864
|
+
# elif isinstance(resp.get("items"), list):
|
|
865
|
+
# results = resp.get("items", [])
|
|
866
|
+
# elif isinstance(resp, list):
|
|
867
|
+
# results = resp
|
|
868
|
+
|
|
869
|
+
# if not results:
|
|
870
|
+
# self.logger.debug("No identity match found from API")
|
|
871
|
+
# return None, "Unknown", 0.0, None, {}, "unknown"
|
|
872
|
+
|
|
873
|
+
# item = results[0] if isinstance(results, list) else results
|
|
874
|
+
# self.logger.debug(f'Top-1 match from API: {item}')
|
|
875
|
+
# # Be defensive with keys and types
|
|
876
|
+
# staff_id = item.get("staffId") if isinstance(item, dict) else None
|
|
877
|
+
# employee_id = str(item.get("_id")) if isinstance(item, dict) and item.get("_id") is not None else None
|
|
878
|
+
# score = float(item.get("score", 0.0)) if isinstance(item, dict) else 0.0
|
|
879
|
+
# detection_type = str(item.get("detectionType", "unknown")) if isinstance(item, dict) else "unknown"
|
|
880
|
+
# staff_details = item.get("staffDetails", {}) if isinstance(item, dict) else {}
|
|
881
|
+
# # Extract a person name from staff_details
|
|
882
|
+
# person_name = "Unknown"
|
|
883
|
+
# if isinstance(staff_details, dict) and staff_details:
|
|
884
|
+
# first_name = staff_details.get("firstName")
|
|
885
|
+
# last_name = staff_details.get("lastName")
|
|
886
|
+
# name = staff_details.get("name")
|
|
887
|
+
# if name:
|
|
888
|
+
# person_name = str(name)
|
|
889
|
+
# else:
|
|
890
|
+
# if first_name or last_name:
|
|
891
|
+
# person_name = f"{first_name or ''} {last_name or ''}".strip() or "UnknowNN" #TODO:ebugging change to normal once done
|
|
892
|
+
# # If API says unknown or missing staff_id, treat as unknown
|
|
893
|
+
# if not staff_id: #or detection_type == "unknown"
|
|
894
|
+
# self.logger.debug(f"API returned unknown or missing staff_id - score={score}, employee_id={employee_id}")
|
|
895
|
+
# return None, "Unknown", float(score), employee_id, staff_details if isinstance(staff_details, dict) else {}, "unknown"
|
|
896
|
+
# self.logger.info(f"API identified face - staff_id={staff_id}, person_name={person_name}, score={score:.3f}")
|
|
897
|
+
# return str(staff_id), person_name, float(score), employee_id, staff_details if isinstance(staff_details, dict) else {}, "known"
|
|
898
|
+
# except Exception as e:
|
|
899
|
+
# self.logger.error(f"Error parsing API response in _compute_best_identity: {e}", exc_info=True)
|
|
900
|
+
# return None, "Unknown", 0.0, None, {}, "unknown"
|
|
901
|
+
#---------------------------------BACKUP MONGODB API SLOW CALL--------------------------------------------------------------------------------------
|
|
902
|
+
|
|
903
|
+
# If we reach here, no match was found through any method
|
|
904
|
+
self.logger.debug("No identity match found - returning unknown")
|
|
905
|
+
return None, "Unknown", 0.0, None, {}, "unknown"
|
|
272
906
|
|
|
273
|
-
async def _compute_best_identity_from_history(
|
|
907
|
+
async def _compute_best_identity_from_history(
|
|
908
|
+
self,
|
|
909
|
+
track_state: Dict[str, object],
|
|
910
|
+
location: str = "",
|
|
911
|
+
timestamp: str = "",
|
|
912
|
+
search_id: Optional[str] = None,
|
|
913
|
+
) -> Tuple[Optional[str], str, float, Optional[str], Dict[str, Any], str]:
|
|
274
914
|
hist: deque = track_state.get("embedding_history", deque()) # type: ignore
|
|
275
915
|
if not hist:
|
|
276
916
|
return None, "Unknown", 0.0, None, {}, "unknown"
|
|
@@ -281,7 +921,12 @@ class TemporalIdentityManager:
|
|
|
281
921
|
except Exception as e:
|
|
282
922
|
self.logger.error(f"Error computing prototype from history: {e}", exc_info=True)
|
|
283
923
|
proto_list = []
|
|
284
|
-
return await self._compute_best_identity(
|
|
924
|
+
return await self._compute_best_identity(
|
|
925
|
+
proto_list,
|
|
926
|
+
location=location,
|
|
927
|
+
timestamp=timestamp,
|
|
928
|
+
search_id=search_id,
|
|
929
|
+
)
|
|
285
930
|
|
|
286
931
|
async def update(
|
|
287
932
|
self,
|
|
@@ -290,6 +935,7 @@ class TemporalIdentityManager:
|
|
|
290
935
|
eligible_for_recognition: bool,
|
|
291
936
|
location: str = "",
|
|
292
937
|
timestamp: str = "",
|
|
938
|
+
search_id: Optional[str] = None,
|
|
293
939
|
) -> Tuple[Optional[str], str, float, Optional[str], Dict[str, Any], str]:
|
|
294
940
|
"""
|
|
295
941
|
Update temporal identity state for a track and return a stabilized identity.
|
|
@@ -321,7 +967,7 @@ class TemporalIdentityManager:
|
|
|
321
967
|
if eligible_for_recognition and emb:
|
|
322
968
|
st8=time.time()
|
|
323
969
|
staff_id, person_name, inst_score, employee_id, staff_details, det_type = await self._compute_best_identity(
|
|
324
|
-
emb, location=location, timestamp=timestamp
|
|
970
|
+
emb, location=location, timestamp=timestamp, search_id=search_id
|
|
325
971
|
)
|
|
326
972
|
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY_I----------------------------")
|
|
327
973
|
# print("LATENCY:",(time.time() - st8)*1000,"| Throughput fps:",(1.0 / (time.time() - st8)) if (time.time() - st8) > 0 else None)
|
|
@@ -386,7 +1032,10 @@ class TemporalIdentityManager:
|
|
|
386
1032
|
|
|
387
1033
|
# Fallback: use prototype from history
|
|
388
1034
|
st9=time.time()
|
|
389
|
-
|
|
1035
|
+
history_search_id = f"{search_id}_hist" if search_id else None
|
|
1036
|
+
fb_staff_id, fb_name, fb_score, fb_employee_id, fb_details, fb_type = await self._compute_best_identity_from_history(
|
|
1037
|
+
s, location=location, timestamp=timestamp, search_id=history_search_id
|
|
1038
|
+
)
|
|
390
1039
|
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY FROM HISTORY----------------------------")
|
|
391
1040
|
# print("LATENCY:",(time.time() - st9)*1000,"| Throughput fps:",(1.0 / (time.time() - st9)) if (time.time() - st9) > 0 else None)
|
|
392
1041
|
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY FROM HISTORY----------------------------")
|
|
@@ -427,9 +1076,9 @@ class FaceRecognitionEmbeddingConfig(BaseConfig):
|
|
|
427
1076
|
smoothing_confidence_range_factor: float = 0.5
|
|
428
1077
|
|
|
429
1078
|
# Base confidence threshold (separate from embedding similarity threshold)
|
|
430
|
-
similarity_threshold: float = 0.
|
|
1079
|
+
similarity_threshold: float = 0.3 # Lowered to match local code - 0.45 was too conservative
|
|
431
1080
|
# Base confidence threshold (separate from embedding similarity threshold)
|
|
432
|
-
confidence_threshold: float = 0.
|
|
1081
|
+
confidence_threshold: float = 0.06 # Detection confidence threshold
|
|
433
1082
|
|
|
434
1083
|
# Face recognition optional features
|
|
435
1084
|
enable_face_tracking: bool = True # Enable BYTE TRACKER advanced face tracking -- KEEP IT TRUE ALWAYS
|
|
@@ -523,12 +1172,13 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
523
1172
|
|
|
524
1173
|
# Initialize EmbeddingManager - will be configured in process method
|
|
525
1174
|
self.embedding_manager = None
|
|
1175
|
+
self.redis_face_matcher = None
|
|
526
1176
|
# Temporal identity manager for API-based top-1 identity smoothing
|
|
527
1177
|
self.temporal_identity_manager = None
|
|
528
1178
|
# Removed lightweight face tracker fallback; we always use AdvancedTracker
|
|
529
1179
|
# Optional gating similar to compare_similarity
|
|
530
1180
|
self._track_first_seen: Dict[int, int] = {}
|
|
531
|
-
self._probation_frames: int =
|
|
1181
|
+
self._probation_frames: int = 30 # Reduced from 260 - only for "Unknown" label suppression, not recognition
|
|
532
1182
|
self._min_face_w: int = 30
|
|
533
1183
|
self._min_face_h: int = 30
|
|
534
1184
|
|
|
@@ -542,7 +1192,7 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
542
1192
|
# Initialization must be done by calling await initialize(config) after instantiation
|
|
543
1193
|
# This is handled in PostProcessor._get_use_case_instance()
|
|
544
1194
|
|
|
545
|
-
async def initialize(self, config: Optional[FaceRecognitionEmbeddingConfig] = None) -> None:
|
|
1195
|
+
async def initialize(self, config: Optional[FaceRecognitionEmbeddingConfig] = None, emb:bool=False) -> None:
|
|
546
1196
|
"""
|
|
547
1197
|
Async initialization method to set up face client and all components.
|
|
548
1198
|
Must be called after __init__ before process() can be called.
|
|
@@ -563,7 +1213,7 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
563
1213
|
|
|
564
1214
|
Args:
|
|
565
1215
|
config: Optional config to use. If not provided, uses config from __init__.
|
|
566
|
-
|
|
1216
|
+
emb: Optional boolean to indicate if embedding manager should be loaded. If True, embedding manager will be loaded.
|
|
567
1217
|
Raises:
|
|
568
1218
|
RuntimeError: If embeddings fail to load or verification fails
|
|
569
1219
|
"""
|
|
@@ -596,60 +1246,68 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
596
1246
|
self.people_activity_logging = PeopleActivityLogging(self.face_client)
|
|
597
1247
|
# PeopleActivityLogging starts its background thread in __init__
|
|
598
1248
|
self.logger.info("People activity logging enabled and started")
|
|
1249
|
+
|
|
1250
|
+
# Initialize Redis face matcher for fast remote similarity search
|
|
1251
|
+
try:
|
|
1252
|
+
redis_session = getattr(self.face_client, "session", None)
|
|
1253
|
+
except Exception:
|
|
1254
|
+
redis_session = None
|
|
1255
|
+
self.redis_face_matcher = RedisFaceMatcher(
|
|
1256
|
+
session=redis_session,
|
|
1257
|
+
logger=self.logger,
|
|
1258
|
+
face_client=self.face_client,
|
|
1259
|
+
)
|
|
599
1260
|
|
|
600
1261
|
# Initialize EmbeddingManager
|
|
601
|
-
# print("=============== STEP 2: INITIALIZING EMBEDDING MANAGER ===============")
|
|
602
1262
|
if not init_config.embedding_config:
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
1263
|
+
|
|
1264
|
+
init_config.embedding_config = EmbeddingConfig(
|
|
1265
|
+
similarity_threshold=init_config.similarity_threshold,
|
|
1266
|
+
confidence_threshold=init_config.confidence_threshold,
|
|
1267
|
+
enable_track_id_cache=init_config.enable_track_id_cache,
|
|
1268
|
+
cache_max_size=init_config.cache_max_size,
|
|
1269
|
+
cache_ttl=3600,
|
|
1270
|
+
background_refresh_interval=43200,
|
|
1271
|
+
staff_embeddings_cache_ttl=43200,
|
|
1272
|
+
)
|
|
613
1273
|
self.embedding_manager = EmbeddingManager(init_config.embedding_config, self.face_client)
|
|
614
|
-
|
|
1274
|
+
|
|
615
1275
|
self.logger.info("Embedding manager initialized")
|
|
1276
|
+
if emb:
|
|
1277
|
+
|
|
1278
|
+
# Load staff embeddings immediately for fast startup (avoid race conditions)
|
|
1279
|
+
# This MUST succeed before we can proceed - fail fast if it doesn't
|
|
1280
|
+
|
|
1281
|
+
embeddings_loaded = await self.embedding_manager._load_staff_embeddings()
|
|
1282
|
+
|
|
1283
|
+
if not embeddings_loaded:
|
|
1284
|
+
error_msg = "CRITICAL: Failed to load staff embeddings at initialization - cannot proceed without embeddings"
|
|
1285
|
+
print(f"=============== {error_msg} ===============")
|
|
1286
|
+
self.logger.error(error_msg)
|
|
1287
|
+
raise RuntimeError(error_msg)
|
|
1288
|
+
|
|
1289
|
+
# Verify embeddings are actually loaded using is_ready() method
|
|
1290
|
+
if not self.embedding_manager.is_ready():
|
|
1291
|
+
status = self.embedding_manager.get_status()
|
|
1292
|
+
error_msg = f"CRITICAL: Embeddings not ready after load - status: {status}"
|
|
1293
|
+
print(f"=============== {error_msg} ===============")
|
|
1294
|
+
self.logger.error(error_msg)
|
|
1295
|
+
raise RuntimeError(error_msg)
|
|
1296
|
+
|
|
1297
|
+
self.logger.info(f"Successfully loaded {len(self.embedding_manager.staff_embeddings)} staff embeddings at initialization")
|
|
616
1298
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
if not embeddings_loaded:
|
|
624
|
-
error_msg = "CRITICAL: Failed to load staff embeddings at initialization - cannot proceed without embeddings"
|
|
625
|
-
print(f"=============== {error_msg} ===============")
|
|
626
|
-
self.logger.error(error_msg)
|
|
627
|
-
raise RuntimeError(error_msg)
|
|
628
|
-
|
|
629
|
-
# Verify embeddings are actually loaded using is_ready() method
|
|
630
|
-
if not self.embedding_manager.is_ready():
|
|
631
|
-
status = self.embedding_manager.get_status()
|
|
632
|
-
error_msg = f"CRITICAL: Embeddings not ready after load - status: {status}"
|
|
633
|
-
print(f"=============== {error_msg} ===============")
|
|
634
|
-
self.logger.error(error_msg)
|
|
635
|
-
raise RuntimeError(error_msg)
|
|
636
|
-
|
|
637
|
-
# print(f"=============== STAFF EMBEDDINGS COUNT: {len(self.embedding_manager.staff_embeddings)} ===============")
|
|
638
|
-
# print(f"=============== EMBEDDINGS MATRIX SHAPE: {self.embedding_manager.embeddings_matrix.shape} ===============")
|
|
639
|
-
# print(f"=============== EMBEDDINGS LOADED FLAG: {self.embedding_manager._embeddings_loaded} ===============")
|
|
640
|
-
self.logger.info(f"Successfully loaded {len(self.embedding_manager.staff_embeddings)} staff embeddings at initialization")
|
|
641
|
-
|
|
642
|
-
# NOW start background refresh after successful initial load (prevents race conditions)
|
|
643
|
-
if init_config.embedding_config.enable_background_refresh:
|
|
644
|
-
# print("=============== STEP 4: STARTING BACKGROUND REFRESH ===============")
|
|
645
|
-
self.embedding_manager.start_background_refresh()
|
|
646
|
-
self.logger.info("Background embedding refresh started after successful initial load")
|
|
1299
|
+
# NOW start background refresh after successful initial load (prevents race conditions)
|
|
1300
|
+
if init_config.embedding_config.enable_background_refresh:
|
|
1301
|
+
# print("=============== STEP 4: STARTING BACKGROUND REFRESH ===============")
|
|
1302
|
+
self.embedding_manager.start_background_refresh()
|
|
1303
|
+
self.logger.info("Background embedding refresh started after successful initial load")
|
|
647
1304
|
|
|
648
1305
|
# Initialize TemporalIdentityManager with EmbeddingManager for fast local search
|
|
649
1306
|
# print("=============== STEP 5: INITIALIZING TEMPORAL IDENTITY MANAGER ===============")
|
|
650
1307
|
self.temporal_identity_manager = TemporalIdentityManager(
|
|
651
1308
|
face_client=self.face_client,
|
|
652
1309
|
embedding_manager=self.embedding_manager,
|
|
1310
|
+
redis_matcher=self.redis_face_matcher,
|
|
653
1311
|
recognition_threshold=float(init_config.similarity_threshold),
|
|
654
1312
|
history_size=20,
|
|
655
1313
|
unknown_patience=7,
|
|
@@ -659,26 +1317,21 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
659
1317
|
self.logger.info("Temporal identity manager initialized with embedding manager for local similarity search")
|
|
660
1318
|
|
|
661
1319
|
# Final verification before marking as initialized
|
|
662
|
-
# print("=============== STEP 6: FINAL VERIFICATION ===============")
|
|
663
|
-
if not self.embedding_manager.is_ready():
|
|
664
|
-
status = self.embedding_manager.get_status()
|
|
665
|
-
error_msg = f"CRITICAL: Final verification failed - embeddings not ready. Status: {status}"
|
|
666
|
-
print(f"=============== {error_msg} ===============")
|
|
667
|
-
self.logger.error(error_msg)
|
|
668
|
-
raise RuntimeError(error_msg)
|
|
669
1320
|
|
|
670
|
-
#
|
|
671
|
-
status = self.embedding_manager.get_status()
|
|
672
|
-
#
|
|
673
|
-
#
|
|
674
|
-
#
|
|
675
|
-
#
|
|
676
|
-
|
|
1321
|
+
# if not self.embedding_manager.is_ready():
|
|
1322
|
+
# status = self.embedding_manager.get_status()
|
|
1323
|
+
# error_msg = f"CRITICAL: Final verification failed - embeddings not ready. Status: {status}"
|
|
1324
|
+
# print(f"=============== {error_msg} ===============")
|
|
1325
|
+
# self.logger.error(error_msg)
|
|
1326
|
+
# raise RuntimeError(error_msg)
|
|
1327
|
+
|
|
1328
|
+
# # Log detailed status for debugging
|
|
1329
|
+
# status = self.embedding_manager.get_status()
|
|
1330
|
+
|
|
677
1331
|
|
|
678
1332
|
self._initialized = True
|
|
679
1333
|
self.logger.info("Face recognition use case fully initialized and verified")
|
|
680
|
-
|
|
681
|
-
|
|
1334
|
+
|
|
682
1335
|
except Exception as e:
|
|
683
1336
|
self.logger.error(f"Error during use case initialization: {e}", exc_info=True)
|
|
684
1337
|
raise RuntimeError(f"Failed to initialize face recognition use case: {e}") from e
|
|
@@ -692,17 +1345,49 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
692
1345
|
self.logger.info(
|
|
693
1346
|
f"Initializing face recognition client with server ID: {config.facial_recognition_server_id}"
|
|
694
1347
|
)
|
|
1348
|
+
print(f"=============== CONFIG: {config} ===============")
|
|
1349
|
+
print(f"=============== CONFIG.SESSION: {config.session} ===============")
|
|
1350
|
+
account_number = os.getenv("MATRICE_ACCOUNT_NUMBER", "")
|
|
1351
|
+
access_key_id = os.getenv("MATRICE_ACCESS_KEY_ID", "")
|
|
1352
|
+
secret_key = os.getenv("MATRICE_SECRET_ACCESS_KEY", "")
|
|
1353
|
+
project_id = os.getenv("MATRICE_PROJECT_ID", "")
|
|
1354
|
+
|
|
1355
|
+
self.session1 = Session(
|
|
1356
|
+
account_number=account_number,
|
|
1357
|
+
access_key=access_key_id,
|
|
1358
|
+
secret_key=secret_key,
|
|
1359
|
+
project_id=project_id,
|
|
1360
|
+
)
|
|
695
1361
|
self.face_client = FacialRecognitionClient(
|
|
696
|
-
server_id=config.facial_recognition_server_id, session=
|
|
1362
|
+
server_id=config.facial_recognition_server_id, session=self.session1
|
|
697
1363
|
)
|
|
698
1364
|
self.logger.info("Face recognition client initialized")
|
|
699
|
-
|
|
1365
|
+
|
|
700
1366
|
# Call update_deployment if deployment_id is provided
|
|
701
1367
|
if config.deployment_id:
|
|
702
1368
|
try:
|
|
1369
|
+
# Create temporary RedisFaceMatcher to get app_deployment_id using verified method
|
|
1370
|
+
redis_session = getattr(self.face_client, "session", None) or config.session
|
|
1371
|
+
temp_redis_matcher = RedisFaceMatcher(
|
|
1372
|
+
session=redis_session,
|
|
1373
|
+
logger=self.logger,
|
|
1374
|
+
face_client=self.face_client,
|
|
1375
|
+
)
|
|
1376
|
+
app_deployment_id = await temp_redis_matcher._ensure_app_deployment_id()
|
|
1377
|
+
|
|
1378
|
+
if app_deployment_id:
|
|
1379
|
+
self.logger.info(f"Updating deployment action with app_deployment_id: {app_deployment_id}")
|
|
1380
|
+
response = await self.face_client.update_deployment_action(app_deployment_id)
|
|
1381
|
+
if response:
|
|
1382
|
+
self.logger.info(f"Successfully updated deployment action {app_deployment_id}")
|
|
1383
|
+
else:
|
|
1384
|
+
self.logger.warning(f"Failed to update deployment: {response.get('error', 'Unknown error')}")
|
|
1385
|
+
else:
|
|
1386
|
+
self.logger.warning("Could not resolve app_deployment_id, skipping deployment action update")
|
|
1387
|
+
|
|
703
1388
|
self.logger.info(f"Updating deployment with ID: {config.deployment_id}")
|
|
704
1389
|
response = await self.face_client.update_deployment(config.deployment_id)
|
|
705
|
-
if response
|
|
1390
|
+
if response:
|
|
706
1391
|
self.logger.info(f"Successfully updated deployment {config.deployment_id}")
|
|
707
1392
|
else:
|
|
708
1393
|
self.logger.warning(f"Failed to update deployment: {response.get('error', 'Unknown error')}")
|
|
@@ -820,9 +1505,7 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
820
1505
|
)
|
|
821
1506
|
self.logger.debug("Applied category filtering")
|
|
822
1507
|
|
|
823
|
-
|
|
824
|
-
# print(self._initialized,"LATENCY:",(time.time() - processing_start)*1000,"| Throughput fps:",(1.0 / (time.time() - processing_start)) if (time.time() - processing_start) > 0 else None)
|
|
825
|
-
# print("------------------TILL TRACKER MS----------------------------")
|
|
1508
|
+
|
|
826
1509
|
# Advanced tracking (BYTETracker-like) - only if enabled
|
|
827
1510
|
if config.enable_face_tracking:
|
|
828
1511
|
from ..advanced_tracker import AdvancedTracker
|
|
@@ -835,8 +1518,8 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
835
1518
|
track_low_thresh=0.05,
|
|
836
1519
|
new_track_thresh=0.5,
|
|
837
1520
|
match_thresh=0.8,
|
|
838
|
-
track_buffer=int(
|
|
839
|
-
max_time_lost=int(
|
|
1521
|
+
track_buffer=int(600), # Increased to match local code - allows longer occlusions
|
|
1522
|
+
max_time_lost=int(300), # Increased to match local code
|
|
840
1523
|
fuse_score=True,
|
|
841
1524
|
enable_gmc=False,
|
|
842
1525
|
frame_rate=int(20)
|
|
@@ -862,24 +1545,21 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
862
1545
|
recognized_persons = {}
|
|
863
1546
|
current_frame_staff_details = {}
|
|
864
1547
|
|
|
865
|
-
# print("------------------TRACKER INIT END----------------------------")
|
|
866
|
-
# print("LATENCY:",(time.time() - processing_start)*1000,"| Throughput fps:",(1.0 / (time.time() - processing_start)) if (time.time() - processing_start) > 0 else None)
|
|
867
|
-
# print("------------------TRACKER INIT END----------------------------")
|
|
868
1548
|
|
|
869
1549
|
# Process face recognition for each detection (if enabled)
|
|
870
1550
|
if config.enable_face_recognition:
|
|
871
1551
|
# Additional safety check: verify embeddings are still loaded and ready
|
|
872
|
-
if not self.embedding_manager or not self.embedding_manager.is_ready():
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1552
|
+
# if not self.embedding_manager or not self.embedding_manager.is_ready():
|
|
1553
|
+
# status = self.embedding_manager.get_status() if self.embedding_manager else {}
|
|
1554
|
+
# error_msg = f"CRITICAL: Cannot process face recognition - embeddings not ready. Status: {status}"
|
|
1555
|
+
# self.logger.error(error_msg)
|
|
1556
|
+
# print(f"ERROR: {error_msg}")
|
|
1557
|
+
# return self.create_error_result(
|
|
1558
|
+
# error_msg,
|
|
1559
|
+
# usecase=self.name,
|
|
1560
|
+
# category=self.category,
|
|
1561
|
+
# context=context,
|
|
1562
|
+
# )
|
|
883
1563
|
|
|
884
1564
|
face_recognition_result = await self._process_face_recognition(
|
|
885
1565
|
processed_data, config, stream_info, input_bytes
|
|
@@ -893,9 +1573,6 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
893
1573
|
detection["recognition_status"] = "disabled"
|
|
894
1574
|
detection["enrolled"] = False
|
|
895
1575
|
|
|
896
|
-
# print("------------------FACE RECONG CONFIG ENABLED----------------------------")
|
|
897
|
-
# print("LATENCY:",(time.time() - processing_start)*1000,"| Throughput fps:",(1.0 / (time.time() - processing_start)) if (time.time() - processing_start) > 0 else None)
|
|
898
|
-
# print("------------------FACE RECONG CONFIG ENABLED----------------------------")
|
|
899
1576
|
|
|
900
1577
|
# Update tracking state for total count per label
|
|
901
1578
|
self._update_tracking_state(processed_data)
|
|
@@ -929,9 +1606,6 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
929
1606
|
current_recognized_count, current_unknown_count, recognized_persons
|
|
930
1607
|
))
|
|
931
1608
|
|
|
932
|
-
# print("------------------TILL FACE RECOG SUMMARY----------------------------")
|
|
933
|
-
# print("LATENCY:",(time.time() - processing_start)*1000,"| Throughput fps:",(1.0 / (time.time() - processing_start)) if (time.time() - processing_start) > 0 else None)
|
|
934
|
-
# print("------------------TILL FACE RECOG SUMMARY----------------------------")
|
|
935
1609
|
|
|
936
1610
|
# Add detections to the counting summary (standard pattern for detection use cases)
|
|
937
1611
|
# Ensure display label is present for UI (does not affect logic/counters)
|
|
@@ -964,10 +1638,6 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
964
1638
|
)
|
|
965
1639
|
summary = summary_list[0] if summary_list else {}
|
|
966
1640
|
|
|
967
|
-
# print("------------------TILL TRACKING STATS----------------------------")
|
|
968
|
-
# print("LATENCY:",(time.time() - processing_start)*1000,"| Throughput fps:",(1.0 / (time.time() - processing_start)) if (time.time() - processing_start) > 0 else None)
|
|
969
|
-
# print("------------------TILL TRACKING STATS----------------------------")
|
|
970
|
-
|
|
971
1641
|
|
|
972
1642
|
agg_summary = {
|
|
973
1643
|
str(frame_number): {
|
|
@@ -1041,6 +1711,12 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
1041
1711
|
|
|
1042
1712
|
return processed_data
|
|
1043
1713
|
|
|
1714
|
+
def _build_search_id(self, track_key: Any, frame_id: Optional[Any]) -> str:
|
|
1715
|
+
"""Generate a deterministic Redis search identifier per detection."""
|
|
1716
|
+
base_frame = frame_id if frame_id is not None else self._total_frame_counter
|
|
1717
|
+
safe_track = str(track_key if track_key is not None else "na").replace(" ", "_")
|
|
1718
|
+
return f"face_{base_frame}_{safe_track}"
|
|
1719
|
+
|
|
1044
1720
|
def _extract_frame_from_data(self, input_bytes: bytes) -> Optional[np.ndarray]:
|
|
1045
1721
|
"""
|
|
1046
1722
|
Extract frame from original model data
|
|
@@ -1186,6 +1862,9 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
1186
1862
|
h_box = max(1, y2 - y1)
|
|
1187
1863
|
frame_id = detection.get("frame_id", None) #TODO: Maybe replace this with stream_info frame_id
|
|
1188
1864
|
|
|
1865
|
+
track_key = track_id if track_id is not None else f"no_track_{id(detection)}"
|
|
1866
|
+
search_id = self._build_search_id(track_key, frame_id)
|
|
1867
|
+
|
|
1189
1868
|
# Track probation age strictly by internal tracker id
|
|
1190
1869
|
if track_id is not None:
|
|
1191
1870
|
if track_id not in self._track_first_seen:
|
|
@@ -1197,18 +1876,18 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
1197
1876
|
else:
|
|
1198
1877
|
age_frames = 1
|
|
1199
1878
|
|
|
1879
|
+
# Eligible for recognition if face is large enough (lowered threshold to match local code behavior)
|
|
1200
1880
|
eligible_for_recognition = (w_box >= self._min_face_w and h_box >= self._min_face_h)
|
|
1201
1881
|
|
|
1202
1882
|
# Primary: API-based identity smoothing via TemporalIdentityManager
|
|
1203
1883
|
staff_id = None
|
|
1204
|
-
person_name = "
|
|
1884
|
+
person_name = ""
|
|
1205
1885
|
similarity_score = 0.0
|
|
1206
1886
|
employee_id = None
|
|
1207
1887
|
staff_details: Dict[str, Any] = {}
|
|
1208
1888
|
detection_type = "unknown"
|
|
1209
1889
|
try:
|
|
1210
1890
|
if self.temporal_identity_manager:
|
|
1211
|
-
track_key = track_id if track_id is not None else f"no_track_{id(detection)}"
|
|
1212
1891
|
if not eligible_for_recognition:
|
|
1213
1892
|
# Mirror compare_similarity: when not eligible, keep stable label if present
|
|
1214
1893
|
s = self.temporal_identity_manager.tracks.get(track_key, {})
|
|
@@ -1244,6 +1923,7 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
1244
1923
|
eligible_for_recognition=True,
|
|
1245
1924
|
location=location,
|
|
1246
1925
|
timestamp=current_timestamp,
|
|
1926
|
+
search_id=search_id,
|
|
1247
1927
|
)
|
|
1248
1928
|
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE----------------------------")
|
|
1249
1929
|
# print("LATENCY:",(time.time() - st3)*1000,"| Throughput fps:",(1.0 / (time.time() - st3)) if (time.time() - st3) > 0 else None)
|
|
@@ -1273,16 +1953,21 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
1273
1953
|
# Update detection object directly (avoid relying on SearchResult type)
|
|
1274
1954
|
detection = detection.copy()
|
|
1275
1955
|
detection["person_id"] = staff_id
|
|
1276
|
-
detection["person_name"] = person_name or "
|
|
1956
|
+
detection["person_name"] = person_name or ""
|
|
1277
1957
|
detection["recognition_status"] = "known" if staff_id else "unknown"
|
|
1278
1958
|
detection["employee_id"] = employee_id
|
|
1279
1959
|
detection["staff_details"] = staff_details if isinstance(staff_details, dict) else {}
|
|
1280
1960
|
detection["similarity_score"] = float(similarity_score)
|
|
1281
1961
|
detection["enrolled"] = bool(staff_id)
|
|
1282
|
-
# Display label policy: show
|
|
1962
|
+
# Display label policy: ALWAYS show identified faces immediately, only suppress "Unknown" during probation
|
|
1283
1963
|
is_identified = (staff_id is not None and detection_type == "known")
|
|
1284
|
-
|
|
1285
|
-
|
|
1964
|
+
if is_identified:
|
|
1965
|
+
# Identified faces: show name immediately (no probation delay)
|
|
1966
|
+
detection["display_name"] = person_name
|
|
1967
|
+
else:
|
|
1968
|
+
# Unknown faces: only show "Unknown" label after probation period to avoid flicker
|
|
1969
|
+
show_unknown_label = (age_frames >= self._probation_frames)
|
|
1970
|
+
detection["display_name"] = "" if show_unknown_label else "" #TODO: Maybe replace this with "Unknown" bec probationif fail we show unknown.
|
|
1286
1971
|
# Preserve original category (e.g., 'face') for tracking/counting
|
|
1287
1972
|
|
|
1288
1973
|
# Update global tracking per unique internal track id to avoid double-counting within a frame
|
|
@@ -1335,7 +2020,7 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
1335
2020
|
except Exception as e:
|
|
1336
2021
|
self.logger.error(f"Error enqueueing detection for activity logging: {e}")
|
|
1337
2022
|
# print("------------------PROCESS FACE LATENCY TOTAL----------------------------")
|
|
1338
|
-
print("LATENCY:",(time.time() - st2)*1000,"| Throughput fps:",(1.0 / (time.time() - st2)) if (time.time() - st2) > 0 else None)
|
|
2023
|
+
#print("LATENCY:",(time.time() - st2)*1000,"| Throughput fps:",(1.0 / (time.time() - st2)) if (time.time() - st2) > 0 else None)
|
|
1339
2024
|
# print("------------------PROCESS FACE LATENCY TOTAL----------------------------")
|
|
1340
2025
|
|
|
1341
2026
|
return detection
|
|
@@ -1663,7 +2348,7 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
1663
2348
|
)
|
|
1664
2349
|
|
|
1665
2350
|
|
|
1666
|
-
human_text_lines = [f"CURRENT FRAME @ {current_timestamp}"]
|
|
2351
|
+
human_text_lines = [f"CURRENT FRAME @ {current_timestamp}:"]
|
|
1667
2352
|
|
|
1668
2353
|
current_recognized = current_frame.get("recognized", 0)
|
|
1669
2354
|
current_unknown = current_frame.get("unknown", 0)
|
|
@@ -1671,8 +2356,8 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
1671
2356
|
total_current = current_recognized + current_unknown
|
|
1672
2357
|
|
|
1673
2358
|
# Show staff names and IDs being recognized in current frame (with tabs)
|
|
1674
|
-
human_text_lines.append(f"\
|
|
1675
|
-
human_text_lines.append(f"\
|
|
2359
|
+
human_text_lines.append(f"\t- Current Total Faces: {total_current}")
|
|
2360
|
+
human_text_lines.append(f"\t- Current Recognized: {current_recognized}")
|
|
1676
2361
|
|
|
1677
2362
|
if recognized_persons:
|
|
1678
2363
|
for person_id in recognized_persons.keys():
|
|
@@ -1680,15 +2365,15 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
1680
2365
|
staff_name = (current_frame_staff_details or {}).get(
|
|
1681
2366
|
person_id, f"Staff {person_id}"
|
|
1682
2367
|
)
|
|
1683
|
-
human_text_lines.append(f"\
|
|
1684
|
-
human_text_lines.append(f"\
|
|
2368
|
+
human_text_lines.append(f"\t\t- Name: {staff_name} (ID: {person_id})")
|
|
2369
|
+
human_text_lines.append(f"\t- Current Unknown: {current_unknown}")
|
|
1685
2370
|
|
|
1686
2371
|
# Show current frame counts only (with tabs)
|
|
1687
2372
|
human_text_lines.append("")
|
|
1688
|
-
human_text_lines.append(f"TOTAL SINCE @ {start_timestamp}")
|
|
1689
|
-
human_text_lines.append(f"\tTotal Faces: {cumulative_total}")
|
|
1690
|
-
human_text_lines.append(f"\tRecognized: {face_summary.get('session_totals',{}).get('total_recognized', 0)}")
|
|
1691
|
-
human_text_lines.append(f"\tUnknown: {face_summary.get('session_totals',{}).get('total_unknown', 0)}")
|
|
2373
|
+
# human_text_lines.append(f"TOTAL SINCE @ {start_timestamp}")
|
|
2374
|
+
# human_text_lines.append(f"\tTotal Faces: {cumulative_total}")
|
|
2375
|
+
# human_text_lines.append(f"\tRecognized: {face_summary.get('session_totals',{}).get('total_recognized', 0)}")
|
|
2376
|
+
# human_text_lines.append(f"\tUnknown: {face_summary.get('session_totals',{}).get('total_unknown', 0)}")
|
|
1692
2377
|
# Additional counts similar to compare_similarity HUD
|
|
1693
2378
|
# try:
|
|
1694
2379
|
# human_text_lines.append(f"\tCurrent Faces (detections): {total_detections}")
|
|
@@ -1698,15 +2383,15 @@ class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
|
1698
2383
|
|
|
1699
2384
|
human_text = "\n".join(human_text_lines)
|
|
1700
2385
|
|
|
1701
|
-
if alerts:
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
else:
|
|
1707
|
-
|
|
2386
|
+
# if alerts:
|
|
2387
|
+
# for alert in alerts:
|
|
2388
|
+
# human_text_lines.append(
|
|
2389
|
+
# f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}"
|
|
2390
|
+
# )
|
|
2391
|
+
# else:
|
|
2392
|
+
# human_text_lines.append("Alerts: None")
|
|
1708
2393
|
|
|
1709
|
-
human_text = "\n".join(human_text_lines)
|
|
2394
|
+
# human_text = "\n".join(human_text_lines)
|
|
1710
2395
|
reset_settings = [
|
|
1711
2396
|
{"interval_type": "daily", "reset_time": {"value": 9, "time_unit": "hour"}}
|
|
1712
2397
|
]
|