File size: 11,852 Bytes
d78d209
d83ea8a
 
d78d209
 
e3dac32
 
d83ea8a
3bc2e22
d78d209
 
d83ea8a
3bc2e22
5d55d9f
3bc2e22
a78e6a9
d83ea8a
 
 
 
 
3bc2e22
 
d83ea8a
 
a5c72cb
 
 
 
 
 
 
 
 
 
 
 
d83ea8a
 
 
 
 
 
e3dac32
 
d83ea8a
a5c72cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d83ea8a
 
 
 
 
 
e3dac32
d83ea8a
e3dac32
 
d83ea8a
 
 
 
 
 
 
475623e
d83ea8a
 
 
 
 
475623e
 
d83ea8a
 
bf2c053
 
e3dac32
bf2c053
 
 
475623e
bf2c053
 
 
 
 
 
 
 
 
 
 
88cf4ae
 
 
 
 
 
 
 
 
475623e
 
88cf4ae
bf2c053
475623e
 
 
 
 
bf2c053
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88cf4ae
 
 
bf2c053
 
e3dac32
 
bf2c053
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65fbd80
bf2c053
 
65fbd80
 
 
 
 
 
 
 
bf2c053
 
 
 
 
 
 
 
 
 
65fbd80
bf2c053
 
 
 
bc88e75
 
 
e3dac32
 
 
 
 
 
 
 
 
 
 
 
 
 
bf2c053
 
 
 
 
 
 
65fbd80
bf2c053
 
 
 
 
 
 
 
 
 
 
 
 
475623e
 
 
 
 
bf2c053
 
 
 
 
 
 
 
 
 
 
 
 
6592f0b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bf2c053
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d83ea8a
 
 
 
 
 
 
 
e3dac32
 
 
d83ea8a
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
---
title: Face Verification
emoji: 🧠
colorFrom: blue
colorTo: green
sdk: docker
app_port: 7860
short_description: "Face verification demo with FaceNet and VectorDB"
tags: ["face-verification", "computer-vision", "facenet", "vector-database"]
pinned: false
license: mit
---
![CI](https://github.com/leandrodevai/faceverification/actions/workflows/ci.yml/badge.svg)
![Container Security](https://github.com/leandrodevai/faceverification/actions/workflows/container-build-scan-publish.yml/badge.svg)
![Deploy](https://github.com/leandrodevai/faceverification/actions/workflows/deploy-to-HSspaces.yml/badge.svg)
![Azure Deploy](https://github.com/leandrodevai/faceverification/actions/workflows/deploy-azure-container-app.yml/badge.svg)

# Face Verification

AI Engineering Demo project demonstrating an end-to-end face verification workflow with FaceNet embeddings, ChromaDB vector search, and an interactive Gradio interface.

Try it on Hugging Face Spaces: https://huggingface.co/spaces/leandrodevai/faceverification

The app lets users add known people to a local embeddings database and verify whether a new face image matches one of the stored identities.

![Face verification demo](images/faceverification_demo.gif)

This demo was built to show that FaceNet, despite being an older architecture,
is still a strong and practical baseline for face embedding workflows. Its
moderate runtime footprint also makes it an interesting candidate for constrained
deployments, including edge-style scenarios when hardware, latency, and accuracy
requirements are compatible.

The project is also intended as a reusable AI engineering template: the same
structure can be adapted to build CI/CD pipelines for other models, or extended
from image uploads into a video pipeline for near real-time face recognition.

## Features

- Face detection and preprocessing from uploaded images
- FaceNet embedding extraction with PyTorch
- Similarity search with ChromaDB
- Interactive Gradio UI with add-person and verify-identity flows
- FastAPI interface for containerized API deployments
- Docker-based Hugging Face Space deployment

## Architecture

```mermaid
flowchart LR
    A[Upload] --> B[MTCNN]
    B --> C[Crop]
    C --> D[FaceNet]
    D --> E[Normalize]
    E --> F[ChromaDB]
    F --> G[Threshold]
    G --> H[Match]
```

Images are first passed through MTCNN for face detection and cropping. FaceNet
then produces an embedding, which is L2-normalized before being stored or
queried in ChromaDB. Verification compares the nearest stored embedding against
the calibrated distance threshold.

## Model Evaluation

The verification threshold was calibrated with a small study on the
`bitmind/lfw` dataset, a Hugging Face version of Labeled Faces in the Wild
(LFW). The study builds same-person and different-person image pairs, splits
them into calibration and held-out evaluation sets, and evaluates L2 distance
between L2-normalized FaceNet embeddings.

For this demo, the selected threshold is:

```text
same person if L2 distance <= 1.0764
different person if L2 distance > 1.0764
```

This value was chosen on the calibration split because it maximized balanced
accuracy while staying below the distance region where different-person pairs
start to dominate. The held-out evaluation split contains 720 same-person and
720 different-person pairs.

![Evaluation distance distributions](images/Densities.png)

Held-out evaluation results:

| Metric | Value |
| --- | ---: |
| AUC-ROC | 0.9838 |
| Accuracy | 0.9653 |
| FAR | 0.0097 |
| FRR | 0.0597 |
| TPR | 0.9403 |
| Calibrated threshold | 1.0764 |
| Calibration EER threshold | 1.1857 |

![Held-out evaluation ROC curve](images/ROC.png)

![Held-out evaluation confusion matrix](images/confusion_matrix.png)

These metrics are intended to justify the threshold for this demo dataset, not
as a production biometric benchmark.

## Tech Stack

- Python
- PyTorch
- FaceNet / facenet-pytorch
- ChromaDB
- FastAPI
- Gradio
- Docker / GHCR
- Hugging Face Spaces Docker SDK

## Run Locally

Using uv:

```bash
uv sync
uv run faceverification
```

Using pip:

```bash
pip install -r requirements.txt -e .
python -m faceverification.interfaces.gradio_app
```

## FastAPI Interface

The project includes an HTTP API for the same enroll-and-verify workflow.
Run it locally with:

```bash
uv run uvicorn faceverification.interfaces.fastapi_app:app --port 8000
```

Interactive API documentation is available at:

- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc

### Authentication

Protected endpoints require a bearer token. The default demo credentials are
`demo` / `demo123`; override them with `FACEVERIFICATION_DEMO_USERNAME` and
`FACEVERIFICATION_DEMO_PASSWORD` in `.env`. The JWT secret is also configurable
and should be changed outside local demos.

```env
FACEVERIFICATION_DEMO_USERNAME=demo
FACEVERIFICATION_DEMO_PASSWORD=demo123
FACEVERIFICATION_JWT_SECRET_KEY=replace-this-with-a-long-random-secret
FACEVERIFICATION_JWT_ACCESS_TOKEN_EXPIRE_MINUTES=60
FACEVERIFICATION_MAX_UPLOAD_BYTES=5242880
FACEVERIFICATION_DEBUG=false
FACEVERIFICATION_LOG_FORMAT=json
```

`FACEVERIFICATION_DEBUG=true` raises application logging to debug level and uses
a readable text formatter by default, which is useful for local troubleshooting.
Keep it `false` in normal deployments; Container Apps sends stdout logs to Log
Analytics, where the default JSON format is easier to query.

```bash
curl -X POST http://localhost:8000/auth/login \
  -F "username=demo" \
  -F "password=demo123"
```

Use the returned token in the `Authorization` header:

```bash
Authorization: Bearer <access_token>
```

### Endpoints

- `GET /health`: returns API status.
- `POST /auth/login`: returns a JWT access token for the demo user.
- `POST /persons`: enrolls a known person from an uploaded image and form `name`.
- `POST /verify`: verifies whether an uploaded face matches a known person.

Both face endpoints return an annotated image by default. Add
`?include_image=false` when the client only needs the JSON result.

## Deployment Notes

For API deployments, the recommended baseline is the FastAPI container running
Uvicorn:

```bash
uvicorn faceverification.interfaces.fastapi_app:app --host 0.0.0.0 --port 8000
```

This keeps the demo lightweight and avoids loading the FaceNet/MTCNN models in
multiple worker processes unnecessarily. Because each worker can hold its own
model instance in memory, increasing worker count should be done only after
checking available RAM and expected traffic.

Gunicorn with Uvicorn workers and an Nginx reverse proxy are valid production
options, but they are intentionally not required for the baseline deployment:

- Use Gunicorn/Uvicorn workers when the service needs a traditional process
  manager or multiple worker processes.
- Use Nginx when deploying on a self-managed VM that needs TLS termination,
  upload-size limits, reverse proxy routing, compression, or centralized access
  logs.
- On managed platforms such as Render, Railway, Fly.io, Cloud Run, or similar,
  the platform usually provides the external reverse proxy and TLS layer, so
  running Uvicorn directly inside the application container is sufficient.

### Docker

Build the service images when the `Dockerfile` or dependencies change:

```bash
docker compose build api
docker compose build gradio
```

Run the FastAPI service:

```bash
docker compose --profile api up api
```

The API will be available at:

- http://localhost:8000/health
- http://localhost:8000/docs

Run the optional Gradio interface:

```bash
docker compose --profile gradio up gradio
```

The Gradio UI will be available at http://localhost:7860.

Both services are built from the same `Dockerfile`. The heavy application
layers live in the shared `app` stage; the `fastapi` and `gradio` stages only
set different default commands:

- `fastapi`: published to GHCR by the container workflow.
- `gradio`: used by the Hugging Face Docker Space.

The published FastAPI image is tagged as:

```text
ghcr.io/leandrodevai/faceverification:fastapi
```

Each successful container workflow also publishes an immutable SHA tag with the
`fastapi-sha-*` prefix.

By default, ChromaDB runs in memory, so
enrolled faces are ephemeral and disappear when the container restarts. This is
intentional for the demo baseline.

To persist embeddings to disk, provide a database name:

```bash
FACEVERIFICATION_VECTOR_DB_NAME=local-demo \
  docker compose -f docker-compose.yml -f docker-compose.persist.yml --profile api up api
```

The base `docker-compose.yml` does not mount any volume, so the default
deployment stays ephemeral. The optional `docker-compose.persist.yml` override
mounts the `faceverification-data` volume at `/data` and translates
`FACEVERIFICATION_VECTOR_DB_NAME` into
`FACEVERIFICATION_VECTOR_DB_PERSIST_DIRECTORY=/data/chroma/<name>` before the
application starts. The application itself still defaults to in-memory ChromaDB
unless `FACEVERIFICATION_VECTOR_DB_PERSIST_DIRECTORY` is explicitly provided.

The default container configuration sets `FACEVERIFICATION_DEVICE=cpu` to keep
deployment portable.

Local Docker Compose defaults `FACEVERIFICATION_DEBUG=true` and
`FACEVERIFICATION_LOG_FORMAT=text` for developer ergonomics. Azure Container
Apps sets `FACEVERIFICATION_DEBUG=false` and `FACEVERIFICATION_LOG_FORMAT=json`
for lower-volume structured logs in Log Analytics.

The shared local ChromaDB volume is intended for a small demo deployment when a
persist name is enabled. For a multi-container production setup with concurrent
writers or multiple replicas, use an external database/vector-store service or
make one service the clear owner of writes.

For production deployments, override at least:

```bash
FACEVERIFICATION_DEMO_USERNAME
FACEVERIFICATION_DEMO_PASSWORD
FACEVERIFICATION_JWT_SECRET_KEY
```

### Azure Container Apps

The FastAPI image can also be deployed as an ephemeral Azure Container App from
the GHCR image published by the container workflow.

The deployment is defined in `infra/bicep/main.bicep` and is wired to
`.github/workflows/deploy-azure-container-app.yml`. The workflow:

- creates or reuses the Azure resource group configured in GitHub repository
  variables;
- deploys a Log Analytics workspace, Container Apps environment, and FastAPI
  Container App;
- runs the published `ghcr.io/leandrodevai/faceverification:fastapi` image;
- keeps the app ephemeral with no mounted volume or external vector database;
- configures `minReplicas: 0` and `maxReplicas: 1`;
- smoke tests `GET /health` after deployment.

Required GitHub repository variables:

```text
AZURE_CLIENT_ID
AZURE_LOCATION
AZURE_RESOURCE_GROUP
AZURE_TENANT_ID
AZURE_SUBSCRIPTION_ID
```

Required GitHub repository secrets:

```text
FACEVERIFICATION_DEMO_USERNAME
FACEVERIFICATION_DEMO_PASSWORD
FACEVERIFICATION_JWT_SECRET_KEY
```

To avoid ongoing cost while keeping the OIDC and RBAC setup intact, remove the
demo resources inside the configured resource group from Azure when they are no
longer needed.

Example enrollment request:

```bash
curl -X POST http://localhost:8000/persons \
  -H "Authorization: Bearer <access_token>" \
  -F "name=Ada Lovelace" \
  -F "image=@test/images/person_anchor.jpg"
```

Example verification request:

```bash
curl -X POST http://localhost:8000/verify \
  -H "Authorization: Bearer <access_token>" \
  -F "image=@test/images/person_positive.jpg"
```

## Project Structure

```text
src/faceverification/
  config.py
  core/
    image_processor.py
    vectordb.py
  interfaces/
    fastapi_app.py
    gradio_app.py
  services/
    face_verification.py
```

## Demo Focus

This project highlights practical AI engineering skills: model-based feature extraction, vector database integration, application packaging, automated deployment, and a simple user-facing ML interface.