File size: 14,734 Bytes
9b5157d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
---
title: FaceCheck
emoji: 🀳
colorFrom: blue
colorTo: purple
sdk: docker
pinned: false
---

# Photo Verification API

A production-grade **selfie verification API** that mimics the pose-challenge system used by apps like Bumble and Tinder. It detects whether you're a real person (liveness check) and whether you correctly completed a pose challenge (look left, look right, smile, etc.).

Ships with a **browser UI** for testing and a **Docker setup** for one-command local runs and free cloud deployment.

Built with FastAPI, MediaPipe, and DeepFace. No custom model training required β€” everything uses pretrained weights.

---

## What It Does

The API runs a 3-stage pipeline on every submitted photo:

1. **Liveness Detection** β€” Uses DeepFace (MiniFASNet / Silent-Face-Anti-Spoofing) to detect whether the image is a real live person or a printed photo / screen replay.
2. **Face Landmark Extraction** β€” Uses MediaPipe FaceLandmarker to extract 478 3D facial landmarks.
3. **Head Pose Estimation + Challenge Matching** β€” Solves a PnP (Perspective-n-Point) problem using OpenCV to compute yaw, pitch, and roll in degrees, then checks if the pose matches the requested challenge.

---

## Project Structure

```
photo-app/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ main.py                      # FastAPI app factory, lifespan, middleware wiring
β”‚   β”œβ”€β”€ core/
β”‚   β”‚   β”œβ”€β”€ config.py                # All settings (env-var backed via pydantic-settings)
β”‚   β”‚   β”œβ”€β”€ dependencies.py          # CV thread pool, validated image upload dependency
β”‚   β”‚   β”œβ”€β”€ exceptions.py            # Domain exception hierarchy (ImageDecodeError, etc.)
β”‚   β”‚   └── logging_config.py        # JSON / text structured logging setup
β”‚   β”œβ”€β”€ middleware/
β”‚   β”‚   β”œβ”€β”€ correlation_id.py        # X-Request-ID propagation via ContextVar
β”‚   β”‚   β”œβ”€β”€ timing.py                # Per-request structured access log
β”‚   β”‚   └── rate_limit.py            # slowapi limiter singleton
β”‚   β”œβ”€β”€ metrics/
β”‚   β”‚   └── prometheus.py            # Prometheus instrumentation + domain metrics
β”‚   β”œβ”€β”€ routers/
β”‚   β”‚   β”œβ”€β”€ health.py                # GET /api/v1/health
β”‚   β”‚   └── verification.py          # POST /api/v1/verify/challenge & /submit/{type}
β”‚   β”œβ”€β”€ schemas/
β”‚   β”‚   β”œβ”€β”€ verification.py          # Pydantic request/response models
β”‚   β”‚   └── errors.py                # Typed error response schema
β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”œβ”€β”€ face_analysis.py         # MediaPipe FaceLandmarker + PnP head pose
β”‚   β”‚   β”œβ”€β”€ liveness.py              # DeepFace anti-spoofing
β”‚   β”‚   └── challenge_matcher.py     # Pose threshold logic per challenge type
β”‚   └── static/
β”‚       β”œβ”€β”€ index.html               # Browser UI
β”‚       β”œβ”€β”€ style.css                # Dark card-based styling
β”‚       └── app.js                   # Camera capture + API calls + result display
β”œβ”€β”€ models/
β”‚   └── face_landmarker.task         # MediaPipe pretrained model (~3.7MB)
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ conftest.py                  # Pytest fixtures with mock services
β”‚   └── test_pipeline.py             # Integration tests
β”œβ”€β”€ Dockerfile                       # Multi-stage build for containerisation
β”œβ”€β”€ docker-compose.yml               # Local dev stack with health checks + volumes
β”œβ”€β”€ .dockerignore                    # Excludes venv/git/tests from build context
β”œβ”€β”€ .env.example                     # Template for environment variable overrides
β”œβ”€β”€ requirements.txt
└── README.md
```

---

## API Endpoints

All endpoints are versioned under `/api/v1`.

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/v1/health` | Service health, model readiness, uptime |
| POST | `/api/v1/verify/challenge` | Get a random (or specific) pose challenge |
| POST | `/api/v1/verify/submit/{challenge_type}` | Submit a selfie for verification |
| GET | `/metrics` | Prometheus metrics (HTTP + domain counters) |
| GET | `/docs` | Swagger UI β€” interactive API explorer |

### Challenge Types

| Challenge | What To Do |
|-----------|-----------|
| `look_left` | Turn head to the left (yaw < -15Β°) |
| `look_right` | Turn head to the right (yaw > +15Β°) |
| `look_up` | Tilt head up (pitch < -12Β°) |
| `look_down` | Tilt head down (pitch > +12Β°) |
| `smile` | Show a big smile (smile score > 0.35) |

---

## Running with Docker (recommended)

Docker is the easiest way to run the app β€” no Python install, no dependency conflicts.

### Requirements
- [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed and running

### Option A β€” Docker Compose (recommended)

```bash
# Clone the repo
git clone https://github.com/Uzbekswe/verifier.git
cd verifier

# Build and start (first build takes ~5–10 min to install TensorFlow)
docker compose up
```

The app will be available at **http://localhost:8000**

- The UI opens automatically at the root URL
- API docs at **http://localhost:8000/docs**
- Health check at **http://localhost:8000/api/v1/health**

Check container health:
```bash
docker compose ps      # shows "healthy" after ~60s (model load time)
docker compose logs    # stream structured logs
docker compose down    # stop everything
```

### Option B β€” Plain Docker

```bash
# Build the image
docker build -t photo-verifier .

# Run the container
docker run -p 8000:7860 photo-verifier
```

With environment variable overrides:
```bash
docker run \
  -p 8000:7860 \
  -e LOG_FORMAT=text \
  -e LIVENESS_THRESHOLD=0.7 \
  -e CV_THREAD_POOL_SIZE=4 \
  photo-verifier
```

With a volume to persist DeepFace model weights across runs:
```bash
docker run \
  -p 8000:7860 \
  -v deepface-cache:/root/.deepface \
  photo-verifier
```

### How the Dockerfile works

The image uses a **multi-stage build**:

```
Stage 1 (builder)   β†’ installs all Python deps into /install/deps
Stage 2 (final)     β†’ copies only the installed packages + app code
                       into a clean python:3.12-slim image
```

`requirements.txt` is copied before the application code so the expensive `pip install` layer is **cached** β€” rebuilds after code-only changes take seconds, not minutes.

The `PORT` environment variable controls which port uvicorn listens on (default `7860`). Docker Compose maps host port `8000` β†’ container port `7860`.

---

## Running Locally (without Docker)

### Requirements
- Python 3.12 (TensorFlow 2.x does not support Python 3.13+ yet)

### Install Python 3.12
```bash
brew install python@3.12
```

### Create virtual environment and install dependencies
```bash
cd photo-app
/opt/homebrew/bin/python3.12 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```

> Note: `pip install` pulls in `tensorflow`, `keras`, and other heavy ML dependencies through `deepface`. Total install is ~1.5GB. This is normal.

### Download the face landmark model
The MediaPipe model is already included at `models/face_landmarker.task`. If you ever need to re-download it:
```bash
curl -L -o models/face_landmarker.task \
  "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task"
```

### Start the server
```bash
source venv/bin/activate
uvicorn app.main:app --reload --port 8000
```

The server starts on **http://localhost:8000**

---

## Browser UI

The app ships with a browser UI served at the root URL (`/`).

**Flow:**
1. **Get Challenge** β€” click the button, receive a random pose instruction
2. **Selfie** β€” the webcam opens; position your face and follow the instruction, then capture
3. **Result** β€” the image is submitted to the API and the full result is displayed: verified βœ…/❌, liveness score, pose angles, confidence, and per-stage details

The UI works on desktop and mobile (front-facing camera). No frameworks β€” plain HTML/CSS/JS, served directly by FastAPI's `StaticFiles`.

---

## Testing

### Swagger UI (recommended)
Open **http://localhost:8000/docs** β€” interactive endpoint explorer with file upload support.

### Health check
```bash
curl http://localhost:8000/api/v1/health
```

### Full verification flow (terminal)

**Step 1 β€” Get a challenge:**
```bash
curl -s -X POST http://localhost:8000/api/v1/verify/challenge | python3 -m json.tool
```

Example response:
```json
{
  "challenge_id": "a3f2...",
  "challenge_type": "look_left",
  "instruction": "πŸ‘ˆ Turn your head to the LEFT",
  "expires_in_seconds": 120
}
```

**Step 2 β€” Submit a selfie:**
```bash
curl -s -X POST \
  "http://localhost:8000/api/v1/verify/submit/look_left" \
  -F "file=@/path/to/your/photo.jpg" | python3 -m json.tool
```

Example response:
```json
{
  "verified": true,
  "challenge_type": "look_left",
  "liveness_score": 0.91,
  "liveness_passed": true,
  "pose_matched": true,
  "pose_angles": { "yaw": -22.4, "pitch": 1.2, "roll": 0.8 },
  "face_detected": true,
  "confidence": 0.87,
  "message": "βœ… Verified! Challenge 'look_left' passed.",
  "details": {
    "smile_score": 0.12,
    "challenge_measured": -22.4,
    "challenge_required_range": [-90.0, -15.0],
    "liveness_real_prob": 0.91,
    "liveness_spoof_prob": 0.09,
    "match_reason": "βœ… Challenge passed: yaw=-22.4Β° (required < -15)"
  }
}
```

### Run pipeline tests
```bash
source venv/bin/activate
python3 tests/test_pipeline.py
```

---

## Free Cloud Deployment

The Dockerfile is pre-configured for **Hugging Face Spaces** β€” the best free option for ML apps (no RAM limits, no credit card required).

1. Create an account at [huggingface.co](https://huggingface.co)
2. New Space β†’ SDK: **Docker** β†’ Hardware: **CPU Basic (free)**
3. Connect the `Uzbekswe/verifier` GitHub repo β€” auto-deploys on every push
4. Hugging Face sets `PORT=7860` automatically β€” the Dockerfile already reads it

Your public URL: `https://huggingface.co/spaces/Uzbekswe/verifier`

**Alternative:** [Railway.app](https://railway.app) β€” connect GitHub repo, free $5/month credit, auto-detects the Dockerfile.

---

## How It Was Built

### Problem
Build a backend API that verifies a user is a real, live human and can perform an on-screen gesture β€” the same kind of challenge used in dating apps to prevent fake profile photos.

### Stack choices

**FastAPI** was chosen for its async support, automatic OpenAPI docs, Pydantic validation, and built-in `StaticFiles` support β€” ideal for a photo-processing API that also serves a browser UI.

**MediaPipe FaceLandmarker** (Tasks API, v0.10.33) extracts 478 3D facial landmarks per frame. The newer Tasks API was used instead of the legacy `mp.solutions` because `mp.solutions` was removed in mediapipe >= 0.10.14. The pretrained model runs entirely on-device.

**Head Pose via PnP Solver** β€” Maps 6 stable MediaPipe landmarks (nose tip, chin, eye corners, mouth corners) to a known 3D canonical face model, then calls OpenCV's `solvePnP` to solve for the rotation vector. Decomposed into yaw, pitch, and roll using Rodrigues + `decomposeProjectionMatrix`. No training needed.

**DeepFace anti-spoofing** wraps MinivisionAI's Silent-Face-Anti-Spoofing (MiniFASNet). It classifies each face as real or spoof with a confidence score. Weights download automatically from GitHub on first use (~4MB). A texture-based Laplacian variance fallback is included in case DeepFace fails.

**Python 3.12** is required because TensorFlow 2.x does not yet have wheels for Python 3.13+.

### Key engineering decisions
- Models load once at startup via FastAPI's `lifespan` context β€” not on the first request β€” so the first API call is fast.
- MediaPipe, OpenCV, and DeepFace run in a dedicated `ThreadPoolExecutor` via `run_in_executor` β€” the asyncio event loop is never blocked, enabling real concurrency under load.
- Liveness crops the face region with 15% padding before running anti-spoof, reducing background noise.
- The multi-stage Dockerfile copies `requirements.txt` before application code so the 1.5GB dependency layer is cached and code-only rebuilds take seconds.
- Challenge thresholds, rate limits, thread pool size, and all other tunables are configurable via environment variables without touching source code.

---

## Configuration

All settings are backed by environment variables and can be overridden via a `.env` file (copy `.env.example` to `.env`):

| Setting | Default | Meaning |
|---------|---------|---------|
| `YAW_THRESHOLD` | 15.0Β° | Degrees of head turn needed for look_left / look_right |
| `PITCH_THRESHOLD` | 12.0Β° | Degrees of tilt needed for look_up / look_down |
| `LIVENESS_THRESHOLD` | 0.6 | Minimum anti-spoof score to pass liveness |
| `MAX_IMAGE_SIZE_MB` | 10 | Max upload size |
| `MAX_IMAGE_DIMENSION` | 4096 | Max image width or height in pixels |
| `CV_THREAD_POOL_SIZE` | 4 | Workers for blocking CV operations |
| `RATE_LIMIT_GLOBAL` | 200/minute | Global rate limit per IP |
| `RATE_LIMIT_SUBMIT` | 10/minute | Rate limit on `/verify/submit` |
| `RATE_LIMIT_CHALLENGE` | 30/minute | Rate limit on `/verify/challenge` |
| `LOG_LEVEL` | INFO | Python logging level |
| `LOG_FORMAT` | json | `json` for production, `text` for local dev |
| `CORS_ORIGINS` | ["*"] | Allowed CORS origins (restrict in production) |
| `PORT` | 7860 | Port uvicorn listens on inside the container |

---

## Production Features

- **Async CV execution** β€” MediaPipe, OpenCV, and DeepFace run in a dedicated `ThreadPoolExecutor`, keeping the event loop unblocked under concurrent load.
- **Structured JSON logging** β€” Every log line is JSON with `request_id`, `logger`, `level`, and timing fields. Correlation IDs propagate from `X-Request-ID` headers through all logs for a request.
- **Rate limiting** β€” `slowapi` enforces configurable per-IP limits globally and per-endpoint. Swap to Redis (`RATE_LIMIT_STORAGE_URI=redis://...`) for multi-instance deployments.
- **Prometheus metrics** at `/metrics` β€” auto-instrumented HTTP metrics plus domain counters: `verification_attempts_total`, `cv_processing_seconds`, `liveness_score_distribution`.
- **Typed error responses** β€” All errors return `{ "error": { "error_code": "...", "message": "...", "request_id": "...", "context": {} } }` with machine-readable codes (`IMAGE_TOO_LARGE`, `INVALID_MIME_TYPE`, `RATE_LIMIT_EXCEEDED`, etc.).
- **Input validation** β€” MIME type (JPEG/PNG/WebP only), file size, and pixel dimension checks run before any CV processing.
- **API versioning** β€” All endpoints live under `/api/v1`.
- **Docker** β€” Multi-stage Dockerfile with layer caching, named volumes for model persistence, and Compose health checks.