Smart Road β€” road distress detection for ASTM D6433

Code on GitHub Β· Weights (here) Β· Dataset on Kaggle

Project partner

Raximjon Soataliyev β€” road engineer, lecturer at Tashkent State Transport University Β· LinkedIn Β· ResearchGate

This model exists because of him. The concept, the ASTM D6433 framing that decides which classes the detector even has, and the domain judgement behind the taxonomy are his. So is the hand-measured field survey of 1,810 m of Yangizamon street in Tashkent β€” 12 sections, 9 distress types β€” that the PCI engine is validated against, and the funding for the GPU time that trained these weights.

A partner in the work, not a contributor to it.


A YOLO11 detector over eight road-surface distress classes, named and numbered to match ASTM D6433, the standard that turns a distress survey into a Pavement Condition Index. The point of matching the standard's taxonomy is that the detections feed the PCI calculation directly, without a lossy mapping step in between.

Code, the PCI engine, and the tiled-inference wrapper: https://github.com/uzbtrust/smart-road

The system running

Two minutes, unedited: a photograph graded against the standard, the severity range the answer occupies, the deduct chain that produced the score, tiled inference switched on and off, then a 15 s drive analysed frame by frame. The interface is in the repository.

Files

File Size What it is
yolo11l_640_best.pt 51 MB Use this. YOLO11-L @ 640, best epoch (113)
yolo11l_640_last.pt 51 MB Final epoch (143) β€” resume or fine-tune from here
yolo11m_1024_best.pt 41 MB YOLO11-M @ 1024, best epoch (31)
yolo11m_1024_last.pt 41 MB Final epoch (61)

Results

3,550 validation images, 9,680 boxes.

mAP50 mAP50-95 Precision Recall
yolo11l @ 640 0.657 0.401 0.682 0.604
yolo11m @ 1024 0.544 0.316 0.584 0.530

The L model wins on every measure and runs 2.6Γ— faster (2.2 ms vs 5.7 ms per image), which is why it is the recommended checkpoint despite the M model seeing twice the resolution.

Per class, yolo11l @ 640:

# Class ASTM β„– Train boxes mAP50 mAP50-95
3 patching 11 7,719 0.783 0.564
7 marking / manhole β€” 17,334 0.781 0.501
6 lane / shoulder drop-off 9 308 0.731 0.319
1 alligator crack 1 16,056 0.672 0.388
0 longitudinal & transverse crack 10 53,293 0.637 0.364
5 weathering / raveling 19 1,805 0.602 0.325
4 pothole 13 7,394 0.549 0.248
2 block crack 3 59 0.502 0.502

Reproduced independently on Apple silicon (MPS, torch 2.13) at mAP50 0.649 β€” 1.2 % below the CUDA figure, with per-class numbers tracking to within 0.01.

Known limitations

Two classes have too little data to judge. Block cracking has 59 training boxes and 2 in validation; lane/shoulder drop-off has 4 validation images. Their metrics above are noise, not measurements. Do not rely on either class.

Pothole is the weakest class that does have data, and it is the costliest place to be weak: ASTM's deduct curve for potholes is the steepest of the set, so an error there moves the final PCI further than an error anywhere else.

The checkpoint declares ten classes, not eight. It was trained against a data.yaml carrying two extra entries (edge_crack, bumps_and_sags) that have zero examples in both splits. Class indices 0–7 are correct and unshifted, but heads 8 and 9 were never trained and will fire on anything. Pass classes=range(8).

Domain gap on dashcam-style 4K. The training images are street-level photographs with a long side of at most 1,024 px. On 3840Γ—2160 survey footage the model under-detects, and feeding the whole frame at 640 is worse still β€” that is a 6Γ— downscale, and a crack four pixels wide in training falls below one pixel and vanishes. Use tiled inference (below).

Usage

from ultralytics import YOLO
from huggingface_hub import hf_hub_download

weights = hf_hub_download("uzbtrust/smart-road-pci-yolo11", "yolo11l_640_best.pt")
model = YOLO(weights)

results = model.predict("road.jpg", imgsz=640, conf=0.25, classes=range(8))

For imagery larger than about 1,024 px, tile it rather than resizing:

import numpy as np, PIL.Image
from smartroad.detect.tiled import detect_tiled, road_roi   # from the GitHub repo

frame = np.array(PIL.Image.open("frame_4k.jpg").convert("RGB"))
h, w = frame.shape[:2]
dets = detect_tiled(model, frame, tile=640, overlap=0.2,
                    roi=road_roi(w, h), classes=range(8))

Measured on our own survey frames: whole-frame inference returned 0 and 2 detections where the tiled path returned 2 and 4.

Training

Base yolo11l.pt
Image size 640
Batch 32
Optimiser AdamW, lr0=1e-3, lrf=0.01, cosine schedule, 3 warm-up epochs
Augmentation mosaic 1.0 (closed for last 10), mixup 0.1, copy-paste 0.3, HSV, scale 0.5, fliplr 0.5, erasing 0.4
Epochs 143 of 150 β€” early stopping, patience=30, best at 113
Hardware 1 Γ— RTX 5090 (32 GB), 17.8 h
Stack Ultralytics 8.3.155, torch 2.12.0+cu130, Python 3.12
Seed 1337, deterministic=True

patience=30 was too tight in hindsight: the best epoch was 113, so the curve was still improving well past epoch 100. close_mosaic also did not deliver its usual late boost β€” mAP50 went 0.655 β†’ 0.653 when mosaic switched off, while cls_loss fell from 0.929 to 0.678. The model fitted the training distribution harder without generalising better.

Data

Four public road-damage datasets merged into one ASTM taxonomy: 40,994 training and 3,550 validation images, 113,648 boxes.

Source Images
RDD2022 26,661
RDD2018 (Japan) β€” training split only 9,052
SVRDD (street view) 8,000
Attain (Tehran) 840

Published as uzbtrust/smartroad-yolo on Kaggle. Source datasets are CC BY-SA 4.0; the merged set carries the same licence.

Licence

AGPL-3.0, inherited from Ultralytics YOLO11.

Downloads last month
198
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Space using uzbtrust/smart-road-pci-yolo11 1

Evaluation results

  • mAP@50 on Smart Road unified road-distress set
    self-reported
    0.657
  • mAP@50-95 on Smart Road unified road-distress set
    self-reported
    0.401