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 Runtimecheckpoint_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
Inference Providers NEW
This model isn't deployed by any Inference Provider. π Ask for provider support