RF-DETR License Plate Detector

RF-DETR Base fine-tuned for license plate detection with a single class: license_plate.
Trained primarily on Dutch plates but generalises well to other European formats.

GitHub: rickkosse/dutch-license-plate-detector

Files

  • inference_model.onnx β€” ONNX export for CPU inference via ONNX Runtime
  • checkpoint_best_ema_v4.pth β€” PyTorch EMA checkpoint for fine-tuning

Usage β€” ONNX (Recommended)

import cv2
import numpy as np
import onnxruntime as ort

session = ort.InferenceSession("inference_model.onnx",
                                providers=["CPUExecutionProvider"])

def preprocess(img_bgr, size=784):
    img = cv2.resize(img_bgr, (size, size))
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
    mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
    std  = np.array([0.229, 0.224, 0.225], dtype=np.float32)
    return ((img - mean) / std).transpose(2, 0, 1)[np.newaxis]  # NCHW

img = cv2.imread("photo.jpg")
oh, ow = img.shape[:2]
outputs = session.run(None, {session.get_inputs()[0].name: preprocess(img)})

dets   = outputs[0].squeeze()   # (300, 4) cxcywh normalised
logits = outputs[1].squeeze()   # (300, 2) raw logits
s0 = 1 / (1 + np.exp(-logits[:, 0]))
s1 = 1 / (1 + np.exp(-logits[:, 1]))
scores = s0 if s0.max() > s1.max() else s1  # pick the plate-class column

for i in np.where(scores > 0.3)[0]:
    cx, cy, bw, bh = dets[i]
    x1 = int((cx - bw / 2) * ow); y1 = int((cy - bh / 2) * oh)
    x2 = int((cx + bw / 2) * ow); y2 = int((cy + bh / 2) * oh)
    print(f"Plate: ({x1},{y1},{x2},{y2})  conf={scores[i]:.2f}")
    cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)

cv2.imwrite("result.jpg", img)

Post-processing

Geometry filter (recommended):

w, h = x2 - x1, y2 - y1
if h <= 0 or not (1.5 <= w / h <= 9.0):
    continue   # wrong aspect ratio
if (w * h) / (ow * oh) > 0.15:
    continue   # too large

OCR with fast-plate-ocr

from fast_plate_ocr import LicensePlateRecognizer

ocr = LicensePlateRecognizer("european-plates-mobile-vit-v2-model")
crop = cv2.cvtColor(img[y1:y2, x1:x2], cv2.COLOR_BGR2GRAY)[:, :, np.newaxis]
result = ocr.run(crop)
text = result[0].plate.strip() if result and result[0].plate else ""

Optional format validation (Dutch example β€” adapt for your country)

import re

NL_PLATE = re.compile(
    r'^[A-Z]{2}\d{2}[A-Z]{2}$|^\d{2}[A-Z]{2}\d{2}$|^[A-Z]{4}\d{2}$|'
    r'^\d{4}[A-Z]{2}$|^\d{2}[A-Z]{4}$|^[A-Z]{2}\d{4}$|'
    r'^[A-Z]{2}\d{3}[A-Z]$|^[A-Z]\d{3}[A-Z]{2}$|'
    r'^[A-Z]{3}\d{2}[A-Z]$|^[A-Z]\d{2}[A-Z]{3}$|'
    r'^\d{2}[A-Z]{3}\d$|^\d[A-Z]{3}\d{2}$',
    re.IGNORECASE,
)

if not NL_PLATE.match(text.replace("-", "").upper()):
    continue   # skip if format doesn't match β€” omit this check for other countries

Training Details

Architecture RF-DETR Base, 1 class (license_plate)
Resolution 784Γ—784
Optimizer AdamW, LR 5e-5, encoder LR 1e-5
Scheduler Cosine annealing + 5 warmup epochs
EMA Enabled
Data Synthetic plates on BDD100K + real-world crops

Installation

pip install onnxruntime fast-plate-ocr opencv-python

License

Apache 2.0

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Space using Rickkosse/rfdetr_licences_plate_detector 1