gfram 2.0.0__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.
- gfram/__init__.py +72 -0
- gfram/api/__init__.py +9 -0
- gfram/api/recognizer.py +446 -0
- gfram/core/__init__.py +8 -0
- gfram/detectors/__init__.py +15 -0
- gfram/detectors/face_detector.py +368 -0
- gfram/geometry/__init__.py +25 -0
- gfram/geometry/features.py +934 -0
- gfram/geometry/landmarks.py +174 -0
- gfram/geometry/moments.py +93 -0
- gfram/geometry/symmetry.py +95 -0
- gfram/geometry/topology.py +135 -0
- gfram/geometry/triangulation.py +96 -0
- gfram/matching/__init__.py +10 -0
- gfram/matching/index.py +365 -0
- gfram/matching/metrics.py +70 -0
- gfram/models/__init__.py +77 -0
- gfram/models/geometric_transformer.py +441 -0
- gfram/models/graph_network.py +509 -0
- gfram/models/losses.py +531 -0
- gfram/models/metric_learning.py +130 -0
- gfram/training/__init__.py +15 -0
- gfram/training/augmentation.py +68 -0
- gfram/training/dataset.py +173 -0
- gfram/training/trainer.py +116 -0
- gfram/utils/__init__.py +27 -0
- gfram/utils/config.py +112 -0
- gfram/utils/io.py +94 -0
- gfram/utils/visualization.py +178 -0
- gfram/version.py +9 -0
- gfram-2.0.0.dist-info/METADATA +212 -0
- gfram-2.0.0.dist-info/RECORD +35 -0
- gfram-2.0.0.dist-info/WHEEL +5 -0
- gfram-2.0.0.dist-info/licenses/LICENSE +21 -0
- gfram-2.0.0.dist-info/top_level.txt +1 -0
gfram/__init__.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GFRAM - Geometric Face Recognition and Matching
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
A professional face recognition library based on geometric features and custom AI models.
|
|
6
|
+
|
|
7
|
+
Basic usage:
|
|
8
|
+
>>> import gfram
|
|
9
|
+
>>> recognizer = gfram.Recognizer.from_pretrained('gfram-base')
|
|
10
|
+
>>> result = recognizer.recognize('photo.jpg')
|
|
11
|
+
|
|
12
|
+
:copyright: (c) 2024 by Ortiqova F.S.
|
|
13
|
+
:license: MIT, see LICENSE for more details.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .version import __version__, __author__, __license__, __description__
|
|
17
|
+
|
|
18
|
+
# Detectors
|
|
19
|
+
from .detectors import FaceDetector, LandmarkNormalizer
|
|
20
|
+
|
|
21
|
+
# Geometry
|
|
22
|
+
from .geometry.features import GeometricFeatureExtractor
|
|
23
|
+
|
|
24
|
+
# Models
|
|
25
|
+
from .models import (
|
|
26
|
+
GeometricTransformer,
|
|
27
|
+
GeometricGNN,
|
|
28
|
+
create_geometric_transformer,
|
|
29
|
+
create_geometric_gnn,
|
|
30
|
+
TripletLoss,
|
|
31
|
+
ArcFaceLoss,
|
|
32
|
+
CosFaceLoss,
|
|
33
|
+
CombinedLoss
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Matching
|
|
37
|
+
from .matching import FaceIndex, DistanceMetrics
|
|
38
|
+
|
|
39
|
+
# High-level API
|
|
40
|
+
from .api import Recognizer
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
# Version
|
|
44
|
+
'__version__',
|
|
45
|
+
'__author__',
|
|
46
|
+
'__license__',
|
|
47
|
+
'__description__',
|
|
48
|
+
|
|
49
|
+
# Detectors
|
|
50
|
+
'FaceDetector',
|
|
51
|
+
'LandmarkNormalizer',
|
|
52
|
+
|
|
53
|
+
# Geometry
|
|
54
|
+
'GeometricFeatureExtractor',
|
|
55
|
+
|
|
56
|
+
# Models
|
|
57
|
+
'GeometricTransformer',
|
|
58
|
+
'GeometricGNN',
|
|
59
|
+
'create_geometric_transformer',
|
|
60
|
+
'create_geometric_gnn',
|
|
61
|
+
'TripletLoss',
|
|
62
|
+
'ArcFaceLoss',
|
|
63
|
+
'CosFaceLoss',
|
|
64
|
+
'CombinedLoss',
|
|
65
|
+
|
|
66
|
+
# Matching
|
|
67
|
+
'FaceIndex',
|
|
68
|
+
'DistanceMetrics',
|
|
69
|
+
|
|
70
|
+
# High-level API
|
|
71
|
+
'Recognizer',
|
|
72
|
+
]
|
gfram/api/__init__.py
ADDED
gfram/api/recognizer.py
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level Face Recognition API.
|
|
3
|
+
|
|
4
|
+
Provides simple interface for face recognition tasks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
import numpy as np
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Union, List, Dict, Tuple
|
|
11
|
+
import logging
|
|
12
|
+
import pickle
|
|
13
|
+
import json
|
|
14
|
+
|
|
15
|
+
from ..detectors import FaceDetector, LandmarkNormalizer
|
|
16
|
+
from ..geometry.features import GeometricFeatureExtractor
|
|
17
|
+
from ..models import GeometricTransformer, GeometricGNN
|
|
18
|
+
from ..matching import FaceIndex
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Recognizer:
|
|
24
|
+
"""
|
|
25
|
+
High-level face recognition interface.
|
|
26
|
+
|
|
27
|
+
Combines detection, feature extraction, and matching in a simple API.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
>>> recognizer = Recognizer.from_pretrained("gfram-base")
|
|
31
|
+
>>> result = recognizer.recognize("photo.jpg")
|
|
32
|
+
>>> print(f"Identity: {result['name']}, Confidence: {result['confidence']}")
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
detector: Optional[FaceDetector] = None,
|
|
38
|
+
feature_extractor: Optional[GeometricFeatureExtractor] = None,
|
|
39
|
+
model: Optional[torch.nn.Module] = None,
|
|
40
|
+
face_index: Optional['FaceIndex'] = None,
|
|
41
|
+
device: str = "cpu",
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Initialize Recognizer.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
detector: Face detector instance.
|
|
48
|
+
feature_extractor: Geometric feature extractor.
|
|
49
|
+
model: Deep learning model for embeddings.
|
|
50
|
+
face_index: Face database index for matching.
|
|
51
|
+
device: Device to run models on ('cpu' or 'cuda').
|
|
52
|
+
"""
|
|
53
|
+
self.device = device
|
|
54
|
+
|
|
55
|
+
# Initialize components
|
|
56
|
+
self.detector = detector or FaceDetector()
|
|
57
|
+
self.feature_extractor = feature_extractor or GeometricFeatureExtractor()
|
|
58
|
+
self.model = model
|
|
59
|
+
self.face_index = face_index
|
|
60
|
+
self.normalizer = LandmarkNormalizer()
|
|
61
|
+
|
|
62
|
+
# Move model to device
|
|
63
|
+
if self.model is not None:
|
|
64
|
+
self.model.to(device)
|
|
65
|
+
self.model.eval()
|
|
66
|
+
|
|
67
|
+
logger.info(f"Recognizer initialized on device: {device}")
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_pretrained(
|
|
71
|
+
cls,
|
|
72
|
+
model_name: str = "gfram-base",
|
|
73
|
+
device: str = "cpu",
|
|
74
|
+
**kwargs
|
|
75
|
+
) -> 'Recognizer':
|
|
76
|
+
"""
|
|
77
|
+
Load a pretrained recognizer.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
model_name: Name of pretrained model.
|
|
81
|
+
device: Device to load model on.
|
|
82
|
+
**kwargs: Additional arguments for model loading.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Initialized Recognizer.
|
|
86
|
+
"""
|
|
87
|
+
# TODO: Implement model loading from hub or local cache
|
|
88
|
+
logger.info(f"Loading pretrained model: {model_name}")
|
|
89
|
+
|
|
90
|
+
# For now, create with default configuration
|
|
91
|
+
return cls(device=device)
|
|
92
|
+
|
|
93
|
+
def recognize(
|
|
94
|
+
self,
|
|
95
|
+
image: Union[str, np.ndarray],
|
|
96
|
+
threshold: float = 0.5,
|
|
97
|
+
top_k: int = 1,
|
|
98
|
+
) -> Dict:
|
|
99
|
+
"""
|
|
100
|
+
Recognize faces in an image.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
image: Image path or numpy array.
|
|
104
|
+
threshold: Confidence threshold for recognition.
|
|
105
|
+
top_k: Number of top matches to return.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dictionary with recognition results:
|
|
109
|
+
{
|
|
110
|
+
'faces': List of detected faces with identities,
|
|
111
|
+
'num_faces': Number of faces detected
|
|
112
|
+
}
|
|
113
|
+
"""
|
|
114
|
+
# Load image if path provided
|
|
115
|
+
if isinstance(image, str):
|
|
116
|
+
import cv2
|
|
117
|
+
image = cv2.imread(image)
|
|
118
|
+
if image is None:
|
|
119
|
+
raise ValueError(f"Failed to load image: {image}")
|
|
120
|
+
|
|
121
|
+
# Detect faces
|
|
122
|
+
detected_faces = self.detector.detect(image)
|
|
123
|
+
|
|
124
|
+
if not detected_faces:
|
|
125
|
+
return {
|
|
126
|
+
'faces': [],
|
|
127
|
+
'num_faces': 0
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Process each detected face
|
|
131
|
+
results = []
|
|
132
|
+
for face_data in detected_faces:
|
|
133
|
+
result = self._process_face(face_data, threshold, top_k)
|
|
134
|
+
results.append(result)
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
'faces': results,
|
|
138
|
+
'num_faces': len(results)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
def _process_face(
|
|
142
|
+
self,
|
|
143
|
+
face_data: Dict,
|
|
144
|
+
threshold: float,
|
|
145
|
+
top_k: int
|
|
146
|
+
) -> Dict:
|
|
147
|
+
"""
|
|
148
|
+
Process a single detected face.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
face_data: Dictionary with face landmarks and bbox.
|
|
152
|
+
threshold: Confidence threshold.
|
|
153
|
+
top_k: Number of matches to return.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Dictionary with recognition results for this face.
|
|
157
|
+
"""
|
|
158
|
+
landmarks = face_data['landmarks']
|
|
159
|
+
|
|
160
|
+
# Normalize landmarks
|
|
161
|
+
landmarks_norm = self.normalizer.normalize(landmarks)
|
|
162
|
+
|
|
163
|
+
# Extract geometric features
|
|
164
|
+
geometric_features = self.feature_extractor.extract(landmarks_norm)
|
|
165
|
+
|
|
166
|
+
# Get deep learning embedding if model available
|
|
167
|
+
embedding = None
|
|
168
|
+
if self.model is not None:
|
|
169
|
+
landmarks_tensor = torch.from_numpy(landmarks_norm).unsqueeze(0).float()
|
|
170
|
+
landmarks_tensor = landmarks_tensor.to(self.device)
|
|
171
|
+
|
|
172
|
+
with torch.no_grad():
|
|
173
|
+
_, embedding = self.model(landmarks_tensor, return_embedding=True)
|
|
174
|
+
if embedding is not None:
|
|
175
|
+
embedding = embedding.cpu().numpy().flatten()
|
|
176
|
+
|
|
177
|
+
# Match against database
|
|
178
|
+
matches = []
|
|
179
|
+
if self.face_index is not None:
|
|
180
|
+
query_vector = embedding if embedding is not None else geometric_features
|
|
181
|
+
matches = self.face_index.search(query_vector, k=top_k, threshold=threshold)
|
|
182
|
+
|
|
183
|
+
# Build result
|
|
184
|
+
result = {
|
|
185
|
+
'bbox': face_data.get('bbox'),
|
|
186
|
+
'landmarks': landmarks,
|
|
187
|
+
'geometric_features': geometric_features,
|
|
188
|
+
'embedding': embedding.tolist() if embedding is not None else None,
|
|
189
|
+
'matches': matches,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# Add identity if confident match found
|
|
193
|
+
if matches and len(matches) > 0:
|
|
194
|
+
best_match = matches[0]
|
|
195
|
+
if best_match['similarity'] >= threshold:
|
|
196
|
+
result['name'] = best_match['name']
|
|
197
|
+
result['confidence'] = best_match['similarity']
|
|
198
|
+
else:
|
|
199
|
+
result['name'] = 'Unknown'
|
|
200
|
+
result['confidence'] = 0.0
|
|
201
|
+
else:
|
|
202
|
+
result['name'] = 'Unknown'
|
|
203
|
+
result['confidence'] = 0.0
|
|
204
|
+
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
def add_person(
|
|
208
|
+
self,
|
|
209
|
+
name: str,
|
|
210
|
+
images: Union[str, List[str], np.ndarray, List[np.ndarray]],
|
|
211
|
+
) -> bool:
|
|
212
|
+
"""
|
|
213
|
+
Add a person to the recognition database.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
name: Person's name/identifier.
|
|
217
|
+
images: Image(s) of the person (paths or arrays).
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
True if successful, False otherwise.
|
|
221
|
+
"""
|
|
222
|
+
if self.face_index is None:
|
|
223
|
+
raise RuntimeError("No face index initialized. Create one first.")
|
|
224
|
+
|
|
225
|
+
# Ensure images is a list
|
|
226
|
+
if not isinstance(images, list):
|
|
227
|
+
images = [images]
|
|
228
|
+
|
|
229
|
+
# Process each image
|
|
230
|
+
embeddings = []
|
|
231
|
+
for img in images:
|
|
232
|
+
# Load if path
|
|
233
|
+
if isinstance(img, str):
|
|
234
|
+
import cv2
|
|
235
|
+
img = cv2.imread(img)
|
|
236
|
+
if img is None:
|
|
237
|
+
logger.warning(f"Failed to load image: {img}")
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
# Detect face
|
|
241
|
+
faces = self.detector.detect(img)
|
|
242
|
+
if not faces:
|
|
243
|
+
logger.warning(f"No face detected in image")
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
# Use first detected face
|
|
247
|
+
face_data = faces[0]
|
|
248
|
+
landmarks = face_data['landmarks']
|
|
249
|
+
landmarks_norm = self.normalizer.normalize(landmarks)
|
|
250
|
+
|
|
251
|
+
# Extract features
|
|
252
|
+
if self.model is not None:
|
|
253
|
+
landmarks_tensor = torch.from_numpy(landmarks_norm).unsqueeze(0).float()
|
|
254
|
+
landmarks_tensor = landmarks_tensor.to(self.device)
|
|
255
|
+
|
|
256
|
+
with torch.no_grad():
|
|
257
|
+
_, embedding = self.model(landmarks_tensor, return_embedding=True)
|
|
258
|
+
if embedding is not None:
|
|
259
|
+
embeddings.append(embedding.cpu().numpy().flatten())
|
|
260
|
+
else:
|
|
261
|
+
# Use geometric features
|
|
262
|
+
features = self.feature_extractor.extract(landmarks_norm)
|
|
263
|
+
embeddings.append(features)
|
|
264
|
+
|
|
265
|
+
if not embeddings:
|
|
266
|
+
logger.error(f"Failed to extract features for person: {name}")
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
# Add to index
|
|
270
|
+
embeddings_array = np.array(embeddings)
|
|
271
|
+
self.face_index.add(name, embeddings_array)
|
|
272
|
+
|
|
273
|
+
logger.info(f"Added person '{name}' with {len(embeddings)} images")
|
|
274
|
+
return True
|
|
275
|
+
|
|
276
|
+
def create_database(
|
|
277
|
+
self,
|
|
278
|
+
save_path: Optional[str] = None
|
|
279
|
+
) -> 'FaceIndex':
|
|
280
|
+
"""
|
|
281
|
+
Create a new face database.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
save_path: Path to save the database.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
FaceIndex instance.
|
|
288
|
+
"""
|
|
289
|
+
from ..matching import FaceIndex
|
|
290
|
+
|
|
291
|
+
# Determine embedding dimension
|
|
292
|
+
if self.model is not None:
|
|
293
|
+
embed_dim = self.model.embed_dim
|
|
294
|
+
else:
|
|
295
|
+
# Use geometric feature dimension
|
|
296
|
+
embed_dim = 150 # Default from GeometricFeatureExtractor
|
|
297
|
+
|
|
298
|
+
self.face_index = FaceIndex(dimension=embed_dim)
|
|
299
|
+
|
|
300
|
+
if save_path:
|
|
301
|
+
self.face_index.save(save_path)
|
|
302
|
+
|
|
303
|
+
return self.face_index
|
|
304
|
+
|
|
305
|
+
def load_database(
|
|
306
|
+
self,
|
|
307
|
+
path: str
|
|
308
|
+
) -> 'FaceIndex':
|
|
309
|
+
"""
|
|
310
|
+
Load an existing face database.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
path: Path to saved database.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Loaded FaceIndex instance.
|
|
317
|
+
"""
|
|
318
|
+
from ..matching import FaceIndex
|
|
319
|
+
|
|
320
|
+
self.face_index = FaceIndex.load(path)
|
|
321
|
+
logger.info(f"Loaded face database from: {path}")
|
|
322
|
+
|
|
323
|
+
return self.face_index
|
|
324
|
+
|
|
325
|
+
def save(
|
|
326
|
+
self,
|
|
327
|
+
path: str,
|
|
328
|
+
include_database: bool = True
|
|
329
|
+
):
|
|
330
|
+
"""
|
|
331
|
+
Save the recognizer state.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
path: Directory to save recognizer.
|
|
335
|
+
include_database: Whether to include face database.
|
|
336
|
+
"""
|
|
337
|
+
path = Path(path)
|
|
338
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
339
|
+
|
|
340
|
+
# Save model if present
|
|
341
|
+
if self.model is not None:
|
|
342
|
+
model_path = path / "model.pth"
|
|
343
|
+
torch.save({
|
|
344
|
+
'model_state_dict': self.model.state_dict(),
|
|
345
|
+
'model_class': self.model.__class__.__name__,
|
|
346
|
+
}, model_path)
|
|
347
|
+
logger.info(f"Saved model to {model_path}")
|
|
348
|
+
|
|
349
|
+
# Save database if present and requested
|
|
350
|
+
if include_database and self.face_index is not None:
|
|
351
|
+
db_path = path / "face_database"
|
|
352
|
+
self.face_index.save(str(db_path))
|
|
353
|
+
logger.info(f"Saved database to {db_path}")
|
|
354
|
+
|
|
355
|
+
# Save config
|
|
356
|
+
config = {
|
|
357
|
+
'device': self.device,
|
|
358
|
+
'has_model': self.model is not None,
|
|
359
|
+
'has_database': self.face_index is not None,
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
config_path = path / "config.json"
|
|
363
|
+
with open(config_path, 'w') as f:
|
|
364
|
+
json.dump(config, f, indent=2)
|
|
365
|
+
|
|
366
|
+
logger.info(f"Recognizer saved to {path}")
|
|
367
|
+
|
|
368
|
+
@classmethod
|
|
369
|
+
def load(
|
|
370
|
+
cls,
|
|
371
|
+
path: str,
|
|
372
|
+
device: str = "cpu"
|
|
373
|
+
) -> 'Recognizer':
|
|
374
|
+
"""
|
|
375
|
+
Load a saved recognizer.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
path: Directory containing saved recognizer.
|
|
379
|
+
device: Device to load on.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Loaded Recognizer instance.
|
|
383
|
+
"""
|
|
384
|
+
path = Path(path)
|
|
385
|
+
|
|
386
|
+
# Load config
|
|
387
|
+
config_path = path / "config.json"
|
|
388
|
+
with open(config_path, 'r') as f:
|
|
389
|
+
config = json.load(f)
|
|
390
|
+
|
|
391
|
+
# Initialize recognizer
|
|
392
|
+
recognizer = cls(device=device)
|
|
393
|
+
|
|
394
|
+
# Load model if present
|
|
395
|
+
model_path = path / "model.pth"
|
|
396
|
+
if model_path.exists() and config['has_model']:
|
|
397
|
+
checkpoint = torch.load(model_path, map_location=device)
|
|
398
|
+
# TODO: Reconstruct model from class name
|
|
399
|
+
logger.info(f"Loaded model from {model_path}")
|
|
400
|
+
|
|
401
|
+
# Load database if present
|
|
402
|
+
db_path = path / "face_database"
|
|
403
|
+
if db_path.exists() and config['has_database']:
|
|
404
|
+
recognizer.load_database(str(db_path))
|
|
405
|
+
|
|
406
|
+
logger.info(f"Recognizer loaded from {path}")
|
|
407
|
+
return recognizer
|
|
408
|
+
|
|
409
|
+
def get_embedding(
|
|
410
|
+
self,
|
|
411
|
+
image: Union[str, np.ndarray]
|
|
412
|
+
) -> Optional[np.ndarray]:
|
|
413
|
+
"""
|
|
414
|
+
Extract embedding vector from an image.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
image: Image path or array.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Embedding vector or None if no face detected.
|
|
421
|
+
"""
|
|
422
|
+
# Load image if needed
|
|
423
|
+
if isinstance(image, str):
|
|
424
|
+
import cv2
|
|
425
|
+
image = cv2.imread(image)
|
|
426
|
+
|
|
427
|
+
# Detect face
|
|
428
|
+
faces = self.detector.detect(image)
|
|
429
|
+
if not faces:
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
# Process first face
|
|
433
|
+
landmarks = faces[0]['landmarks']
|
|
434
|
+
landmarks_norm = self.normalizer.normalize(landmarks)
|
|
435
|
+
|
|
436
|
+
# Extract embedding
|
|
437
|
+
if self.model is not None:
|
|
438
|
+
landmarks_tensor = torch.from_numpy(landmarks_norm).unsqueeze(0).float()
|
|
439
|
+
landmarks_tensor = landmarks_tensor.to(self.device)
|
|
440
|
+
|
|
441
|
+
with torch.no_grad():
|
|
442
|
+
_, embedding = self.model(landmarks_tensor, return_embedding=True)
|
|
443
|
+
return embedding.cpu().numpy().flatten() if embedding is not None else None
|
|
444
|
+
else:
|
|
445
|
+
# Use geometric features
|
|
446
|
+
return self.feature_extractor.extract(landmarks_norm)
|
gfram/core/__init__.py
ADDED