Asif EjazAI engineering / interactive demo← All demos
A YOLOv8 character detector on CPU. No deep learning framework at run time.

A Saudi plate says its number twice. I read both and compare.

Upload a photo of a Saudi licence plate. Every character is found, the Arabic row and the Latin row are read separately, and each row is used to check the other. You also get the colour of the strip, which says what kind of vehicle the plate belongs to.

54
characters it tells apart: 27 Arabic and 27 Latin
…
plates read exactly right, both rows, on photos it never trained on
…
per plate on a laptop processor, with no graphics card
Try it

Pick a sample plate, or upload your own

The samples are cropped from the public dataset the detector was tested on. Your own photo is read in memory and then dropped: nothing is stored. Crop close to the plate for the best result.

Samples
Or drop one here. JPEG or PNG, up to 8 MB. One plate per photo.
Loading a sample plate.
What I found when I reopened this project

The score on record was earned on photos the model had studied

This was company work in 2023: a Colab notebook that trained the detector, and a model file exported for a phone app. The notebook's own numbers look perfect. Measured again on photos the model had not seen, three things came out.

0.995

Precision on record

The last line of the training log says precision 0.995, recall 1.00 and mAP50 0.995. That was measured on a validation set of 40 images, and 39 of those were made from photos that are also in the training set, as other noisy copies of the same picture. The score mostly measured memory.

…

Latin letters found on unseen photos

The test set is a fairer exam: … of its … labelled images come from photos the training set never held. On those, the detector finds … to … of the Arabic characters and the Latin digits, but far fewer of the Latin letters, even on plates that are perfectly sharp.

…

The same letters, with noise added

The dataset export had sprinkled salt and pepper noise inside the labelled character boxes of the training images. The detector seems to have learned to look for the sprinkles. Put the same noise over a clean photo and the missing Latin letters appear, but Arabic characters drop out and it draws … boxes that match nothing, against … without. So I do not serve that trick.

What I serve instead: the plate checks itself

Every character is printed twice, the Latin one directly under its Arabic twin. So a letter the detector misses in one row can be filled in from the other, and a character the two rows disagree on can be settled by which reading the detector was surer of. The table shows how many of the … unseen plates come out exactly right, each way of reading the same detections.

The check changed a reading on … of those plates. It turned … wrong Latin readings and … wrong Arabic readings into right ones, and broke …. My first version of the rules, written before I had scored a single test image, did slightly worse than the notebook's simple merge. The rules served here came after reading the misses on these same photos, so expect a little less on new ones: with … plates, … right is consistent with anything from about … to ….

The labels have mistakes of their own. In … of the … test label files the Arabic row and the Latin row spell different plates. I looked at each of those images and wrote down what the plate says; the list, with the reason for every change, sits beside the build scripts.

Under the hood

From a notebook to a service on a shared processor

Detector

YOLOv8, one class per character

The model looks at the photo once and proposes 8,400 boxes, each scored against 27 Arabic and 27 Latin characters. A 55th class, the outline of the plate, is in its class list, but no label in the dataset ever used it and the model never predicts it. So the outline you see on the picture is worked out from the characters.

Two rows

Paired column by column

Characters are grouped into a plate, then each Arabic character is paired with the Latin character under it, by position along the row, after sliding one row over the other until the most twins line up. On plates turned by 10 degrees that slide rescued … readings in …. If twins differ, the surer reading wins. If one row has a character the other lacks, it is carried over. The digit ١ and the letter ا are the same stroke, so there the Latin row decides.

Arabic is read from right to left, so the Arabic reading runs the opposite way to the Latin letters printed under it. The notebook also spelled two of the seventeen letters wrongly, with ج for J and ي for V. The plates carry ح and ى.

What changed

No framework, half the file

The original needed PyTorch and Ultralytics to run. Here the model runs through ONNX Runtime, and everything around it is plain numpy and Pillow: fitting the photo into a square, turning the raw output into boxes, thinning overlapping boxes, mapping them back onto your photo. The model file turned out to give its boxes as fractions of the picture instead of pixels, the way a phone export does; that had to be noticed before anything worked.

I stored the weights as 16 bit numbers, which halves the file from … to … MB and changed … of … test readings. An 8 bit version is a quarter of the size and, to my surprise, finds more characters than the original. But it is slower on this processor and it changes readings, so I kept the copy that behaves exactly like the model that was trained. The table below has all four. One read takes about … seconds on a laptop processor, and a process that does nothing else peaks near … MB. The models load on the first request, read one photo at a time and are released after fifteen idle minutes.

Colour

The weakest part, so it is checked

The project's colour model is a small ResNet18 trained on about 300 pictures in five classes. It scores … of … on its own held back pictures. But … of its … white training pictures are plates set on black bars, against … of the … held back pictures of the other colours, so it learned the bars: put bars around its blue, gray, green and yellow held back pictures and it calls … of the … white. Fed real crops of the … test plates that have no coloured strip, it named a colour that is not there … times.

So the colour stated here comes from the pixels: a strip counts when strongly blue, green or yellow pixels cover a small share of the plate and sit together in one upright band. That check matched what I see on … of the … test plates the detector found, and the folder name on … of the colour model's own … held back pictures. The model's answer is shown beside it. White strips mark private cars, yellow taxis and public transport, blue trucks and utility vehicles, green diplomatic vehicles. I could not confirm what the gray strip means, so I do not say.

Limits

What to expect

The detector learned from … photos of plates that fill the frame, copied into … training images. It reads best when the plate fills most of the picture and is level. Training used no rotation at all, and it shows: a plate turned by 20 degrees is almost never read. When the plate is small in the frame I take a second and sometimes a third, closer look; the table shows how far that goes, and it is not far enough for a photo of a whole car. Dirt, glare, night shots and damaged plates are not measured here, because the test set has almost none. Only the two Saudi plate formats in its training photos are known to it: a plate from anywhere else is read as nonsense or not at all. It reads one plate per photo. This is a demo, not a system to act on.

Dataset: LPR, final dataset, from Roboflow Universe, used under CC BY 4.0. Model trained with Ultralytics YOLOv8, licensed AGPL 3.0. Strip colour meanings follow the Wikipedia article on vehicle registration plates of Saudi Arabia.