Iranian license plate detection with Persian OCR, multi-camera RTSP monitoring, and a bilingual dashboard.
Point it at an image or an RTSP stream and it returns the plate, the vehicle class, and the issuing province — then logs every passing vehicle and flags the ones on your block list.
فارسی · Quick start · How it works · API · Configuration
| 🔍 Plate detection | YOLO detector plus a Persian CRNN OCR model, from an uploaded image or a phone camera |
| 🏷️ Plate decoding | Splits the plate, identifies the vehicle class from its letter (private, taxi, government, police, army…), and resolves the two-digit suffix to a province and city |
| 📹 Multi-camera RTSP | One worker thread per camera with automatic reconnect, bounded connect timeouts, and a live preview |
| 🚦 Access control | Allow/block lists per plate; blocked plates raise an alert the moment they are seen |
| 📜 Access log | Every entry and exit recorded with camera, role, confidence, timestamp, and the cropped plate image |
| ⚡ Live updates | Server-Sent Events push detections and camera health to the dashboard with no polling |
| 🌐 Bilingual UI | Full English and Persian, RTL-aware, switchable at runtime with no reload |
git clone https://github.com/saeed205/IranPlate-Vision.git
cd IranPlate-Vision
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python app.pyOpen http://localhost:5000.
The first start downloads the Persian OCR model (~50 MB). Until it finishes,
/detect answers 503 and the dashboard shows a loading indicator — poll
/health if you need to wait for readiness programmatically.
Docker
docker compose up --buildThe image serves through waitress as a non-root user. The database and the model cache live in named volumes, so a rebuild does not re-download the model.
Scanning from a phone
Browsers only expose the camera on a secure origin, so plain HTTP over your LAN will not work. Run:
python run_https.pyIt generates a self-signed certificate that includes your LAN address in its SAN, then prints the URL to open on the phone. Accept the browser warning via Advanced → Proceed.
Production
python app.py is Flask's development server. For anything shared, use:
make serve # waitress-serve --host=0.0.0.0 --port=5000 --threads=8 app:app[!WARNING] There is no authentication. Anyone who can reach the port can view your cameras and edit your plate lists. Keep it on a trusted network, or put it behind a reverse proxy that handles TLS and auth. See SECURITY.md.
flowchart LR
subgraph Inputs
A[Image upload<br/>or phone camera]
B[RTSP cameras]
end
subgraph "models.py — serialised inference"
D[YOLO detector<br/>best.pt]
E[Persian CRNN OCR<br/>hezar]
end
A -->|POST /detect| D
B -->|worker thread<br/>per camera| D
D -->|plate crops| E
E -->|raw text| F["plates.py<br/>canonical form"]
F --> G["plate_data.json<br/>class · province · city"]
F --> H[(SQLite<br/>traffic.db)]
H --> I[Access log<br/>allow / block lists]
F -->|Server-Sent Events| J[Dashboard<br/>EN / FA]
G --> J
I --> J
Both entry points share one inference module, and every model call is serialised behind a lock — torch modules are not safe to call from several threads at once.
Plate format. Everything is normalised to one canonical string,
24ن144-66: two digits, the letter, three digits, a dash, and the two-digit
province code. Persian and Arabic-Indic digits are accepted, separators are
optional, and the interchangeable letter spellings ا/الف, ه/ھ, ي/ی,
ك/ک are folded together. Comparing raw OCR output instead of the canonical
form is what previously broke the allow/block list.
| Letter | Vehicle class | Letter | Vehicle class | |
|---|---|---|---|---|
ب ج د س ص ط ق ل م ن و ه |
Private | الف |
Government | |
ت |
Taxi | پ |
Police | |
ع |
Public transport | ش |
Army | |
ک |
Agricultural | ث |
IRGC | |
ژ |
Disabled veterans | ف ز |
Armed forces | |
گ |
Temporary transit |
Every response is JSON, including errors: {"error": "English | فارسی"}.
| Method | Path | Notes |
|---|---|---|
GET |
/status |
{ready, error, cameras, clients} |
GET |
/health |
200 once models are loaded, 503 before |
POST |
/detect |
multipart image; max 16 MB |
GET POST |
/api/cameras |
RTSP passwords are masked in responses |
GET PUT DELETE |
/api/cameras/<id> |
|
POST |
/api/cameras/<id>/toggle |
body {enabled} |
GET |
/api/cameras/<id>/snapshot |
{image, age} |
GET |
/api/events |
SSE stream: detection, camera_status |
GET DELETE |
/api/log |
?limit= 1–1000 |
GET |
/api/log/<id>/crop |
the stored plate crop |
GET POST |
/api/vehicles |
allow/block lists |
DELETE |
/api/vehicles/<plate> |
Example: detect a plate
curl -s -F image=@car.jpg http://localhost:5000/detect | jq '{plate, best_conf, plate_info}'{
"plate": "24ن144-66",
"best_conf": 0.9328,
"plate_info": {
"prefix": "24", "letter": "ن", "middle": "144", "suffix": "66",
"canonical": "24ن144-66",
"vehicle_type": { "id": "shakhsi", "type": "خودروهای شخصی", "bg": "#f2ede0" },
"locations": [{ "province": "تهران", "city": "تهران" }]
}
}All optional; defaults in parentheses.
| Variable | Default | Purpose |
|---|---|---|
PLATE_HOST / PLATE_PORT |
0.0.0.0 / 5000 |
Bind address |
PLATE_DB |
./traffic.db |
SQLite path |
PLATE_LOG_LEVEL |
INFO |
Logging level |
PLATE_MAX_UPLOAD_MB |
16 |
Upload cap for /detect |
PLATE_MAX_IMAGE_SIDE |
1920 |
Uploads are downscaled to this |
PLATE_DET_CONF |
0.4 |
Detector confidence threshold |
PLATE_WEIGHTS |
./best.pt |
Detector weights |
PLATE_OCR_MODEL |
hezarai/crnn-fa-…-v2 |
OCR model id |
PLATE_DETECT_INTERVAL |
2.0 |
Seconds between RTSP detections |
PLATE_SNAPSHOT_FPS |
4 |
Live-preview encode rate |
PLATE_OPEN_TIMEOUT_MS |
6000 |
RTSP connect timeout |
PLATE_RECONNECT_WAIT |
5.0 |
Seconds before an RTSP retry |
PLATE_LOG_MAX_ROWS |
5000 |
Access-log cap (rows store JPEG crops) |
PLATE_ALLOW_LOCAL_SOURCES |
unset | Allow file paths as camera sources — testing only, see SECURITY.md |
pip install -r requirements-dev.txt
make test # 69 offline checks — no server, no models needed
make smoke # every endpoint against a running server
ruff check . # lintmake test covers plate normalisation, the province lookup, the JSON API, and
the RTSP worker's detection bookkeeping. Because the models load lazily, it runs
in seconds without ultralytics or torch installed.
Project layout
app.py Flask routes, plate_data indexes, JSON error handling
models.py Lazy model loading, serialised inference
plates.py Canonical plate parsing and normalisation
camera_manager.py RTSP worker threads, SSE event bus
db.py SQLite schema, queries, migrations
run_https.py Self-signed TLS for phone camera access
plate_data.json Province, city and vehicle-class tables
best.pt YOLO detector weights
templates/ menu.html · index.html (scan) · cameras.html
static/i18n.js Runtime EN/FA switching
scripts/ Test suites and RTSP probes
Issues and pull requests are welcome — see CONTRIBUTING.md and the Code of Conduct. Good first areas: additional plate formats (motorcycle, diplomatic), OCR accuracy on night footage, and authentication.
Please never attach real RTSP credentials or footage of identifiable people to an issue.
- Plate detection built on Ultralytics YOLO
- Persian OCR by hezarai/crnn-fa-license-plate-recognition-v2
- Maintained as a fork of 12345zahraa/Persian-Plates-Detection
MIT. The bundled model weights and fonts keep their own licenses.



