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 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
@@ -0,0 +1,9 @@
1
+ """
2
+ High-level API for face recognition.
3
+ """
4
+
5
+ from .recognizer import Recognizer
6
+
7
+ __all__ = [
8
+ 'Recognizer',
9
+ ]
@@ -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
@@ -0,0 +1,8 @@
1
+ """
2
+ Core module for GFRAM.
3
+ """
4
+
5
+ # Core components will be imported here if needed
6
+ # For now, this module is a placeholder for future core functionality
7
+
8
+ __all__ = []
@@ -0,0 +1,15 @@
1
+ """
2
+ Face detection and landmark extraction module.
3
+ """
4
+
5
+ from .face_detector import (
6
+ FaceDetector,
7
+ LandmarkNormalizer,
8
+ load_image
9
+ )
10
+
11
+ __all__ = [
12
+ 'FaceDetector',
13
+ 'LandmarkNormalizer',
14
+ 'load_image',
15
+ ]