matrice-analytics 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of matrice-analytics might be problematic. Click here for more details.

Files changed (160) hide show
  1. matrice_analytics/__init__.py +28 -0
  2. matrice_analytics/boundary_drawing_internal/README.md +305 -0
  3. matrice_analytics/boundary_drawing_internal/__init__.py +45 -0
  4. matrice_analytics/boundary_drawing_internal/boundary_drawing_internal.py +1207 -0
  5. matrice_analytics/boundary_drawing_internal/boundary_drawing_tool.py +429 -0
  6. matrice_analytics/boundary_drawing_internal/boundary_tool_template.html +1036 -0
  7. matrice_analytics/boundary_drawing_internal/data/.gitignore +12 -0
  8. matrice_analytics/boundary_drawing_internal/example_usage.py +206 -0
  9. matrice_analytics/boundary_drawing_internal/usage/README.md +110 -0
  10. matrice_analytics/boundary_drawing_internal/usage/boundary_drawer_launcher.py +102 -0
  11. matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py +107 -0
  12. matrice_analytics/post_processing/README.md +455 -0
  13. matrice_analytics/post_processing/__init__.py +732 -0
  14. matrice_analytics/post_processing/advanced_tracker/README.md +650 -0
  15. matrice_analytics/post_processing/advanced_tracker/__init__.py +17 -0
  16. matrice_analytics/post_processing/advanced_tracker/base.py +99 -0
  17. matrice_analytics/post_processing/advanced_tracker/config.py +77 -0
  18. matrice_analytics/post_processing/advanced_tracker/kalman_filter.py +370 -0
  19. matrice_analytics/post_processing/advanced_tracker/matching.py +195 -0
  20. matrice_analytics/post_processing/advanced_tracker/strack.py +230 -0
  21. matrice_analytics/post_processing/advanced_tracker/tracker.py +367 -0
  22. matrice_analytics/post_processing/config.py +142 -0
  23. matrice_analytics/post_processing/core/__init__.py +63 -0
  24. matrice_analytics/post_processing/core/base.py +704 -0
  25. matrice_analytics/post_processing/core/config.py +3188 -0
  26. matrice_analytics/post_processing/core/config_utils.py +925 -0
  27. matrice_analytics/post_processing/face_reg/__init__.py +43 -0
  28. matrice_analytics/post_processing/face_reg/compare_similarity.py +556 -0
  29. matrice_analytics/post_processing/face_reg/embedding_manager.py +681 -0
  30. matrice_analytics/post_processing/face_reg/face_recognition.py +1870 -0
  31. matrice_analytics/post_processing/face_reg/face_recognition_client.py +339 -0
  32. matrice_analytics/post_processing/face_reg/people_activity_logging.py +283 -0
  33. matrice_analytics/post_processing/ocr/__init__.py +0 -0
  34. matrice_analytics/post_processing/ocr/easyocr_extractor.py +248 -0
  35. matrice_analytics/post_processing/ocr/postprocessing.py +271 -0
  36. matrice_analytics/post_processing/ocr/preprocessing.py +52 -0
  37. matrice_analytics/post_processing/post_processor.py +1153 -0
  38. matrice_analytics/post_processing/test_cases/__init__.py +1 -0
  39. matrice_analytics/post_processing/test_cases/run_tests.py +143 -0
  40. matrice_analytics/post_processing/test_cases/test_advanced_customer_service.py +841 -0
  41. matrice_analytics/post_processing/test_cases/test_basic_counting_tracking.py +523 -0
  42. matrice_analytics/post_processing/test_cases/test_comprehensive.py +531 -0
  43. matrice_analytics/post_processing/test_cases/test_config.py +852 -0
  44. matrice_analytics/post_processing/test_cases/test_customer_service.py +585 -0
  45. matrice_analytics/post_processing/test_cases/test_data_generators.py +583 -0
  46. matrice_analytics/post_processing/test_cases/test_people_counting.py +510 -0
  47. matrice_analytics/post_processing/test_cases/test_processor.py +524 -0
  48. matrice_analytics/post_processing/test_cases/test_utilities.py +356 -0
  49. matrice_analytics/post_processing/test_cases/test_utils.py +743 -0
  50. matrice_analytics/post_processing/usecases/Histopathological_Cancer_Detection_img.py +604 -0
  51. matrice_analytics/post_processing/usecases/__init__.py +267 -0
  52. matrice_analytics/post_processing/usecases/abandoned_object_detection.py +797 -0
  53. matrice_analytics/post_processing/usecases/advanced_customer_service.py +1601 -0
  54. matrice_analytics/post_processing/usecases/age_detection.py +842 -0
  55. matrice_analytics/post_processing/usecases/age_gender_detection.py +1043 -0
  56. matrice_analytics/post_processing/usecases/anti_spoofing_detection.py +656 -0
  57. matrice_analytics/post_processing/usecases/assembly_line_detection.py +841 -0
  58. matrice_analytics/post_processing/usecases/banana_defect_detection.py +624 -0
  59. matrice_analytics/post_processing/usecases/basic_counting_tracking.py +667 -0
  60. matrice_analytics/post_processing/usecases/blood_cancer_detection_img.py +881 -0
  61. matrice_analytics/post_processing/usecases/car_damage_detection.py +834 -0
  62. matrice_analytics/post_processing/usecases/car_part_segmentation.py +946 -0
  63. matrice_analytics/post_processing/usecases/car_service.py +1601 -0
  64. matrice_analytics/post_processing/usecases/cardiomegaly_classification.py +864 -0
  65. matrice_analytics/post_processing/usecases/cell_microscopy_segmentation.py +897 -0
  66. matrice_analytics/post_processing/usecases/chicken_pose_detection.py +648 -0
  67. matrice_analytics/post_processing/usecases/child_monitoring.py +814 -0
  68. matrice_analytics/post_processing/usecases/color/clip.py +232 -0
  69. matrice_analytics/post_processing/usecases/color/clip_processor/merges.txt +48895 -0
  70. matrice_analytics/post_processing/usecases/color/clip_processor/preprocessor_config.json +28 -0
  71. matrice_analytics/post_processing/usecases/color/clip_processor/special_tokens_map.json +30 -0
  72. matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer.json +245079 -0
  73. matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer_config.json +32 -0
  74. matrice_analytics/post_processing/usecases/color/clip_processor/vocab.json +1 -0
  75. matrice_analytics/post_processing/usecases/color/color_map_utils.py +70 -0
  76. matrice_analytics/post_processing/usecases/color/color_mapper.py +468 -0
  77. matrice_analytics/post_processing/usecases/color_detection.py +1835 -0
  78. matrice_analytics/post_processing/usecases/color_map_utils.py +70 -0
  79. matrice_analytics/post_processing/usecases/concrete_crack_detection.py +827 -0
  80. matrice_analytics/post_processing/usecases/crop_weed_detection.py +781 -0
  81. matrice_analytics/post_processing/usecases/customer_service.py +1008 -0
  82. matrice_analytics/post_processing/usecases/defect_detection_products.py +936 -0
  83. matrice_analytics/post_processing/usecases/distracted_driver_detection.py +822 -0
  84. matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +930 -0
  85. matrice_analytics/post_processing/usecases/drowsy_driver_detection.py +829 -0
  86. matrice_analytics/post_processing/usecases/dwell_detection.py +829 -0
  87. matrice_analytics/post_processing/usecases/emergency_vehicle_detection.py +827 -0
  88. matrice_analytics/post_processing/usecases/face_emotion.py +813 -0
  89. matrice_analytics/post_processing/usecases/face_recognition.py +827 -0
  90. matrice_analytics/post_processing/usecases/fashion_detection.py +835 -0
  91. matrice_analytics/post_processing/usecases/field_mapping.py +902 -0
  92. matrice_analytics/post_processing/usecases/fire_detection.py +1112 -0
  93. matrice_analytics/post_processing/usecases/flare_analysis.py +891 -0
  94. matrice_analytics/post_processing/usecases/flower_segmentation.py +1006 -0
  95. matrice_analytics/post_processing/usecases/gas_leak_detection.py +837 -0
  96. matrice_analytics/post_processing/usecases/gender_detection.py +832 -0
  97. matrice_analytics/post_processing/usecases/human_activity_recognition.py +871 -0
  98. matrice_analytics/post_processing/usecases/intrusion_detection.py +1672 -0
  99. matrice_analytics/post_processing/usecases/leaf.py +821 -0
  100. matrice_analytics/post_processing/usecases/leaf_disease.py +840 -0
  101. matrice_analytics/post_processing/usecases/leak_detection.py +837 -0
  102. matrice_analytics/post_processing/usecases/license_plate_detection.py +914 -0
  103. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +1194 -0
  104. matrice_analytics/post_processing/usecases/litter_monitoring.py +717 -0
  105. matrice_analytics/post_processing/usecases/mask_detection.py +869 -0
  106. matrice_analytics/post_processing/usecases/natural_disaster.py +907 -0
  107. matrice_analytics/post_processing/usecases/parking.py +787 -0
  108. matrice_analytics/post_processing/usecases/parking_space_detection.py +822 -0
  109. matrice_analytics/post_processing/usecases/pcb_defect_detection.py +888 -0
  110. matrice_analytics/post_processing/usecases/pedestrian_detection.py +808 -0
  111. matrice_analytics/post_processing/usecases/people_counting.py +1728 -0
  112. matrice_analytics/post_processing/usecases/people_tracking.py +1842 -0
  113. matrice_analytics/post_processing/usecases/pipeline_detection.py +605 -0
  114. matrice_analytics/post_processing/usecases/plaque_segmentation_img.py +874 -0
  115. matrice_analytics/post_processing/usecases/pothole_segmentation.py +915 -0
  116. matrice_analytics/post_processing/usecases/ppe_compliance.py +645 -0
  117. matrice_analytics/post_processing/usecases/price_tag_detection.py +822 -0
  118. matrice_analytics/post_processing/usecases/proximity_detection.py +1901 -0
  119. matrice_analytics/post_processing/usecases/road_lane_detection.py +623 -0
  120. matrice_analytics/post_processing/usecases/road_traffic_density.py +832 -0
  121. matrice_analytics/post_processing/usecases/road_view_segmentation.py +915 -0
  122. matrice_analytics/post_processing/usecases/shelf_inventory_detection.py +583 -0
  123. matrice_analytics/post_processing/usecases/shoplifting_detection.py +822 -0
  124. matrice_analytics/post_processing/usecases/shopping_cart_analysis.py +899 -0
  125. matrice_analytics/post_processing/usecases/skin_cancer_classification_img.py +864 -0
  126. matrice_analytics/post_processing/usecases/smoker_detection.py +833 -0
  127. matrice_analytics/post_processing/usecases/solar_panel.py +810 -0
  128. matrice_analytics/post_processing/usecases/suspicious_activity_detection.py +1030 -0
  129. matrice_analytics/post_processing/usecases/template_usecase.py +380 -0
  130. matrice_analytics/post_processing/usecases/theft_detection.py +648 -0
  131. matrice_analytics/post_processing/usecases/traffic_sign_monitoring.py +724 -0
  132. matrice_analytics/post_processing/usecases/underground_pipeline_defect_detection.py +775 -0
  133. matrice_analytics/post_processing/usecases/underwater_pollution_detection.py +842 -0
  134. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +950 -0
  135. matrice_analytics/post_processing/usecases/warehouse_object_segmentation.py +899 -0
  136. matrice_analytics/post_processing/usecases/waterbody_segmentation.py +923 -0
  137. matrice_analytics/post_processing/usecases/weapon_detection.py +771 -0
  138. matrice_analytics/post_processing/usecases/weld_defect_detection.py +615 -0
  139. matrice_analytics/post_processing/usecases/wildlife_monitoring.py +898 -0
  140. matrice_analytics/post_processing/usecases/windmill_maintenance.py +834 -0
  141. matrice_analytics/post_processing/usecases/wound_segmentation.py +856 -0
  142. matrice_analytics/post_processing/utils/__init__.py +150 -0
  143. matrice_analytics/post_processing/utils/advanced_counting_utils.py +400 -0
  144. matrice_analytics/post_processing/utils/advanced_helper_utils.py +317 -0
  145. matrice_analytics/post_processing/utils/advanced_tracking_utils.py +461 -0
  146. matrice_analytics/post_processing/utils/alerting_utils.py +213 -0
  147. matrice_analytics/post_processing/utils/category_mapping_utils.py +94 -0
  148. matrice_analytics/post_processing/utils/color_utils.py +592 -0
  149. matrice_analytics/post_processing/utils/counting_utils.py +182 -0
  150. matrice_analytics/post_processing/utils/filter_utils.py +261 -0
  151. matrice_analytics/post_processing/utils/format_utils.py +293 -0
  152. matrice_analytics/post_processing/utils/geometry_utils.py +300 -0
  153. matrice_analytics/post_processing/utils/smoothing_utils.py +358 -0
  154. matrice_analytics/post_processing/utils/tracking_utils.py +234 -0
  155. matrice_analytics/py.typed +0 -0
  156. matrice_analytics-0.1.2.dist-info/METADATA +481 -0
  157. matrice_analytics-0.1.2.dist-info/RECORD +160 -0
  158. matrice_analytics-0.1.2.dist-info/WHEEL +5 -0
  159. matrice_analytics-0.1.2.dist-info/licenses/LICENSE.txt +21 -0
  160. matrice_analytics-0.1.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,43 @@
1
+ """
2
+ Face Recognition with Embeddings Module
3
+
4
+ This module provides facial recognition capabilities with embedding extraction
5
+ for staff identification and unknown face management.
6
+
7
+ Key Features:
8
+ - Face detection and embedding extraction using MTCNN + MobileFaceNet
9
+ - Staff recognition via vector search API
10
+ - Unknown face processing with image upload
11
+ - Real-time tracking and analytics
12
+ - Metadata extraction (quality score, capture angle, timestamp)
13
+
14
+ Quick Start:
15
+ from matrice_analytics.post_processing.face_reg import (
16
+ FaceRecognitionEmbeddingUseCase,
17
+ FaceRecognitionEmbeddingConfig,
18
+ FacialRecognitionClient
19
+ )
20
+
21
+ # Create config
22
+ config = FaceRecognitionEmbeddingConfig(
23
+ similarity_threshold=0.8,
24
+ confidence_threshold=0.5
25
+ )
26
+
27
+ # Process face recognition
28
+ processor = FaceRecognitionEmbeddingUseCase()
29
+ result = processor.process(model_output, config)
30
+ """
31
+
32
+ from .face_recognition import FaceRecognitionEmbeddingUseCase, FaceRecognitionEmbeddingConfig
33
+ from .face_recognition_client import FacialRecognitionClient, create_face_client
34
+ from .embedding_manager import EmbeddingManager, EmbeddingConfig
35
+
36
+ __all__ = [
37
+ 'FaceRecognitionEmbeddingUseCase',
38
+ 'FaceRecognitionEmbeddingConfig',
39
+ 'FacialRecognitionClient',
40
+ 'create_face_client',
41
+ 'EmbeddingManager',
42
+ 'EmbeddingConfig'
43
+ ]
@@ -0,0 +1,556 @@
1
+ import os
2
+ from typing import List, Dict, Tuple, Optional, Any
3
+ from pathlib import Path
4
+ from collections import deque, defaultdict
5
+
6
+ import numpy as np
7
+ import cv2
8
+ from deepface import DeepFace
9
+ import hashlib
10
+
11
+ # Advanced tracker (ByteTrack-like)
12
+ from advanced_tracker import AdvancedTracker
13
+ from advanced_tracker.config import TrackerConfig
14
+
15
+ # Global DeepFace configuration
16
+ MODEL_NAME = "Facenet512" # per DeepFace docs
17
+ DETECTOR_BACKEND = "retinaface" # RetinaFace
18
+ ALIGN = True # enable face alignment
19
+
20
+
21
+ def normalize_embedding(vec: List[float]) -> List[float]:
22
+ """Normalize an embedding vector to unit length (L2).
23
+
24
+ Returns a float32 list to ensure consistent downstream math and JSON safety.
25
+ """
26
+ arr = np.asarray(vec, dtype=np.float32)
27
+ if arr.size == 0:
28
+ return []
29
+ n = np.linalg.norm(arr)
30
+ if n > 0:
31
+ arr = arr / n
32
+ return arr.tolist()
33
+
34
+
35
+ def cosine_similarity(vec1: List[float], vec2: List[float]) -> float:
36
+ """Cosine similarity using NumPy operations with numeric safety."""
37
+ a = np.asarray(vec1, dtype=np.float32)
38
+ b = np.asarray(vec2, dtype=np.float32)
39
+ if a.size == 0 or b.size == 0:
40
+ return 0.0
41
+ an = np.linalg.norm(a)
42
+ bn = np.linalg.norm(b)
43
+ if an == 0.0 or bn == 0.0:
44
+ return 0.0
45
+ sim = float(np.dot(a, b) / (an * bn))
46
+ if sim > 1.0:
47
+ sim = 1.0
48
+ elif sim < -1.0:
49
+ sim = -1.0
50
+ return sim
51
+
52
+
53
+ class FaceTracker:
54
+ """
55
+ Embedding-based face tracker (mirrors tracker logic in face_recognition_model.py):
56
+ - Matches new face embeddings to existing tracks via cosine similarity
57
+ - Creates a new track when no match exceeds the similarity threshold
58
+ """
59
+
60
+ def __init__(self, similarity_threshold: float = 0.60) -> None:
61
+ self.similarity_threshold = similarity_threshold
62
+ self.tracks: Dict[str, Dict[str, object]] = {}
63
+ self.track_counter: int = 1
64
+
65
+ def _find_matching_track(self, new_embedding: List[float]) -> str | None:
66
+ if not new_embedding:
67
+ return None
68
+ best_similarity: float = 0.0
69
+ best_track_id: str | None = None
70
+ for track_id, data in self.tracks.items():
71
+ stored_embedding = data.get("embedding")
72
+ if stored_embedding:
73
+ sim = cosine_similarity(new_embedding, stored_embedding)
74
+ if sim > self.similarity_threshold and sim > best_similarity:
75
+ best_similarity = sim
76
+ best_track_id = track_id
77
+ return best_track_id
78
+
79
+ def assign_track_id(self, embedding: List[float], frame_id: int | None = None) -> str:
80
+ match_id = self._find_matching_track(embedding)
81
+ if match_id is not None and match_id in self.tracks:
82
+ # Update last seen frame for the matched track
83
+ self.tracks[match_id]["last_seen_frame"] = frame_id
84
+ return match_id
85
+
86
+ # Create a new track
87
+ new_id = f"face_id_{self.track_counter}"
88
+ self.tracks[new_id] = {
89
+ "embedding": normalize_embedding(embedding),
90
+ "created_frame": frame_id,
91
+ "last_seen_frame": frame_id,
92
+ }
93
+ self.track_counter += 1
94
+ return new_id
95
+
96
+
97
+ def get_embedding(image_path: str) -> List[float]:
98
+ """Return the first face embedding from an image using DeepFace.represent, normalized to unit length."""
99
+ reps = DeepFace.represent(
100
+ img_path=image_path,
101
+ model_name=MODEL_NAME,
102
+ detector_backend=DETECTOR_BACKEND,
103
+ align=ALIGN,
104
+ )
105
+ #TODO: Normalize embedding to unit length?
106
+
107
+ # DeepFace.represent returns a list of dicts; take the first face
108
+ if reps:
109
+ return reps[0]["embedding"]
110
+ else: return None
111
+
112
+
113
+ def compute_pairwise_similarities(embeddings: List[List[float]]) -> Dict[Tuple[int, int], float]:
114
+ """
115
+ Computes pairwise cosine similarities for a list of embeddings using NumPy.
116
+ """
117
+ # Convert the list of lists to a NumPy array
118
+ embedding_matrix = np.array(embeddings)
119
+
120
+
121
+ norms = np.linalg.norm(embedding_matrix, axis=1, keepdims=True)
122
+ # Avoid division by zero for zero-vectors
123
+ norms = np.where(norms == 0, 1, norms)
124
+ normalized_embeddings = embedding_matrix / norms
125
+
126
+ # 2. Compute the dot product of the normalized matrix with its transpose.
127
+ # For unit vectors, the dot product is equivalent to the cosine similarity.
128
+ similarity_matrix = normalized_embeddings @ normalized_embeddings.T
129
+
130
+ # 3. Extract the upper triangle of the matrix (where j > i) to match the original output.
131
+ n = len(embeddings)
132
+ # np.triu_indices(n, k=1) gets the indices (rows, cols) of the upper triangle,
133
+ # excluding the diagonal (k=1).
134
+ rows, cols = np.triu_indices(n, k=1)
135
+
136
+ # 4. Create the dictionary from the indices and the corresponding similarity values.
137
+ similarity_dict = {(r, c): similarity_matrix[r, c] for r, c in zip(rows, cols)}
138
+
139
+ return similarity_dict
140
+
141
+
142
+ def get_embeddings_from_folder(folder_path: str, max_images: int | None = None) -> Tuple[List[List[float]], List[str]]:
143
+ image_paths = sorted([p for p in Path(folder_path).iterdir() if p.suffix.lower() in {'.jpg', '.jpeg', '.png'}])
144
+ if max_images is not None:
145
+ image_paths = image_paths[:max_images]
146
+ embeddings: List[List[float]] = []
147
+ img_names: List[str] = []
148
+ for img_path in image_paths:
149
+ try:
150
+ emb = get_embedding(str(img_path))
151
+ embeddings.append(emb)
152
+ img_names.append(img_path.name)
153
+ except Exception as e:
154
+ print(f"Skipping {img_path}: {e}")
155
+ return embeddings, img_names
156
+
157
+
158
+ def get_embeddings_per_person(identity_root: str, max_images_per_person: int | None = None) -> Dict[str, List[List[float]]]:
159
+ """Build a mapping: person (subdirectory name) -> list of embeddings from all images inside it."""
160
+ root = Path(identity_root)
161
+ if not root.exists():
162
+ raise FileNotFoundError(f"Identity root does not exist: {identity_root}")
163
+
164
+ # discover subdirectories (persons)
165
+ person_dirs = [p for p in sorted(root.iterdir()) if p.is_dir()]
166
+
167
+ person_to_embeddings: Dict[str, List[List[float]]] = {}
168
+ for person_dir in person_dirs:
169
+ image_paths = [p for p in sorted(person_dir.iterdir()) if p.suffix.lower() in {'.jpg', '.jpeg', '.png'}]
170
+ if max_images_per_person is not None:
171
+ image_paths = image_paths[:max_images_per_person]
172
+
173
+ embeddings: List[List[float]] = []
174
+ for img_path in image_paths:
175
+ try:
176
+ embeddings.append(get_embedding(str(img_path)))
177
+ except Exception as e:
178
+ print(f"Skipping {img_path}: {e}")
179
+ if embeddings:
180
+ person_to_embeddings[person_dir.name] = embeddings
181
+
182
+ # Fallback: if there were no subdirectories, try treating root images as one person (root name)
183
+ if not person_to_embeddings:
184
+ root_embs, _ = get_embeddings_from_folder(identity_root, max_images_per_person)
185
+ if root_embs:
186
+ person_to_embeddings[root.name] = root_embs
187
+
188
+ return person_to_embeddings
189
+
190
+
191
+ def compare_identity_and_samples(identity_folder: str, sample_folder: str, threshold: float = 0.82):
192
+ """Compare each sample image against all identities (subdirectories) using average similarity."""
193
+ person_to_embs = get_embeddings_per_person(identity_folder)
194
+ sample_embs, sample_names = get_embeddings_from_folder(sample_folder)
195
+
196
+ if not person_to_embs or not sample_embs:
197
+ print("No embeddings extracted from one of the folders – aborting.")
198
+ return
199
+
200
+ print(f"Computed identities: {list(person_to_embs.keys())}")
201
+ print(f"Computed {sum(len(v) for v in person_to_embs.values())} identity embeddings across {len(person_to_embs)} persons and {len(sample_embs)} sample embeddings.")
202
+
203
+ for s_emb, s_name in zip(sample_embs, sample_names):
204
+ print(f"\nAverage similarity for sample '{s_name}':")
205
+ best_person = None
206
+ best_avg = -1.0
207
+ for person, embs in person_to_embs.items():
208
+ if not embs:
209
+ continue
210
+ scores = [cosine_similarity(s_emb, e) for e in embs]
211
+ avg_score = float(np.mean(scores)) if scores else 0.0
212
+ flag = "<-- MATCH" if avg_score >= threshold else ""
213
+ print(f" {person:20s}: {avg_score:.4f} {flag}")
214
+ if avg_score > best_avg:
215
+ best_avg = avg_score
216
+ best_person = person
217
+ print(f"--> Top-1: {best_person} ({best_avg:.4f})")
218
+
219
+
220
+ class TemporalIdentityManager:
221
+ """
222
+ Maintains stable identity labels per track using temporal smoothing and embedding history.
223
+
224
+ - Suppresses brief misclassifications (1-2 frames)
225
+ - Holds previous identity during short UNKNOWN gaps using unknown_patience
226
+ - Fallback: when current is UNKNOWN, match the prototype (mean) embedding history to identities
227
+ """
228
+
229
+ def __init__(
230
+ self,
231
+ person_to_embs: Dict[str, List[List[float]]],
232
+ recognition_threshold: float = 0.7,
233
+ history_size: int = 20,
234
+ unknown_patience: int = 7,
235
+ switch_patience: int = 5,
236
+ fallback_margin: float = 0.05,
237
+ ) -> None:
238
+ self.person_to_embs = person_to_embs
239
+ self.threshold = recognition_threshold
240
+ self.history_size = history_size
241
+ self.unknown_patience = unknown_patience
242
+ self.switch_patience = switch_patience
243
+ self.fallback_margin = fallback_margin
244
+ self.tracks: Dict[int, Dict[str, object]] = {}
245
+
246
+ def _ensure_track(self, track_id: int) -> None:
247
+ if track_id not in self.tracks:
248
+ self.tracks[track_id] = {
249
+ "stable_label": None,
250
+ "label_votes": defaultdict(int), # type: ignore
251
+ "embedding_history": deque(maxlen=self.history_size),
252
+ "unknown_streak": 0,
253
+ "streaks": defaultdict(int), # label -> consecutive frames count
254
+ }
255
+
256
+ def _compute_best_identity(self, emb: List[float]) -> Tuple[Optional[str], float]:
257
+ best_person = None
258
+ best_avg = -1.0
259
+ if not emb:
260
+ return None, -1.0
261
+ for person, embs in self.person_to_embs.items():
262
+ if not embs:
263
+ continue
264
+ scores = [cosine_similarity(emb, e) for e in embs]
265
+ avg_score = float(np.mean(scores)) if scores else 0.0
266
+ if avg_score > best_avg:
267
+ best_avg = avg_score
268
+ best_person = person
269
+ return best_person, best_avg
270
+
271
+ def _compute_best_identity_from_history(self, track_state: Dict[str, object]) -> Tuple[Optional[str], float]:
272
+ hist: deque = track_state["embedding_history"] # type: ignore
273
+ if not hist:
274
+ return None, -1.0
275
+ proto = np.mean(np.asarray(hist, dtype=np.float32), axis=0)
276
+ return self._compute_best_identity(proto.tolist())
277
+
278
+ def update(self, track_id: int, emb: List[float], inst_label: Optional[str], inst_sim: float) -> Tuple[str, float]:
279
+ self._ensure_track(track_id)
280
+ s = self.tracks[track_id]
281
+
282
+ # Update embedding history
283
+ if emb:
284
+ s["embedding_history"].append(emb) # type: ignore
285
+
286
+ stable: Optional[str] = s["stable_label"] # type: ignore
287
+
288
+ # Determine candidate from instantaneous prediction
289
+ if inst_label is not None and inst_label != "Unknown" and inst_sim >= self.threshold:
290
+ s["label_votes"][inst_label] += 1 # type: ignore
291
+ s["streaks"][inst_label] += 1 # type: ignore
292
+ s["unknown_streak"] = 0 # type: ignore
293
+
294
+ if stable is None:
295
+ s["stable_label"] = inst_label
296
+ return inst_label, inst_sim
297
+
298
+ if inst_label == stable:
299
+ return stable, inst_sim
300
+
301
+ # Competing identity: switch only if sustained
302
+ if s["streaks"][inst_label] >= self.switch_patience: # type: ignore
303
+ prev_votes = s["label_votes"][stable] if stable else 0 # type: ignore
304
+ cand_votes = s["label_votes"][inst_label] # type: ignore
305
+ if cand_votes >= max(2, 0.75 * prev_votes) and inst_sim >= (self.threshold + 0.02):
306
+ s["stable_label"] = inst_label
307
+ # Reset other streaks to prevent oscillations
308
+ for k in list(s["streaks"].keys()): # type: ignore
309
+ if k != inst_label:
310
+ s["streaks"][k] = 0 # type: ignore
311
+ return inst_label, inst_sim
312
+
313
+ # Do not switch yet
314
+ return stable if stable is not None else "Unknown", inst_sim
315
+
316
+ # Instantaneous is UNK or low similarity
317
+ s["unknown_streak"] = int(s["unknown_streak"]) + 1 # type: ignore
318
+ # Short UNK bursts: keep previous label
319
+ if stable is not None and s["unknown_streak"] <= self.unknown_patience: # type: ignore
320
+ return stable, inst_sim
321
+
322
+ # Fallback: use prototype from history to infer identity
323
+ fb_label, fb_sim = self._compute_best_identity_from_history(s)
324
+ if fb_label is not None and fb_sim >= max(0.0, self.threshold - self.fallback_margin):
325
+ s["label_votes"][fb_label] += 1 # type: ignore
326
+ s["stable_label"] = fb_label
327
+ s["unknown_streak"] = 0 # type: ignore
328
+ return fb_label, fb_sim
329
+
330
+ # No confident identity
331
+ s["stable_label"] = stable # keep whatever was last (may be None)
332
+ return (stable if stable is not None else "Unknown"), inst_sim
333
+
334
+
335
+ def detect_identity_in_video(
336
+ video_path: str,
337
+ identity_folder: str,
338
+ output_path: str = "output_identity_detection.mp4", threshold: float = 0.75, person_to_embs: Any=None):
339
+
340
+ # Build per-person embeddings from identity root
341
+ if not person_to_embs:
342
+ person_to_embs = get_embeddings_per_person(identity_folder)
343
+ if not person_to_embs:
344
+ print("No identity embeddings – aborting video processing.")
345
+ return
346
+
347
+ print(f"Identities discovered: {list(person_to_embs.keys())}")
348
+ else:
349
+ print(f"Using Pre-computed Identities: {list(person_to_embs.keys())}")
350
+
351
+ cap = cv2.VideoCapture(video_path)
352
+ if not cap.isOpened():
353
+ print("Could not open video", video_path)
354
+ return
355
+
356
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
357
+ fps = cap.get(cv2.CAP_PROP_FPS) or 30
358
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
359
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
360
+ out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
361
+
362
+ # Advanced BYTETrack-like tracker configuration tuned for faces
363
+ tracker_config = TrackerConfig(
364
+ track_high_thresh=0.5,
365
+ track_low_thresh=0.05,
366
+ new_track_thresh=0.5,
367
+ match_thresh=0.8,
368
+ track_buffer=int(max(30, fps) * 10), # allow short occlusions
369
+ max_time_lost=int(max(30, fps) * 5),
370
+ fuse_score=True,
371
+ enable_gmc=False,
372
+ frame_rate=int(max(1, fps))
373
+ )
374
+ adv_tracker = AdvancedTracker(tracker_config)
375
+
376
+ # Temporal identity smoothing manager
377
+ id_manager = TemporalIdentityManager(
378
+ person_to_embs=person_to_embs,
379
+ recognition_threshold=threshold,
380
+ history_size=20,
381
+ unknown_patience=7,
382
+ switch_patience=5,
383
+ fallback_margin=0.05,
384
+ )
385
+
386
+ # Unique track IDs across the whole video
387
+ unique_track_ids = set()
388
+
389
+ # Display and recognition gating settings
390
+ PROBATION_FRAMES = 260 # suppress UNK label until this many frames for a track
391
+ MIN_FACE_W = 40 # require minimum width for recognition attempt
392
+ MIN_FACE_H = 80 # require minimum height for recognition attempt
393
+
394
+ ##TODO: Consider aspect ratio of bounding box as well -- width/height > 0.5
395
+
396
+ # Colors: pending/unknown (red), identified Navy Blue #(dark cyan-ish)
397
+ COLOR_PENDING = (0, 0, 255)
398
+ COLOR_IDENTIFIED = (128,0,0) #(160, 160, 0)
399
+
400
+ # Track first seen frame index for probation logic
401
+ track_first_seen: Dict[int, int] = {}
402
+
403
+ def _track_id_to_color(track_id: str) -> Tuple[int, int, int]:
404
+ """Deterministically map a track_id to a visible BGR color."""
405
+ h = hashlib.md5((track_id or "").encode("utf-8")).digest()
406
+ b, g, r = int(h[0]), int(h[1]), int(h[2])
407
+ b = int(0.6 * b + 0.4 * 255)
408
+ g = int(0.6 * g + 0.4 * 255)
409
+ r = int(0.6 * r + 0.4 * 255)
410
+ return (b, g, r)
411
+
412
+ frame_idx = 0
413
+ while True:
414
+ ret, frame = cap.read()
415
+ if not ret:
416
+ break
417
+
418
+ try:
419
+ # Get all face representations in the frame
420
+ reps = DeepFace.represent(
421
+ img_path=frame,
422
+ model_name=MODEL_NAME,
423
+ detector_backend=DETECTOR_BACKEND,
424
+ align=ALIGN,
425
+ )
426
+ except Exception:
427
+ reps = []
428
+
429
+ # Prepare detections for the advanced tracker
430
+ detections: List[Dict] = []
431
+ for rep in reps:
432
+ emb = rep.get("embedding", [])
433
+ region = rep.get("facial_area", None)
434
+ conf = rep.get("face_confidence", 0.99)
435
+ if not region:
436
+ continue
437
+ x, y, w, h = region.get("x", 0), region.get("y", 0), region.get("w", 0), region.get("h", 0)
438
+ x1, y1, x2, y2 = int(x), int(y), int(x + w), int(y + h)
439
+ det = {
440
+ "bounding_box": {"xmin": x1, "ymin": y1, "xmax": x2, "ymax": y2},
441
+ "confidence": float(conf if isinstance(conf, (int, float)) else 0.99),
442
+ "category": "face",
443
+ "embedding": normalize_embedding(emb) if emb is not None else [],
444
+ "facial_area": region,
445
+ "frame_id": frame_idx,
446
+ }
447
+ detections.append(det)
448
+
449
+ # Run tracker update
450
+ tracked_dets: List[Dict] = adv_tracker.update(detections, img=frame) if detections else []
451
+
452
+ # Draw and label tracked faces
453
+ for det in tracked_dets:
454
+ bbox = det.get("bounding_box", {})
455
+ x1, y1, x2, y2 = int(bbox.get("xmin", 0)), int(bbox.get("ymin", 0)), int(bbox.get("xmax", 0)), int(bbox.get("ymax", 0))
456
+ track_id = int(det.get("track_id", 0))
457
+ conf = float(det.get("confidence", 0.0))
458
+ emb = det.get("embedding", [])
459
+ region = det.get("facial_area", {})
460
+
461
+ # Track probation age & size gating
462
+ if track_id not in track_first_seen:
463
+ track_first_seen[track_id] = frame_idx
464
+ age_frames = frame_idx - track_first_seen[track_id] + 1
465
+ w_box = max(1, x2 - x1)
466
+ h_box = max(1, y2 - y1)
467
+ eligible_for_recognition = (w_box >= MIN_FACE_W and h_box >= MIN_FACE_H)
468
+
469
+ # Compute instantaneous prediction only if eligible by size
470
+ best_person = None
471
+ best_avg = -1.0
472
+ if emb: # embeddings from DeepFace are already list-like; we normalized when building dets
473
+ for person, embs in person_to_embs.items():
474
+ if not embs:
475
+ continue
476
+ # person library may not be normalized if precomputed; normalize on the fly once
477
+ scores = [cosine_similarity(emb, normalize_embedding(e)) for e in embs]
478
+ avg_score = float(np.mean(scores)) if scores else 0.0
479
+ if avg_score > best_avg:
480
+ best_avg = avg_score
481
+ best_person = person
482
+
483
+ # Update temporal identity only if eligible; otherwise keep last stable
484
+ if eligible_for_recognition:
485
+ inst_label = best_person if (best_person is not None and best_avg >= threshold) else "Unknown"
486
+ final_label, final_sim = id_manager.update(track_id, emb, inst_label, best_avg)
487
+ else:
488
+ track_state = id_manager.tracks.get(track_id, {})
489
+ stable_label = track_state.get("stable_label") if isinstance(track_state, dict) else None
490
+ final_label = stable_label if stable_label is not None else "Unknown"
491
+ final_sim = best_avg
492
+
493
+ # Determine color and whether to show label
494
+ unique_track_ids.add(track_id)
495
+ is_identified = (final_label is not None and final_label != "Unknown")
496
+ box_color = COLOR_IDENTIFIED if is_identified else COLOR_PENDING
497
+ cv2.rectangle(frame, (x1, y1), (x2, y2), box_color, 3)
498
+
499
+ # Label text: show only if identified OR probation exceeded and still unknown
500
+ resolution = str(w_box) + "x" + str(h_box)
501
+ show_label = is_identified or (age_frames >= PROBATION_FRAMES and not is_identified)
502
+ if show_label:
503
+ label = final_label if is_identified else "Unknown"
504
+ #label_text = f"{label} id:{track_id} conf:{conf:.2f} sim:{final_sim:.2f} res:{resolution}"
505
+ label_text = f"{label}"
506
+ text_org = (x1, max(0, y1 - 10))
507
+ # Get text size (width, height)
508
+ (text_w, text_h), baseline = cv2.getTextSize(label_text, cv2.FONT_HERSHEY_SIMPLEX, 1.35, 3)
509
+
510
+ # Draw white rectangle as background
511
+ cv2.rectangle(frame, (text_org[0], text_org[1] - text_h - baseline), # top-left corner
512
+ (text_org[0] + text_w, text_org[1] + baseline), # bottom-right corner
513
+ (255, 255, 255), # white background
514
+ -1) # thickness=-1 → filled
515
+ cv2.putText(frame, label_text, text_org, cv2.FONT_HERSHEY_SIMPLEX, 1.35, box_color, 3, cv2.LINE_AA)
516
+
517
+ # # Draw landmarks as red dots if present in region
518
+ # landmarks = ["left_eye", "right_eye", "nose", "mouth_left", "mouth_right"]
519
+ # for lm in landmarks:
520
+ # if isinstance(region, dict) and lm in region and region[lm]:
521
+ # lx, ly = region[lm]
522
+ # cv2.circle(frame, (int(lx), int(ly)), 4, (0, 0, 255), -1)
523
+
524
+ # Overlay counts (top-right)
525
+ # curr_count = len(tracked_dets)
526
+ # total_count = len(unique_track_ids)
527
+ # hud_text = f"Curr:{curr_count} Total:{total_count} Frame:{frame_idx}"
528
+ # (tw, th), _ = cv2.getTextSize(hud_text, cv2.FONT_HERSHEY_SIMPLEX, 0.8, 2)
529
+ # cv2.rectangle(frame, (width - tw - 20, 10), (width - 10, 10 + th + 10), (0, 0, 0), -1)
530
+ # cv2.putText(frame, hud_text, (width - tw - 15, 10 + th), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2, cv2.LINE_AA)
531
+ out.write(frame)
532
+ frame_idx += 1
533
+
534
+ cap.release()
535
+ out.release()
536
+ print("Video saved to", output_path)
537
+
538
+ if __name__ == "__main__":
539
+ IDENTITY_FOLDER = "/content/JBK_Stream_Faces_Identities_EMP_NEW"
540
+ SAMPLE_FOLDER = "/content/Test"
541
+
542
+
543
+ VIDEO_PATH = "/content/Turnstile_Entry_Short_3.mp4"
544
+ OUTPUT_VIDEO = "/content/entry_short3_debug1.mp4"
545
+
546
+
547
+ THRESHOLD = 0.6
548
+
549
+ # Image-folder comparison can keep a threshold for reporting
550
+ #compare_identity_and_samples(IDENTITY_FOLDER, SAMPLE_FOLDER, THRESHOLD)
551
+
552
+ # Video detection: no threshold; always draw best label
553
+ if VIDEO_PATH and os.path.exists(VIDEO_PATH):
554
+ detect_identity_in_video(VIDEO_PATH, IDENTITY_FOLDER, OUTPUT_VIDEO, THRESHOLD)
555
+ else:
556
+ print("Skipping video detection – VIDEO_PATH not set or file does not exist.")