Rick Kosse commited on
Commit
1dd4645
·
1 Parent(s): 22f149e

Replace with RF-DETR-Large NL plate detector (ONNX, Apache 2.0)

Browse files
LICENSE ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 Rick Kosse
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
18
+
19
+ Full license text: https://www.apache.org/licenses/LICENSE-2.0.txt
README.md CHANGED
@@ -1,122 +1,81 @@
1
  ---
2
  license: apache-2.0
3
- task_categories:
4
- - object-detection
 
5
  tags:
6
- - license-plate
7
- - netherlands
8
- - rf-detr
9
- - onnx
10
- - european
11
- language:
12
- - nl
13
  ---
14
 
15
- # RF-DETR License Plate Detector
16
- RF-DETR Base fine-tuned for license plate detection with a single class: `license_plate`.
17
- Trained primarily on Dutch plates but generalises well to other European formats.
18
 
19
- **GitHub**: [rickkosse/dutch-license-plate-detector](https://github.com/rickkosse/dutch-license-plate-detector)
 
 
20
 
21
- ## Files
22
- - **`inference_model.onnx`** ONNX export for CPU inference via ONNX Runtime
23
- - **`checkpoint_best_ema_v4.pth`** PyTorch EMA checkpoint for fine-tuning
 
 
24
 
25
- ## Usage — ONNX (Recommended)
 
 
26
 
27
- ```python
28
- import cv2
29
- import numpy as np
30
- import onnxruntime as ort
31
-
32
- session = ort.InferenceSession("inference_model.onnx",
33
- providers=["CPUExecutionProvider"])
34
-
35
- def preprocess(img_bgr, size=784):
36
- img = cv2.resize(img_bgr, (size, size))
37
- img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
38
- mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
39
- std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
40
- return ((img - mean) / std).transpose(2, 0, 1)[np.newaxis] # NCHW
41
-
42
- img = cv2.imread("photo.jpg")
43
- oh, ow = img.shape[:2]
44
- outputs = session.run(None, {session.get_inputs()[0].name: preprocess(img)})
45
-
46
- dets = outputs[0].squeeze() # (300, 4) cxcywh normalised
47
- logits = outputs[1].squeeze() # (300, 2) raw logits
48
- s0 = 1 / (1 + np.exp(-logits[:, 0]))
49
- s1 = 1 / (1 + np.exp(-logits[:, 1]))
50
- scores = s0 if s0.max() > s1.max() else s1 # pick the plate-class column
51
-
52
- for i in np.where(scores > 0.3)[0]:
53
- cx, cy, bw, bh = dets[i]
54
- x1 = int((cx - bw / 2) * ow); y1 = int((cy - bh / 2) * oh)
55
- x2 = int((cx + bw / 2) * ow); y2 = int((cy + bh / 2) * oh)
56
- print(f"Plate: ({x1},{y1},{x2},{y2}) conf={scores[i]:.2f}")
57
- cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
58
-
59
- cv2.imwrite("result.jpg", img)
60
- ```
61
-
62
- ## Post-processing
63
-
64
- Geometry filter (recommended):
65
-
66
- ```python
67
- w, h = x2 - x1, y2 - y1
68
- if h <= 0 or not (1.5 <= w / h <= 9.0):
69
- continue # wrong aspect ratio
70
- if (w * h) / (ow * oh) > 0.15:
71
- continue # too large
72
- ```
73
 
74
- ## OCR with `fast-plate-ocr`
75
 
76
  ```python
77
- from fast_plate_ocr import LicensePlateRecognizer
78
-
79
- ocr = LicensePlateRecognizer("european-plates-mobile-vit-v2-model")
80
- crop = cv2.cvtColor(img[y1:y2, x1:x2], cv2.COLOR_BGR2GRAY)[:, :, np.newaxis]
81
- result = ocr.run(crop)
82
- text = result[0].plate.strip() if result and result[0].plate else ""
83
- ```
84
-
85
- ## Optional format validation (Dutch example — adapt for your country)
86
-
87
- ```python
88
- import re
89
-
90
- NL_PLATE = re.compile(
91
- r'^[A-Z]{2}\d{2}[A-Z]{2}$|^\d{2}[A-Z]{2}\d{2}$|^[A-Z]{4}\d{2}$|'
92
- r'^\d{4}[A-Z]{2}$|^\d{2}[A-Z]{4}$|^[A-Z]{2}\d{4}$|'
93
- r'^[A-Z]{2}\d{3}[A-Z]$|^[A-Z]\d{3}[A-Z]{2}$|'
94
- r'^[A-Z]{3}\d{2}[A-Z]$|^[A-Z]\d{2}[A-Z]{3}$|'
95
- r'^\d{2}[A-Z]{3}\d$|^\d[A-Z]{3}\d{2}$',
96
- re.IGNORECASE,
97
- )
98
-
99
- if not NL_PLATE.match(text.replace("-", "").upper()):
100
- continue # skip if format doesn't match omit this check for other countries
101
- ```
102
-
103
- ## Training Details
104
-
105
- | | |
106
- |---|---|
107
- | Architecture | RF-DETR Base, 1 class (`license_plate`) |
108
- | Resolution | 784×784 |
109
- | Optimizer | AdamW, LR 5e-5, encoder LR 1e-5 |
110
- | Scheduler | Cosine annealing + 5 warmup epochs |
111
- | EMA | Enabled |
112
- | Data | Synthetic plates on BDD100K + real-world crops |
113
-
114
- ## Installation
115
-
116
- ```bash
117
- pip install onnxruntime fast-plate-ocr opencv-python
118
  ```
119
 
120
- ## License
 
 
 
 
121
 
122
- Apache 2.0
 
 
1
  ---
2
  license: apache-2.0
3
+ library_name: rfdetr
4
+ pipeline_tag: object-detection
5
+ base_model: roboflow/rf-detr-large
6
  tags:
7
+ - object-detection
8
+ - license-plate-detection
9
+ - alpr
10
+ - rf-detr
11
+ - onnx
12
+ - netherlands
 
13
  ---
14
 
15
+ # RF-DETR-Large — Dutch License Plate Detector
 
 
16
 
17
+ A single-class license-plate **detector** fine-tuned from
18
+ [`roboflow/rf-detr-large`](https://huggingface.co/roboflow/rf-detr-large) on Dutch +
19
+ synthetic plate data, exported to ONNX (fixed 768×768 input).
20
 
21
+ - **Task:** license-plate detection (one class: `license_plate`)
22
+ - **Base model:** RF-DETR-Large (Apache 2.0)
23
+ - **Input:** `[1, 3, 768, 768]` RGB, ImageNet-normalized
24
+ - **Outputs:** `dets` (boxes, cx/cy/w/h normalized) and `labels` (per-query scores)
25
+ - **License:** Apache 2.0
26
 
27
+ ## Live demo
28
+ Try it in the Space: **[Rickkosse/license-plate-detector](https://huggingface.co/spaces/Rickkosse/license-plate-detector)**
29
+ (detection + `fast-plate-ocr` reading, with an "unreadable" gate for low-confidence plates).
30
 
31
+ ## Intended use & limitations
32
+ Detects Dutch-style plates well when they are reasonably large and frontal. Small,
33
+ distant, or strongly angled plates in wide scenes are harder (a known data-coverage
34
+ limitation). This is a **prototype**: training data was cc-by-nc / synthetic, so it
35
+ is not a certified production model. For production, retrain on rights-clean,
36
+ hand-verified data — the pipeline is unchanged.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
+ ## Usage (ONNX Runtime)
39
 
40
  ```python
41
+ import numpy as np, onnxruntime as ort
42
+ from PIL import Image
43
+
44
+ RES = 768
45
+ MEAN = np.array([0.485, 0.456, 0.406], np.float32)
46
+ STD = np.array([0.229, 0.224, 0.225], np.float32)
47
+
48
+ sess = ort.InferenceSession("rfdetr-large.onnx", providers=ort.get_available_providers())
49
+ in_name = sess.get_inputs()[0].name
50
+
51
+ def detect(path, conf=0.5):
52
+ pil = Image.open(path).convert("RGB")
53
+ w0, h0 = pil.size
54
+ img = pil.resize((RES, RES), Image.BILINEAR)
55
+ x = (np.asarray(img, np.float32) / 255.0 - MEAN) / STD
56
+ x = np.ascontiguousarray(x.transpose(2, 0, 1)[None], np.float32)
57
+ dets, labels = sess.run(["dets", "labels"], {in_name: x})
58
+ d = dets[0] if dets.ndim == 3 else dets
59
+ l = labels[0] if labels.ndim == 3 else labels
60
+ scores = l.max(axis=-1) if l.ndim > 1 else l
61
+ if scores.max() > 1 or scores.min() < 0:
62
+ scores = 1 / (1 + np.exp(-scores)) # logits -> prob
63
+ out = []
64
+ for (cx, cy, bw, bh), s in zip(d, scores):
65
+ if s < conf:
66
+ continue
67
+ out.append((max(0,(cx-bw/2)*w0), max(0,(cy-bh/2)*h0),
68
+ min(w0,(cx+bw/2)*w0), min(h0,(cy+bh/2)*h0), float(s)))
69
+ return out
70
+
71
+ print(detect("car.jpg")) # [(x1, y1, x2, y2, score), ...] in pixels
 
 
 
 
 
 
 
 
 
 
72
  ```
73
 
74
+ ## Reading plates (optional, two-stage ALPR)
75
+ Pair detection with [`fast-plate-ocr`](https://github.com/ankandrew/fast-plate-ocr)
76
+ (MIT): crop each detected box and read it. The OCR expects a **grayscale** crop with
77
+ a channel axis `(H, W, 1)`. See the Space `app.py` for a full example, including the
78
+ confidence gate that flags unreadable plates instead of guessing.
79
 
80
+ ## Citation
81
+ Built on RF-DETR by Roboflow. OCR by `fast-plate-ocr` (ankandrew).
inference.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal RF-DETR ONNX license-plate detector. License: Apache 2.0."""
2
+ import argparse
3
+ import numpy as np
4
+ import onnxruntime as ort
5
+ from PIL import Image
6
+
7
+ RES = 768
8
+ MEAN = np.array([0.485, 0.456, 0.406], np.float32)
9
+ STD = np.array([0.229, 0.224, 0.225], np.float32)
10
+
11
+
12
+ def detect(sess, in_name, pil, conf=0.5):
13
+ w0, h0 = pil.size
14
+ img = pil.convert("RGB").resize((RES, RES), Image.BILINEAR)
15
+ x = (np.asarray(img, np.float32) / 255.0 - MEAN) / STD
16
+ x = np.ascontiguousarray(x.transpose(2, 0, 1)[None], np.float32)
17
+ dets, labels = sess.run(["dets", "labels"], {in_name: x})
18
+ d = dets[0] if dets.ndim == 3 else dets
19
+ l = labels[0] if labels.ndim == 3 else labels
20
+ scores = l.max(axis=-1) if l.ndim > 1 else l
21
+ if scores.max() > 1 or scores.min() < 0:
22
+ scores = 1 / (1 + np.exp(-scores))
23
+ out = []
24
+ for (cx, cy, bw, bh), s in zip(d, scores):
25
+ if s < conf:
26
+ continue
27
+ out.append((max(0, (cx - bw / 2) * w0), max(0, (cy - bh / 2) * h0),
28
+ min(w0, (cx + bw / 2) * w0), min(h0, (cy + bh / 2) * h0), float(s)))
29
+ return out
30
+
31
+
32
+ if __name__ == "__main__":
33
+ ap = argparse.ArgumentParser()
34
+ ap.add_argument("image")
35
+ ap.add_argument("--onnx", default="rfdetr-large.onnx")
36
+ ap.add_argument("--conf", type=float, default=0.5)
37
+ args = ap.parse_args()
38
+ sess = ort.InferenceSession(args.onnx, providers=ort.get_available_providers())
39
+ for box in detect(sess, sess.get_inputs()[0].name, Image.open(args.image), args.conf):
40
+ print(f"x1={box[0]:.0f} y1={box[1]:.0f} x2={box[2]:.0f} y2={box[3]:.0f} score={box[4]:.2f}")
inference_model.onnx DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:5886b25016751238979c21336bae2b02b944b0db07f544e14ec5311191f934b7
3
- size 115501245
 
 
 
 
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ onnxruntime
2
+ numpy
3
+ pillow
checkpoint_best_ema_v4.pth → rfdetr-large.onnx RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:73b8efc467c728dc75854494fc8158cca75c503993402b382b59916f40995b28
3
- size 127629096
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f2f6f93c64c5844246ed343b00b298b4dc01ba377912d462bc4882da0816c012
3
+ size 128345033