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,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]) -> Optional[str]:
|
|
66
|
+
if not new_embedding:
|
|
67
|
+
return None
|
|
68
|
+
best_similarity: float = 0.0
|
|
69
|
+
best_track_id: Optional[str] = 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: Optional[int] = 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: Optional[int] = 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: Optional[int] = 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.")
|