martinbadrous commited on
Commit
8a4d3a7
·
verified ·
1 Parent(s): 2da0f3e

Upload 11 files

Browse files
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"]