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.
|