similarfaces 1.0.0__tar.gz

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.
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: similarfaces
3
+ Version: 1.0.0
4
+ Summary: A production-ready Face Recognition library powered by ONNX
5
+ Author-email: Narek <your.email@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/narek/similarfaces
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: numpy>=1.20.0
11
+ Requires-Dist: opencv-python>=4.5.1
12
+ Requires-Dist: onnxruntime>=1.15.0
13
+ Requires-Dist: scikit-image>=0.19.0
14
+ Requires-Dist: scipy>=1.7.0
15
+ Requires-Dist: Pillow>=9.0.0
16
+ Requires-Dist: matplotlib>=3.5.0
17
+
18
+ # similarfaces: High-Performance Face Recognition
19
+
20
+ A production-ready, clean, and robust face recognition pipeline powered by ONNX Runtime.
21
+
22
+ ---
23
+
24
+ ## 🌟 Overview
25
+
26
+ **similarfaces** is a streamlined, high-performance Python library for face detection, alignment, quality assessment, and recognition. Designed with modularity and ease of use in mind, it provides a functional API that leverages state-of-the-art models optimized for ONNX Runtime.
27
+
28
+ ### ✨ New Key Features
29
+
30
+ - 🏗️ **Functional API**: Clean and intuitive functional wrappers (`detect_faces`, `extract_features`, `compare_faces`, `align_face`) without the need to manually manage processor objects.
31
+ - 📦 **Structured Data Models**: All functions utilize a unified `Face` dataclass, ensuring type safety and easy access to bounding boxes, landmarks, quality scores, and embeddings.
32
+ - 🎯 **Integrated Detection & Quality**: `detect_faces()` now performs both robust face localization and automatic quality assessment in a single, efficient pass.
33
+ - 📐 **Optimal Alignment**: Similarity transforms for standardized 112x112 face cropping.
34
+ - 🧠 **High-accuracy Recognition**: Extract deep feature embeddings for high-accuracy face comparison.
35
+ - ⚡ **ONNX Powered**: Sub-millisecond inference speeds with minimal dependencies across CPU and GPU environments.
36
+
37
+ ---
38
+
39
+ ## 🚀 Quick Start
40
+
41
+ ### Installation
42
+
43
+ ```bash
44
+ pip install -r requirements.txt
45
+ pip install -e . # Install in editable mode for development
46
+ ```
47
+
48
+ ### Basic Usage
49
+
50
+ Compare two faces with high-quality filtering using the functional API:
51
+
52
+ ```python
53
+ import cv2
54
+ from similarfaces import detect_faces, extract_features, compare_faces
55
+
56
+ # Load images (cv2 loads as BGR)
57
+ img1 = cv2.imread("images/image1.png")
58
+ img2 = cv2.imread("images/image2.png")
59
+
60
+ # Detect faces (includes quality scores by default)
61
+ faces1 = detect_faces(img1)
62
+ faces2 = detect_faces(img2)
63
+
64
+ if faces1 and faces2:
65
+ # Pick the best face from each image based on quality score
66
+ face1 = max(faces1, key=lambda x: x.quality_score)
67
+ face2 = max(faces2, key=lambda x: x.quality_score)
68
+
69
+ # Extract embeddings
70
+ face1.embedding = extract_features(img1, face1)
71
+ face2.embedding = extract_features(img2, face2)
72
+
73
+ # Compare faces
74
+ similarity = compare_faces(face1, face2)
75
+ print(f"Similarity: {similarity:.4f}")
76
+
77
+ if similarity > 0.6:
78
+ print("Outcome: Matches (Same Person)")
79
+ else:
80
+ print("Outcome: No Match")
81
+ else:
82
+ print("Error: Could not find faces in one or both images.")
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 🛠 Project Structure
88
+
89
+ The library is designed to be developer-friendly and easy to extend:
90
+
91
+ - `similarfaces.detector`: High-performance face detection logic.
92
+ - `similarfaces.aligner`: Face alignment and warping.
93
+ - `similarfaces.scorer`: Quality assessment model.
94
+ - `similarfaces.encoder`: Feature embedding extraction.
95
+ - `similarfaces.models`: Data models including the unified `Face` dataclass.
96
+
97
+ ---
98
+
99
+ ## 📊 Performance
100
+
101
+ | Module | Model | Input Size | Accuracy |
102
+ | :--- | :--- | :--- | :--- |
103
+ | **Detection** | MobileNet-based | 640x640 | High |
104
+ | **Recognition** | IR50-based | 112x112 | SOTA |
105
+ | **Quality** | FaceQuality-ONNX | 128x128 | Robust |
106
+
107
+ ---
108
+
109
+ ## 📝 License
110
+
111
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
112
+
113
+ ---
114
+
115
+ <p align="center">
116
+ Developed with ❤️ by Narek Bektashyan
117
+ </p>
@@ -0,0 +1,100 @@
1
+ # similarfaces: High-Performance Face Recognition
2
+
3
+ A production-ready, clean, and robust face recognition pipeline powered by ONNX Runtime.
4
+
5
+ ---
6
+
7
+ ## 🌟 Overview
8
+
9
+ **similarfaces** is a streamlined, high-performance Python library for face detection, alignment, quality assessment, and recognition. Designed with modularity and ease of use in mind, it provides a functional API that leverages state-of-the-art models optimized for ONNX Runtime.
10
+
11
+ ### ✨ New Key Features
12
+
13
+ - 🏗️ **Functional API**: Clean and intuitive functional wrappers (`detect_faces`, `extract_features`, `compare_faces`, `align_face`) without the need to manually manage processor objects.
14
+ - 📦 **Structured Data Models**: All functions utilize a unified `Face` dataclass, ensuring type safety and easy access to bounding boxes, landmarks, quality scores, and embeddings.
15
+ - 🎯 **Integrated Detection & Quality**: `detect_faces()` now performs both robust face localization and automatic quality assessment in a single, efficient pass.
16
+ - 📐 **Optimal Alignment**: Similarity transforms for standardized 112x112 face cropping.
17
+ - 🧠 **High-accuracy Recognition**: Extract deep feature embeddings for high-accuracy face comparison.
18
+ - ⚡ **ONNX Powered**: Sub-millisecond inference speeds with minimal dependencies across CPU and GPU environments.
19
+
20
+ ---
21
+
22
+ ## 🚀 Quick Start
23
+
24
+ ### Installation
25
+
26
+ ```bash
27
+ pip install -r requirements.txt
28
+ pip install -e . # Install in editable mode for development
29
+ ```
30
+
31
+ ### Basic Usage
32
+
33
+ Compare two faces with high-quality filtering using the functional API:
34
+
35
+ ```python
36
+ import cv2
37
+ from similarfaces import detect_faces, extract_features, compare_faces
38
+
39
+ # Load images (cv2 loads as BGR)
40
+ img1 = cv2.imread("images/image1.png")
41
+ img2 = cv2.imread("images/image2.png")
42
+
43
+ # Detect faces (includes quality scores by default)
44
+ faces1 = detect_faces(img1)
45
+ faces2 = detect_faces(img2)
46
+
47
+ if faces1 and faces2:
48
+ # Pick the best face from each image based on quality score
49
+ face1 = max(faces1, key=lambda x: x.quality_score)
50
+ face2 = max(faces2, key=lambda x: x.quality_score)
51
+
52
+ # Extract embeddings
53
+ face1.embedding = extract_features(img1, face1)
54
+ face2.embedding = extract_features(img2, face2)
55
+
56
+ # Compare faces
57
+ similarity = compare_faces(face1, face2)
58
+ print(f"Similarity: {similarity:.4f}")
59
+
60
+ if similarity > 0.6:
61
+ print("Outcome: Matches (Same Person)")
62
+ else:
63
+ print("Outcome: No Match")
64
+ else:
65
+ print("Error: Could not find faces in one or both images.")
66
+ ```
67
+
68
+ ---
69
+
70
+ ## 🛠 Project Structure
71
+
72
+ The library is designed to be developer-friendly and easy to extend:
73
+
74
+ - `similarfaces.detector`: High-performance face detection logic.
75
+ - `similarfaces.aligner`: Face alignment and warping.
76
+ - `similarfaces.scorer`: Quality assessment model.
77
+ - `similarfaces.encoder`: Feature embedding extraction.
78
+ - `similarfaces.models`: Data models including the unified `Face` dataclass.
79
+
80
+ ---
81
+
82
+ ## 📊 Performance
83
+
84
+ | Module | Model | Input Size | Accuracy |
85
+ | :--- | :--- | :--- | :--- |
86
+ | **Detection** | MobileNet-based | 640x640 | High |
87
+ | **Recognition** | IR50-based | 112x112 | SOTA |
88
+ | **Quality** | FaceQuality-ONNX | 128x128 | Robust |
89
+
90
+ ---
91
+
92
+ ## 📝 License
93
+
94
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
95
+
96
+ ---
97
+
98
+ <p align="center">
99
+ Developed with ❤️ by Narek Bektashyan
100
+ </p>
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "similarfaces"
7
+ version = "1.0.0"
8
+ description = "A production-ready Face Recognition library powered by ONNX"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Narek", email = "your.email@example.com"}
14
+ ]
15
+ dependencies = [
16
+ "numpy>=1.20.0",
17
+ "opencv-python>=4.5.1",
18
+ "onnxruntime>=1.15.0",
19
+ "scikit-image>=0.19.0",
20
+ "scipy>=1.7.0",
21
+ "Pillow>=9.0.0",
22
+ "matplotlib>=3.5.0",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/narek/similarfaces"
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["."]
30
+ include = ["similarfaces*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,93 @@
1
+ from .detector import FaceDetector
2
+ from .encoder import FaceEncoder
3
+ from .aligner import FaceAligner
4
+ from .scorer import FaceQualityScorer
5
+ from .models import Face
6
+
7
+ __version__ = "1.0.0"
8
+
9
+ # Lazy-loaded model instances
10
+ _detector = None
11
+ _encoder = None
12
+
13
+ def _get_detector():
14
+ """Internal helper for lazy initialization of the FaceDetector."""
15
+ global _detector
16
+ if _detector is None:
17
+ _detector = FaceDetector()
18
+ return _detector
19
+
20
+ def _get_encoder():
21
+ """Internal helper for lazy initialization of the FaceEncoder."""
22
+ global _encoder
23
+ if _encoder is None:
24
+ _encoder = FaceEncoder()
25
+ return _encoder
26
+
27
+ def detect_faces(image: "np.ndarray", score_quality: bool = True) -> "List[Face]":
28
+ """
29
+ Detect faces in an image and calculate their quality scores.
30
+
31
+ Args:
32
+ image (np.ndarray): Image in BGR format.
33
+ score_quality (bool): Whether to calculate quality scores (default: True).
34
+
35
+ Returns:
36
+ List[Face]: List of detected faces with bounding boxes, landmarks, and quality.
37
+ """
38
+ return _get_detector().detect(image, score_quality=score_quality)
39
+
40
+ def extract_features(image: "np.ndarray", face: "Face") -> "np.ndarray":
41
+ """
42
+ Extract facial features (512-d embedding) from an image.
43
+
44
+ Args:
45
+ image (np.ndarray): Original full image (BGR).
46
+ face (Face): Face object containing landmarks.
47
+
48
+ Returns:
49
+ np.ndarray: L2-normalized face embedding vector.
50
+ """
51
+ return _get_encoder().encode(image, face.landmarks)
52
+
53
+ def align_face(image: "np.ndarray", face: "Face") -> "np.ndarray":
54
+ """
55
+ Align a face to a standard 112x112 size for identification tasks.
56
+
57
+ Args:
58
+ image (np.ndarray): Original full image (BGR).
59
+ face (Face): Face object containing landmarks.
60
+
61
+ Returns:
62
+ np.ndarray: Aligned 112x112 face image.
63
+ """
64
+ aligned, _ = _get_encoder().aligner.align(image, face.landmarks)
65
+ return aligned
66
+
67
+ def compare_faces(face1: "Face", face2: "Face") -> float:
68
+ """
69
+ Compute similarity between two faces using their embeddings.
70
+
71
+ Args:
72
+ face1 (Face): First face with embedding.
73
+ face2 (Face): Second face with embedding.
74
+
75
+ Returns:
76
+ float: Similarity score (0.0 to 1.0).
77
+ """
78
+ if face1.embedding is None or face2.embedding is None:
79
+ raise ValueError("Both faces must have embeddings for comparison.")
80
+ import numpy as np
81
+ return float(np.dot(face1.embedding, face2.embedding))
82
+
83
+ __all__ = [
84
+ "Face",
85
+ "FaceDetector",
86
+ "FaceEncoder",
87
+ "FaceAligner",
88
+ "FaceQualityScorer",
89
+ "detect_faces",
90
+ "extract_features",
91
+ "align_face",
92
+ "compare_faces"
93
+ ]
@@ -0,0 +1,57 @@
1
+ import cv2
2
+ import numpy as np
3
+ from skimage.transform import SimilarityTransform
4
+ from typing import Tuple, Optional
5
+
6
+
7
+ class FaceAligner:
8
+ """
9
+ Face Alignement utility using 5-point facial landmarks.
10
+ """
11
+
12
+ # Standard 5-point facial landmark reference (ArcFace-style)
13
+ STANDARD_LANDMARK_TEMPLATE = np.array([
14
+ [38.2946, 51.6963], # Left eye
15
+ [73.5318, 51.5014], # Right eye
16
+ [56.0252, 71.7366], # Nose tip
17
+ [41.5493, 92.3655], # Left mouth corner
18
+ [70.7299, 92.2041], # Right mouth corner
19
+ ], dtype=np.float32)
20
+
21
+ def __init__(self, target_size: int = 112) -> None:
22
+ self.target_size = target_size
23
+
24
+ def compute_alignment_matrix(self, points: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
25
+ """
26
+ Compute the transformation matrix to align a face.
27
+ """
28
+ points = np.array(points, dtype=np.float32)
29
+ if points.shape != (5, 2):
30
+ raise ValueError(f"Expected 5-point landmarks, got {points.shape}")
31
+
32
+ scale = self.target_size / 112.0
33
+ ref_points = self.STANDARD_LANDMARK_TEMPLATE.copy() * scale
34
+
35
+ transform = SimilarityTransform()
36
+ transform.estimate(points, ref_points)
37
+
38
+ affine = transform.params[:2, :]
39
+ affine_inv = np.linalg.inv(transform.params)[:2, :]
40
+ return affine, affine_inv
41
+
42
+ def align(self, image: np.ndarray, keypoints: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
43
+ """
44
+ Perform facial alignment.
45
+
46
+ Args:
47
+ image: Original image (HWC).
48
+ keypoints: 5-point landmarks (5, 2).
49
+
50
+ Returns:
51
+ The aligned face image and the inverse affine matrix.
52
+ """
53
+ affine_matrix, inverse_matrix = self.compute_alignment_matrix(keypoints)
54
+ aligned_face = cv2.warpAffine(
55
+ image, affine_matrix, (self.target_size, self.target_size), borderValue=0.0
56
+ )
57
+ return aligned_face, inverse_matrix
@@ -0,0 +1,238 @@
1
+ import cv2
2
+ import numpy as np
3
+ import onnxruntime as ort
4
+ from typing import List, Tuple, Any
5
+ from .utils import get_model_path
6
+
7
+ from .models import Face
8
+ from .utils import get_model_path
9
+ from .aligner import FaceAligner
10
+ from .scorer import FaceQualityScorer
11
+
12
+
13
+ def letterbox_resize(image: np.ndarray, target_size: Tuple[int, int]) -> Tuple[np.ndarray, float, Tuple[int, int]]:
14
+ """
15
+ Resize image with unchanged aspect ratio using padding (letterbox).
16
+
17
+ Args:
18
+ image (np.ndarray): Input image in BGR format.
19
+ target_size (tuple): Target (width, height).
20
+
21
+ Returns:
22
+ tuple: (padded_image, scale_factor, (dw, dh) padding).
23
+ """
24
+ img_h, img_w = image.shape[:2]
25
+ target_w, target_h = target_size
26
+
27
+ scale = min(target_w / img_w, target_h / img_h)
28
+ new_w = int(img_w * scale)
29
+ new_h = int(img_h * scale)
30
+
31
+ resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
32
+
33
+ dw = target_w - new_w
34
+ dh = target_h - new_h
35
+ top, bottom = dh // 2, dh - (dh // 2)
36
+ left, right = dw // 2, dw - (dw // 2)
37
+
38
+ padded = cv2.copyMakeBorder(
39
+ resized, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(0, 0, 0)
40
+ )
41
+
42
+ return padded, scale, (dw, dh)
43
+
44
+
45
+ class FaceDetector:
46
+ """
47
+ High-performance face detector for localization and landmark extraction.
48
+ Integrates quality scoring to provide a comprehensive detection result.
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ model_path: str = None,
54
+ input_size: Tuple[int, int] = (640, 640),
55
+ score_threshold: float = 0.8,
56
+ nms_threshold: float = 0.4,
57
+ use_letterbox: bool = True,
58
+ providers: List[str] = ["CPUExecutionProvider"]
59
+ ):
60
+ """
61
+ Initialize the FaceDetector.
62
+
63
+ Args:
64
+ model_path (str, optional): Path to the detection ONNX model.
65
+ input_size (tuple): Model input size (width, height). Defaults to (640, 640).
66
+ score_threshold (float): Confidence threshold for detection. Defaults to 0.8.
67
+ nms_threshold (float): Non-maximum suppression threshold. Defaults to 0.4.
68
+ use_letterbox (bool): Whether to use letterbox resizing. Defaults to True.
69
+ providers (list): ONNX Runtime execution providers.
70
+ """
71
+ self.model_path = model_path or get_model_path("detection.onnx")
72
+ self.input_width, self.input_height = input_size
73
+ self.score_threshold = score_threshold
74
+ self.nms_threshold = nms_threshold
75
+ self.use_letterbox = use_letterbox
76
+ self.providers = providers
77
+
78
+ # Anchor settings
79
+ self.min_sizes = [[16, 32], [64, 128], [256, 512]]
80
+ self.steps = [8, 16, 32]
81
+ self.variance = [0.1, 0.2]
82
+
83
+ # Core session
84
+ try:
85
+ self.session = ort.InferenceSession(self.model_path, providers=providers)
86
+ self.input_name = self.session.get_inputs()[0].name
87
+ self.output_names = [out.name for out in self.session.get_outputs()]
88
+ except Exception as e:
89
+ raise RuntimeError(f"Failed to load detection model: {e}")
90
+
91
+ # Lazy-loaded assistants for integrated quality scoring
92
+ self._aligner = None
93
+ self._scorer = None
94
+
95
+ self.priors = self._generate_priors((self.input_height, self.input_width))
96
+
97
+ def _generate_priors(self, image_size: Tuple[int, int]) -> np.ndarray:
98
+ """Generate anchor boxes for detection."""
99
+ anchors = []
100
+ im_h, im_w = image_size
101
+
102
+ for k, step in enumerate(self.steps):
103
+ f_h = im_h // step
104
+ f_w = im_w // step
105
+ for i in range(f_h):
106
+ for j in range(f_w):
107
+ for min_size in self.min_sizes[k]:
108
+ s_kx = min_size / im_w
109
+ s_ky = min_size / im_h
110
+ cx = (j + 0.5) * step / im_w
111
+ cy = (i + 0.5) * step / im_h
112
+ anchors.append([cx, cy, s_kx, s_ky])
113
+ return np.array(anchors, dtype=np.float32)
114
+
115
+ def _decode_boxes(self, loc: np.ndarray, priors: np.ndarray) -> np.ndarray:
116
+ """Decode bounding boxes from model output and priors."""
117
+ boxes = np.concatenate([
118
+ priors[:, :2] + loc[:, :2] * self.variance[0] * priors[:, 2:],
119
+ priors[:, 2:] * np.exp(loc[:, 2:] * self.variance[1])
120
+ ], axis=1)
121
+ boxes[:, :2] -= boxes[:, 2:] / 2
122
+ boxes[:, 2:] += boxes[:, :2]
123
+ return boxes
124
+
125
+ def _decode_landmarks(self, ldm: np.ndarray, priors: np.ndarray) -> np.ndarray:
126
+ """Decode facial landmarks from model output and priors."""
127
+ landmarks = np.concatenate([
128
+ priors[:, :2] + ldm[:, i:i+2] * self.variance[0] * priors[:, 2:]
129
+ for i in range(0, 10, 2)
130
+ ], axis=1)
131
+ return landmarks.reshape(-1, 5, 2)
132
+
133
+ def preprocess(self, image: np.ndarray) -> Tuple[np.ndarray, float, Tuple[int, int]]:
134
+ """Preprocess image for detection."""
135
+ if self.use_letterbox:
136
+ resized, scale, pad = letterbox_resize(image, (self.input_width, self.input_height))
137
+ else:
138
+ resized = cv2.resize(image, (self.input_width, self.input_height))
139
+ scale = self.input_width / image.shape[1]
140
+ pad = (0, 0)
141
+
142
+ img = resized.astype(np.float32)
143
+ img -= np.array([104, 117, 123], dtype=np.float32)
144
+ img = img.transpose(2, 0, 1)
145
+ img = np.expand_dims(img, axis=0)
146
+ return img, scale, pad
147
+
148
+ def postprocess(
149
+ self,
150
+ outputs: List[np.ndarray],
151
+ scale: float,
152
+ pad: Tuple[int, int],
153
+ orig_shape: Tuple[int, int]
154
+ ) -> List[Face]:
155
+ """Convert model outputs into Face objects."""
156
+ loc, conf, landms = outputs[0][0], outputs[1][0], outputs[2][0]
157
+
158
+ scores = conf[:, 1]
159
+ mask = scores > self.score_threshold
160
+ if not np.any(mask):
161
+ return []
162
+
163
+ loc, scores, priors, landms = loc[mask], scores[mask], self.priors[mask], landms[mask]
164
+
165
+ boxes = self._decode_boxes(loc, priors)
166
+ landmarks = self._decode_landmarks(landms, priors)
167
+
168
+ # Rescale
169
+ boxes[:, [0, 2]] *= self.input_width
170
+ boxes[:, [1, 3]] *= self.input_height
171
+ landmarks[:, :, 0] *= self.input_width
172
+ landmarks[:, :, 1] *= self.input_height
173
+
174
+ # Subtract padding
175
+ dw, dh = pad
176
+ boxes[:, [0, 2]] -= dw // 2
177
+ boxes[:, [1, 3]] -= dh // 2
178
+ landmarks[:, :, 0] -= dw // 2
179
+ landmarks[:, :, 1] -= dh // 2
180
+
181
+ # Final scale
182
+ boxes /= scale
183
+ landmarks /= scale
184
+
185
+ # Clip
186
+ orig_h, orig_w = orig_shape
187
+ boxes[:, [0, 2]] = np.clip(boxes[:, [0, 2]], 0, orig_w)
188
+ boxes[:, [1, 3]] = np.clip(boxes[:, [1, 3]], 0, orig_h)
189
+ landmarks[:, :, 0] = np.clip(landmarks[:, :, 0], 0, orig_w)
190
+ landmarks[:, :, 1] = np.clip(landmarks[:, :, 1], 0, orig_h)
191
+
192
+ # NMS
193
+ keep = cv2.dnn.NMSBoxes(boxes.tolist(), scores.tolist(), 0.1, self.nms_threshold)
194
+
195
+ results = []
196
+ if len(keep) > 0:
197
+ indices = np.array(keep).flatten()
198
+ for idx in indices:
199
+ results.append(Face(
200
+ bbox=boxes[idx],
201
+ score=float(scores[idx]),
202
+ landmarks=landmarks[idx]
203
+ ))
204
+ return results
205
+
206
+ def detect(self, image: np.ndarray, score_quality: bool = True) -> List[Face]:
207
+ """
208
+ Detect faces and optionally assess their quality.
209
+
210
+ Args:
211
+ image (np.ndarray): Image in BGR format.
212
+ score_quality (bool): Whether to calculate quality scores for each face.
213
+
214
+ Returns:
215
+ List[Face]: Detected faces.
216
+ """
217
+ if image is None or image.size == 0:
218
+ return []
219
+
220
+ # 1. Base Detection
221
+ orig_shape = image.shape[:2]
222
+ input_tensor, scale, pad = self.preprocess(image)
223
+ outputs = self.session.run(self.output_names, {self.input_name: input_tensor})
224
+ faces = self.postprocess(outputs, scale, pad, orig_shape)
225
+
226
+ # 2. Integrated Quality Check
227
+ if score_quality and faces:
228
+ if self._aligner is None:
229
+ self._aligner = FaceAligner()
230
+ if self._scorer is None:
231
+ self._scorer = FaceQualityScorer(providers=self.providers)
232
+
233
+ for face in faces:
234
+ aligned, _ = self._aligner.align(image, face.landmarks)
235
+ face.quality_score = self._scorer.score(aligned)
236
+
237
+ return faces
238
+
@@ -0,0 +1,101 @@
1
+ import os
2
+ import cv2
3
+ import numpy as np
4
+ from onnxruntime import InferenceSession
5
+ from typing import List, Optional
6
+ from .utils import get_model_path
7
+ from .aligner import FaceAligner
8
+
9
+ from .models import Face
10
+ from .utils import get_model_path
11
+ from .aligner import FaceAligner
12
+
13
+
14
+ class FaceEncoder:
15
+ """
16
+ Face Encoder for extracting robust face embeddings.
17
+ Uses ONNX Runtime for inference. Default embedding size is 512.
18
+ """
19
+
20
+ def __init__(self, model_path: Optional[str] = None, providers: List[str] = ["CPUExecutionProvider"]) -> None:
21
+ """
22
+ Initialize the FaceEncoder.
23
+
24
+ Args:
25
+ model_path (str, optional): Path to the embedding ONNX model.
26
+ providers (list): ONNX Runtime execution providers.
27
+ """
28
+ self.model_path = model_path or get_model_path("features_extraction.onnx")
29
+ self.input_size = (112, 112)
30
+ self.normalization_mean = 127.5
31
+ self.normalization_scale = 127.5
32
+ self.aligner = FaceAligner()
33
+
34
+ try:
35
+ self.session = InferenceSession(self.model_path, providers=providers)
36
+ input_config = self.session.get_inputs()[0]
37
+ self.input_name = input_config.name
38
+ self.output_names = [o.name for o in self.session.get_outputs()]
39
+ except Exception as e:
40
+ raise RuntimeError(f"Failed to initialize Face Embedding model: {e}")
41
+
42
+ def preprocess(self, face_image: np.ndarray) -> np.ndarray:
43
+ """
44
+ Resize, normalize and convert cropped face to NCHW format.
45
+
46
+ Args:
47
+ face_image (np.ndarray): Cropped face image (BGR).
48
+
49
+ Returns:
50
+ np.ndarray: Preprocessed tensor (1, 3, 112, 112).
51
+ """
52
+ resized_face = cv2.resize(face_image, self.input_size)
53
+ face_blob = cv2.dnn.blobFromImage(
54
+ resized_face,
55
+ scalefactor=1.0 / self.normalization_scale,
56
+ size=self.input_size,
57
+ mean=(self.normalization_mean,) * 3,
58
+ swapRB=True
59
+ )
60
+ return face_blob
61
+
62
+ def encode_aligned(self, aligned_face: np.ndarray, normalize: bool = True) -> np.ndarray:
63
+ """
64
+ Extract embedding from an already aligned face image.
65
+
66
+ Args:
67
+ aligned_face (np.ndarray): Aligned face image (112x112).
68
+ normalize (bool): Whether to L2-normalize the output vector.
69
+
70
+ Returns:
71
+ np.ndarray: Face embedding vector (512-d).
72
+ """
73
+ face_blob = self.preprocess(aligned_face)
74
+ embedding = self.session.run(self.output_names, {self.input_name: face_blob})[0]
75
+ embedding = embedding.flatten()
76
+
77
+ if normalize:
78
+ norm = np.linalg.norm(embedding)
79
+ if norm > 1e-6:
80
+ embedding /= norm
81
+ return embedding
82
+
83
+ def encode(
84
+ self,
85
+ image: np.ndarray,
86
+ keypoints: np.ndarray,
87
+ normalize: bool = True
88
+ ) -> np.ndarray:
89
+ """
90
+ Align face and extract embedding in one step.
91
+
92
+ Args:
93
+ image (np.ndarray): Full image in BGR.
94
+ keypoints (np.ndarray): 5-point facial landmarks.
95
+ normalize (bool): Whether to L2-normalize the output vector.
96
+
97
+ Returns:
98
+ np.ndarray: Face embedding vector.
99
+ """
100
+ aligned_face, _ = self.aligner.align(image, keypoints)
101
+ return self.encode_aligned(aligned_face, normalize=normalize)
@@ -0,0 +1,59 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import List, Optional, Union
3
+ import numpy as np
4
+ import cv2
5
+
6
+ @dataclass
7
+ class Face:
8
+ """
9
+ Structured representation of a detected face.
10
+
11
+ Attributes:
12
+ bbox (np.ndarray): Bounding box in [x1, y1, x2, y2] format.
13
+ score (float): Confidence score of the detection.
14
+ landmarks (Optional[np.ndarray]): 5-point facial landmarks (eyes, nose, mouth corners).
15
+ quality_score (Optional[float]): Quality score of the face (0.0 to 1.0).
16
+ embedding (Optional[np.ndarray]): 512-dimensional face embedding (L2 normalized).
17
+ """
18
+ bbox: np.ndarray
19
+ score: float
20
+ landmarks: Optional[np.ndarray] = None
21
+ quality_score: Optional[float] = None
22
+ embedding: Optional[np.ndarray] = None
23
+
24
+ def to_dict(self) -> dict:
25
+ """Convert the Face object to a JSON-serializable dictionary."""
26
+ return {
27
+ "bbox": self.bbox.tolist(),
28
+ "score": float(self.score),
29
+ "landmarks": self.landmarks.tolist() if self.landmarks is not None else None,
30
+ "quality_score": float(self.quality_score) if self.quality_score is not None else None,
31
+ "embedding": self.embedding.tolist() if self.embedding is not None else None,
32
+ }
33
+
34
+ def draw(self, image: np.ndarray, color=(0, 255, 0), thickness=2) -> np.ndarray:
35
+ """
36
+ Draw bounding box and landmarks on the provided image.
37
+
38
+ Args:
39
+ image (np.ndarray): Image to draw on (BGR).
40
+ color (tuple): BGR color for the box and points.
41
+ thickness (int): Line thickness.
42
+
43
+ Returns:
44
+ np.ndarray: Image with visualizations.
45
+ """
46
+ img = image.copy()
47
+ x1, y1, x2, y2 = map(int, self.bbox)
48
+ cv2.rectangle(img, (x1, y1), (x2, y2), color, thickness)
49
+
50
+ if self.landmarks is not None:
51
+ for pt in self.landmarks:
52
+ cv2.circle(img, (int(pt[0]), int(pt[1])), 2, color, -1)
53
+
54
+ # Draw quality score if available
55
+ if self.quality_score is not None:
56
+ cv2.putText(img, f"Q: {self.quality_score:.2f}", (x1, y1 - 10),
57
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)
58
+
59
+ return img
@@ -0,0 +1,96 @@
1
+ import onnxruntime as ort
2
+ import numpy as np
3
+ import cv2
4
+ from PIL import Image
5
+ from typing import List, Union, Optional
6
+ from .utils import get_model_path
7
+
8
+
9
+ from .models import Face
10
+ from .utils import get_model_path
11
+
12
+
13
+ class FaceQualityScorer:
14
+ """
15
+ ONNX-based Face Quality Scorer.
16
+ Determines if a face image (aligned or cropped) is suitable for recognition.
17
+ A higher score (closer to 1.0) means higher quality.
18
+ """
19
+
20
+ def __init__(self, model_path: Optional[str] = None, providers: List[str] = ["CPUExecutionProvider"]):
21
+ """
22
+ Initialize the FaceQualityScorer.
23
+
24
+ Args:
25
+ model_path (str, optional): Path to the quality assessment ONNX model.
26
+ providers (list): ONNX Runtime execution providers.
27
+ """
28
+ self.model_path = model_path or get_model_path("quality_assessment.onnx")
29
+
30
+ # Ensure the external weights data file is downloaded
31
+ if not model_path:
32
+ get_model_path("model.onnx.data")
33
+ try:
34
+ self.session = ort.InferenceSession(self.model_path, providers=providers)
35
+ self.input_name = self.session.get_inputs()[0].name
36
+ except Exception as e:
37
+ raise RuntimeError(f"Failed to load quality model from {self.model_path}: {e}")
38
+
39
+ # Normalization parameters (ImageNet)
40
+ self.input_size = (128, 128)
41
+ self.mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
42
+ self.std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
43
+
44
+ def _preprocess(self, image: Union[np.ndarray, Image.Image]) -> np.ndarray:
45
+ """
46
+ Preprocess single image for quality scoring.
47
+
48
+ Args:
49
+ image (Union[np.ndarray, Image.Image]): Input image.
50
+
51
+ Returns:
52
+ np.ndarray: Preprocessed tensor (1, 3, 128, 128).
53
+ """
54
+ # Handle color channel conversion (assuming BGR for numpy array from cv2)
55
+ if isinstance(image, np.ndarray):
56
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
57
+ elif isinstance(image, Image.Image):
58
+ image = np.array(image.convert("RGB"))
59
+
60
+ # Resize
61
+ img = cv2.resize(image, self.input_size)
62
+
63
+ # Normalize
64
+ img = img.astype(np.float32) / 255.0
65
+ img = (img - self.mean) / self.std
66
+
67
+ # CHW + Batch
68
+ img = np.transpose(img, (2, 0, 1))
69
+ img = np.expand_dims(img, axis=0)
70
+ return img
71
+
72
+ def score(self, image: Union[np.ndarray, Image.Image]) -> float:
73
+ """
74
+ Predict quality score for a single face image.
75
+
76
+ Args:
77
+ image (Union[np.ndarray, Image.Image]): Cropped or aligned face image.
78
+
79
+ Returns:
80
+ float: Quality score, typically between 0.0 and 1.0.
81
+ """
82
+ input_tensor = self._preprocess(image)
83
+ outputs = self.session.run(None, {self.input_name: input_tensor})
84
+ return float(outputs[0][0, 0])
85
+
86
+ def score_batch(self, images: List[Union[np.ndarray, Image.Image]]) -> List[float]:
87
+ """
88
+ Predict quality scores for a batch of images.
89
+
90
+ Args:
91
+ images (list): List of cropped/aligned face images.
92
+
93
+ Returns:
94
+ List[float]: List of quality scores.
95
+ """
96
+ return [self.score(img) for img in images]
@@ -0,0 +1,75 @@
1
+ import numpy as np
2
+ import cv2
3
+ from pathlib import Path
4
+ import os
5
+ import sys
6
+ import urllib.request
7
+
8
+ HF_MODEL_REPOS = {
9
+ "detection.onnx": "https://huggingface.co/similarfaces/face-detector/resolve/main/",
10
+ "features_extraction.onnx": "https://huggingface.co/similarfaces/face-features/resolve/main/",
11
+ "quality_assessment.onnx": "https://huggingface.co/similarfaces/face-quality/resolve/main/",
12
+ "model.onnx.data": "https://huggingface.co/similarfaces/face-quality/resolve/main/",
13
+ }
14
+
15
+ def download_model(model_name: str, model_path: str):
16
+ """Download a model from Hugging Face if it doesn't exist locally."""
17
+ if not os.path.exists(model_path):
18
+ base_url = HF_MODEL_REPOS.get(model_name, "https://huggingface.co/similarfaces/face-quality/resolve/main/")
19
+ url = base_url + model_name
20
+ print(f"Downloading {model_name} from Hugging Face...")
21
+ try:
22
+ os.makedirs(os.path.dirname(model_path), exist_ok=True)
23
+
24
+ def reporthook(count, block_size, total_size):
25
+ if total_size > 0:
26
+ percent = int(count * block_size * 100 / total_size)
27
+ sys.stdout.write(f"\rDownloading... {min(percent, 100)}%")
28
+ sys.stdout.flush()
29
+
30
+ urllib.request.urlretrieve(url, model_path, reporthook=reporthook)
31
+ print() # new line after progress
32
+ except Exception as e:
33
+ print(f"\nFailed to download {model_name}: {e}")
34
+ raise RuntimeError(f"Could not download model {model_name} from {url}: {e}")
35
+
36
+ def get_model_path(model_name: str) -> str:
37
+ """Get the absolute path to a model file within the package."""
38
+ model_path = str(Path(__file__).parent / "models" / model_name)
39
+ download_model(model_name, model_path)
40
+ return model_path
41
+
42
+ def crop_face(image: np.ndarray, box: np.ndarray) -> np.ndarray:
43
+ """
44
+ Crop face from image based on bounding box.
45
+
46
+ Args:
47
+ image: Original image in BGR/RGB format.
48
+ box: [x1, y1, x2, y2] bounding box.
49
+
50
+ Returns:
51
+ Cropped face image.
52
+ """
53
+ x1, y1, x2, y2 = map(int, box)
54
+ return image[max(0, y1):y2, max(0, x1):x2]
55
+
56
+ def adjust_keypoints(keypoints: np.ndarray, crop_box: np.ndarray) -> np.ndarray:
57
+ """
58
+ Adjust keypoints from original image coordinates to cropped region coordinates.
59
+
60
+ Args:
61
+ keypoints: Array of [x, y] points for the original image.
62
+ crop_box: [x1, y1, x2, y2] bounding box of the cropped region.
63
+
64
+ Returns:
65
+ Adjusted keypoints for the cropped region.
66
+ """
67
+ x1, y1, _, _ = map(int, crop_box)
68
+ keypoints = np.array(keypoints, dtype=np.float32)
69
+ keypoints[:, 0] -= x1
70
+ keypoints[:, 1] -= y1
71
+ return keypoints
72
+
73
+ def draw_point(image: np.ndarray, point: tuple, color=(0, 255, 0), radius=2):
74
+ """Draw a point on the image."""
75
+ cv2.circle(image, (int(point[0]), int(point[1])), radius, color, -1)
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: similarfaces
3
+ Version: 1.0.0
4
+ Summary: A production-ready Face Recognition library powered by ONNX
5
+ Author-email: Narek <your.email@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/narek/similarfaces
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: numpy>=1.20.0
11
+ Requires-Dist: opencv-python>=4.5.1
12
+ Requires-Dist: onnxruntime>=1.15.0
13
+ Requires-Dist: scikit-image>=0.19.0
14
+ Requires-Dist: scipy>=1.7.0
15
+ Requires-Dist: Pillow>=9.0.0
16
+ Requires-Dist: matplotlib>=3.5.0
17
+
18
+ # similarfaces: High-Performance Face Recognition
19
+
20
+ A production-ready, clean, and robust face recognition pipeline powered by ONNX Runtime.
21
+
22
+ ---
23
+
24
+ ## 🌟 Overview
25
+
26
+ **similarfaces** is a streamlined, high-performance Python library for face detection, alignment, quality assessment, and recognition. Designed with modularity and ease of use in mind, it provides a functional API that leverages state-of-the-art models optimized for ONNX Runtime.
27
+
28
+ ### ✨ New Key Features
29
+
30
+ - 🏗️ **Functional API**: Clean and intuitive functional wrappers (`detect_faces`, `extract_features`, `compare_faces`, `align_face`) without the need to manually manage processor objects.
31
+ - 📦 **Structured Data Models**: All functions utilize a unified `Face` dataclass, ensuring type safety and easy access to bounding boxes, landmarks, quality scores, and embeddings.
32
+ - 🎯 **Integrated Detection & Quality**: `detect_faces()` now performs both robust face localization and automatic quality assessment in a single, efficient pass.
33
+ - 📐 **Optimal Alignment**: Similarity transforms for standardized 112x112 face cropping.
34
+ - 🧠 **High-accuracy Recognition**: Extract deep feature embeddings for high-accuracy face comparison.
35
+ - ⚡ **ONNX Powered**: Sub-millisecond inference speeds with minimal dependencies across CPU and GPU environments.
36
+
37
+ ---
38
+
39
+ ## 🚀 Quick Start
40
+
41
+ ### Installation
42
+
43
+ ```bash
44
+ pip install -r requirements.txt
45
+ pip install -e . # Install in editable mode for development
46
+ ```
47
+
48
+ ### Basic Usage
49
+
50
+ Compare two faces with high-quality filtering using the functional API:
51
+
52
+ ```python
53
+ import cv2
54
+ from similarfaces import detect_faces, extract_features, compare_faces
55
+
56
+ # Load images (cv2 loads as BGR)
57
+ img1 = cv2.imread("images/image1.png")
58
+ img2 = cv2.imread("images/image2.png")
59
+
60
+ # Detect faces (includes quality scores by default)
61
+ faces1 = detect_faces(img1)
62
+ faces2 = detect_faces(img2)
63
+
64
+ if faces1 and faces2:
65
+ # Pick the best face from each image based on quality score
66
+ face1 = max(faces1, key=lambda x: x.quality_score)
67
+ face2 = max(faces2, key=lambda x: x.quality_score)
68
+
69
+ # Extract embeddings
70
+ face1.embedding = extract_features(img1, face1)
71
+ face2.embedding = extract_features(img2, face2)
72
+
73
+ # Compare faces
74
+ similarity = compare_faces(face1, face2)
75
+ print(f"Similarity: {similarity:.4f}")
76
+
77
+ if similarity > 0.6:
78
+ print("Outcome: Matches (Same Person)")
79
+ else:
80
+ print("Outcome: No Match")
81
+ else:
82
+ print("Error: Could not find faces in one or both images.")
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 🛠 Project Structure
88
+
89
+ The library is designed to be developer-friendly and easy to extend:
90
+
91
+ - `similarfaces.detector`: High-performance face detection logic.
92
+ - `similarfaces.aligner`: Face alignment and warping.
93
+ - `similarfaces.scorer`: Quality assessment model.
94
+ - `similarfaces.encoder`: Feature embedding extraction.
95
+ - `similarfaces.models`: Data models including the unified `Face` dataclass.
96
+
97
+ ---
98
+
99
+ ## 📊 Performance
100
+
101
+ | Module | Model | Input Size | Accuracy |
102
+ | :--- | :--- | :--- | :--- |
103
+ | **Detection** | MobileNet-based | 640x640 | High |
104
+ | **Recognition** | IR50-based | 112x112 | SOTA |
105
+ | **Quality** | FaceQuality-ONNX | 128x128 | Robust |
106
+
107
+ ---
108
+
109
+ ## 📝 License
110
+
111
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
112
+
113
+ ---
114
+
115
+ <p align="center">
116
+ Developed with ❤️ by Narek Bektashyan
117
+ </p>
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ similarfaces/__init__.py
4
+ similarfaces/aligner.py
5
+ similarfaces/detector.py
6
+ similarfaces/encoder.py
7
+ similarfaces/models.py
8
+ similarfaces/scorer.py
9
+ similarfaces/utils.py
10
+ similarfaces.egg-info/PKG-INFO
11
+ similarfaces.egg-info/SOURCES.txt
12
+ similarfaces.egg-info/dependency_links.txt
13
+ similarfaces.egg-info/requires.txt
14
+ similarfaces.egg-info/top_level.txt
@@ -0,0 +1,7 @@
1
+ numpy>=1.20.0
2
+ opencv-python>=4.5.1
3
+ onnxruntime>=1.15.0
4
+ scikit-image>=0.19.0
5
+ scipy>=1.7.0
6
+ Pillow>=9.0.0
7
+ matplotlib>=3.5.0
@@ -0,0 +1 @@
1
+ similarfaces