End-to-end license plate recognition: plate detection → tracking → OCR → Turkish format parsing → temporal voting → privacy-aware persistence, served behind a FastAPI HTTP + WebSocket API.
Originally written in 2022 with a YOLOv5 fork + EasyOCR + Flask 1.1. Rewritten in 2026 on top of
fast-alpr+fast-plate-ocr+ FastAPI + uv. The original code is preserved verbatim underlegacy/for reference.
pip install anpr-pipeline
export ANPR_PLATE_HMAC_PEPPER="$(python -c 'import secrets; print(secrets.token_hex(32))')"
anpr serve # FastAPI on http://localhost:8000
anpr infer path/to/plate.jpg # one-shot CLIexport ANPR_PLATE_HMAC_PEPPER="$(python -c 'import secrets; print(secrets.token_hex(32))')"
docker compose up --buildOpen http://localhost:8000.
git clone https://github.com/mftnakrsu/Automatic_Number_Plate_Recognition_YOLO_OCR.git
cd Automatic_Number_Plate_Recognition_YOLO_OCR
uv sync --all-extras --group dev
cp .env.example .env
# Generate a pepper and paste it into .env
uv run anpr generate-pepper
uv run anpr serve # FastAPI on http://localhost:8000
# or: single-image inference
uv run anpr infer tests/fixtures/sample_plate.jpg| Layer | Implementation |
|---|---|
| Detector | fast-alpr's bundled YOLOv9-t 384 ONNX license-plate model (MIT, 65+ countries) |
| OCR | fast-plate-ocr cct-s-v2-global-model (sub-millisecond GPU, per-character confidences) |
| Tracker | Dependency-free greedy IoU tracker (swap to ByteTrack via the Tracker Protocol) |
| Turkish parser | All 81 province codes, strict regex on NN [A]{1-3} [N]{2-4} |
| Confusion fix | Position-aware OCR fixups (0/O, 1/I/L, 5/S, 8/B, 2/Z, 6/G, 0/D/Q) |
| Temporal voting | Per-character-position majority vote over top-K confidence-weighted reads per track |
| Web | FastAPI 0.115 + uvicorn, REST + WebSocket, dark-theme HTMX UI |
| Storage | SQLModel + aiosqlite default; HMAC-SHA256 plate hashing (no raw plate text ever stored) |
| Observability | structlog + Prometheus /metrics + opt-in OpenTelemetry (otel extras) |
| Packaging | uv + pyproject.toml, ruff (lint + format), pyright, pytest (60+ tests) |
| Endpoint | Method | What |
|---|---|---|
/ |
GET | HTMX UI (upload + live webcam) |
/health |
GET | k8s probe |
/version |
GET | running version |
/metrics |
GET | Prometheus text-format metrics |
/api/v1/infer |
POST (multipart) | single-image inference |
/api/v1/detections |
GET | recent persisted reads (HMAC-hashed) |
/ws/stream |
WS | binary JPEG frames in → JSON read events out |
Full OpenAPI at http://localhost:8000/docs.
┌──────────────────────┐ ┌─────────────────────┐
│ Webcam / Upload │──────►│ FastAPI │
└──────────────────────┘ │ /infer /ws/stream │
└──────────┬──────────┘
▼
┌──────────────────────────────────────────────────────┐
│ Pipeline (async, frame-skip + drop-on-full queue) │
│ │
│ Detector ──► IoUTracker ──► dewarp ──► PlateReader │
│ fast-alpr greedy cv2 fast-plate │
│ ONNX ONNX │
│ │ │
│ ▼ │
│ parse_turkish_plate │
│ correct_confusions │
│ TemporalVoter (majority) │
└──────────────────────────────────────────────────────┘
│
▼
┌──────────────────────┐ ┌─────────────────────┐
│ SQLite / Postgres │◄──────│ HMAC-SHA256(plate) │
│ (only hashes) │ │ KVKK + GDPR safe │
└──────────────────────┘ └─────────────────────┘
All settings live under the ANPR_ env prefix and can be overridden via .env. See .env.example for the full reference.
| Env | Default | Notes |
|---|---|---|
ANPR_PLATE_HMAC_PEPPER |
(generated; warns) | Required in prod. Generate with anpr generate-pepper. Must be ≥ 32 chars. |
ANPR_DATABASE_URL |
sqlite+aiosqlite:///data/anpr.db |
Switch to postgresql+asyncpg://... for production. |
ANPR_RETENTION_HOURS |
720 (30d) |
Background worker purges rows older than this. |
ANPR_DETECT_EVERY_N_FRAMES |
3 |
Detect on every Nth frame; track between. |
ANPR_MIN_TRACK_DWELL |
5 |
Frames a track must persist before the voter emits confirmed. |
ANPR_DETECTOR_MODEL |
yolo-v9-t-384-license-plate-end2end |
Pass a path to use a fine-tuned ONNX. |
ANPR_OCR_MODEL |
cct-s-v2-global-model |
fast-plate-ocr model name. |
ANPR_DEVICE |
auto |
auto / cpu / cuda / openvino. |
ANPR_LOG_JSON |
false |
Set to true for structured logs to OTLP / Loki. |
OTEL_EXPORTER_OTLP_ENDPOINT |
(unset) | When set, installs OTel FastAPI instrumentation (requires otel extras). |
License plates are personal data under both Türkiye's KVKK (Law 6698, art. 3(d)) and the EU GDPR (Reg. 2016/679 art. 4(1)). This project is privacy-aware by default:
- Plates are never stored as plaintext. The database holds only HMAC-SHA256 of the normalized plate string, salted with a per-deployment pepper. Hashes are deterministic for matching but not reversible without the pepper.
- A retention worker purges rows older than
ANPR_RETENTION_HOURSautomatically. - The web UI (
templates/index.html) shows the human-readable plate live in the browser but does not persist the raw text.
If you deploy this for an organization, you'll likely need to publish a KVKK Aydınlatma Metni / GDPR Article 13 notice. A starter template is in KVKK_AYDINLATMA_METNI.md — adapt it to your actual processing context, this is not legal advice.
uv sync --all-extras --group dev
make lint # ruff check + format check
make type # pyright
make test # pytest (60+ tests)
make serve # uvicorn --reloadRun the end-to-end test against real bundled models (downloads ~30MB on first run):
ANPR_RUN_REAL=1 uv run pytest tests/integration/test_pipeline_real.pysrc/anpr/
├── api/ FastAPI app, routes, templates, static
│ ├── app.py
│ ├── deps.py lifespan-loaded singletons
│ ├── middleware.py request_id + Prometheus middleware
│ └── routes/ health, infer, stream (WS), detections
├── detector/ Detector Protocol + fast-alpr adapter
├── ocr/ PlateReader Protocol + fast-plate-ocr adapter
├── postprocess/ turkish, confusion, temporal, dewarp
├── storage/ SQLModel Detection + HMAC + repository + retention
├── tracker/ IoU greedy tracker
├── observability.py Prometheus metrics + optional OTel
├── config.py pydantic-settings
├── logging.py structlog
├── cli.py typer
└── pipeline.py sync infer_image + async Pipeline
tests/
├── unit/ turkish, confusion, temporal, iou_tracker, hashing (+ hypothesis properties)
└── integration/ pipeline_smoke, api_smoke, metrics, storage, infer_response_shape, pipeline_real
legacy/ 2022 codebase preserved verbatim
The 2022 code is not removed — it sits intact under legacy/. What changed:
| 2022 | 2026 |
|---|---|
Vendored YOLOv5 fork (~7900 LOC in utils/ + models/) |
fast-alpr 0.4 → YOLOv9-t-384 ONNX, MIT, ~150 LOC of glue |
| EasyOCR 1.4 + PaddleOCR + Tesseract (mixed/inconsistent) | fast-plate-ocr 1.1 cct-s-v2 (single source of truth) |
| Flask 1.1.2 (CVE) | FastAPI 0.115 + uvicorn |
python main.py (webcam) and python app.py (Flask, parallel + no OCR) |
anpr infer image.jpg, anpr serve |
| No tracking | IoU greedy + temporal majority voting |
| No plate validation | Turkish format parser + confusion correction |
requirements.txt (broken: tensorrt==0.0.1.dev5, etc.) |
pyproject.toml + uv.lock |
| No tests, no CI | 60+ tests, GitHub Actions matrix |
print() |
structlog + Prometheus + opt-in OTel |
| CSV append, raw plate text | SQLModel + HMAC-SHA256 + retention worker |
If you depended on the old import paths or the model/best.pt checkpoint, pin to the pre-merge tag and follow legacy/README.md (yet to be written) for context.
This rewrite stands on the work of:
ankandrew/fast-alpr— turn-key MIT-licensed ANPR pipelineankandrew/fast-plate-ocr— fast plate-specialized OCR with thecct-s-v2-global-model- Ultralytics — original YOLOv5 the 2022 pipeline built on
- Astral —
uvandruff