FaceCheck / README.md
Mukhammadali Bakhodirov
Deploy detection fixes
9b5157d
metadata
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

Option A β€” Docker Compose (recommended)

# 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

Check container health:

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

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

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

With environment variable overrides:

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:

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

brew install python@3.12

Create virtual environment and install dependencies

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:

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

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

curl http://localhost:8000/api/v1/health

Full verification flow (terminal)

Step 1 β€” Get a challenge:

curl -s -X POST http://localhost:8000/api/v1/verify/challenge | python3 -m json.tool

Example response:

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

Step 2 β€” Submit a selfie:

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:

{
  "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

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
  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 β€” 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.