Upload 11 files
Browse files- LICENSE +21 -0
- README.md +150 -0
- app.py +52 -0
- assets/.gitkeep +0 -0
- configs/.gitkeep +0 -0
- notebooks/.gitkeep +0 -0
- requirements.txt +7 -0
- src/__init__.py +17 -0
- src/detect_faces.py +69 -0
- src/extract_embeddings.py +78 -0
- src/verify_faces.py +66 -0
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Martin Badrous
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language: en
|
| 3 |
+
license: mit
|
| 4 |
+
tags:
|
| 5 |
+
- computer-vision
|
| 6 |
+
- face-recognition
|
| 7 |
+
- face-verification
|
| 8 |
+
- biometrics
|
| 9 |
+
- deep-learning
|
| 10 |
+
pipeline_tag: image-similarity
|
| 11 |
+
model-index:
|
| 12 |
+
- name: Facial Recognition & Verification (Martin Badrous)
|
| 13 |
+
results:
|
| 14 |
+
- task:
|
| 15 |
+
type: image-similarity
|
| 16 |
+
name: Face Verification
|
| 17 |
+
dataset:
|
| 18 |
+
name: LFW
|
| 19 |
+
type: face-images
|
| 20 |
+
metrics:
|
| 21 |
+
- name: Accuracy
|
| 22 |
+
type: accuracy
|
| 23 |
+
value: 0.99
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
# 👥 Facial Recognition & Verification
|
| 27 |
+
|
| 28 |
+
**Author:** Martin Badrous
|
| 29 |
+
|
| 30 |
+
This repository exposes a practical face‑verification pipeline built on top of
|
| 31 |
+
pretrained face recognition models. Given two photographs, it extracts fixed
|
| 32 |
+
length embeddings and computes their similarity to decide whether they depict
|
| 33 |
+
the same person. The project is designed for demonstration and research
|
| 34 |
+
purposes and is not intended for biometric authentication in critical
|
| 35 |
+
applications.
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
## 🧭 Overview
|
| 40 |
+
|
| 41 |
+
The original [Facial Recognition](https://github.com/martinbadrous/Facial-Recognition)
|
| 42 |
+
repository provides a modern PyTorch training pipeline for facial expression
|
| 43 |
+
or identity classification. It features automatic dataset splitting,
|
| 44 |
+
transfer learning with ResNet18 or EfficientNet‑B0, mixed precision and
|
| 45 |
+
extensive logging【689067851530192†L16-L27】. While powerful, it focuses on
|
| 46 |
+
classification rather than verification. This project refactors that work
|
| 47 |
+
into a face verification system. Instead of predicting a discrete label,
|
| 48 |
+
we map each face into a 512‑dimensional embedding space and measure how
|
| 49 |
+
close two embeddings are.
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
## 🏗️ Model Architecture
|
| 54 |
+
|
| 55 |
+
We use the [FaceNet](https://huggingface.co/py-feat/facenet) architecture,
|
| 56 |
+
an Inception‑ResNet network pretrained on the VGGFace2 dataset. The model
|
| 57 |
+
provides a 512‑dimensional embedding for each detected face【547754386862401†L54-L63】.
|
| 58 |
+
During verification, cosine similarity between two embeddings is computed. A
|
| 59 |
+
similarity close to one indicates matching faces; a low similarity indicates
|
| 60 |
+
different people.
|
| 61 |
+
|
| 62 |
+
---
|
| 63 |
+
|
| 64 |
+
## 📦 Dataset
|
| 65 |
+
|
| 66 |
+
For evaluation, we refer to the **Labeled Faces in the Wild (LFW)** dataset, a
|
| 67 |
+
benchmark of celebrity face pairs widely used to assess verification
|
| 68 |
+
algorithms. Each pair is labelled as **same** or **different**. FaceNet
|
| 69 |
+
achieves approximately 99 % accuracy on LFW when fine‑tuned【547754386862401†L54-L63】.
|
| 70 |
+
Although the dataset is not included here due to licensing, you can evaluate
|
| 71 |
+
your model by downloading LFW from public sources and adapting the code.
|
| 72 |
+
|
| 73 |
+
---
|
| 74 |
+
|
| 75 |
+
## ⚙️ Usage
|
| 76 |
+
|
| 77 |
+
Install dependencies using the provided `requirements.txt`:
|
| 78 |
+
|
| 79 |
+
```bash
|
| 80 |
+
python3 -m venv venv
|
| 81 |
+
source venv/bin/activate
|
| 82 |
+
pip install -r requirements.txt
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
Run the Gradio demo locally:
|
| 86 |
+
|
| 87 |
+
```bash
|
| 88 |
+
python app.py
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
Upload two images. The interface detects faces, extracts embeddings and
|
| 92 |
+
displays whether they belong to the same person along with the cosine
|
| 93 |
+
similarity score. If no face is detected, an appropriate message is
|
| 94 |
+
returned.
|
| 95 |
+
|
| 96 |
+
### Verification API
|
| 97 |
+
|
| 98 |
+
The core logic resides in the `src` package. You can import these utilities
|
| 99 |
+
in your own scripts:
|
| 100 |
+
|
| 101 |
+
```python
|
| 102 |
+
from PIL import Image
|
| 103 |
+
from src.verify_faces import verify_images
|
| 104 |
+
|
| 105 |
+
img1 = Image.open('path/to/photo1.jpg')
|
| 106 |
+
img2 = Image.open('path/to/photo2.jpg')
|
| 107 |
+
similarity, is_same = verify_images(img1, img2, threshold=0.8)
|
| 108 |
+
print(f"Cosine similarity: {similarity:.3f}")
|
| 109 |
+
print("Same person" if is_same else "Different people")
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
---
|
| 113 |
+
|
| 114 |
+
## 📈 Performance
|
| 115 |
+
|
| 116 |
+
Pretrained FaceNet models typically achieve **≈99 % accuracy** on the LFW
|
| 117 |
+
benchmark, with average cosine similarities > 0.8 for matching pairs and
|
| 118 |
+
< 0.5 for non‑matching pairs【547754386862401†L54-L63】. Your mileage may
|
| 119 |
+
vary depending on image quality and lighting conditions. For production
|
| 120 |
+
systems, consider fine‑tuning on domain‑specific data and adjusting the
|
| 121 |
+
threshold.
|
| 122 |
+
|
| 123 |
+
---
|
| 124 |
+
|
| 125 |
+
## ⚠️ Limitations
|
| 126 |
+
|
| 127 |
+
- **Bias and fairness:** Pretrained face recognition models may exhibit
|
| 128 |
+
demographic bias, performing better on some groups than others. Do not
|
| 129 |
+
deploy this system for critical decisions (e.g. law enforcement, hiring,
|
| 130 |
+
access control) without careful evaluation.
|
| 131 |
+
- **Privacy:** Handling biometric data requires compliance with data
|
| 132 |
+
protection laws (e.g. GDPR). Always anonymise and secure sensitive
|
| 133 |
+
images and embeddings.
|
| 134 |
+
- **Security:** This demo does not include anti‑spoofing or liveness
|
| 135 |
+
detection. Simple photographs may fool the system.
|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
+
## 📜 License
|
| 140 |
+
|
| 141 |
+
This project is licensed under the MIT License. See the `LICENSE` file for
|
| 142 |
+
details.
|
| 143 |
+
|
| 144 |
+
---
|
| 145 |
+
|
| 146 |
+
## 📫 Citation & Contact
|
| 147 |
+
|
| 148 |
+
If you use this project in academic work, please cite the original
|
| 149 |
+
FaceNet paper【547754386862401†L54-L63】. For questions or collaborations,
|
| 150 |
+
contact [[email protected]](mailto:[email protected]).
|
app.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Gradio demo for facial verification.
|
| 3 |
+
|
| 4 |
+
This script exposes a web interface where users can upload two images and
|
| 5 |
+
receive immediate feedback about whether the faces match. It utilises
|
| 6 |
+
MTCNN for face detection and InceptionResnetV1 for feature extraction via
|
| 7 |
+
the utilities defined in ``src``.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import gradio as gr
|
| 11 |
+
from PIL import Image
|
| 12 |
+
|
| 13 |
+
from src.verify_faces import verify_images
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def verify_fn(img1: Image.Image, img2: Image.Image) -> str:
|
| 17 |
+
"""Wrap the verification function for Gradio.
|
| 18 |
+
|
| 19 |
+
Parameters
|
| 20 |
+
----------
|
| 21 |
+
img1, img2: PIL.Image.Image
|
| 22 |
+
Input images from the user interface.
|
| 23 |
+
|
| 24 |
+
Returns
|
| 25 |
+
-------
|
| 26 |
+
str
|
| 27 |
+
A human‑readable message indicating whether the faces match and the
|
| 28 |
+
similarity score.
|
| 29 |
+
"""
|
| 30 |
+
# Run verification. We rely on CPU to keep the demo accessible on free tier.
|
| 31 |
+
similarity, is_same, message = verify_images(img1, img2, threshold=0.8, device="cpu")
|
| 32 |
+
if similarity is None:
|
| 33 |
+
return message
|
| 34 |
+
return f"{message}\nCosine similarity: {similarity:.3f}"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
demo = gr.Interface(
|
| 38 |
+
fn=verify_fn,
|
| 39 |
+
inputs=[gr.Image(type="pil", label="Image 1"), gr.Image(type="pil", label="Image 2")],
|
| 40 |
+
outputs=gr.Textbox(label="Result"),
|
| 41 |
+
title="Facial Recognition Verification",
|
| 42 |
+
description=(
|
| 43 |
+
"Upload two face images to verify if they belong to the same person. "
|
| 44 |
+
"We use a pretrained FaceNet model to extract 512‑dimensional embeddings "
|
| 45 |
+
"and compute their cosine similarity. A similarity above 0.8 indicates a match."
|
| 46 |
+
),
|
| 47 |
+
allow_flagging="never",
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
if __name__ == "__main__":
|
| 52 |
+
demo.launch()
|
assets/.gitkeep
ADDED
|
File without changes
|
configs/.gitkeep
ADDED
|
File without changes
|
notebooks/.gitkeep
ADDED
|
File without changes
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch>=2.1.0
|
| 2 |
+
torchvision>=0.16.0
|
| 3 |
+
facenet-pytorch>=2.5.2
|
| 4 |
+
numpy>=1.24
|
| 5 |
+
scikit-learn>=1.3
|
| 6 |
+
pillow>=9.0
|
| 7 |
+
gradio>=4.10.0
|
src/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Facial recognition verification package.
|
| 2 |
+
|
| 3 |
+
This package groups together utilities for face detection, embedding
|
| 4 |
+
extraction and verification used by the Gradio demo and can be reused in
|
| 5 |
+
other projects.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from .detect_faces import detect_faces # noqa: F401
|
| 9 |
+
from .extract_embeddings import extract_embedding # noqa: F401
|
| 10 |
+
from .verify_faces import verify_images, cosine_similarity # noqa: F401
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
"detect_faces",
|
| 14 |
+
"extract_embedding",
|
| 15 |
+
"verify_images",
|
| 16 |
+
"cosine_similarity",
|
| 17 |
+
]
|
src/detect_faces.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Face detection utility using MTCNN from facenet_pytorch.
|
| 3 |
+
|
| 4 |
+
This module exposes a simple function to detect faces in a PIL Image. It
|
| 5 |
+
returns bounding boxes for all detected faces. The detection model is
|
| 6 |
+
constructed lazily on the first call to avoid unnecessary GPU/CPU
|
| 7 |
+
initialisation when the module is imported.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from typing import List, Tuple, Optional
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
from PIL import Image
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
from facenet_pytorch import MTCNN
|
| 17 |
+
except ImportError as exc:
|
| 18 |
+
raise ImportError(
|
| 19 |
+
"facenet_pytorch is required for face detection. Install it with `pip install facenet-pytorch`."
|
| 20 |
+
) from exc
|
| 21 |
+
|
| 22 |
+
_mtcnn: Optional[MTCNN] = None
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _get_mtcnn(device: str = "cpu") -> MTCNN:
|
| 26 |
+
"""Return a singleton MTCNN detector instance.
|
| 27 |
+
|
| 28 |
+
Parameters
|
| 29 |
+
----------
|
| 30 |
+
device: str, optional
|
| 31 |
+
PyTorch device on which to run the detector. Defaults to ``"cpu"``.
|
| 32 |
+
|
| 33 |
+
Returns
|
| 34 |
+
-------
|
| 35 |
+
MTCNN
|
| 36 |
+
The configured multi-task cascaded CNN detector.
|
| 37 |
+
"""
|
| 38 |
+
global _mtcnn
|
| 39 |
+
if _mtcnn is None:
|
| 40 |
+
_mtcnn = MTCNN(image_size=160, margin=0, keep_all=True, device=device)
|
| 41 |
+
return _mtcnn
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def detect_faces(image: Image.Image, device: str = "cpu") -> List[Tuple[float, float, float, float]]:
|
| 45 |
+
"""Detect faces in a PIL image.
|
| 46 |
+
|
| 47 |
+
Parameters
|
| 48 |
+
----------
|
| 49 |
+
image: PIL.Image.Image
|
| 50 |
+
The input image in which to detect faces.
|
| 51 |
+
device: str, optional
|
| 52 |
+
Device on which to run the detector (``"cpu"`` or ``"cuda"``). Defaults to ``"cpu"``.
|
| 53 |
+
|
| 54 |
+
Returns
|
| 55 |
+
-------
|
| 56 |
+
List[Tuple[float, float, float, float]]
|
| 57 |
+
A list of bounding boxes (x1, y1, x2, y2) for each detected face. If
|
| 58 |
+
no faces are found, returns an empty list.
|
| 59 |
+
"""
|
| 60 |
+
mtcnn = _get_mtcnn(device)
|
| 61 |
+
# MTCNN returns (boxes, probs). We only need boxes.
|
| 62 |
+
boxes, _ = mtcnn.detect(image)
|
| 63 |
+
if boxes is None:
|
| 64 |
+
return []
|
| 65 |
+
# Convert numpy array of shape (n, 4) into list of tuples.
|
| 66 |
+
return [tuple(map(float, box)) for box in np.array(boxes)]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
__all__ = ["detect_faces"]
|
src/extract_embeddings.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Facial embedding extraction using FaceNet (InceptionResnetV1).
|
| 3 |
+
|
| 4 |
+
This module wraps the FaceNet model from facenet_pytorch to produce
|
| 5 |
+
512‑dimensional embeddings for detected faces. It relies on MTCNN
|
| 6 |
+
for cropping the largest face in the image. If no face is detected, it
|
| 7 |
+
returns ``None``.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from typing import Optional
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
from PIL import Image
|
| 14 |
+
import torch
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
from facenet_pytorch import MTCNN, InceptionResnetV1
|
| 18 |
+
except ImportError as exc:
|
| 19 |
+
raise ImportError(
|
| 20 |
+
"facenet_pytorch is required for embedding extraction. Install it with `pip install facenet-pytorch`."
|
| 21 |
+
) from exc
|
| 22 |
+
|
| 23 |
+
_mtcnn: Optional[MTCNN] = None
|
| 24 |
+
_resnet: Optional[InceptionResnetV1] = None
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _get_models(device: str = "cpu") -> tuple[MTCNN, InceptionResnetV1]:
|
| 28 |
+
"""Initialise and cache MTCNN and InceptionResnet models.
|
| 29 |
+
|
| 30 |
+
Parameters
|
| 31 |
+
----------
|
| 32 |
+
device: str, optional
|
| 33 |
+
Device on which to run the models. Defaults to ``"cpu"``.
|
| 34 |
+
|
| 35 |
+
Returns
|
| 36 |
+
-------
|
| 37 |
+
tuple[MTCNN, InceptionResnetV1]
|
| 38 |
+
The face detector and feature extractor.
|
| 39 |
+
"""
|
| 40 |
+
global _mtcnn, _resnet
|
| 41 |
+
if _mtcnn is None:
|
| 42 |
+
_mtcnn = MTCNN(image_size=160, margin=0, select_largest=True, device=device)
|
| 43 |
+
if _resnet is None:
|
| 44 |
+
_resnet = InceptionResnetV1(pretrained="vggface2").eval().to(device)
|
| 45 |
+
return _mtcnn, _resnet
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def extract_embedding(image: Image.Image, device: str = "cpu") -> Optional[np.ndarray]:
|
| 49 |
+
"""Extract a 512‑dimensional face embedding from an image.
|
| 50 |
+
|
| 51 |
+
Parameters
|
| 52 |
+
----------
|
| 53 |
+
image: PIL.Image.Image
|
| 54 |
+
The input image containing a face.
|
| 55 |
+
device: str, optional
|
| 56 |
+
Device on which to run the models. Defaults to ``"cpu"``.
|
| 57 |
+
|
| 58 |
+
Returns
|
| 59 |
+
-------
|
| 60 |
+
np.ndarray or None
|
| 61 |
+
A numpy array of shape (512,) containing the embedding. If no face
|
| 62 |
+
is detected, returns ``None``.
|
| 63 |
+
"""
|
| 64 |
+
mtcnn, resnet = _get_models(device)
|
| 65 |
+
# Detect face and crop to 160x160. MTCNN returns a tensor of shape (3, 160, 160).
|
| 66 |
+
face, prob = mtcnn(image, return_prob=True)
|
| 67 |
+
if face is None:
|
| 68 |
+
return None
|
| 69 |
+
# Add batch dimension and send to device.
|
| 70 |
+
face = face.to(device).unsqueeze(0)
|
| 71 |
+
# Extract embedding.
|
| 72 |
+
with torch.no_grad():
|
| 73 |
+
emb = resnet(face)
|
| 74 |
+
# Return as 1D numpy array on CPU.
|
| 75 |
+
return emb.squeeze(0).cpu().numpy()
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
__all__ = ["extract_embedding"]
|
src/verify_faces.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Face verification utilities.
|
| 3 |
+
|
| 4 |
+
This module provides functions to compare face embeddings and decide
|
| 5 |
+
whether two faces belong to the same person. It relies on
|
| 6 |
+
``extract_embeddings.extract_embedding`` to obtain the embeddings.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from typing import Tuple, Optional
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
from PIL import Image
|
| 13 |
+
|
| 14 |
+
from .extract_embeddings import extract_embedding
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
|
| 18 |
+
"""Compute the cosine similarity between two vectors.
|
| 19 |
+
|
| 20 |
+
Parameters
|
| 21 |
+
----------
|
| 22 |
+
a, b: np.ndarray
|
| 23 |
+
1D vectors of the same length.
|
| 24 |
+
|
| 25 |
+
Returns
|
| 26 |
+
-------
|
| 27 |
+
float
|
| 28 |
+
The cosine similarity ranging from -1 (opposite) to 1 (identical).
|
| 29 |
+
"""
|
| 30 |
+
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def verify_images(img1: Image.Image, img2: Image.Image, threshold: float = 0.8, device: str = "cpu") -> Tuple[Optional[float], bool, str]:
|
| 34 |
+
"""Verify whether two images depict the same person.
|
| 35 |
+
|
| 36 |
+
This function detects faces in each image, extracts embeddings and
|
| 37 |
+
computes the cosine similarity. A threshold decides whether the
|
| 38 |
+
similarity represents the same identity.
|
| 39 |
+
|
| 40 |
+
Parameters
|
| 41 |
+
----------
|
| 42 |
+
img1, img2: PIL.Image.Image
|
| 43 |
+
The two images to compare.
|
| 44 |
+
threshold: float, optional
|
| 45 |
+
Similarity threshold above which the faces are considered the
|
| 46 |
+
same person. Defaults to 0.8.
|
| 47 |
+
device: str, optional
|
| 48 |
+
Device to run the embedding extraction on. Defaults to ``"cpu"``.
|
| 49 |
+
|
| 50 |
+
Returns
|
| 51 |
+
-------
|
| 52 |
+
Tuple[Optional[float], bool, str]
|
| 53 |
+
A tuple of (similarity score, decision, message). If no face is
|
| 54 |
+
detected in either image, the similarity is ``None`` and the
|
| 55 |
+
decision is ``False``.
|
| 56 |
+
"""
|
| 57 |
+
emb1 = extract_embedding(img1, device=device)
|
| 58 |
+
emb2 = extract_embedding(img2, device=device)
|
| 59 |
+
if emb1 is None or emb2 is None:
|
| 60 |
+
return None, False, "Face not detected in one or both images."
|
| 61 |
+
sim = cosine_similarity(emb1, emb2)
|
| 62 |
+
is_same = sim >= threshold
|
| 63 |
+
return sim, is_same, "Same person" if is_same else "Different people"
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
__all__ = ["verify_images", "cosine_similarity"]
|