matrice-analytics 0.1.60__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/__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 +146 -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 +3291 -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 +950 -0
- matrice_analytics/post_processing/face_reg/face_recognition.py +2234 -0
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +606 -0
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +321 -0
- matrice_analytics/post_processing/ocr/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/easyocr_extractor.py +250 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
- matrice_analytics/post_processing/ocr/postprocessing.py +270 -0
- matrice_analytics/post_processing/ocr/preprocessing.py +52 -0
- matrice_analytics/post_processing/post_processor.py +1175 -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_usecases.py +165 -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 +1085 -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 +660 -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 +1936 -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 +585 -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 +1146 -0
- matrice_analytics/post_processing/usecases/flare_analysis.py +836 -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 +1188 -0
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +1781 -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 +706 -0
- matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -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 +1029 -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.60.dist-info/METADATA +481 -0
- matrice_analytics-0.1.60.dist-info/RECORD +196 -0
- matrice_analytics-0.1.60.dist-info/WHEEL +5 -0
- matrice_analytics-0.1.60.dist-info/licenses/LICENSE.txt +21 -0
- matrice_analytics-0.1.60.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Face Recognition with Local Embedding Search Optimization
|
|
3
|
+
|
|
4
|
+
This module uses local similarity search for face recognition to achieve high performance:
|
|
5
|
+
|
|
6
|
+
Performance Optimization Strategy:
|
|
7
|
+
1. EmbeddingManager loads all staff embeddings from API at startup (~1 API call)
|
|
8
|
+
2. Embeddings are cached in memory as a normalized numpy matrix
|
|
9
|
+
3. Face recognition uses fast local cosine similarity search (~1-5ms per face)
|
|
10
|
+
4. API calls are avoided during normal operation (2000x speedup vs API calls)
|
|
11
|
+
5. Background refresh updates embeddings periodically (configurable TTL)
|
|
12
|
+
|
|
13
|
+
Processing Flow:
|
|
14
|
+
1. TemporalIdentityManager receives face embedding from detection
|
|
15
|
+
2. Uses EmbeddingManager._find_best_local_match() for fast local similarity search
|
|
16
|
+
3. Returns best match if similarity >= threshold, otherwise returns "Unknown"
|
|
17
|
+
4. Only falls back to API if EmbeddingManager unavailable (rare)
|
|
18
|
+
|
|
19
|
+
Configuration options:
|
|
20
|
+
- enable_track_id_cache: Enable/disable track-level caching
|
|
21
|
+
- cache_max_size: Maximum number of cached track IDs (default: 3000)
|
|
22
|
+
- cache_ttl: Cache time-to-live in seconds (default: 3600)
|
|
23
|
+
- background_refresh_interval: Embedding refresh interval (default: 43200s = 12h)
|
|
24
|
+
- similarity_threshold: Minimum similarity for recognition (default: 0.45)
|
|
25
|
+
"""
|
|
26
|
+
import subprocess
|
|
27
|
+
import logging
|
|
28
|
+
import asyncio
|
|
29
|
+
import os
|
|
30
|
+
log_file = open("pip_jetson_btii.log", "w")
|
|
31
|
+
cmd = ["pip", "install", "httpx"]
|
|
32
|
+
subprocess.run(
|
|
33
|
+
cmd,
|
|
34
|
+
stdout=log_file,
|
|
35
|
+
stderr=subprocess.STDOUT,
|
|
36
|
+
#preexec_fn=os.setpgrp
|
|
37
|
+
)
|
|
38
|
+
log_file.close()
|
|
39
|
+
|
|
40
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
41
|
+
import time
|
|
42
|
+
import base64
|
|
43
|
+
import cv2
|
|
44
|
+
import numpy as np
|
|
45
|
+
import threading
|
|
46
|
+
from datetime import datetime, timezone
|
|
47
|
+
from collections import deque
|
|
48
|
+
|
|
49
|
+
from ..core.base import (
|
|
50
|
+
BaseProcessor,
|
|
51
|
+
ProcessingContext,
|
|
52
|
+
ProcessingResult,
|
|
53
|
+
ConfigProtocol,
|
|
54
|
+
)
|
|
55
|
+
from ..utils import (
|
|
56
|
+
filter_by_confidence,
|
|
57
|
+
filter_by_categories,
|
|
58
|
+
apply_category_mapping,
|
|
59
|
+
calculate_counting_summary,
|
|
60
|
+
match_results_structure,
|
|
61
|
+
)
|
|
62
|
+
from dataclasses import dataclass, field
|
|
63
|
+
from ..core.config import BaseConfig, AlertConfig
|
|
64
|
+
from .face_recognition_client import FacialRecognitionClient
|
|
65
|
+
from .people_activity_logging import PeopleActivityLogging
|
|
66
|
+
from .embedding_manager import EmbeddingManager, EmbeddingConfig
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---- Lightweight identity tracking and temporal smoothing (adapted from compare_similarity.py) ---- #
|
|
70
|
+
from collections import deque, defaultdict
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _normalize_embedding(vec: List[float]) -> List[float]:
|
|
76
|
+
"""Normalize an embedding vector to unit length (L2). Returns float32 list."""
|
|
77
|
+
arr = np.asarray(vec, dtype=np.float32)
|
|
78
|
+
if arr.size == 0:
|
|
79
|
+
return []
|
|
80
|
+
n = np.linalg.norm(arr)
|
|
81
|
+
if n > 0:
|
|
82
|
+
arr = arr / n
|
|
83
|
+
return arr.tolist()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
## Removed FaceTracker fallback (using AdvancedTracker only)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TemporalIdentityManager:
|
|
90
|
+
"""
|
|
91
|
+
Maintains stable identity labels per tracker ID using temporal smoothing and embedding history.
|
|
92
|
+
|
|
93
|
+
Adaptation for production: _compute_best_identity uses EmbeddingManager for local similarity
|
|
94
|
+
search first (fast), then falls back to API only if needed (slow).
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
face_client: FacialRecognitionClient,
|
|
100
|
+
embedding_manager = None,
|
|
101
|
+
recognition_threshold: float = 0.35,
|
|
102
|
+
history_size: int = 20,
|
|
103
|
+
unknown_patience: int = 7,
|
|
104
|
+
switch_patience: int = 5,
|
|
105
|
+
fallback_margin: float = 0.05,
|
|
106
|
+
) -> None:
|
|
107
|
+
self.logger = logging.getLogger(__name__)
|
|
108
|
+
self.face_client = face_client
|
|
109
|
+
self.embedding_manager = embedding_manager
|
|
110
|
+
self.threshold = float(recognition_threshold)
|
|
111
|
+
self.history_size = int(history_size)
|
|
112
|
+
self.unknown_patience = int(unknown_patience)
|
|
113
|
+
self.switch_patience = int(switch_patience)
|
|
114
|
+
self.fallback_margin = float(fallback_margin)
|
|
115
|
+
self.tracks: Dict[Any, Dict[str, object]] = {}
|
|
116
|
+
|
|
117
|
+
def _ensure_track(self, track_id: Any) -> None:
|
|
118
|
+
if track_id not in self.tracks:
|
|
119
|
+
self.tracks[track_id] = {
|
|
120
|
+
"stable_staff_id": None,
|
|
121
|
+
"stable_person_name": None,
|
|
122
|
+
"stable_employee_id": None,
|
|
123
|
+
"stable_score": 0.0,
|
|
124
|
+
"stable_staff_details": {},
|
|
125
|
+
"label_votes": defaultdict(int), # staff_id -> votes
|
|
126
|
+
"embedding_history": deque(maxlen=self.history_size),
|
|
127
|
+
"unknown_streak": 0,
|
|
128
|
+
"streaks": defaultdict(int), # staff_id -> consecutive frames
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async def _compute_best_identity(self, emb: List[float], location: str = "", timestamp: str = "") -> Tuple[Optional[str], str, float, Optional[str], Dict[str, Any], str]:
|
|
132
|
+
"""
|
|
133
|
+
Find best identity match using local similarity search (fast) with optional API fallback.
|
|
134
|
+
Returns (staff_id, person_name, score, employee_id, staff_details, detection_type).
|
|
135
|
+
|
|
136
|
+
Performance optimization: Uses EmbeddingManager for local similarity search to avoid
|
|
137
|
+
slow API calls (~2000ms). Only falls back to API if local search is unavailable.
|
|
138
|
+
"""
|
|
139
|
+
if not emb or not isinstance(emb, list):
|
|
140
|
+
return None, "Unknown", 0.0, None, {}, "unknown"
|
|
141
|
+
|
|
142
|
+
st10 = time.time()
|
|
143
|
+
|
|
144
|
+
# PRIMARY PATH: Local similarity search using EmbeddingManager (FAST - ~1-5ms)
|
|
145
|
+
if self.embedding_manager:
|
|
146
|
+
# Defensive check: ensure embeddings are loaded before attempting search
|
|
147
|
+
if not self.embedding_manager.is_ready():
|
|
148
|
+
status = self.embedding_manager.get_status()
|
|
149
|
+
self.logger.error(f"EmbeddingManager not ready for search - status: {status}")
|
|
150
|
+
print(f"ERROR: _compute_best_identity - embeddings not ready. Status: {status}")
|
|
151
|
+
return None, "Unknown", 0.0, None, {}, "unknown"
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
local_match = self.embedding_manager._find_best_local_match(emb)
|
|
155
|
+
|
|
156
|
+
if local_match:
|
|
157
|
+
staff_embedding, similarity_score = local_match
|
|
158
|
+
|
|
159
|
+
# Extract person name from staff details
|
|
160
|
+
person_name = "Unknown"
|
|
161
|
+
staff_details = staff_embedding.staff_details if isinstance(staff_embedding.staff_details, dict) else {}
|
|
162
|
+
|
|
163
|
+
if staff_details:
|
|
164
|
+
first_name = staff_details.get("firstName")
|
|
165
|
+
last_name = staff_details.get("lastName")
|
|
166
|
+
name = staff_details.get("name")
|
|
167
|
+
if name:
|
|
168
|
+
person_name = str(name)
|
|
169
|
+
elif first_name or last_name:
|
|
170
|
+
person_name = f"{first_name or ''} {last_name or ''}".strip() or "Unknown"
|
|
171
|
+
|
|
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
|
+
|
|
177
|
+
self.logger.info(f"Local embedding match - staff_id={staff_embedding.staff_id}, person_name={person_name}, score={similarity_score:.3f}")
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
str(staff_embedding.staff_id),
|
|
181
|
+
person_name,
|
|
182
|
+
float(similarity_score),
|
|
183
|
+
str(staff_embedding.employee_id),
|
|
184
|
+
staff_details,
|
|
185
|
+
"known"
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
# No local match found; log best similarity for observability
|
|
189
|
+
best_sim = 0.0
|
|
190
|
+
try:
|
|
191
|
+
best_sim = float(self.embedding_manager.get_best_similarity(emb))
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
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
|
+
|
|
200
|
+
return None, "Unknown", 0.0, None, {}, "unknown"
|
|
201
|
+
|
|
202
|
+
except Exception as e:
|
|
203
|
+
self.logger.warning(f"Local similarity search failed, falling back to API: {e}")
|
|
204
|
+
# Fall through to API call below
|
|
205
|
+
|
|
206
|
+
# FALLBACK PATH: API call (SLOW - ~2000ms) - only if embedding manager not available
|
|
207
|
+
# This path should rarely be used in production
|
|
208
|
+
try:
|
|
209
|
+
self.logger.warning("Using slow API fallback for identity search - consider checking embedding manager initialization")
|
|
210
|
+
resp = await self.face_client.search_similar_faces(
|
|
211
|
+
face_embedding=emb,
|
|
212
|
+
threshold=0.01, # low threshold to always get top-1
|
|
213
|
+
limit=1,
|
|
214
|
+
collection="staff_enrollment",
|
|
215
|
+
location=location,
|
|
216
|
+
timestamp=timestamp,
|
|
217
|
+
)
|
|
218
|
+
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY (API FALLBACK)----------------------------")
|
|
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)----------------------------")
|
|
222
|
+
|
|
223
|
+
except Exception as e:
|
|
224
|
+
self.logger.error(f"API ERROR: Failed to search similar faces in _compute_best_identity: {e}", exc_info=True)
|
|
225
|
+
return None, "Unknown", 0.0, None, {}, "unknown"
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
results: List[Any] = []
|
|
229
|
+
self.logger.debug('API Response received for identity search')
|
|
230
|
+
if isinstance(resp, dict):
|
|
231
|
+
if isinstance(resp.get("data"), list):
|
|
232
|
+
results = resp.get("data", [])
|
|
233
|
+
elif isinstance(resp.get("results"), list):
|
|
234
|
+
results = resp.get("results", [])
|
|
235
|
+
elif isinstance(resp.get("items"), list):
|
|
236
|
+
results = resp.get("items", [])
|
|
237
|
+
elif isinstance(resp, list):
|
|
238
|
+
results = resp
|
|
239
|
+
|
|
240
|
+
if not results:
|
|
241
|
+
self.logger.debug("No identity match found from API")
|
|
242
|
+
return None, "Unknown", 0.0, None, {}, "unknown"
|
|
243
|
+
|
|
244
|
+
item = results[0] if isinstance(results, list) else results
|
|
245
|
+
self.logger.debug(f'Top-1 match from API: {item}')
|
|
246
|
+
# Be defensive with keys and types
|
|
247
|
+
staff_id = item.get("staffId") if isinstance(item, dict) else None
|
|
248
|
+
employee_id = str(item.get("_id")) if isinstance(item, dict) and item.get("_id") is not None else None
|
|
249
|
+
score = float(item.get("score", 0.0)) if isinstance(item, dict) else 0.0
|
|
250
|
+
detection_type = str(item.get("detectionType", "unknown")) if isinstance(item, dict) else "unknown"
|
|
251
|
+
staff_details = item.get("staffDetails", {}) if isinstance(item, dict) else {}
|
|
252
|
+
# Extract a person name from staff_details
|
|
253
|
+
person_name = "Unknown"
|
|
254
|
+
if isinstance(staff_details, dict) and staff_details:
|
|
255
|
+
first_name = staff_details.get("firstName")
|
|
256
|
+
last_name = staff_details.get("lastName")
|
|
257
|
+
name = staff_details.get("name")
|
|
258
|
+
if name:
|
|
259
|
+
person_name = str(name)
|
|
260
|
+
else:
|
|
261
|
+
if first_name or last_name:
|
|
262
|
+
person_name = f"{first_name or ''} {last_name or ''}".strip() or "UnknowNN" #TODO:ebugging change to normal once done
|
|
263
|
+
# If API says unknown or missing staff_id, treat as unknown
|
|
264
|
+
if not staff_id: #or detection_type == "unknown"
|
|
265
|
+
self.logger.debug(f"API returned unknown or missing staff_id - score={score}, employee_id={employee_id}")
|
|
266
|
+
return None, "Unknown", float(score), employee_id, staff_details if isinstance(staff_details, dict) else {}, "unknown"
|
|
267
|
+
self.logger.info(f"API identified face - staff_id={staff_id}, person_name={person_name}, score={score:.3f}")
|
|
268
|
+
return str(staff_id), person_name, float(score), employee_id, staff_details if isinstance(staff_details, dict) else {}, "known"
|
|
269
|
+
except Exception as e:
|
|
270
|
+
self.logger.error(f"Error parsing API response in _compute_best_identity: {e}", exc_info=True)
|
|
271
|
+
return None, "Unknown", 0.0, None, {}, "unknown"
|
|
272
|
+
|
|
273
|
+
async def _compute_best_identity_from_history(self, track_state: Dict[str, object], location: str = "", timestamp: str = "") -> Tuple[Optional[str], str, float, Optional[str], Dict[str, Any], str]:
|
|
274
|
+
hist: deque = track_state.get("embedding_history", deque()) # type: ignore
|
|
275
|
+
if not hist:
|
|
276
|
+
return None, "Unknown", 0.0, None, {}, "unknown"
|
|
277
|
+
try:
|
|
278
|
+
self.logger.debug(f"Computing identity from embedding history - history_size={len(hist)}")
|
|
279
|
+
proto = np.mean(np.asarray(list(hist), dtype=np.float32), axis=0)
|
|
280
|
+
proto_list = proto.tolist() if isinstance(proto, np.ndarray) else list(proto)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
self.logger.error(f"Error computing prototype from history: {e}", exc_info=True)
|
|
283
|
+
proto_list = []
|
|
284
|
+
return await self._compute_best_identity(proto_list, location=location, timestamp=timestamp)
|
|
285
|
+
|
|
286
|
+
async def update(
|
|
287
|
+
self,
|
|
288
|
+
track_id: Any,
|
|
289
|
+
emb: List[float],
|
|
290
|
+
eligible_for_recognition: bool,
|
|
291
|
+
location: str = "",
|
|
292
|
+
timestamp: str = "",
|
|
293
|
+
) -> Tuple[Optional[str], str, float, Optional[str], Dict[str, Any], str]:
|
|
294
|
+
"""
|
|
295
|
+
Update temporal identity state for a track and return a stabilized identity.
|
|
296
|
+
Returns (staff_id, person_name, score, employee_id, staff_details, detection_type).
|
|
297
|
+
"""
|
|
298
|
+
st7=time.time()
|
|
299
|
+
self._ensure_track(track_id)
|
|
300
|
+
s = self.tracks[track_id]
|
|
301
|
+
|
|
302
|
+
# Update embedding history
|
|
303
|
+
if emb:
|
|
304
|
+
try:
|
|
305
|
+
history: deque = s["embedding_history"] # type: ignore
|
|
306
|
+
history.append(_normalize_embedding(emb))
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
# Defaults for return values
|
|
311
|
+
stable_staff_id = s.get("stable_staff_id")
|
|
312
|
+
stable_person_name = s.get("stable_person_name")
|
|
313
|
+
stable_employee_id = s.get("stable_employee_id")
|
|
314
|
+
stable_score = float(s.get("stable_score", 0.0))
|
|
315
|
+
stable_staff_details = s.get("stable_staff_details", {}) if isinstance(s.get("stable_staff_details"), dict) else {}
|
|
316
|
+
|
|
317
|
+
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - STABLE VALUES----------------------------")
|
|
318
|
+
# print("LATENCY:",(time.time() - st7)*1000,"| Throughput fps:",(1.0 / (time.time() - st7)) if (time.time() - st7) > 0 else None)
|
|
319
|
+
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - STABLE VALUES----------------------------")
|
|
320
|
+
|
|
321
|
+
if eligible_for_recognition and emb:
|
|
322
|
+
st8=time.time()
|
|
323
|
+
staff_id, person_name, inst_score, employee_id, staff_details, det_type = await self._compute_best_identity(
|
|
324
|
+
emb, location=location, timestamp=timestamp
|
|
325
|
+
)
|
|
326
|
+
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY_I----------------------------")
|
|
327
|
+
# print("LATENCY:",(time.time() - st8)*1000,"| Throughput fps:",(1.0 / (time.time() - st8)) if (time.time() - st8) > 0 else None)
|
|
328
|
+
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY_I----------------------------")
|
|
329
|
+
|
|
330
|
+
is_inst_known = staff_id is not None and inst_score >= self.threshold
|
|
331
|
+
if is_inst_known:
|
|
332
|
+
s["label_votes"][staff_id] += 1 # type: ignore
|
|
333
|
+
s["streaks"][staff_id] += 1 # type: ignore
|
|
334
|
+
s["unknown_streak"] = 0
|
|
335
|
+
|
|
336
|
+
# Initialize stable if not set
|
|
337
|
+
if stable_staff_id is None:
|
|
338
|
+
s["stable_staff_id"] = staff_id
|
|
339
|
+
s["stable_person_name"] = person_name
|
|
340
|
+
s["stable_employee_id"] = employee_id
|
|
341
|
+
s["stable_score"] = float(inst_score)
|
|
342
|
+
s["stable_staff_details"] = staff_details
|
|
343
|
+
return staff_id, person_name, float(inst_score), employee_id, staff_details, "known"
|
|
344
|
+
|
|
345
|
+
# If same as stable, keep it and update score
|
|
346
|
+
if staff_id == stable_staff_id:
|
|
347
|
+
s["stable_score"] = float(inst_score)
|
|
348
|
+
# prefer latest name/details if present
|
|
349
|
+
if person_name and person_name != stable_person_name:
|
|
350
|
+
s["stable_person_name"] = person_name
|
|
351
|
+
if isinstance(staff_details, dict) and staff_details:
|
|
352
|
+
s["stable_staff_details"] = staff_details
|
|
353
|
+
if employee_id:
|
|
354
|
+
s["stable_employee_id"] = employee_id
|
|
355
|
+
return staff_id, s.get("stable_person_name") or person_name, float(inst_score), s.get("stable_employee_id") or employee_id, s.get("stable_staff_details", {}), "known"
|
|
356
|
+
|
|
357
|
+
# Competing identity: switch only if sustained and with margin & votes ratio (local parity)
|
|
358
|
+
if s["streaks"][staff_id] >= self.switch_patience: # type: ignore
|
|
359
|
+
try:
|
|
360
|
+
prev_votes = s["label_votes"].get(stable_staff_id, 0) if stable_staff_id is not None else 0 # type: ignore
|
|
361
|
+
cand_votes = s["label_votes"].get(staff_id, 0) # type: ignore
|
|
362
|
+
except Exception:
|
|
363
|
+
prev_votes, cand_votes = 0, 0
|
|
364
|
+
if cand_votes >= max(2, 0.75 * prev_votes) and float(inst_score) >= (self.threshold + 0.02):
|
|
365
|
+
s["stable_staff_id"] = staff_id
|
|
366
|
+
s["stable_person_name"] = person_name
|
|
367
|
+
s["stable_employee_id"] = employee_id
|
|
368
|
+
s["stable_score"] = float(inst_score)
|
|
369
|
+
s["stable_staff_details"] = staff_details
|
|
370
|
+
# reset other streaks
|
|
371
|
+
try:
|
|
372
|
+
for k in list(s["streaks"].keys()): # type: ignore
|
|
373
|
+
if k != staff_id:
|
|
374
|
+
s["streaks"][k] = 0 # type: ignore
|
|
375
|
+
except Exception:
|
|
376
|
+
pass
|
|
377
|
+
return staff_id, person_name, float(inst_score), employee_id, staff_details, "known"
|
|
378
|
+
|
|
379
|
+
# Do not switch yet; keep stable but return instant score/name
|
|
380
|
+
return stable_staff_id, stable_person_name or person_name, float(inst_score), stable_employee_id or employee_id, stable_staff_details, "known" if stable_staff_id else "unknown"
|
|
381
|
+
|
|
382
|
+
# Instantaneous is unknown or low score
|
|
383
|
+
s["unknown_streak"] = int(s.get("unknown_streak", 0)) + 1
|
|
384
|
+
if stable_staff_id is not None and s["unknown_streak"] <= self.unknown_patience: # type: ignore
|
|
385
|
+
return stable_staff_id, stable_person_name or "Unknown", float(inst_score), stable_employee_id, stable_staff_details, "known"
|
|
386
|
+
|
|
387
|
+
# Fallback: use prototype from history
|
|
388
|
+
st9=time.time()
|
|
389
|
+
fb_staff_id, fb_name, fb_score, fb_employee_id, fb_details, fb_type = await self._compute_best_identity_from_history(s, location=location, timestamp=timestamp)
|
|
390
|
+
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY FROM HISTORY----------------------------")
|
|
391
|
+
# print("LATENCY:",(time.time() - st9)*1000,"| Throughput fps:",(1.0 / (time.time() - st9)) if (time.time() - st9) > 0 else None)
|
|
392
|
+
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE - COMPUTE BEST IDENTITY FROM HISTORY----------------------------")
|
|
393
|
+
|
|
394
|
+
if fb_staff_id is not None and fb_score >= max(0.0, self.threshold - self.fallback_margin):
|
|
395
|
+
s["label_votes"][fb_staff_id] += 1 # type: ignore
|
|
396
|
+
s["stable_staff_id"] = fb_staff_id
|
|
397
|
+
s["stable_person_name"] = fb_name
|
|
398
|
+
s["stable_employee_id"] = fb_employee_id
|
|
399
|
+
s["stable_score"] = float(fb_score)
|
|
400
|
+
s["stable_staff_details"] = fb_details
|
|
401
|
+
s["unknown_streak"] = 0
|
|
402
|
+
return fb_staff_id, fb_name, float(fb_score), fb_employee_id, fb_details, "known"
|
|
403
|
+
|
|
404
|
+
# No confident identity
|
|
405
|
+
s["stable_staff_id"] = stable_staff_id
|
|
406
|
+
s["stable_person_name"] = stable_person_name
|
|
407
|
+
s["stable_employee_id"] = stable_employee_id
|
|
408
|
+
s["stable_score"] = float(stable_score)
|
|
409
|
+
s["stable_staff_details"] = stable_staff_details
|
|
410
|
+
return None, "Unknown", float(inst_score), None, {}, "unknown"
|
|
411
|
+
|
|
412
|
+
# Not eligible or no embedding; keep stable if present
|
|
413
|
+
if stable_staff_id is not None:
|
|
414
|
+
return stable_staff_id, stable_person_name or "Unknown", float(stable_score), stable_employee_id, stable_staff_details, "known"
|
|
415
|
+
return None, "Unknown", 0.0, None, {}, "unknown"
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@dataclass
|
|
419
|
+
class FaceRecognitionEmbeddingConfig(BaseConfig):
|
|
420
|
+
"""Configuration for face recognition with embeddings use case."""
|
|
421
|
+
|
|
422
|
+
# Smoothing configuration
|
|
423
|
+
enable_smoothing: bool = False
|
|
424
|
+
smoothing_algorithm: str = "observability" # "window" or "observability"
|
|
425
|
+
smoothing_window_size: int = 20
|
|
426
|
+
smoothing_cooldown_frames: int = 5
|
|
427
|
+
smoothing_confidence_range_factor: float = 0.5
|
|
428
|
+
|
|
429
|
+
# Base confidence threshold (separate from embedding similarity threshold)
|
|
430
|
+
similarity_threshold: float = 0.45 #-- KEEP IT AT 0.45 ALWAYS
|
|
431
|
+
# Base confidence threshold (separate from embedding similarity threshold)
|
|
432
|
+
confidence_threshold: float = 0.1 #-- KEEP IT AT 0.1 ALWAYS
|
|
433
|
+
|
|
434
|
+
# Face recognition optional features
|
|
435
|
+
enable_face_tracking: bool = True # Enable BYTE TRACKER advanced face tracking -- KEEP IT TRUE ALWAYS
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
enable_auto_enrollment: bool = False # Enable auto-enrollment of unknown faces
|
|
439
|
+
enable_face_recognition: bool = (
|
|
440
|
+
True # Enable face recognition (requires credentials)
|
|
441
|
+
)
|
|
442
|
+
enable_unknown_face_processing: bool = (
|
|
443
|
+
False # TODO: Unable when we will be saving unkown faces # Enable unknown face cropping/uploading (requires frame data)
|
|
444
|
+
)
|
|
445
|
+
enable_people_activity_logging: bool = True # Enable logging of known face activities
|
|
446
|
+
|
|
447
|
+
usecase_categories: List[str] = field(default_factory=lambda: ["face"])
|
|
448
|
+
|
|
449
|
+
target_categories: List[str] = field(default_factory=lambda: ["face"])
|
|
450
|
+
|
|
451
|
+
alert_config: Optional[AlertConfig] = None
|
|
452
|
+
|
|
453
|
+
index_to_category: Optional[Dict[int, str]] = field(
|
|
454
|
+
default_factory=lambda: {0: "face"}
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
facial_recognition_server_id: str = ""
|
|
458
|
+
session: Any = None # Matrice session for face recognition client
|
|
459
|
+
deployment_id: Optional[str] = None # deployment ID for update_deployment call
|
|
460
|
+
|
|
461
|
+
# Embedding configuration
|
|
462
|
+
embedding_config: Optional[Any] = None # Will be set to EmbeddingConfig instance
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# Track ID cache optimization settings
|
|
466
|
+
enable_track_id_cache: bool = True
|
|
467
|
+
cache_max_size: int = 3000
|
|
468
|
+
cache_ttl: int = 3600 # Cache time-to-live in seconds (1 hour)
|
|
469
|
+
|
|
470
|
+
# Search settings
|
|
471
|
+
search_limit: int = 5
|
|
472
|
+
search_collection: str = "staff_enrollment"
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class FaceRecognitionEmbeddingUseCase(BaseProcessor):
|
|
476
|
+
# Human-friendly display names for categories
|
|
477
|
+
CATEGORY_DISPLAY = {"face": "face"}
|
|
478
|
+
|
|
479
|
+
def __init__(self, config: Optional[FaceRecognitionEmbeddingConfig] = None):
|
|
480
|
+
super().__init__("face_recognition")
|
|
481
|
+
self.category = "security"
|
|
482
|
+
|
|
483
|
+
self.CASE_TYPE: Optional[str] = "face_recognition"
|
|
484
|
+
self.CASE_VERSION: Optional[str] = "1.0"
|
|
485
|
+
# List of categories to track
|
|
486
|
+
self.target_categories = ["face"]
|
|
487
|
+
|
|
488
|
+
# Initialize smoothing tracker
|
|
489
|
+
self.smoothing_tracker = None
|
|
490
|
+
|
|
491
|
+
# Initialize advanced tracker (will be created on first use)
|
|
492
|
+
self.tracker = None
|
|
493
|
+
# Initialize tracking state variables
|
|
494
|
+
self._total_frame_counter = 0
|
|
495
|
+
self._global_frame_offset = 0
|
|
496
|
+
|
|
497
|
+
# Track start time for "TOTAL SINCE" calculation
|
|
498
|
+
self._tracking_start_time = datetime.now(
|
|
499
|
+
timezone.utc
|
|
500
|
+
) # Store as datetime object for UTC
|
|
501
|
+
|
|
502
|
+
self._track_aliases: Dict[Any, Any] = {}
|
|
503
|
+
self._canonical_tracks: Dict[Any, Dict[str, Any]] = {}
|
|
504
|
+
# Tunable parameters – adjust if necessary for specific scenarios
|
|
505
|
+
self._track_merge_iou_threshold: float = 0.05 # IoU ≥ 0.05 →
|
|
506
|
+
self._track_merge_time_window: float = 7.0 # seconds within which to merge
|
|
507
|
+
|
|
508
|
+
self._ascending_alert_list: List[int] = []
|
|
509
|
+
self.current_incident_end_timestamp: str = "N/A"
|
|
510
|
+
|
|
511
|
+
# Session totals tracked per unique internal track id (thread-safe)
|
|
512
|
+
self._recognized_track_ids = set()
|
|
513
|
+
self._unknown_track_ids = set()
|
|
514
|
+
self._tracking_lock = threading.Lock()
|
|
515
|
+
|
|
516
|
+
# Person tracking: {person_id: [{"camera_id": str, "timestamp": str}, ...]}
|
|
517
|
+
self.person_tracking: Dict[str, List[Dict[str, str]]] = {}
|
|
518
|
+
|
|
519
|
+
self.face_client = None
|
|
520
|
+
|
|
521
|
+
# Initialize PeopleActivityLogging without face client initially
|
|
522
|
+
self.people_activity_logging = None
|
|
523
|
+
|
|
524
|
+
# Initialize EmbeddingManager - will be configured in process method
|
|
525
|
+
self.embedding_manager = None
|
|
526
|
+
# Temporal identity manager for API-based top-1 identity smoothing
|
|
527
|
+
self.temporal_identity_manager = None
|
|
528
|
+
# Removed lightweight face tracker fallback; we always use AdvancedTracker
|
|
529
|
+
# Optional gating similar to compare_similarity
|
|
530
|
+
self._track_first_seen: Dict[int, int] = {}
|
|
531
|
+
self._probation_frames: int = 260 # default gate ~4 seconds at 60 fps; tune per stream
|
|
532
|
+
self._min_face_w: int = 30
|
|
533
|
+
self._min_face_h: int = 30
|
|
534
|
+
|
|
535
|
+
self.start_timer = None
|
|
536
|
+
|
|
537
|
+
# Store config for async initialization
|
|
538
|
+
self._default_config = config
|
|
539
|
+
self._initialized = False
|
|
540
|
+
|
|
541
|
+
# Don't call asyncio.run() in __init__ - it will fail if called from async context
|
|
542
|
+
# Initialization must be done by calling await initialize(config) after instantiation
|
|
543
|
+
# This is handled in PostProcessor._get_use_case_instance()
|
|
544
|
+
|
|
545
|
+
async def initialize(self, config: Optional[FaceRecognitionEmbeddingConfig] = None) -> None:
|
|
546
|
+
"""
|
|
547
|
+
Async initialization method to set up face client and all components.
|
|
548
|
+
Must be called after __init__ before process() can be called.
|
|
549
|
+
|
|
550
|
+
CRITICAL INITIALIZATION SEQUENCE:
|
|
551
|
+
1. Initialize face client and update deployment
|
|
552
|
+
2. Create EmbeddingManager (does NOT load embeddings yet)
|
|
553
|
+
3. Synchronously load embeddings with _load_staff_embeddings() - MUST succeed
|
|
554
|
+
4. Verify embeddings are actually loaded (fail-fast if not)
|
|
555
|
+
5. Start background refresh thread (only after successful load)
|
|
556
|
+
6. Initialize TemporalIdentityManager with loaded EmbeddingManager
|
|
557
|
+
7. Final verification of all components
|
|
558
|
+
|
|
559
|
+
This sequence ensures:
|
|
560
|
+
- No race conditions between main load and background thread
|
|
561
|
+
- Fail-fast behavior if embeddings can't be loaded
|
|
562
|
+
- All components have verified embeddings before use
|
|
563
|
+
|
|
564
|
+
Args:
|
|
565
|
+
config: Optional config to use. If not provided, uses config from __init__.
|
|
566
|
+
|
|
567
|
+
Raises:
|
|
568
|
+
RuntimeError: If embeddings fail to load or verification fails
|
|
569
|
+
"""
|
|
570
|
+
print("=============== INITIALIZE() CALLED ===============")
|
|
571
|
+
if self._initialized:
|
|
572
|
+
self.logger.debug("Use case already initialized, skipping")
|
|
573
|
+
print("=============== ALREADY INITIALIZED, SKIPPING ===============")
|
|
574
|
+
return
|
|
575
|
+
|
|
576
|
+
# Use provided config or fall back to default config from __init__
|
|
577
|
+
init_config = config or self._default_config
|
|
578
|
+
|
|
579
|
+
if not init_config:
|
|
580
|
+
raise ValueError("No config provided for initialization - config is required")
|
|
581
|
+
|
|
582
|
+
# Validate config type
|
|
583
|
+
if not isinstance(init_config, FaceRecognitionEmbeddingConfig):
|
|
584
|
+
raise TypeError(f"Invalid config type for initialization: {type(init_config)}, expected FaceRecognitionEmbeddingConfig")
|
|
585
|
+
|
|
586
|
+
self.logger.info("Initializing face recognition use case with provided config")
|
|
587
|
+
# print("=============== STEP 1: INITIALIZING FACE CLIENT ===============")
|
|
588
|
+
|
|
589
|
+
# Initialize face client (includes deployment update)
|
|
590
|
+
try:
|
|
591
|
+
self.face_client = await self._get_facial_recognition_client(init_config)
|
|
592
|
+
# print(f"=============== FACE CLIENT INITIALIZED: {self.face_client is not None} ===============")
|
|
593
|
+
|
|
594
|
+
# Initialize People activity logging if enabled
|
|
595
|
+
if init_config.enable_people_activity_logging:
|
|
596
|
+
self.people_activity_logging = PeopleActivityLogging(self.face_client)
|
|
597
|
+
# PeopleActivityLogging starts its background thread in __init__
|
|
598
|
+
self.logger.info("People activity logging enabled and started")
|
|
599
|
+
|
|
600
|
+
# Initialize EmbeddingManager
|
|
601
|
+
# print("=============== STEP 2: INITIALIZING EMBEDDING MANAGER ===============")
|
|
602
|
+
if not init_config.embedding_config:
|
|
603
|
+
# print("=============== CREATING EMBEDDING CONFIG ===============")
|
|
604
|
+
init_config.embedding_config = EmbeddingConfig(
|
|
605
|
+
similarity_threshold=init_config.similarity_threshold,
|
|
606
|
+
confidence_threshold=init_config.confidence_threshold,
|
|
607
|
+
enable_track_id_cache=init_config.enable_track_id_cache,
|
|
608
|
+
cache_max_size=init_config.cache_max_size,
|
|
609
|
+
cache_ttl=3600,
|
|
610
|
+
background_refresh_interval=43200,
|
|
611
|
+
staff_embeddings_cache_ttl=43200,
|
|
612
|
+
)
|
|
613
|
+
self.embedding_manager = EmbeddingManager(init_config.embedding_config, self.face_client)
|
|
614
|
+
# print(f"=============== EMBEDDING MANAGER CREATED: {self.embedding_manager is not None} ===============")
|
|
615
|
+
self.logger.info("Embedding manager initialized")
|
|
616
|
+
|
|
617
|
+
# Load staff embeddings immediately for fast startup (avoid race conditions)
|
|
618
|
+
# This MUST succeed before we can proceed - fail fast if it doesn't
|
|
619
|
+
# print("=============== STEP 3: CALLING _load_staff_embeddings() ===============")
|
|
620
|
+
embeddings_loaded = await self.embedding_manager._load_staff_embeddings()
|
|
621
|
+
# print(f"=============== EMBEDDINGS LOADED: {embeddings_loaded} ===============")
|
|
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")
|
|
647
|
+
|
|
648
|
+
# Initialize TemporalIdentityManager with EmbeddingManager for fast local search
|
|
649
|
+
# print("=============== STEP 5: INITIALIZING TEMPORAL IDENTITY MANAGER ===============")
|
|
650
|
+
self.temporal_identity_manager = TemporalIdentityManager(
|
|
651
|
+
face_client=self.face_client,
|
|
652
|
+
embedding_manager=self.embedding_manager,
|
|
653
|
+
recognition_threshold=float(init_config.similarity_threshold),
|
|
654
|
+
history_size=20,
|
|
655
|
+
unknown_patience=7,
|
|
656
|
+
switch_patience=5,
|
|
657
|
+
fallback_margin=0.05,
|
|
658
|
+
)
|
|
659
|
+
self.logger.info("Temporal identity manager initialized with embedding manager for local similarity search")
|
|
660
|
+
|
|
661
|
+
# 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
|
+
|
|
670
|
+
# Log detailed status for debugging
|
|
671
|
+
status = self.embedding_manager.get_status()
|
|
672
|
+
# print(f"=============== FINAL CHECKS PASSED ===============")
|
|
673
|
+
# print(f" - Face client: {self.face_client is not None}")
|
|
674
|
+
# print(f" - Embedding manager: {self.embedding_manager is not None}")
|
|
675
|
+
# print(f" - Embedding manager status: {status}")
|
|
676
|
+
# print(f" - Temporal identity manager: {self.temporal_identity_manager is not None}")
|
|
677
|
+
|
|
678
|
+
self._initialized = True
|
|
679
|
+
self.logger.info("Face recognition use case fully initialized and verified")
|
|
680
|
+
# print("=============== INITIALIZATION COMPLETE ===============")
|
|
681
|
+
|
|
682
|
+
except Exception as e:
|
|
683
|
+
self.logger.error(f"Error during use case initialization: {e}", exc_info=True)
|
|
684
|
+
raise RuntimeError(f"Failed to initialize face recognition use case: {e}") from e
|
|
685
|
+
|
|
686
|
+
async def _get_facial_recognition_client(
|
|
687
|
+
self, config: FaceRecognitionEmbeddingConfig
|
|
688
|
+
) -> FacialRecognitionClient:
|
|
689
|
+
"""Get facial recognition client and update deployment"""
|
|
690
|
+
# Initialize face recognition client if not already done
|
|
691
|
+
if self.face_client is None:
|
|
692
|
+
self.logger.info(
|
|
693
|
+
f"Initializing face recognition client with server ID: {config.facial_recognition_server_id}"
|
|
694
|
+
)
|
|
695
|
+
self.face_client = FacialRecognitionClient(
|
|
696
|
+
server_id=config.facial_recognition_server_id, session=config.session
|
|
697
|
+
)
|
|
698
|
+
self.logger.info("Face recognition client initialized")
|
|
699
|
+
|
|
700
|
+
# Call update_deployment if deployment_id is provided
|
|
701
|
+
if config.deployment_id:
|
|
702
|
+
try:
|
|
703
|
+
self.logger.info(f"Updating deployment with ID: {config.deployment_id}")
|
|
704
|
+
response = await self.face_client.update_deployment(config.deployment_id)
|
|
705
|
+
if response.get('success', False):
|
|
706
|
+
self.logger.info(f"Successfully updated deployment {config.deployment_id}")
|
|
707
|
+
else:
|
|
708
|
+
self.logger.warning(f"Failed to update deployment: {response.get('error', 'Unknown error')}")
|
|
709
|
+
except Exception as e:
|
|
710
|
+
self.logger.error(f"Exception while updating deployment: {e}", exc_info=True)
|
|
711
|
+
else:
|
|
712
|
+
self.logger.debug("No deployment_id provided, skipping deployment update")
|
|
713
|
+
|
|
714
|
+
return self.face_client
|
|
715
|
+
|
|
716
|
+
async def process(
|
|
717
|
+
self,
|
|
718
|
+
data: Any,
|
|
719
|
+
config: ConfigProtocol,
|
|
720
|
+
input_bytes: Optional[bytes] = None,
|
|
721
|
+
context: Optional[ProcessingContext] = None,
|
|
722
|
+
stream_info: Optional[Dict[str, Any]] = None,
|
|
723
|
+
) -> ProcessingResult:
|
|
724
|
+
"""
|
|
725
|
+
Main entry point for face recognition with embeddings post-processing.
|
|
726
|
+
Applies all standard processing plus face recognition and auto-enrollment.
|
|
727
|
+
|
|
728
|
+
Thread-safe: Uses local variables for per-request state and locks for global totals.
|
|
729
|
+
Order-preserving: Processes detections sequentially to maintain input order.
|
|
730
|
+
"""
|
|
731
|
+
processing_start = time.time()
|
|
732
|
+
# Ensure config is correct type
|
|
733
|
+
if not isinstance(config, FaceRecognitionEmbeddingConfig):
|
|
734
|
+
return self.create_error_result(
|
|
735
|
+
"Invalid config type",
|
|
736
|
+
usecase=self.name,
|
|
737
|
+
category=self.category,
|
|
738
|
+
context=context,
|
|
739
|
+
)
|
|
740
|
+
if context is None:
|
|
741
|
+
context = ProcessingContext()
|
|
742
|
+
|
|
743
|
+
# Defensive check: Ensure context is ProcessingContext object (production safety)
|
|
744
|
+
# This handles edge cases where parameter mismatch might pass a dict as context
|
|
745
|
+
if not isinstance(context, ProcessingContext):
|
|
746
|
+
self.logger.warning(
|
|
747
|
+
f"Context parameter is not ProcessingContext (got {type(context).__name__}, {context}). "
|
|
748
|
+
"Creating new ProcessingContext. This may indicate a parameter mismatch in the caller."
|
|
749
|
+
)
|
|
750
|
+
context = ProcessingContext()
|
|
751
|
+
|
|
752
|
+
# Lazy initialization on first process() call (similar to tracker initialization pattern)
|
|
753
|
+
if not self._initialized:
|
|
754
|
+
self.logger.info("Initializing face recognition use case on first process() call...")
|
|
755
|
+
try:
|
|
756
|
+
await self.initialize(config)
|
|
757
|
+
self.logger.info("Face recognition use case initialized successfully")
|
|
758
|
+
except Exception as e:
|
|
759
|
+
self.logger.error(f"Failed to initialize face recognition use case: {e}", exc_info=True)
|
|
760
|
+
return self.create_error_result(
|
|
761
|
+
f"Initialization failed: {e}",
|
|
762
|
+
usecase=self.name,
|
|
763
|
+
category=self.category,
|
|
764
|
+
context=context,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
# Ensure confidence threshold is set
|
|
768
|
+
if not config.confidence_threshold:
|
|
769
|
+
config.confidence_threshold = 0.35
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
# Detect input format and store in context
|
|
773
|
+
input_format = match_results_structure(data)
|
|
774
|
+
context.input_format = input_format
|
|
775
|
+
|
|
776
|
+
context.confidence_threshold = config.confidence_threshold
|
|
777
|
+
|
|
778
|
+
# Parse face recognition model output format (with embeddings)
|
|
779
|
+
processed_data = self._parse_face_model_output(data)
|
|
780
|
+
# Normalize embeddings early for consistency (local parity)
|
|
781
|
+
for _det in processed_data:
|
|
782
|
+
try:
|
|
783
|
+
emb = _det.get("embedding", []) or []
|
|
784
|
+
if emb:
|
|
785
|
+
_det["embedding"] = _normalize_embedding(emb)
|
|
786
|
+
except Exception:
|
|
787
|
+
pass
|
|
788
|
+
# Ignore any pre-existing track_id on detections (we rely on our own tracker)
|
|
789
|
+
for _det in processed_data:
|
|
790
|
+
if isinstance(_det, dict) and "track_id" in _det:
|
|
791
|
+
try:
|
|
792
|
+
del _det["track_id"]
|
|
793
|
+
except Exception:
|
|
794
|
+
_det["track_id"] = None
|
|
795
|
+
|
|
796
|
+
# Apply standard confidence filtering
|
|
797
|
+
if config.confidence_threshold is not None:
|
|
798
|
+
processed_data = filter_by_confidence(
|
|
799
|
+
processed_data, config.confidence_threshold
|
|
800
|
+
)
|
|
801
|
+
self.logger.debug(
|
|
802
|
+
f"Applied confidence filtering with threshold {config.confidence_threshold}"
|
|
803
|
+
)
|
|
804
|
+
else:
|
|
805
|
+
self.logger.debug(
|
|
806
|
+
"Did not apply confidence filtering since threshold not provided"
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
# Apply category mapping if provided
|
|
810
|
+
if config.index_to_category:
|
|
811
|
+
processed_data = apply_category_mapping(
|
|
812
|
+
processed_data, config.index_to_category
|
|
813
|
+
)
|
|
814
|
+
self.logger.debug("Applied category mapping")
|
|
815
|
+
|
|
816
|
+
# Apply category filtering
|
|
817
|
+
if config.target_categories:
|
|
818
|
+
processed_data = filter_by_categories(
|
|
819
|
+
processed_data, config.target_categories
|
|
820
|
+
)
|
|
821
|
+
self.logger.debug("Applied category filtering")
|
|
822
|
+
|
|
823
|
+
# print("------------------TILL TRACKER MS----------------------------")
|
|
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----------------------------")
|
|
826
|
+
# Advanced tracking (BYTETracker-like) - only if enabled
|
|
827
|
+
if config.enable_face_tracking:
|
|
828
|
+
from ..advanced_tracker import AdvancedTracker
|
|
829
|
+
from ..advanced_tracker.config import TrackerConfig
|
|
830
|
+
|
|
831
|
+
# Create tracker instance if it doesn't exist (preserves state across frames)
|
|
832
|
+
if self.tracker is None:
|
|
833
|
+
tracker_config = TrackerConfig(
|
|
834
|
+
track_high_thresh=0.5,
|
|
835
|
+
track_low_thresh=0.05,
|
|
836
|
+
new_track_thresh=0.5,
|
|
837
|
+
match_thresh=0.8,
|
|
838
|
+
track_buffer=int(300), # allow short occlusions
|
|
839
|
+
max_time_lost=int(150),
|
|
840
|
+
fuse_score=True,
|
|
841
|
+
enable_gmc=False,
|
|
842
|
+
frame_rate=int(20)
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
self.tracker = AdvancedTracker(tracker_config)
|
|
846
|
+
self.logger.info(
|
|
847
|
+
"Initialized AdvancedTracker for Face Recognition with thresholds: "
|
|
848
|
+
f"high={tracker_config.track_high_thresh}, "
|
|
849
|
+
f"low={tracker_config.track_low_thresh}, "
|
|
850
|
+
f"new={tracker_config.new_track_thresh}"
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
# The tracker expects the data in the same format as input
|
|
854
|
+
# It will add track_id and frame_id to each detection (we rely ONLY on these)
|
|
855
|
+
processed_data = self.tracker.update(processed_data)
|
|
856
|
+
else:
|
|
857
|
+
self.logger.debug("Advanced face tracking disabled; continuing without external track IDs")
|
|
858
|
+
|
|
859
|
+
# Initialize local recognition summary variables
|
|
860
|
+
current_recognized_count = 0
|
|
861
|
+
current_unknown_count = 0
|
|
862
|
+
recognized_persons = {}
|
|
863
|
+
current_frame_staff_details = {}
|
|
864
|
+
|
|
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
|
+
|
|
869
|
+
# Process face recognition for each detection (if enabled)
|
|
870
|
+
if config.enable_face_recognition:
|
|
871
|
+
# Additional safety check: verify embeddings are still loaded and ready
|
|
872
|
+
if not self.embedding_manager or not self.embedding_manager.is_ready():
|
|
873
|
+
status = self.embedding_manager.get_status() if self.embedding_manager else {}
|
|
874
|
+
error_msg = f"CRITICAL: Cannot process face recognition - embeddings not ready. Status: {status}"
|
|
875
|
+
self.logger.error(error_msg)
|
|
876
|
+
print(f"ERROR: {error_msg}")
|
|
877
|
+
return self.create_error_result(
|
|
878
|
+
error_msg,
|
|
879
|
+
usecase=self.name,
|
|
880
|
+
category=self.category,
|
|
881
|
+
context=context,
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
face_recognition_result = await self._process_face_recognition(
|
|
885
|
+
processed_data, config, stream_info, input_bytes
|
|
886
|
+
)
|
|
887
|
+
processed_data, current_recognized_count, current_unknown_count, recognized_persons, current_frame_staff_details = face_recognition_result
|
|
888
|
+
else:
|
|
889
|
+
# Just add default face recognition fields without actual recognition
|
|
890
|
+
for detection in processed_data:
|
|
891
|
+
detection["person_id"] = None
|
|
892
|
+
detection["person_name"] = "Unknown"
|
|
893
|
+
detection["recognition_status"] = "disabled"
|
|
894
|
+
detection["enrolled"] = False
|
|
895
|
+
|
|
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
|
+
|
|
900
|
+
# Update tracking state for total count per label
|
|
901
|
+
self._update_tracking_state(processed_data)
|
|
902
|
+
|
|
903
|
+
# Update frame counter
|
|
904
|
+
self._total_frame_counter += 1
|
|
905
|
+
|
|
906
|
+
# Extract frame information from stream_info
|
|
907
|
+
frame_number = None
|
|
908
|
+
if stream_info:
|
|
909
|
+
input_settings = stream_info.get("input_settings", {})
|
|
910
|
+
start_frame = input_settings.get("start_frame")
|
|
911
|
+
end_frame = input_settings.get("end_frame")
|
|
912
|
+
# If start and end frame are the same, it's a single frame
|
|
913
|
+
if (
|
|
914
|
+
start_frame is not None
|
|
915
|
+
and end_frame is not None
|
|
916
|
+
and start_frame == end_frame
|
|
917
|
+
):
|
|
918
|
+
frame_number = start_frame
|
|
919
|
+
|
|
920
|
+
# Compute summaries and alerts
|
|
921
|
+
general_counting_summary = calculate_counting_summary(data)
|
|
922
|
+
counting_summary = self._count_categories(processed_data, config)
|
|
923
|
+
# Add total unique counts after tracking using only local state
|
|
924
|
+
total_counts = self.get_total_counts()
|
|
925
|
+
counting_summary["total_counts"] = total_counts
|
|
926
|
+
|
|
927
|
+
# NEW: Add face recognition summary
|
|
928
|
+
counting_summary.update(self._get_face_recognition_summary(
|
|
929
|
+
current_recognized_count, current_unknown_count, recognized_persons
|
|
930
|
+
))
|
|
931
|
+
|
|
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
|
+
|
|
936
|
+
# Add detections to the counting summary (standard pattern for detection use cases)
|
|
937
|
+
# Ensure display label is present for UI (does not affect logic/counters)
|
|
938
|
+
for _d in processed_data:
|
|
939
|
+
if "display_name" not in _d:
|
|
940
|
+
name = _d.get("person_name")
|
|
941
|
+
# Use person_name only if recognized; otherwise leave empty to honor probation logic
|
|
942
|
+
_d["display_name"] = name if _d.get("recognition_status") == "known" else (_d.get("display_name", "") or "")
|
|
943
|
+
counting_summary["detections"] = processed_data
|
|
944
|
+
|
|
945
|
+
alerts = self._check_alerts(counting_summary, frame_number, config)
|
|
946
|
+
|
|
947
|
+
# Step: Generate structured incidents, tracking stats and business analytics with frame-based keys
|
|
948
|
+
incidents_list = self._generate_incidents(
|
|
949
|
+
counting_summary, alerts, config, frame_number, stream_info
|
|
950
|
+
)
|
|
951
|
+
tracking_stats_list = self._generate_tracking_stats(
|
|
952
|
+
counting_summary, alerts, config, frame_number, stream_info, current_frame_staff_details
|
|
953
|
+
)
|
|
954
|
+
business_analytics_list = self._generate_business_analytics(
|
|
955
|
+
counting_summary, alerts, config, stream_info, is_empty=True
|
|
956
|
+
)
|
|
957
|
+
summary_list = self._generate_summary(incidents_list, tracking_stats_list, business_analytics_list)
|
|
958
|
+
|
|
959
|
+
# Extract frame-based dictionaries from the lists
|
|
960
|
+
incidents = incidents_list[0] if incidents_list else {}
|
|
961
|
+
tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
|
|
962
|
+
business_analytics = (
|
|
963
|
+
business_analytics_list[0] if business_analytics_list else {}
|
|
964
|
+
)
|
|
965
|
+
summary = summary_list[0] if summary_list else {}
|
|
966
|
+
|
|
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
|
+
|
|
972
|
+
agg_summary = {
|
|
973
|
+
str(frame_number): {
|
|
974
|
+
"incidents": incidents,
|
|
975
|
+
"tracking_stats": tracking_stats,
|
|
976
|
+
"business_analytics": business_analytics,
|
|
977
|
+
"alerts": alerts,
|
|
978
|
+
"human_text": summary,
|
|
979
|
+
"person_tracking": self.get_person_tracking_summary(),
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
context.mark_completed()
|
|
984
|
+
|
|
985
|
+
# Build result object following the standard pattern - same structure as people counting
|
|
986
|
+
result = self.create_result(
|
|
987
|
+
data={"agg_summary": agg_summary},
|
|
988
|
+
usecase=self.name,
|
|
989
|
+
category=self.category,
|
|
990
|
+
context=context,
|
|
991
|
+
)
|
|
992
|
+
proc_time = time.time() - processing_start
|
|
993
|
+
processing_latency_ms = proc_time * 1000.0
|
|
994
|
+
processing_fps = (1.0 / proc_time) if proc_time > 0 else None
|
|
995
|
+
# Log the performance metrics using the module-level logger
|
|
996
|
+
print("latency in ms:",processing_latency_ms,"| Throughput fps:",processing_fps,"| Frame_Number:",self._total_frame_counter)
|
|
997
|
+
|
|
998
|
+
return result
|
|
999
|
+
|
|
1000
|
+
def _parse_face_model_output(self, data: Any) -> List[Dict]:
|
|
1001
|
+
"""Parse face recognition model output to standard detection format, preserving embeddings"""
|
|
1002
|
+
processed_data = []
|
|
1003
|
+
|
|
1004
|
+
if isinstance(data, dict):
|
|
1005
|
+
# Handle frame-based format: {"0": [...], "1": [...]}
|
|
1006
|
+
for frame_id, frame_detections in data.items():
|
|
1007
|
+
if isinstance(frame_detections, list):
|
|
1008
|
+
for detection in frame_detections:
|
|
1009
|
+
if isinstance(detection, dict):
|
|
1010
|
+
# Convert to standard format but preserve face-specific fields
|
|
1011
|
+
standard_detection = {
|
|
1012
|
+
"category": detection.get("category", "face"),
|
|
1013
|
+
"confidence": detection.get("confidence", 0.0),
|
|
1014
|
+
"bounding_box": detection.get("bounding_box", {}),
|
|
1015
|
+
"track_id": detection.get("track_id", ""),
|
|
1016
|
+
"frame_id": detection.get("frame_id", frame_id),
|
|
1017
|
+
# Preserve face-specific fields
|
|
1018
|
+
"embedding": detection.get("embedding", []),
|
|
1019
|
+
"landmarks": detection.get("landmarks", None),
|
|
1020
|
+
"fps": detection.get("fps", 30),
|
|
1021
|
+
}
|
|
1022
|
+
processed_data.append(standard_detection)
|
|
1023
|
+
elif isinstance(data, list):
|
|
1024
|
+
# Handle list format
|
|
1025
|
+
for detection in data:
|
|
1026
|
+
if isinstance(detection, dict):
|
|
1027
|
+
# Convert to standard format and ensure all required fields exist
|
|
1028
|
+
standard_detection = {
|
|
1029
|
+
"category": detection.get("category", "face"),
|
|
1030
|
+
"confidence": detection.get("confidence", 0.0),
|
|
1031
|
+
"bounding_box": detection.get("bounding_box", {}),
|
|
1032
|
+
"track_id": detection.get("track_id", ""),
|
|
1033
|
+
"frame_id": detection.get("frame_id", 0),
|
|
1034
|
+
# Preserve face-specific fields
|
|
1035
|
+
"embedding": detection.get("embedding", []),
|
|
1036
|
+
"landmarks": detection.get("landmarks", None),
|
|
1037
|
+
"fps": detection.get("fps", 30),
|
|
1038
|
+
"metadata": detection.get("metadata", {}),
|
|
1039
|
+
}
|
|
1040
|
+
processed_data.append(standard_detection)
|
|
1041
|
+
|
|
1042
|
+
return processed_data
|
|
1043
|
+
|
|
1044
|
+
def _extract_frame_from_data(self, input_bytes: bytes) -> Optional[np.ndarray]:
|
|
1045
|
+
"""
|
|
1046
|
+
Extract frame from original model data
|
|
1047
|
+
|
|
1048
|
+
Args:
|
|
1049
|
+
original_data: Original data from model (same format as model receives)
|
|
1050
|
+
|
|
1051
|
+
Returns:
|
|
1052
|
+
np.ndarray: Frame data or None if not found
|
|
1053
|
+
"""
|
|
1054
|
+
try:
|
|
1055
|
+
try:
|
|
1056
|
+
if isinstance(input_bytes, str):
|
|
1057
|
+
frame_bytes = base64.b64decode(input_bytes)
|
|
1058
|
+
else:
|
|
1059
|
+
frame_bytes = input_bytes
|
|
1060
|
+
nparr = np.frombuffer(frame_bytes, np.uint8)
|
|
1061
|
+
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
|
1062
|
+
return frame
|
|
1063
|
+
except Exception as e:
|
|
1064
|
+
self.logger.debug(f"Could not decode direct frame data: {e}")
|
|
1065
|
+
|
|
1066
|
+
return None
|
|
1067
|
+
|
|
1068
|
+
except Exception as e:
|
|
1069
|
+
self.logger.debug(f"Error extracting frame from data: {e}")
|
|
1070
|
+
return None
|
|
1071
|
+
|
|
1072
|
+
# Removed unused _calculate_bbox_area_percentage (not referenced)
|
|
1073
|
+
|
|
1074
|
+
async def _process_face_recognition(
|
|
1075
|
+
self,
|
|
1076
|
+
detections: List[Dict],
|
|
1077
|
+
config: FaceRecognitionEmbeddingConfig,
|
|
1078
|
+
stream_info: Optional[Dict[str, Any]] = None,
|
|
1079
|
+
input_bytes: Optional[bytes] = None,
|
|
1080
|
+
) -> List[Dict]:
|
|
1081
|
+
"""Process face recognition for each detection with embeddings"""
|
|
1082
|
+
|
|
1083
|
+
# Face client is initialized in initialize(); if absent, this indicates a prior init failure
|
|
1084
|
+
if not self.face_client:
|
|
1085
|
+
self.logger.error("Face client not initialized; initialize() must succeed before processing")
|
|
1086
|
+
return []
|
|
1087
|
+
|
|
1088
|
+
# Initialize unknown faces storage if not exists
|
|
1089
|
+
if not hasattr(self, "unknown_faces_storage"):
|
|
1090
|
+
self.unknown_faces_storage = {}
|
|
1091
|
+
|
|
1092
|
+
# Initialize frame availability warning flag to avoid spam
|
|
1093
|
+
if not hasattr(self, "_frame_warning_logged"):
|
|
1094
|
+
self._frame_warning_logged = False
|
|
1095
|
+
|
|
1096
|
+
# Initialize per-request tracking (thread-safe)
|
|
1097
|
+
current_recognized_count = 0
|
|
1098
|
+
current_unknown_count = 0
|
|
1099
|
+
recognized_persons = {}
|
|
1100
|
+
current_frame_staff_details = {} # Store staff details for current frame
|
|
1101
|
+
|
|
1102
|
+
# Extract frame from original data for cropping unknown faces
|
|
1103
|
+
current_frame = (
|
|
1104
|
+
self._extract_frame_from_data(input_bytes) if input_bytes else None
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
# Log frame availability once per session
|
|
1108
|
+
if current_frame is None and not self._frame_warning_logged:
|
|
1109
|
+
if config.enable_unknown_face_processing:
|
|
1110
|
+
self.logger.info(
|
|
1111
|
+
"Frame data not available in model output - unknown face cropping/uploading will be skipped. "
|
|
1112
|
+
"To disable this feature entirely, set enable_unknown_face_processing=False"
|
|
1113
|
+
)
|
|
1114
|
+
self._frame_warning_logged = True
|
|
1115
|
+
|
|
1116
|
+
# Get location from stream_info
|
|
1117
|
+
location = (
|
|
1118
|
+
stream_info.get("camera_location", "unknown") if stream_info else "unknown"
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
# Generate current timestamp
|
|
1122
|
+
current_timestamp = datetime.now(timezone.utc).isoformat()
|
|
1123
|
+
|
|
1124
|
+
final_detections = []
|
|
1125
|
+
# Process detections sequentially to preserve order
|
|
1126
|
+
for detection in detections:
|
|
1127
|
+
|
|
1128
|
+
# Process each detection sequentially with await to preserve order
|
|
1129
|
+
st1=time.time()
|
|
1130
|
+
processed_detection = await self._process_face(
|
|
1131
|
+
detection, current_frame, location, current_timestamp, config,
|
|
1132
|
+
current_recognized_count, current_unknown_count,
|
|
1133
|
+
recognized_persons, current_frame_staff_details
|
|
1134
|
+
)
|
|
1135
|
+
# print("------------------WHOLE FACE RECOG PROCESSING DETECTION----------------------------")
|
|
1136
|
+
# print("LATENCY:",(time.time() - st1)*1000,"| Throughput fps:",(1.0 / (time.time() - st1)) if (time.time() - st1) > 0 else None)
|
|
1137
|
+
# print("------------------WHOLE FACE RECOG PROCESSING DETECTION----------------------------")
|
|
1138
|
+
|
|
1139
|
+
# Include both known and unknown faces in final detections (maintains original order)
|
|
1140
|
+
if processed_detection:
|
|
1141
|
+
final_detections.append(processed_detection)
|
|
1142
|
+
# Update local counters based on processed detection
|
|
1143
|
+
if processed_detection.get("recognition_status") == "known":
|
|
1144
|
+
staff_id = processed_detection.get("person_id")
|
|
1145
|
+
if staff_id:
|
|
1146
|
+
current_frame_staff_details[staff_id] = processed_detection.get("person_name", "Unknown")
|
|
1147
|
+
current_recognized_count += 1
|
|
1148
|
+
recognized_persons[staff_id] = recognized_persons.get(staff_id, 0) + 1
|
|
1149
|
+
elif processed_detection.get("recognition_status") == "unknown":
|
|
1150
|
+
current_unknown_count += 1
|
|
1151
|
+
|
|
1152
|
+
return final_detections, current_recognized_count, current_unknown_count, recognized_persons, current_frame_staff_details
|
|
1153
|
+
|
|
1154
|
+
async def _process_face(
|
|
1155
|
+
self,
|
|
1156
|
+
detection: Dict,
|
|
1157
|
+
current_frame: np.ndarray,
|
|
1158
|
+
location: str = "",
|
|
1159
|
+
current_timestamp: str = "",
|
|
1160
|
+
config: FaceRecognitionEmbeddingConfig = None,
|
|
1161
|
+
current_recognized_count: int = 0,
|
|
1162
|
+
current_unknown_count: int = 0,
|
|
1163
|
+
recognized_persons: Dict = None,
|
|
1164
|
+
current_frame_staff_details: Dict = None,
|
|
1165
|
+
) -> Dict:
|
|
1166
|
+
|
|
1167
|
+
# Extract and validate embedding using EmbeddingManager
|
|
1168
|
+
st2=time.time()
|
|
1169
|
+
detection, embedding = self.embedding_manager.extract_embedding_from_detection(detection)
|
|
1170
|
+
if not embedding:
|
|
1171
|
+
return None
|
|
1172
|
+
|
|
1173
|
+
# Internal tracker-provided ID (from AdvancedTracker; ignore upstream IDs entirely)
|
|
1174
|
+
track_id = detection.get("track_id")
|
|
1175
|
+
# print("------------------FACE RECOG EMBEDDING EXTRACTION----------------------------")
|
|
1176
|
+
# print("LATENCY:",(time.time() - st2)*1000,"| Throughput fps:",(1.0 / (time.time() - st2)) if (time.time() - st2) > 0 else None)
|
|
1177
|
+
# print("------------------FACE RECOG EMBEDDING EXTRACTION----------------------------")
|
|
1178
|
+
|
|
1179
|
+
# Determine if detection is eligible for recognition (similar to compare_similarity gating)
|
|
1180
|
+
bbox = detection.get("bounding_box", {}) or {}
|
|
1181
|
+
x1 = int(bbox.get("xmin", bbox.get("x1", 0)))
|
|
1182
|
+
y1 = int(bbox.get("ymin", bbox.get("y1", 0)))
|
|
1183
|
+
x2 = int(bbox.get("xmax", bbox.get("x2", 0)))
|
|
1184
|
+
y2 = int(bbox.get("ymax", bbox.get("y2", 0)))
|
|
1185
|
+
w_box = max(1, x2 - x1)
|
|
1186
|
+
h_box = max(1, y2 - y1)
|
|
1187
|
+
frame_id = detection.get("frame_id", None) #TODO: Maybe replace this with stream_info frame_id
|
|
1188
|
+
|
|
1189
|
+
# Track probation age strictly by internal tracker id
|
|
1190
|
+
if track_id is not None:
|
|
1191
|
+
if track_id not in self._track_first_seen:
|
|
1192
|
+
try:
|
|
1193
|
+
self._track_first_seen[track_id] = int(frame_id) if frame_id is not None else self._total_frame_counter
|
|
1194
|
+
except Exception:
|
|
1195
|
+
self._track_first_seen[track_id] = self._total_frame_counter
|
|
1196
|
+
age_frames = (int(frame_id) if frame_id is not None else self._total_frame_counter) - int(self._track_first_seen.get(track_id, 0)) + 1
|
|
1197
|
+
else:
|
|
1198
|
+
age_frames = 1
|
|
1199
|
+
|
|
1200
|
+
eligible_for_recognition = (w_box >= self._min_face_w and h_box >= self._min_face_h)
|
|
1201
|
+
|
|
1202
|
+
# Primary: API-based identity smoothing via TemporalIdentityManager
|
|
1203
|
+
staff_id = None
|
|
1204
|
+
person_name = "Unknown"
|
|
1205
|
+
similarity_score = 0.0
|
|
1206
|
+
employee_id = None
|
|
1207
|
+
staff_details: Dict[str, Any] = {}
|
|
1208
|
+
detection_type = "unknown"
|
|
1209
|
+
try:
|
|
1210
|
+
if self.temporal_identity_manager:
|
|
1211
|
+
track_key = track_id if track_id is not None else f"no_track_{id(detection)}"
|
|
1212
|
+
if not eligible_for_recognition:
|
|
1213
|
+
# Mirror compare_similarity: when not eligible, keep stable label if present
|
|
1214
|
+
s = self.temporal_identity_manager.tracks.get(track_key, {})
|
|
1215
|
+
if isinstance(s, dict):
|
|
1216
|
+
stable_staff_id = s.get("stable_staff_id")
|
|
1217
|
+
stable_person_name = s.get("stable_person_name") or "Unknown"
|
|
1218
|
+
stable_employee_id = s.get("stable_employee_id")
|
|
1219
|
+
stable_score = float(s.get("stable_score", 0.0))
|
|
1220
|
+
stable_staff_details = s.get("stable_staff_details") if isinstance(s.get("stable_staff_details"), dict) else {}
|
|
1221
|
+
if stable_staff_id is not None:
|
|
1222
|
+
staff_id = stable_staff_id
|
|
1223
|
+
person_name = stable_person_name
|
|
1224
|
+
employee_id = stable_employee_id
|
|
1225
|
+
similarity_score = stable_score
|
|
1226
|
+
staff_details = stable_staff_details
|
|
1227
|
+
detection_type = "known"
|
|
1228
|
+
else:
|
|
1229
|
+
detection_type = "unknown"
|
|
1230
|
+
# Also append embedding to history for temporal smoothing
|
|
1231
|
+
if embedding:
|
|
1232
|
+
try:
|
|
1233
|
+
|
|
1234
|
+
self.temporal_identity_manager._ensure_track(track_key)
|
|
1235
|
+
hist = self.temporal_identity_manager.tracks[track_key]["embedding_history"] # type: ignore
|
|
1236
|
+
hist.append(_normalize_embedding(embedding)) # type: ignore
|
|
1237
|
+
except Exception:
|
|
1238
|
+
pass
|
|
1239
|
+
else: #if eligible for recognition
|
|
1240
|
+
st3=time.time()
|
|
1241
|
+
staff_id, person_name, similarity_score, employee_id, staff_details, detection_type = await self.temporal_identity_manager.update(
|
|
1242
|
+
track_id=track_key,
|
|
1243
|
+
emb=embedding,
|
|
1244
|
+
eligible_for_recognition=True,
|
|
1245
|
+
location=location,
|
|
1246
|
+
timestamp=current_timestamp,
|
|
1247
|
+
)
|
|
1248
|
+
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE----------------------------")
|
|
1249
|
+
# print("LATENCY:",(time.time() - st3)*1000,"| Throughput fps:",(1.0 / (time.time() - st3)) if (time.time() - st3) > 0 else None)
|
|
1250
|
+
# print("------------------FACE RECOG TEMPORAL IDENTITY MANAGER UPDATE----------------------------")
|
|
1251
|
+
except Exception as e:
|
|
1252
|
+
self.logger.warning(f"TemporalIdentityManager update failed: {e}")
|
|
1253
|
+
|
|
1254
|
+
# # Fallback: if still unknown and we have an EmbeddingManager, use local search
|
|
1255
|
+
# if (staff_id is None or detection_type == "unknown") and self.embedding_manager is not None:
|
|
1256
|
+
# try:
|
|
1257
|
+
# search_result = await self.embedding_manager.search_face_embedding(
|
|
1258
|
+
# embedding=embedding,
|
|
1259
|
+
# track_id=track_id,
|
|
1260
|
+
# location=location,
|
|
1261
|
+
# timestamp=current_timestamp,
|
|
1262
|
+
# )
|
|
1263
|
+
# if search_result:
|
|
1264
|
+
# employee_id = search_result.employee_id
|
|
1265
|
+
# staff_id = search_result.staff_id
|
|
1266
|
+
# detection_type = search_result.detection_type
|
|
1267
|
+
# staff_details = search_result.staff_details
|
|
1268
|
+
# person_name = search_result.person_name
|
|
1269
|
+
# similarity_score = search_result.similarity_score
|
|
1270
|
+
# except Exception as e:
|
|
1271
|
+
# self.logger.warning(f"Local embedding search fallback failed: {e}")
|
|
1272
|
+
|
|
1273
|
+
# Update detection object directly (avoid relying on SearchResult type)
|
|
1274
|
+
detection = detection.copy()
|
|
1275
|
+
detection["person_id"] = staff_id
|
|
1276
|
+
detection["person_name"] = person_name or "Unknown"
|
|
1277
|
+
detection["recognition_status"] = "known" if staff_id else "unknown"
|
|
1278
|
+
detection["employee_id"] = employee_id
|
|
1279
|
+
detection["staff_details"] = staff_details if isinstance(staff_details, dict) else {}
|
|
1280
|
+
detection["similarity_score"] = float(similarity_score)
|
|
1281
|
+
detection["enrolled"] = bool(staff_id)
|
|
1282
|
+
# Display label policy: show only if identified OR probation exceeded, else empty label
|
|
1283
|
+
is_identified = (staff_id is not None and detection_type == "known")
|
|
1284
|
+
show_label = is_identified or (age_frames >= self._probation_frames and not is_identified)
|
|
1285
|
+
detection["display_name"] = (person_name if is_identified else ("Unknown" if show_label else ""))
|
|
1286
|
+
# Preserve original category (e.g., 'face') for tracking/counting
|
|
1287
|
+
|
|
1288
|
+
# Update global tracking per unique internal track id to avoid double-counting within a frame
|
|
1289
|
+
# Determine unknown strictly by recognition_status (display label never affects counters)
|
|
1290
|
+
is_truly_unknown = (detection.get("recognition_status") == "unknown")
|
|
1291
|
+
|
|
1292
|
+
try:
|
|
1293
|
+
internal_tid = detection.get("track_id")
|
|
1294
|
+
except Exception:
|
|
1295
|
+
internal_tid = None
|
|
1296
|
+
|
|
1297
|
+
if not is_truly_unknown and detection_type == "known":
|
|
1298
|
+
# Mark recognized and ensure it is not counted as unknown anymore
|
|
1299
|
+
self._track_person(staff_id)
|
|
1300
|
+
with self._tracking_lock:
|
|
1301
|
+
if internal_tid is not None:
|
|
1302
|
+
self._unknown_track_ids.discard(internal_tid)
|
|
1303
|
+
self._recognized_track_ids.add(internal_tid)
|
|
1304
|
+
else:
|
|
1305
|
+
# Only count as unknown in session totals if probation has been exceeded and still unknown
|
|
1306
|
+
matured_unknown = (age_frames >= self._probation_frames)
|
|
1307
|
+
if matured_unknown:
|
|
1308
|
+
with self._tracking_lock:
|
|
1309
|
+
if internal_tid is not None:
|
|
1310
|
+
# If it later becomes recognized, we'll remove it from unknown set above
|
|
1311
|
+
self._unknown_track_ids.add(internal_tid)
|
|
1312
|
+
|
|
1313
|
+
# Enqueue detection for background logging with all required parameters
|
|
1314
|
+
try:
|
|
1315
|
+
# Log known faces for activity tracking (skip any employee_id starting with "unknown_")
|
|
1316
|
+
if (
|
|
1317
|
+
detection["recognition_status"] == "known"
|
|
1318
|
+
and self.people_activity_logging
|
|
1319
|
+
and config
|
|
1320
|
+
and getattr(config, 'enable_people_activity_logging', True)
|
|
1321
|
+
and employee_id
|
|
1322
|
+
and not str(employee_id).startswith("unknown_")
|
|
1323
|
+
):
|
|
1324
|
+
st4=time.time()
|
|
1325
|
+
await self.people_activity_logging.enqueue_detection(
|
|
1326
|
+
detection=detection,
|
|
1327
|
+
current_frame=current_frame,
|
|
1328
|
+
location=location,
|
|
1329
|
+
)
|
|
1330
|
+
# print("------------------FACE RECOG ENQUEUEING DETECTION FOR ACTIVITY LOGGING----------------------------")
|
|
1331
|
+
# print("LATENCY:",(time.time() - st4)*1000,"| Throughput fps:",(1.0 / (time.time() - st4)) if (time.time() - st4) > 0 else None)
|
|
1332
|
+
# print("------------------FACE RECOG ENQUEUEING DETECTION FOR ACTIVITY LOGGING----------------------------")
|
|
1333
|
+
|
|
1334
|
+
self.logger.debug(f"Enqueued known face detection for activity logging: {detection.get('person_name', 'Unknown')}")
|
|
1335
|
+
except Exception as e:
|
|
1336
|
+
self.logger.error(f"Error enqueueing detection for activity logging: {e}")
|
|
1337
|
+
# 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)
|
|
1339
|
+
# print("------------------PROCESS FACE LATENCY TOTAL----------------------------")
|
|
1340
|
+
|
|
1341
|
+
return detection
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
def _return_error_detection(
|
|
1346
|
+
self,
|
|
1347
|
+
detection: Dict,
|
|
1348
|
+
person_id: str,
|
|
1349
|
+
person_name: str,
|
|
1350
|
+
recognition_status: str,
|
|
1351
|
+
enrolled: bool,
|
|
1352
|
+
category: str,
|
|
1353
|
+
error: str,
|
|
1354
|
+
) -> Dict:
|
|
1355
|
+
"""Return error detection"""
|
|
1356
|
+
detection["person_id"] = person_id
|
|
1357
|
+
detection["person_name"] = person_name
|
|
1358
|
+
detection["recognition_status"] = recognition_status
|
|
1359
|
+
detection["enrolled"] = enrolled
|
|
1360
|
+
detection["category"] = category
|
|
1361
|
+
detection["error"] = error
|
|
1362
|
+
return detection
|
|
1363
|
+
|
|
1364
|
+
def _track_person(self, person_id: str) -> None:
|
|
1365
|
+
"""Track person with camera ID and UTC timestamp"""
|
|
1366
|
+
if person_id not in self.person_tracking:
|
|
1367
|
+
self.person_tracking[person_id] = []
|
|
1368
|
+
|
|
1369
|
+
# Add current detection
|
|
1370
|
+
detection_record = {
|
|
1371
|
+
"camera_id": "test_camera_001", # TODO: Get from stream_info in production
|
|
1372
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
1373
|
+
}
|
|
1374
|
+
self.person_tracking[person_id].append(detection_record)
|
|
1375
|
+
|
|
1376
|
+
def get_person_tracking_summary(self) -> Dict:
|
|
1377
|
+
"""Get summary of tracked persons with camera IDs and timestamps"""
|
|
1378
|
+
return dict(self.person_tracking)
|
|
1379
|
+
|
|
1380
|
+
def get_unknown_faces_storage(self) -> Dict[str, bytes]:
|
|
1381
|
+
"""Get stored unknown face images as bytes"""
|
|
1382
|
+
if self.people_activity_logging:
|
|
1383
|
+
return self.people_activity_logging.get_unknown_faces_storage()
|
|
1384
|
+
return {}
|
|
1385
|
+
|
|
1386
|
+
def clear_unknown_faces_storage(self) -> None:
|
|
1387
|
+
"""Clear stored unknown face images"""
|
|
1388
|
+
if self.people_activity_logging:
|
|
1389
|
+
self.people_activity_logging.clear_unknown_faces_storage()
|
|
1390
|
+
|
|
1391
|
+
def _get_face_recognition_summary(self, current_recognized_count: int, current_unknown_count: int, recognized_persons: Dict) -> Dict:
|
|
1392
|
+
"""Get face recognition summary for current frame"""
|
|
1393
|
+
recognition_rate = 0.0
|
|
1394
|
+
total_current = current_recognized_count + current_unknown_count
|
|
1395
|
+
if total_current > 0:
|
|
1396
|
+
recognition_rate = (current_recognized_count / total_current) * 100
|
|
1397
|
+
|
|
1398
|
+
# Get thread-safe global totals
|
|
1399
|
+
with self._tracking_lock:
|
|
1400
|
+
total_recognized = len(self._recognized_track_ids)
|
|
1401
|
+
total_unknown = len(self._unknown_track_ids)
|
|
1402
|
+
|
|
1403
|
+
return {
|
|
1404
|
+
"face_recognition_summary": {
|
|
1405
|
+
"current_frame": {
|
|
1406
|
+
"recognized": current_recognized_count,
|
|
1407
|
+
"unknown": current_unknown_count,
|
|
1408
|
+
"total": total_current,
|
|
1409
|
+
"recognized_persons": dict(recognized_persons),
|
|
1410
|
+
"recognition_rate": round(recognition_rate, 1),
|
|
1411
|
+
},
|
|
1412
|
+
"session_totals": {
|
|
1413
|
+
"total_recognized": total_recognized,
|
|
1414
|
+
"total_unknown": total_unknown,
|
|
1415
|
+
"total_processed": total_recognized + total_unknown,
|
|
1416
|
+
},
|
|
1417
|
+
"person_tracking": self.get_person_tracking_summary(),
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
def _check_alerts(
|
|
1422
|
+
self, summary: dict, frame_number: Any, config: FaceRecognitionEmbeddingConfig
|
|
1423
|
+
) -> List[Dict]:
|
|
1424
|
+
"""
|
|
1425
|
+
Check if any alert thresholds are exceeded and return alert dicts.
|
|
1426
|
+
"""
|
|
1427
|
+
|
|
1428
|
+
def get_trend(data, lookback=900, threshold=0.6):
|
|
1429
|
+
window = data[-lookback:] if len(data) >= lookback else data
|
|
1430
|
+
if len(window) < 2:
|
|
1431
|
+
return True # not enough data to determine trend
|
|
1432
|
+
increasing = 0
|
|
1433
|
+
total = 0
|
|
1434
|
+
for i in range(1, len(window)):
|
|
1435
|
+
if window[i] >= window[i - 1]:
|
|
1436
|
+
increasing += 1
|
|
1437
|
+
total += 1
|
|
1438
|
+
ratio = increasing / total
|
|
1439
|
+
if ratio >= threshold:
|
|
1440
|
+
return True
|
|
1441
|
+
elif ratio <= (1 - threshold):
|
|
1442
|
+
return False
|
|
1443
|
+
|
|
1444
|
+
frame_key = str(frame_number) if frame_number is not None else "current_frame"
|
|
1445
|
+
alerts = []
|
|
1446
|
+
total_detections = summary.get("total_count", 0)
|
|
1447
|
+
face_summary = summary.get("face_recognition_summary", {})
|
|
1448
|
+
current_unknown = face_summary.get("current_frame", {}).get("unknown", 0)
|
|
1449
|
+
|
|
1450
|
+
if not config.alert_config:
|
|
1451
|
+
return alerts
|
|
1452
|
+
|
|
1453
|
+
if (
|
|
1454
|
+
hasattr(config.alert_config, "count_thresholds")
|
|
1455
|
+
and config.alert_config.count_thresholds
|
|
1456
|
+
):
|
|
1457
|
+
for category, threshold in config.alert_config.count_thresholds.items():
|
|
1458
|
+
if category == "unknown_faces" and current_unknown > threshold:
|
|
1459
|
+
alerts.append(
|
|
1460
|
+
{
|
|
1461
|
+
"alert_type": (
|
|
1462
|
+
getattr(config.alert_config, "alert_type", ["Default"])
|
|
1463
|
+
if hasattr(config.alert_config, "alert_type")
|
|
1464
|
+
else ["Default"]
|
|
1465
|
+
),
|
|
1466
|
+
"alert_id": f"alert_unknown_faces_{frame_key}",
|
|
1467
|
+
"incident_category": "unknown_face_detection",
|
|
1468
|
+
"threshold_level": threshold,
|
|
1469
|
+
"current_count": current_unknown,
|
|
1470
|
+
"ascending": get_trend(
|
|
1471
|
+
self._ascending_alert_list, lookback=900, threshold=0.8
|
|
1472
|
+
),
|
|
1473
|
+
"settings": {
|
|
1474
|
+
t: v
|
|
1475
|
+
for t, v in zip(
|
|
1476
|
+
(
|
|
1477
|
+
getattr(
|
|
1478
|
+
config.alert_config,
|
|
1479
|
+
"alert_type",
|
|
1480
|
+
["Default"],
|
|
1481
|
+
)
|
|
1482
|
+
if hasattr(config.alert_config, "alert_type")
|
|
1483
|
+
else ["Default"]
|
|
1484
|
+
),
|
|
1485
|
+
(
|
|
1486
|
+
getattr(
|
|
1487
|
+
config.alert_config, "alert_value", ["JSON"]
|
|
1488
|
+
)
|
|
1489
|
+
if hasattr(config.alert_config, "alert_value")
|
|
1490
|
+
else ["JSON"]
|
|
1491
|
+
),
|
|
1492
|
+
)
|
|
1493
|
+
},
|
|
1494
|
+
}
|
|
1495
|
+
)
|
|
1496
|
+
elif category == "all" and total_detections > threshold:
|
|
1497
|
+
alerts.append(
|
|
1498
|
+
{
|
|
1499
|
+
"alert_type": (
|
|
1500
|
+
getattr(config.alert_config, "alert_type", ["Default"])
|
|
1501
|
+
if hasattr(config.alert_config, "alert_type")
|
|
1502
|
+
else ["Default"]
|
|
1503
|
+
),
|
|
1504
|
+
"alert_id": "alert_" + category + "_" + frame_key,
|
|
1505
|
+
"incident_category": self.CASE_TYPE,
|
|
1506
|
+
"threshold_level": threshold,
|
|
1507
|
+
"ascending": get_trend(
|
|
1508
|
+
self._ascending_alert_list, lookback=900, threshold=0.8
|
|
1509
|
+
),
|
|
1510
|
+
"settings": {
|
|
1511
|
+
t: v
|
|
1512
|
+
for t, v in zip(
|
|
1513
|
+
(
|
|
1514
|
+
getattr(
|
|
1515
|
+
config.alert_config,
|
|
1516
|
+
"alert_type",
|
|
1517
|
+
["Default"],
|
|
1518
|
+
)
|
|
1519
|
+
if hasattr(config.alert_config, "alert_type")
|
|
1520
|
+
else ["Default"]
|
|
1521
|
+
),
|
|
1522
|
+
(
|
|
1523
|
+
getattr(
|
|
1524
|
+
config.alert_config, "alert_value", ["JSON"]
|
|
1525
|
+
)
|
|
1526
|
+
if hasattr(config.alert_config, "alert_value")
|
|
1527
|
+
else ["JSON"]
|
|
1528
|
+
),
|
|
1529
|
+
)
|
|
1530
|
+
},
|
|
1531
|
+
}
|
|
1532
|
+
)
|
|
1533
|
+
|
|
1534
|
+
return alerts
|
|
1535
|
+
|
|
1536
|
+
def _generate_tracking_stats(
|
|
1537
|
+
self,
|
|
1538
|
+
counting_summary: Dict,
|
|
1539
|
+
alerts: List,
|
|
1540
|
+
config: FaceRecognitionEmbeddingConfig,
|
|
1541
|
+
frame_number: Optional[int] = None,
|
|
1542
|
+
stream_info: Optional[Dict[str, Any]] = None,
|
|
1543
|
+
current_frame_staff_details: Dict = None,
|
|
1544
|
+
) -> List[Dict]:
|
|
1545
|
+
"""Generate structured tracking stats matching eg.json format with face recognition data."""
|
|
1546
|
+
camera_info = self.get_camera_info_from_stream(stream_info)
|
|
1547
|
+
tracking_stats = []
|
|
1548
|
+
|
|
1549
|
+
total_detections = counting_summary.get("total_count", 0)
|
|
1550
|
+
total_counts_dict = counting_summary.get("total_counts", {})
|
|
1551
|
+
cumulative_total = sum(total_counts_dict.values()) if total_counts_dict else 0
|
|
1552
|
+
per_category_count = counting_summary.get("per_category_count", {})
|
|
1553
|
+
face_summary = counting_summary.get("face_recognition_summary", {})
|
|
1554
|
+
|
|
1555
|
+
current_timestamp = self._get_current_timestamp_str(
|
|
1556
|
+
stream_info, precision=False
|
|
1557
|
+
)
|
|
1558
|
+
start_timestamp = self._get_start_timestamp_str(stream_info, precision=False)
|
|
1559
|
+
|
|
1560
|
+
# Create high precision timestamps for input_timestamp and reset_timestamp
|
|
1561
|
+
high_precision_start_timestamp = self._get_current_timestamp_str(
|
|
1562
|
+
stream_info, precision=True
|
|
1563
|
+
)
|
|
1564
|
+
high_precision_reset_timestamp = self._get_start_timestamp_str(
|
|
1565
|
+
stream_info, precision=True
|
|
1566
|
+
)
|
|
1567
|
+
|
|
1568
|
+
# Build total_counts array in expected format
|
|
1569
|
+
total_counts = []
|
|
1570
|
+
for cat, count in total_counts_dict.items():
|
|
1571
|
+
if count > 0:
|
|
1572
|
+
total_counts.append({"category": cat, "count": count})
|
|
1573
|
+
|
|
1574
|
+
# Add face recognition specific total counts
|
|
1575
|
+
session_totals = face_summary.get("session_totals", {})
|
|
1576
|
+
total_counts.extend(
|
|
1577
|
+
[
|
|
1578
|
+
{
|
|
1579
|
+
"category": "recognized_faces",
|
|
1580
|
+
"count": session_totals.get("total_recognized", 0),
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
"category": "unknown_faces",
|
|
1584
|
+
"count": session_totals.get("total_unknown", 0),
|
|
1585
|
+
},
|
|
1586
|
+
]
|
|
1587
|
+
)
|
|
1588
|
+
|
|
1589
|
+
# Build current_counts array in expected format
|
|
1590
|
+
current_counts = []
|
|
1591
|
+
for cat, count in per_category_count.items():
|
|
1592
|
+
if count > 0 or total_detections > 0:
|
|
1593
|
+
current_counts.append({"category": cat, "count": count})
|
|
1594
|
+
|
|
1595
|
+
# Add face recognition specific current counts
|
|
1596
|
+
current_frame = face_summary.get("current_frame", {})
|
|
1597
|
+
current_counts.extend(
|
|
1598
|
+
[
|
|
1599
|
+
{
|
|
1600
|
+
"category": "recognized_faces",
|
|
1601
|
+
"count": current_frame.get("recognized", 0),
|
|
1602
|
+
},
|
|
1603
|
+
{"category": "unknown_faces", "count": current_frame.get("unknown", 0)},
|
|
1604
|
+
]
|
|
1605
|
+
)
|
|
1606
|
+
|
|
1607
|
+
# Prepare detections with face recognition info
|
|
1608
|
+
detections = []
|
|
1609
|
+
for detection in counting_summary.get("detections", []):
|
|
1610
|
+
bbox = detection.get("bounding_box", {})
|
|
1611
|
+
category = detection.get("display_name", "")
|
|
1612
|
+
|
|
1613
|
+
detection_obj = self.create_detection_object(category, bbox)
|
|
1614
|
+
# Add face recognition specific fields
|
|
1615
|
+
detection_obj.update(
|
|
1616
|
+
{
|
|
1617
|
+
"person_id": detection.get("person_id"),
|
|
1618
|
+
# Use display_name for front-end label suppression policy
|
|
1619
|
+
"person_name": detection.get("display_name", ""),
|
|
1620
|
+
# Explicit label field for UI overlays
|
|
1621
|
+
"label": detection.get("display_name", ""),
|
|
1622
|
+
"recognition_status": detection.get(
|
|
1623
|
+
"recognition_status", "unknown"
|
|
1624
|
+
),
|
|
1625
|
+
"enrolled": detection.get("enrolled", False),
|
|
1626
|
+
}
|
|
1627
|
+
)
|
|
1628
|
+
detections.append(detection_obj)
|
|
1629
|
+
|
|
1630
|
+
# Build alert_settings array in expected format
|
|
1631
|
+
alert_settings = []
|
|
1632
|
+
if config.alert_config and hasattr(config.alert_config, "alert_type"):
|
|
1633
|
+
alert_settings.append(
|
|
1634
|
+
{
|
|
1635
|
+
"alert_type": (
|
|
1636
|
+
getattr(config.alert_config, "alert_type", ["Default"])
|
|
1637
|
+
if hasattr(config.alert_config, "alert_type")
|
|
1638
|
+
else ["Default"]
|
|
1639
|
+
),
|
|
1640
|
+
"incident_category": self.CASE_TYPE,
|
|
1641
|
+
"threshold_level": (
|
|
1642
|
+
config.alert_config.count_thresholds
|
|
1643
|
+
if hasattr(config.alert_config, "count_thresholds")
|
|
1644
|
+
else {}
|
|
1645
|
+
),
|
|
1646
|
+
"ascending": True,
|
|
1647
|
+
"settings": {
|
|
1648
|
+
t: v
|
|
1649
|
+
for t, v in zip(
|
|
1650
|
+
(
|
|
1651
|
+
getattr(config.alert_config, "alert_type", ["Default"])
|
|
1652
|
+
if hasattr(config.alert_config, "alert_type")
|
|
1653
|
+
else ["Default"]
|
|
1654
|
+
),
|
|
1655
|
+
(
|
|
1656
|
+
getattr(config.alert_config, "alert_value", ["JSON"])
|
|
1657
|
+
if hasattr(config.alert_config, "alert_value")
|
|
1658
|
+
else ["JSON"]
|
|
1659
|
+
),
|
|
1660
|
+
)
|
|
1661
|
+
},
|
|
1662
|
+
}
|
|
1663
|
+
)
|
|
1664
|
+
|
|
1665
|
+
|
|
1666
|
+
human_text_lines = [f"CURRENT FRAME @ {current_timestamp}"]
|
|
1667
|
+
|
|
1668
|
+
current_recognized = current_frame.get("recognized", 0)
|
|
1669
|
+
current_unknown = current_frame.get("unknown", 0)
|
|
1670
|
+
recognized_persons = current_frame.get("recognized_persons", {})
|
|
1671
|
+
total_current = current_recognized + current_unknown
|
|
1672
|
+
|
|
1673
|
+
# Show staff names and IDs being recognized in current frame (with tabs)
|
|
1674
|
+
human_text_lines.append(f"\tCurrent Total Faces: {total_current}")
|
|
1675
|
+
human_text_lines.append(f"\tCurrent Recognized: {current_recognized}")
|
|
1676
|
+
|
|
1677
|
+
if recognized_persons:
|
|
1678
|
+
for person_id in recognized_persons.keys():
|
|
1679
|
+
# Get actual staff name from current frame processing
|
|
1680
|
+
staff_name = (current_frame_staff_details or {}).get(
|
|
1681
|
+
person_id, f"Staff {person_id}"
|
|
1682
|
+
)
|
|
1683
|
+
human_text_lines.append(f"\tName: {staff_name} (ID: {person_id})")
|
|
1684
|
+
human_text_lines.append(f"\tCurrent Unknown: {current_unknown}")
|
|
1685
|
+
|
|
1686
|
+
# Show current frame counts only (with tabs)
|
|
1687
|
+
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)}")
|
|
1692
|
+
# Additional counts similar to compare_similarity HUD
|
|
1693
|
+
# try:
|
|
1694
|
+
# human_text_lines.append(f"\tCurrent Faces (detections): {total_detections}")
|
|
1695
|
+
# human_text_lines.append(f"\tTotal Unique Tracks: {cumulative_total}")
|
|
1696
|
+
# except Exception:
|
|
1697
|
+
# pass
|
|
1698
|
+
|
|
1699
|
+
human_text = "\n".join(human_text_lines)
|
|
1700
|
+
|
|
1701
|
+
if alerts:
|
|
1702
|
+
for alert in alerts:
|
|
1703
|
+
human_text_lines.append(
|
|
1704
|
+
f"Alerts: {alert.get('settings', {})} sent @ {current_timestamp}"
|
|
1705
|
+
)
|
|
1706
|
+
else:
|
|
1707
|
+
human_text_lines.append("Alerts: None")
|
|
1708
|
+
|
|
1709
|
+
human_text = "\n".join(human_text_lines)
|
|
1710
|
+
reset_settings = [
|
|
1711
|
+
{"interval_type": "daily", "reset_time": {"value": 9, "time_unit": "hour"}}
|
|
1712
|
+
]
|
|
1713
|
+
|
|
1714
|
+
tracking_stat = self.create_tracking_stats(
|
|
1715
|
+
total_counts=total_counts,
|
|
1716
|
+
current_counts=current_counts,
|
|
1717
|
+
detections=detections,
|
|
1718
|
+
human_text=human_text,
|
|
1719
|
+
camera_info=camera_info,
|
|
1720
|
+
alerts=alerts,
|
|
1721
|
+
alert_settings=alert_settings,
|
|
1722
|
+
reset_settings=reset_settings,
|
|
1723
|
+
start_time=high_precision_start_timestamp,
|
|
1724
|
+
reset_time=high_precision_reset_timestamp,
|
|
1725
|
+
)
|
|
1726
|
+
|
|
1727
|
+
tracking_stats.append(tracking_stat)
|
|
1728
|
+
return tracking_stats
|
|
1729
|
+
|
|
1730
|
+
# Copy all other methods from face_recognition.py but add face recognition info to human text
|
|
1731
|
+
def _generate_incidents(
|
|
1732
|
+
self,
|
|
1733
|
+
counting_summary: Dict,
|
|
1734
|
+
alerts: List,
|
|
1735
|
+
config: FaceRecognitionEmbeddingConfig,
|
|
1736
|
+
frame_number: Optional[int] = None,
|
|
1737
|
+
stream_info: Optional[Dict[str, Any]] = None,
|
|
1738
|
+
) -> List[Dict]:
|
|
1739
|
+
"""Generate structured incidents for the output format with frame-based keys."""
|
|
1740
|
+
|
|
1741
|
+
incidents = []
|
|
1742
|
+
total_detections = counting_summary.get("total_count", 0)
|
|
1743
|
+
face_summary = counting_summary.get("face_recognition_summary", {})
|
|
1744
|
+
current_frame = face_summary.get("current_frame", {})
|
|
1745
|
+
|
|
1746
|
+
current_timestamp = self._get_current_timestamp_str(stream_info)
|
|
1747
|
+
camera_info = self.get_camera_info_from_stream(stream_info)
|
|
1748
|
+
|
|
1749
|
+
self._ascending_alert_list = (
|
|
1750
|
+
self._ascending_alert_list[-900:]
|
|
1751
|
+
if len(self._ascending_alert_list) > 900
|
|
1752
|
+
else self._ascending_alert_list
|
|
1753
|
+
)
|
|
1754
|
+
|
|
1755
|
+
if total_detections > 0:
|
|
1756
|
+
# Determine event level based on unknown faces ratio
|
|
1757
|
+
level = "low"
|
|
1758
|
+
intensity = 5.0
|
|
1759
|
+
start_timestamp = self._get_start_timestamp_str(stream_info)
|
|
1760
|
+
if start_timestamp and self.current_incident_end_timestamp == "N/A":
|
|
1761
|
+
self.current_incident_end_timestamp = "Incident still active"
|
|
1762
|
+
elif (
|
|
1763
|
+
start_timestamp
|
|
1764
|
+
and self.current_incident_end_timestamp == "Incident still active"
|
|
1765
|
+
):
|
|
1766
|
+
if (
|
|
1767
|
+
len(self._ascending_alert_list) >= 15
|
|
1768
|
+
and sum(self._ascending_alert_list[-15:]) / 15 < 1.5
|
|
1769
|
+
):
|
|
1770
|
+
self.current_incident_end_timestamp = current_timestamp
|
|
1771
|
+
elif (
|
|
1772
|
+
self.current_incident_end_timestamp != "Incident still active"
|
|
1773
|
+
and self.current_incident_end_timestamp != "N/A"
|
|
1774
|
+
):
|
|
1775
|
+
self.current_incident_end_timestamp = "N/A"
|
|
1776
|
+
|
|
1777
|
+
# Base intensity on unknown faces
|
|
1778
|
+
current_unknown = current_frame.get("unknown", 0)
|
|
1779
|
+
unknown_ratio = (
|
|
1780
|
+
current_unknown / total_detections if total_detections > 0 else 0
|
|
1781
|
+
)
|
|
1782
|
+
intensity = min(10.0, unknown_ratio * 10 + (current_unknown / 3))
|
|
1783
|
+
|
|
1784
|
+
if intensity >= 9:
|
|
1785
|
+
level = "critical"
|
|
1786
|
+
self._ascending_alert_list.append(3)
|
|
1787
|
+
elif intensity >= 7:
|
|
1788
|
+
level = "significant"
|
|
1789
|
+
self._ascending_alert_list.append(2)
|
|
1790
|
+
elif intensity >= 5:
|
|
1791
|
+
level = "medium"
|
|
1792
|
+
self._ascending_alert_list.append(1)
|
|
1793
|
+
else:
|
|
1794
|
+
level = "low"
|
|
1795
|
+
self._ascending_alert_list.append(0)
|
|
1796
|
+
|
|
1797
|
+
# Generate human text in new format with face recognition info
|
|
1798
|
+
current_recognized = current_frame.get("recognized", 0)
|
|
1799
|
+
human_text_lines = [f"FACE RECOGNITION INCIDENTS @ {current_timestamp}:"]
|
|
1800
|
+
human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE,level)}")
|
|
1801
|
+
human_text_lines.append(f"\tRecognized Faces: {current_recognized}")
|
|
1802
|
+
human_text_lines.append(f"\tUnknown Faces: {current_unknown}")
|
|
1803
|
+
human_text_lines.append(f"\tTotal Faces: {total_detections}")
|
|
1804
|
+
human_text = "\n".join(human_text_lines)
|
|
1805
|
+
|
|
1806
|
+
alert_settings = []
|
|
1807
|
+
if config.alert_config and hasattr(config.alert_config, "alert_type"):
|
|
1808
|
+
alert_settings.append(
|
|
1809
|
+
{
|
|
1810
|
+
"alert_type": (
|
|
1811
|
+
getattr(config.alert_config, "alert_type", ["Default"])
|
|
1812
|
+
if hasattr(config.alert_config, "alert_type")
|
|
1813
|
+
else ["Default"]
|
|
1814
|
+
),
|
|
1815
|
+
"incident_category": self.CASE_TYPE,
|
|
1816
|
+
"threshold_level": (
|
|
1817
|
+
config.alert_config.count_thresholds
|
|
1818
|
+
if hasattr(config.alert_config, "count_thresholds")
|
|
1819
|
+
else {}
|
|
1820
|
+
),
|
|
1821
|
+
"ascending": True,
|
|
1822
|
+
"settings": {
|
|
1823
|
+
t: v
|
|
1824
|
+
for t, v in zip(
|
|
1825
|
+
(
|
|
1826
|
+
getattr(
|
|
1827
|
+
config.alert_config, "alert_type", ["Default"]
|
|
1828
|
+
)
|
|
1829
|
+
if hasattr(config.alert_config, "alert_type")
|
|
1830
|
+
else ["Default"]
|
|
1831
|
+
),
|
|
1832
|
+
(
|
|
1833
|
+
getattr(
|
|
1834
|
+
config.alert_config, "alert_value", ["JSON"]
|
|
1835
|
+
)
|
|
1836
|
+
if hasattr(config.alert_config, "alert_value")
|
|
1837
|
+
else ["JSON"]
|
|
1838
|
+
),
|
|
1839
|
+
)
|
|
1840
|
+
},
|
|
1841
|
+
}
|
|
1842
|
+
)
|
|
1843
|
+
|
|
1844
|
+
event = self.create_incident(
|
|
1845
|
+
incident_id=self.CASE_TYPE + "_" + str(frame_number),
|
|
1846
|
+
incident_type=self.CASE_TYPE,
|
|
1847
|
+
severity_level=level,
|
|
1848
|
+
human_text=human_text,
|
|
1849
|
+
camera_info=camera_info,
|
|
1850
|
+
alerts=alerts,
|
|
1851
|
+
alert_settings=alert_settings,
|
|
1852
|
+
start_time=start_timestamp,
|
|
1853
|
+
end_time=self.current_incident_end_timestamp,
|
|
1854
|
+
level_settings={"low": 1, "medium": 3, "significant": 4, "critical": 7},
|
|
1855
|
+
)
|
|
1856
|
+
incidents.append(event)
|
|
1857
|
+
|
|
1858
|
+
else:
|
|
1859
|
+
self._ascending_alert_list.append(0)
|
|
1860
|
+
incidents.append({})
|
|
1861
|
+
|
|
1862
|
+
return incidents
|
|
1863
|
+
|
|
1864
|
+
def _generate_business_analytics(
|
|
1865
|
+
self,
|
|
1866
|
+
counting_summary: Dict,
|
|
1867
|
+
alerts: Any,
|
|
1868
|
+
config: FaceRecognitionEmbeddingConfig,
|
|
1869
|
+
stream_info: Optional[Dict[str, Any]] = None,
|
|
1870
|
+
is_empty=False,
|
|
1871
|
+
) -> List[Dict]:
|
|
1872
|
+
"""Generate standardized business analytics for the agg_summary structure."""
|
|
1873
|
+
if is_empty:
|
|
1874
|
+
return []
|
|
1875
|
+
return []
|
|
1876
|
+
|
|
1877
|
+
def _generate_summary(self, incidents: List, tracking_stats: List, business_analytics: List) -> List[str]:
|
|
1878
|
+
"""
|
|
1879
|
+
Generate a human_text string for the tracking_stat, incident, business analytics and alerts.
|
|
1880
|
+
"""
|
|
1881
|
+
lines = []
|
|
1882
|
+
lines.append("Application Name: "+self.CASE_TYPE)
|
|
1883
|
+
lines.append("Application Version: "+self.CASE_VERSION)
|
|
1884
|
+
if len(incidents) > 0:
|
|
1885
|
+
lines.append("Incidents: "+f"\n\t{incidents[0].get('human_text', 'No incidents detected')}")
|
|
1886
|
+
if len(tracking_stats) > 0:
|
|
1887
|
+
lines.append("Tracking Statistics: "+f"\t{tracking_stats[0].get('human_text', 'No tracking statistics detected')}")
|
|
1888
|
+
if len(business_analytics) > 0:
|
|
1889
|
+
lines.append("Business Analytics: "+f"\t{business_analytics[0].get('human_text', 'No business analytics detected')}")
|
|
1890
|
+
|
|
1891
|
+
if len(incidents) == 0 and len(tracking_stats) == 0 and len(business_analytics) == 0:
|
|
1892
|
+
lines.append("Summary: "+"No Summary Data")
|
|
1893
|
+
|
|
1894
|
+
return ["\n".join(lines)]
|
|
1895
|
+
|
|
1896
|
+
# Include all the standard helper methods from face_recognition.py...
|
|
1897
|
+
def _count_categories(
|
|
1898
|
+
self, detections: list, config: FaceRecognitionEmbeddingConfig
|
|
1899
|
+
) -> dict:
|
|
1900
|
+
"""
|
|
1901
|
+
Count the number of detections per category and return a summary dict.
|
|
1902
|
+
The detections list is expected to have 'track_id' (from tracker), 'category', 'bounding_box', etc.
|
|
1903
|
+
Output structure will include 'track_id' for each detection as per AdvancedTracker output.
|
|
1904
|
+
"""
|
|
1905
|
+
counts = {}
|
|
1906
|
+
for det in detections:
|
|
1907
|
+
cat = det.get("category", "unknown")
|
|
1908
|
+
counts[cat] = counts.get(cat, 0) + 1
|
|
1909
|
+
# Each detection dict will now include 'track_id' and face recognition fields
|
|
1910
|
+
return {
|
|
1911
|
+
"total_count": sum(counts.values()),
|
|
1912
|
+
"per_category_count": counts,
|
|
1913
|
+
"detections": [
|
|
1914
|
+
{
|
|
1915
|
+
"bounding_box": det.get("bounding_box"),
|
|
1916
|
+
"category": det.get("category"),
|
|
1917
|
+
"confidence": det.get("confidence"),
|
|
1918
|
+
"track_id": det.get("track_id"),
|
|
1919
|
+
"frame_id": det.get("frame_id"),
|
|
1920
|
+
# Face recognition fields
|
|
1921
|
+
"person_id": det.get("person_id"),
|
|
1922
|
+
"person_name": det.get("person_name"),
|
|
1923
|
+
"label": det.get("display_name", ""),
|
|
1924
|
+
"recognition_status": det.get("recognition_status"),
|
|
1925
|
+
"enrolled": det.get("enrolled"),
|
|
1926
|
+
"embedding": det.get("embedding", []),
|
|
1927
|
+
"landmarks": det.get("landmarks"),
|
|
1928
|
+
"staff_details": det.get(
|
|
1929
|
+
"staff_details"
|
|
1930
|
+
), # Full staff information from API
|
|
1931
|
+
}
|
|
1932
|
+
for det in detections
|
|
1933
|
+
],
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
# Removed unused _extract_predictions (counts and outputs are built elsewhere)
|
|
1937
|
+
|
|
1938
|
+
# Copy all standard tracking, IoU, timestamp methods from face_recognition.py
|
|
1939
|
+
def _update_tracking_state(self, detections: list):
|
|
1940
|
+
"""Track unique categories track_ids per category for total count after tracking."""
|
|
1941
|
+
if not hasattr(self, "_per_category_total_track_ids"):
|
|
1942
|
+
self._per_category_total_track_ids = {
|
|
1943
|
+
cat: set() for cat in self.target_categories
|
|
1944
|
+
}
|
|
1945
|
+
self._current_frame_track_ids = {cat: set() for cat in self.target_categories}
|
|
1946
|
+
|
|
1947
|
+
for det in detections:
|
|
1948
|
+
cat = det.get("category")
|
|
1949
|
+
raw_track_id = det.get("track_id")
|
|
1950
|
+
if cat not in self.target_categories or raw_track_id is None:
|
|
1951
|
+
continue
|
|
1952
|
+
bbox = det.get("bounding_box", det.get("bbox"))
|
|
1953
|
+
canonical_id = self._merge_or_register_track(raw_track_id, bbox)
|
|
1954
|
+
det["track_id"] = canonical_id
|
|
1955
|
+
|
|
1956
|
+
self._per_category_total_track_ids.setdefault(cat, set()).add(canonical_id)
|
|
1957
|
+
self._current_frame_track_ids[cat].add(canonical_id)
|
|
1958
|
+
|
|
1959
|
+
def get_total_counts(self):
|
|
1960
|
+
"""Return total unique track_id count for each category."""
|
|
1961
|
+
return {
|
|
1962
|
+
cat: len(ids)
|
|
1963
|
+
for cat, ids in getattr(self, "_per_category_total_track_ids", {}).items()
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
def _format_timestamp_for_stream(self, timestamp: float) -> str:
|
|
1967
|
+
"""Format timestamp for streams (YYYY:MM:DD HH:MM:SS format)."""
|
|
1968
|
+
dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
|
1969
|
+
return dt.strftime("%Y:%m:%d %H:%M:%S")
|
|
1970
|
+
|
|
1971
|
+
def _format_timestamp_for_video(self, timestamp: float) -> str:
|
|
1972
|
+
"""Format timestamp for video chunks (HH:MM:SS.ms format)."""
|
|
1973
|
+
hours = int(timestamp // 3600)
|
|
1974
|
+
minutes = int((timestamp % 3600) // 60)
|
|
1975
|
+
seconds = round(float(timestamp % 60), 2)
|
|
1976
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:.1f}"
|
|
1977
|
+
|
|
1978
|
+
def _format_timestamp(self, timestamp: Any) -> str:
|
|
1979
|
+
"""Format a timestamp to match the current timestamp format: YYYY:MM:DD HH:MM:SS.
|
|
1980
|
+
|
|
1981
|
+
The input can be either:
|
|
1982
|
+
1. A numeric Unix timestamp (``float`` / ``int``) – it will be converted to datetime.
|
|
1983
|
+
2. A string in the format ``YYYY-MM-DD-HH:MM:SS.ffffff UTC``.
|
|
1984
|
+
|
|
1985
|
+
The returned value will be in the format: YYYY:MM:DD HH:MM:SS (no milliseconds, no UTC suffix).
|
|
1986
|
+
|
|
1987
|
+
Example
|
|
1988
|
+
-------
|
|
1989
|
+
>>> self._format_timestamp("2025-10-27-19:31:20.187574 UTC")
|
|
1990
|
+
'2025:10:27 19:31:20'
|
|
1991
|
+
"""
|
|
1992
|
+
|
|
1993
|
+
# Convert numeric timestamps to datetime first
|
|
1994
|
+
if isinstance(timestamp, (int, float)):
|
|
1995
|
+
dt = datetime.fromtimestamp(timestamp, timezone.utc)
|
|
1996
|
+
return dt.strftime('%Y:%m:%d %H:%M:%S')
|
|
1997
|
+
|
|
1998
|
+
# Ensure we are working with a string from here on
|
|
1999
|
+
if not isinstance(timestamp, str):
|
|
2000
|
+
return str(timestamp)
|
|
2001
|
+
|
|
2002
|
+
# Remove ' UTC' suffix if present
|
|
2003
|
+
timestamp_clean = timestamp.replace(' UTC', '').strip()
|
|
2004
|
+
|
|
2005
|
+
# Remove milliseconds if present (everything after the last dot)
|
|
2006
|
+
if '.' in timestamp_clean:
|
|
2007
|
+
timestamp_clean = timestamp_clean.split('.')[0]
|
|
2008
|
+
|
|
2009
|
+
# Parse the timestamp string and convert to desired format
|
|
2010
|
+
try:
|
|
2011
|
+
# Handle format: YYYY-MM-DD-HH:MM:SS
|
|
2012
|
+
if timestamp_clean.count('-') >= 2:
|
|
2013
|
+
# Replace first two dashes with colons for date part, third with space
|
|
2014
|
+
parts = timestamp_clean.split('-')
|
|
2015
|
+
if len(parts) >= 4:
|
|
2016
|
+
# parts = ['2025', '10', '27', '19:31:20']
|
|
2017
|
+
formatted = f"{parts[0]}:{parts[1]}:{parts[2]} {'-'.join(parts[3:])}"
|
|
2018
|
+
return formatted
|
|
2019
|
+
except Exception:
|
|
2020
|
+
pass
|
|
2021
|
+
|
|
2022
|
+
# If parsing fails, return the cleaned string as-is
|
|
2023
|
+
return timestamp_clean
|
|
2024
|
+
|
|
2025
|
+
def _get_current_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False, frame_id: Optional[str]=None) -> str:
|
|
2026
|
+
"""Get formatted current timestamp based on stream type."""
|
|
2027
|
+
|
|
2028
|
+
if not stream_info:
|
|
2029
|
+
return "00:00:00.00"
|
|
2030
|
+
if precision:
|
|
2031
|
+
if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
|
|
2032
|
+
if frame_id:
|
|
2033
|
+
start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
2034
|
+
else:
|
|
2035
|
+
start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
2036
|
+
stream_time_str = self._format_timestamp_for_video(start_time)
|
|
2037
|
+
|
|
2038
|
+
return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
|
|
2039
|
+
else:
|
|
2040
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
2041
|
+
|
|
2042
|
+
if stream_info.get("input_settings", {}).get("start_frame", "na") != "na":
|
|
2043
|
+
if frame_id:
|
|
2044
|
+
start_time = int(frame_id)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
2045
|
+
else:
|
|
2046
|
+
start_time = stream_info.get("input_settings", {}).get("start_frame", 30)/stream_info.get("input_settings", {}).get("original_fps", 30)
|
|
2047
|
+
|
|
2048
|
+
stream_time_str = self._format_timestamp_for_video(start_time)
|
|
2049
|
+
|
|
2050
|
+
|
|
2051
|
+
return self._format_timestamp(stream_info.get("input_settings", {}).get("stream_time", "NA"))
|
|
2052
|
+
else:
|
|
2053
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
2054
|
+
if stream_time_str:
|
|
2055
|
+
try:
|
|
2056
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
2057
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
2058
|
+
timestamp = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
2059
|
+
return self._format_timestamp_for_stream(timestamp)
|
|
2060
|
+
except:
|
|
2061
|
+
return self._format_timestamp_for_stream(time.time())
|
|
2062
|
+
else:
|
|
2063
|
+
return self._format_timestamp_for_stream(time.time())
|
|
2064
|
+
|
|
2065
|
+
def _get_start_timestamp_str(self, stream_info: Optional[Dict[str, Any]], precision=False) -> str:
|
|
2066
|
+
"""Get formatted start timestamp for 'TOTAL SINCE' based on stream type."""
|
|
2067
|
+
if not stream_info:
|
|
2068
|
+
return "00:00:00"
|
|
2069
|
+
|
|
2070
|
+
if precision:
|
|
2071
|
+
if self.start_timer is None:
|
|
2072
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
2073
|
+
if not candidate or candidate == "NA":
|
|
2074
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
2075
|
+
self.start_timer = candidate
|
|
2076
|
+
return self._format_timestamp(self.start_timer)
|
|
2077
|
+
elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
|
|
2078
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
2079
|
+
if not candidate or candidate == "NA":
|
|
2080
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
2081
|
+
self.start_timer = candidate
|
|
2082
|
+
return self._format_timestamp(self.start_timer)
|
|
2083
|
+
else:
|
|
2084
|
+
return self._format_timestamp(self.start_timer)
|
|
2085
|
+
|
|
2086
|
+
if self.start_timer is None:
|
|
2087
|
+
# Prefer direct input_settings.stream_time if available and not NA
|
|
2088
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
2089
|
+
if not candidate or candidate == "NA":
|
|
2090
|
+
# Fallback to nested stream_info.stream_time used by current timestamp path
|
|
2091
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
2092
|
+
if stream_time_str:
|
|
2093
|
+
try:
|
|
2094
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
2095
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
2096
|
+
self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
2097
|
+
candidate = datetime.fromtimestamp(self._tracking_start_time, timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
2098
|
+
except:
|
|
2099
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
2100
|
+
else:
|
|
2101
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
2102
|
+
self.start_timer = candidate
|
|
2103
|
+
return self._format_timestamp(self.start_timer)
|
|
2104
|
+
elif stream_info.get("input_settings", {}).get("start_frame", "na") == 1:
|
|
2105
|
+
candidate = stream_info.get("input_settings", {}).get("stream_time")
|
|
2106
|
+
if not candidate or candidate == "NA":
|
|
2107
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
2108
|
+
if stream_time_str:
|
|
2109
|
+
try:
|
|
2110
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
2111
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
2112
|
+
ts = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
2113
|
+
candidate = datetime.fromtimestamp(ts, timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
2114
|
+
except:
|
|
2115
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
2116
|
+
else:
|
|
2117
|
+
candidate = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H:%M:%S.%f UTC")
|
|
2118
|
+
self.start_timer = candidate
|
|
2119
|
+
return self._format_timestamp(self.start_timer)
|
|
2120
|
+
|
|
2121
|
+
else:
|
|
2122
|
+
if self.start_timer is not None and self.start_timer != "NA":
|
|
2123
|
+
return self._format_timestamp(self.start_timer)
|
|
2124
|
+
|
|
2125
|
+
if self._tracking_start_time is None:
|
|
2126
|
+
stream_time_str = stream_info.get("input_settings", {}).get("stream_info", {}).get("stream_time", "")
|
|
2127
|
+
if stream_time_str:
|
|
2128
|
+
try:
|
|
2129
|
+
timestamp_str = stream_time_str.replace(" UTC", "")
|
|
2130
|
+
dt = datetime.strptime(timestamp_str, "%Y-%m-%d-%H:%M:%S.%f")
|
|
2131
|
+
self._tracking_start_time = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
2132
|
+
except:
|
|
2133
|
+
self._tracking_start_time = time.time()
|
|
2134
|
+
else:
|
|
2135
|
+
self._tracking_start_time = time.time()
|
|
2136
|
+
|
|
2137
|
+
dt = datetime.fromtimestamp(self._tracking_start_time, tz=timezone.utc)
|
|
2138
|
+
dt = dt.replace(minute=0, second=0, microsecond=0)
|
|
2139
|
+
return dt.strftime('%Y:%m:%d %H:%M:%S')
|
|
2140
|
+
|
|
2141
|
+
|
|
2142
|
+
def _compute_iou(self, box1: Any, box2: Any) -> float:
|
|
2143
|
+
"""Compute IoU between two bounding boxes which may be dicts or lists."""
|
|
2144
|
+
|
|
2145
|
+
def _bbox_to_list(bbox):
|
|
2146
|
+
if bbox is None:
|
|
2147
|
+
return []
|
|
2148
|
+
if isinstance(bbox, list):
|
|
2149
|
+
return bbox[:4] if len(bbox) >= 4 else []
|
|
2150
|
+
if isinstance(bbox, dict):
|
|
2151
|
+
if "xmin" in bbox:
|
|
2152
|
+
return [bbox["xmin"], bbox["ymin"], bbox["xmax"], bbox["ymax"]]
|
|
2153
|
+
if "x1" in bbox:
|
|
2154
|
+
return [bbox["x1"], bbox["y1"], bbox["x2"], bbox["y2"]]
|
|
2155
|
+
values = [v for v in bbox.values() if isinstance(v, (int, float))]
|
|
2156
|
+
return values[:4] if len(values) >= 4 else []
|
|
2157
|
+
return []
|
|
2158
|
+
|
|
2159
|
+
l1 = _bbox_to_list(box1)
|
|
2160
|
+
l2 = _bbox_to_list(box2)
|
|
2161
|
+
if len(l1) < 4 or len(l2) < 4:
|
|
2162
|
+
return 0.0
|
|
2163
|
+
x1_min, y1_min, x1_max, y1_max = l1
|
|
2164
|
+
x2_min, y2_min, x2_max, y2_max = l2
|
|
2165
|
+
|
|
2166
|
+
x1_min, x1_max = min(x1_min, x1_max), max(x1_min, x1_max)
|
|
2167
|
+
y1_min, y1_max = min(y1_min, y1_max), max(y1_min, y1_max)
|
|
2168
|
+
x2_min, x2_max = min(x2_min, x2_max), max(x2_min, x2_max)
|
|
2169
|
+
y2_min, y2_max = min(y2_min, y2_max), max(y2_min, y2_max)
|
|
2170
|
+
|
|
2171
|
+
inter_x_min = max(x1_min, x2_min)
|
|
2172
|
+
inter_y_min = max(y1_min, y2_min)
|
|
2173
|
+
inter_x_max = min(x1_max, x2_max)
|
|
2174
|
+
inter_y_max = min(y1_max, y2_max)
|
|
2175
|
+
|
|
2176
|
+
inter_w = max(0.0, inter_x_max - inter_x_min)
|
|
2177
|
+
inter_h = max(0.0, inter_y_max - inter_y_min)
|
|
2178
|
+
inter_area = inter_w * inter_h
|
|
2179
|
+
|
|
2180
|
+
area1 = (x1_max - x1_min) * (y1_max - y1_min)
|
|
2181
|
+
area2 = (x2_max - x2_min) * (y2_max - y2_min)
|
|
2182
|
+
union_area = area1 + area2 - inter_area
|
|
2183
|
+
|
|
2184
|
+
return (inter_area / union_area) if union_area > 0 else 0.0
|
|
2185
|
+
|
|
2186
|
+
def _merge_or_register_track(self, raw_id: Any, bbox: Any) -> Any:
|
|
2187
|
+
"""Return a stable canonical ID for a raw tracker ID."""
|
|
2188
|
+
if raw_id is None or bbox is None:
|
|
2189
|
+
return raw_id
|
|
2190
|
+
|
|
2191
|
+
now = time.time()
|
|
2192
|
+
|
|
2193
|
+
if raw_id in self._track_aliases:
|
|
2194
|
+
canonical_id = self._track_aliases[raw_id]
|
|
2195
|
+
track_info = self._canonical_tracks.get(canonical_id)
|
|
2196
|
+
if track_info is not None:
|
|
2197
|
+
track_info["last_bbox"] = bbox
|
|
2198
|
+
track_info["last_update"] = now
|
|
2199
|
+
track_info["raw_ids"].add(raw_id)
|
|
2200
|
+
return canonical_id
|
|
2201
|
+
|
|
2202
|
+
for canonical_id, info in self._canonical_tracks.items():
|
|
2203
|
+
if now - info["last_update"] > self._track_merge_time_window:
|
|
2204
|
+
continue
|
|
2205
|
+
iou = self._compute_iou(bbox, info["last_bbox"])
|
|
2206
|
+
if iou >= self._track_merge_iou_threshold:
|
|
2207
|
+
self._track_aliases[raw_id] = canonical_id
|
|
2208
|
+
info["last_bbox"] = bbox
|
|
2209
|
+
info["last_update"] = now
|
|
2210
|
+
info["raw_ids"].add(raw_id)
|
|
2211
|
+
return canonical_id
|
|
2212
|
+
|
|
2213
|
+
canonical_id = raw_id
|
|
2214
|
+
self._track_aliases[raw_id] = canonical_id
|
|
2215
|
+
self._canonical_tracks[canonical_id] = {
|
|
2216
|
+
"last_bbox": bbox,
|
|
2217
|
+
"last_update": now,
|
|
2218
|
+
"raw_ids": {raw_id},
|
|
2219
|
+
}
|
|
2220
|
+
return canonical_id
|
|
2221
|
+
|
|
2222
|
+
def __del__(self):
|
|
2223
|
+
"""Cleanup when object is destroyed"""
|
|
2224
|
+
try:
|
|
2225
|
+
if hasattr(self, "people_activity_logging") and self.people_activity_logging:
|
|
2226
|
+
self.people_activity_logging.stop_background_processing()
|
|
2227
|
+
except:
|
|
2228
|
+
pass
|
|
2229
|
+
|
|
2230
|
+
try:
|
|
2231
|
+
if hasattr(self, "embedding_manager") and self.embedding_manager:
|
|
2232
|
+
self.embedding_manager.stop_background_refresh()
|
|
2233
|
+
except:
|
|
2234
|
+
pass
|