简介:面向道路破损检测场景,这份资源提供了一套完整的YOLOv5检测方案,包含训练好的权重文件和7000多张真实场景标注数据集,可直接用于道路破损识别与模型二次训练。包体共147个文件,涵盖Python脚本、YAML配置文件、PyTorch权重(pt)、标注文件(xml/txt)、训练曲线图(jpg/png)以及Shell脚本和Docker配置,压缩包约443.6MB,目录结构清晰,便于按需调用。数据集包含D40、D44、D0、D20、D01、D11、D10、D50、D43、D0w0共10个破损类别,图片为jpg格式,同时提供VOC与YOLO两种标注格式,省去自行转换的麻烦。随包附带的PR曲线、loss曲线以及批次预测图,能直观反映模型训练效果与泛化能力,方便读者评估和调优。目前已有1081人学习下载,适合目标检测初学者快速上手,也适合工程开发者直接部署或迁移到自有数据集进行针对性训练。
1. 道路破损检测不是建个模型,而是把检测精度压到能落地的程度
道路破损检测是一个典型的中小目标检测场景。裂缝、坑槽、龟裂在整张巡检图里往往只占几个像素块,模型如果只学了公开数据中的形状特征,很难迁移到不同路面材质和拍摄角度。现在很多人手上有一套“训练好的权重”,但并不知道它是从哪里训练的、类别对应什么,也不清楚自己采集的道路破损检测数据集应该怎么切成训练集和验证集。这篇文章按“权重判断 → 推理 → 自己训练 → 精度调优 → 验证迁移”这条路线,把YOLOv5用于道路破损检测的全链路讲清楚,适合做过图像分类但刚接触目标检测、或者已经在用YOLOv5但卡在数据集质量上的读者。
2. YOLOv5模型选型:先吃透“训练好的权重”到底是什么
2.1 为什么道路破损检测默认选YOLOv5
YOLOv5在公开基准上的速度和精度不一定是最强的,但工程生态成熟,训练、导出、C++部署和TensorRT加速的资料齐全。道路破损检测有几个特性,正好落在这个模型体系的舒适区:破损目标相对于整个视野偏小,但不像遥感船舶那样极端微小,YOLOv5的PANet多尺度特征能兼顾中等特征层;坑槽、裂缝、修补这几类目标彼此之间形状差异大,类别数一般在4~6个左右,正适合YOLOv5在单阶段模型上的精度/速度平衡;巡检车辆上的Jetson等设备通常不跑Float32大模型,YOLOv5可以一键导出成fp16或int8的engine文件。对做市政路况巡检的团队来说,模型推理耗时往往比精度更难满足,YOLOv5的anchor-based方式在固定相机高度下反而比anchor-free更可控。
从迁移学习角度看,一个“训练好的权重”通常分两种:第一种是COCO预训练权重,模型已学会边缘、纹理、遮挡等通用视觉特征;第二种是道路破损数据集上微调过的权重,类别输出层已经被改成如D00、D10、D20这类自定义标签。拿到预训练权重后,冻结前10层会让训练速度更快,但道路破损数据集通常只有几千到几万张,完全重新训练成本高,一般做法是在COCO权重基础上保持backbone冻结5个epoch后放开全网络再训练。这里要特别强调的是,预训练权重中的类别数(COCO是80类,道路破损可能是4类)不会直接限制复用,YOLOv5在加载权重时会把不匹配的head参数丢弃,只保留backbone部分。
2.2 训练好的权重和前几个epoch对标关系
训练刚开始时,loss下降快不代表模型真的能稳定检测。权重的真正价值体现在两个时间点上:第1个是训练结束后按mAP选出的best.pt,第2个是后期过拟合开始时val loss回升但test mAP仍在爬的last.pt。很多刚接触YOLOv5的人只盯着总loss,实际训练自己数据集时应该同时看box_loss、obj_loss和cls_loss。道路破损数据集里“正常路面”负样本本身就很多,如果cls_loss持续下降但obj_loss停在0.8以上,说明模型倾向把裂纹区域和普通路面纹理混在一起,这时靠调权重没用,要回去看标注框边缘是否把非破损区域切进去了。所以拿到别人训练好的权重后,不要直接上线,先用少量自己路段图片做推理,观察置信度分布和框位置是否稳定。
2.3 数据格式:YOLO标签与你的标注平台
YOLOv5训练自己数据集要求标签是txt格式,每行代表一个目标:类别id、归一化后的中心x、中心y、宽、高。坐标以图像宽高为分母,不是像素值。道路破损检测数据集的标签常见有两种来源:一是RDD2022这类公开道路损坏数据,里面有D00(纵向裂缝)、D10(横向裂缝)、D20(龟裂)、D40(坑槽)等类别;二是LabelImg、X-AnyLabeling这类标注工具直接导出YOLO格式。两类来源混在一起时最容易出的问题就是类别映射不一致,有的数据集把index设成0开始,有的把坑槽放在第一类,你会看到一张裂缝图被训练成坑槽。该不该修改原标签?常见做法是把所有来源统一映射到同一个类别顺序,用一个python脚本重新生成标签。
以下转换脚本是统一类别映射的常用做法:
import os import glob from pathlib import Path # 源标签目录和目标标签目录 src_dir = Path('./raw_labels') # 原始yolo格式标签 dst_dir = Path('./labels_unified') dst_dir.mkdir(exist_ok=True) # 定义统一类别映射:原始类别id -> 新类别id mapping = { 0: 1, # D00 纵向裂缝 -> 新类别1 1: 0, # D10 横向裂缝 -> 新类别0 2: 2, # D20 龟裂 -> 新类别2 3: 3, # D40 坑槽 -> 新类别3 } for label in glob.glob(str(src_dir / '*.txt')): lines = [] with open(label, encoding='utf-8') as f: for line in f: parts = line.strip().split() if not parts: continue cls_id = int(parts[0]) if cls_id not in mapping: continue # 丢弃未知类别 x_c, y_c, w, h = parts[1:5] lines.append(f'{mapping[cls_id]} {x_c} {y_c} {w} {h}\n') out_file = dst_dir / Path(label).name with open(out_file, 'w', encoding='utf-8') as f: f.writelines(lines)这段脚本在转换时丢弃了没映射的类别ID,保证训练时类别不会错位。处理道路破损检测数据集时要额外检查的一点是框坐标是否越界,拍摄设备校正后会出现边角拉伸,有些标签的中心点会超出0~1范围,YOLOv5训练时会报错或者产生NaN权重。把宽容度放在0.01到0.99之间是很多生产工程里采用的过滤阈值,这个由你自己按标注工具实际输出为准,我一般直接在脚本里加一个if not (0 <= x_c <= 1 and 0 <= y_c <= 1): continue,同时把只有1像素宽的目标忽略掉。
3. 装在机器上第一次推理:用训练好的权重跑通单张/视频
3.1 最小环境与依赖安装
YOLOv5官方仓库可以直接用git clone拉取,PyTorch环境兼容度比较高,GPU和CPU都能跑,差别只在推理耗时。先准备好Python环境,常见做法是直接用conda建一个独立环境,避免系统里的OpenCV版本把依赖链搞乱。
git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt如果机器是纯CPU环境跑推理,从官方源安装torch要慢一些,但没关系,detect.py会自己检测设备类型。用训练好的权重文件时,唯一要记住的是把权重文件放好位置,比如放在weights/目录下面。requirements.txt里装完的依赖包括opencv-python、matplotlib和pandas,这几个库会在画检测框和分析统计结果时用到。装完后可以先用仓库自带的yolov5s.pt跑通demo,再换成道路破损检测权重;先用简单的验证环境是否正常是目前工程里最稳妥的排除法,不然权重、数据、环境三个变量混在一起,出了错很难定位。
3.2 用detect.py做推理
单张图片推理是验证训练好的权重最直接的一步。下面是针对道路破损检测场景的完整命令:
python detect.py \ --weights weights/road_damage.pt \ --source ./test_images/road_001.jpg \ --conf-thres 0.25 \ --iou-thres 0.45 \ --img-size 640 \ --device 0 \ --save-txt \ --save-conf主要参数功能如下:
| 参数 | 作用 | 道路破损场景建议 |
|---|---|---|
--weights | 权重文件路径 | 优先用best.pt而不是last.pt |
--source | 图片、目录或视频路径 | 同一路段不同时间照片 |
--conf-thres | 置信度阈值 | 巡检观望0.25,落地报告0.4 |
--iou-thres | NMS交并比阈值 | 裂缝重叠用0.45 |
--img-size | 输入尺寸 | 640;小目标多可以试960 |
--save-txt | 保存检测标签txt | 后续做统计需要 |
--save-conf | 在txt里输出置信度 | 配合计算精确率时需要 |
注意,--img-size在YOLOv5中默认对宽和高做等比缩放补灰边,如果巡检相机是横向宽幅图像,直接把img-size拉到1280可能显存不够。更合理的做法是先看标注框实际尺寸,裂缝目标高度只有8像素时,640下已经退化到1~2个像素了,这时应该考虑切片后检测,而不是一味加大分辨率。命令执行完会在runs/detect/exp下生成带框的图片和txt文件,只看到框位置还不够,要去核对框的坐标是否紧贴裂缝延伸方向,很多情况是框上下边沿超出了真实损伤范围,后面微调权重时重点看这些误差。
3.3 用输出的冷门信息判断权重来源
推理完成后,YOLOv5会生成标注的目标类别和置信度。道路破损检测权重最怕的是“什么都能框,但类别全错”。判断方法很简单:把验证集里同一张图分别用--conf-thres 0.1和--conf-thres 0.7跑一遍,低阈值时如果出现大量同一位置重复框,说明NMS或anchor尺寸和你的目标分布不匹配。低阈值跑出来的框数量如果是正常情况的三倍以上,那这个权重很可能是在数据分布差异很大的路段上训练的。做道路破损检测项目时,第一次拿别人提供的权重,我会先跑20张图,用终端输出的FPS和框数量做一个表,框数量突然变化的时间点往往就是路面材质变化点,比只看整体mAP更能暴露过拟合问题。
4. 自己训练一套道路破损检测模型:数据集、标注、训练
4.1 数据集目录结构与样本平衡
把道路破损检测数据集组织成YOLOv5结构时,最简单可靠的方式是:
dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml文件夹名字不要乱改,尤其别把train/写成training/,YOLOv5读取时依赖固定名称。从巡检视频抽帧时,相邻帧之间高度相似,直接按顺序切分会把同一路段重复画面同时放进训练集和验证集,导致模型看起来效果很好,换到新路段马上掉点。常见的做法是按时间或路段ID划分,比如把前三个路段的视频放到train,第四个路段放到val,而不是随机抽帧。道路破损数据的类别分布天然不均衡,坑槽往往只占5%,这时如果总数只有2000张,做样本重采样比直接堆数据更有效。
数据量方面,在一线做项目时我发现:用COCO预训练权重起步,训练自己数据集最少也要每类300张以上,才能让类别特征不跟背景纹理混淆。如果某个类别少于100张,就不要单独做一类。把D00和D10合成“裂缝”类别,D40保留为“坑槽”,效果通常比大类硬扛好。数据泄露问题尤其要小心,公开道路破损检测数据集里同一条路的不同图像经常来自连续视频帧,单凭文件名前缀去划分数据集需要考虑是否有同一路段重复取景,不看采集时间戳就容易把验证集做成朋友圈式自嗨。
4.2 把公开数据集转换成YOLO格式并制作data.yaml
RDD2022这类数据集原始标注是PASCAL VOC的xml格式,要转成YOLO格式的txt。下面是一段简化的转换函数:
import xml.etree.ElementTree as ET from pathlib import Path def convert_voc_to_yolo(xml_path: str, output_path: str, class_names: list, width: int, height: int): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in class_names: continue cls_id = class_names.index(name) bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) x_c = (xmin + xmax) / 2 / width y_c = (ymin + ymax) / 2 / height w = (xmax - xmin) / width h = (ymax - ymin) / height lines.append(f'{cls_id} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}\n') Path(output_path).write_text(''.join(lines), encoding='utf-8')转换函数里width和height是图像原始尺寸,很多公开标签里不会写全,必须从图像文件读取,不然坐标归一化直接错掉。然后创建data.yaml:
train: ./dataset/images/train val: ./dataset/images/val nc: 2 names: ['crack', 'pothole']names顺序要和转换脚本中的class_names一致。如果你从网上下载了别人的道路破损检测数据集,txt标签本身已经是YOLO格式,但类别顺序可能完全不同。这时候改data.yaml里的names顺序,比写脚本改txt更省事,前提是你确认原数据集的类别编号。有一个实际工程里经常踩的坑:有些公开数据集把多个损坏类别合并成一个大类,比如把“修补”和“坑槽”归为damage,转换脚本前要先用一张图核对框边界是否贴合。
4.3 YOLOv5模型配置与训练命令
模型结构建议先用yolov5s或yolov5m,能用yolov5s解决的不要直接上yolov5x。道路破损目标不大,但单张图中目标数量并不密集,这个场景下小模型的漏检更多来自特征层,不是模型容量。yolov5s配置文件里的nc要改成你自己的类别数,同时可以调整anchor参数。训练命令参考:
cd yolov5 python train.py \ --data dataset/data.yaml \ --cfg models/yolov5s.yaml \ --weights yolov5s.pt \ --batch-size 16 \ --epochs 200 \ --img-size 640 \ --hyp data/hyps/hyp.scratch-low.yaml \ --device 0 \ --name road_damage_run参数说明:--weights yolov5s.pt用的是COCO预训练权重,类别数不同没关系,YOLOv5会自动丢掉最后一层类别权重。--hyp在这个场景里我一般选hyp.scratch-low.yaml,augmentation低一点,因为道路破损的裂缝纹理很容易被过度增强弄丢;如果你用hyp.scratch.yaml默认增强,训练中会出现大量“裂缝消失”的假负样本。如果只有一块8G显存的GPU,--batch-size可以降到8,同时把--img-size从640降到512,否则训练会直接OOM。训练过程中的results.png会动态更新,里面包含box_loss、obj_loss、cls_loss、mAP@0.5和mAP@0.5:0.95五个重要曲线,需要重点观察mAP@0.5:0.95在最后50个epoch是否还在上升。过早停在一个看起来已经收敛的loss上,道路破损检测很容易丢失对细长裂缝的召回。
4.4 训练后权重处理与数据类别映射
训练结束后在runs/train/road_damage_run/weights/里会生成best.pt和last.pt。落地时用best.pt,继续训练时用last.pt,这条规则在YOLOv5训练自己数据集过程中通用。把best.pt转成TorchScript或ONNX来部署前,要再用验证集跑一次,把输出的标签类别与你自己的标注类别对应起来。如果训练时用2类,部署端还按原来4类解析,会出现类别错位。给推理侧团队的类别表里要写清楚0: crack还是0: D00,这个细节最容易在交接时造成事故。把权重给其他同事时,最佳实践是连同data.yaml一起发出去,没有这份文件,对方无法知道权重对应类别,正常推理都会出错。
5. “检测不准”不是模型差,是数据平衡方法没用对
5.1 先分辨“漏检”来自前景还是背景
“检测不准”说起来是模型问题,拆开看往往是数据问题。漏检有两种类型,第一种是真实破损区域完全没有框,第二种是框只框住了裂缝的一半。前者通常因为背景太杂乱,比如路上有树叶阴影或其他纹理;后者则多因为标注框本身的边界不一致。YOLOv5训练自己数据集的过程中,最简单的诊断方法是按置信度倒序输出验证集结果。置信度低的框如果集中在同一种路面材质,说明需要把这种路面场景更多的图片补充到数据集里。置信度高的框如果总是比实际目标大,说明anchor没有根据实际目标尺寸重设。和分类模型不同,目标检测里的False Negative很难只靠调高置信度阈值消除。
5.2 数据增强和重采样的落地做法
对于道路破损检测数据集,增强策略里要少用大幅度旋转。裂缝具有方向性,90度旋转后原来的横向裂缝和纵向裂缝在视觉上容易混淆,YOLOv5自带的增强里fliplr和flipud也建议只保留水平翻转。如果你把hsv_h调高,路面颜色变化太剧烈会让模型依赖颜色而不是纹理,这类数据增强在道路破损场景里弊大于利。常见做法是用马赛克增强提高小目标的上下文,但把mosaic概率从默认1.0降到0.5左右,训练后期再用关掉mosaic的验证模式跑一遍。数据不足时不要无限增强,更有效的是把同一破损目标从不同视频帧里裁出来放进背景替换的负样本库。类别不平衡的简单解决办法是给稀有的坑槽样本多复制几次,但复制过多会导致过拟合,权重泛化能力弱。更稳妥的办法是按类别比例过采样,让batch中每个类别的样本数量不低于1/3。--hyp文件里的cls_pw参数可以调整类别损失的权重,提高cls_pw会加大稀有类别在损失函数里的占比,但它不能代替足够多的标注样本。
5.3 用小目标切片和分块推理找回裂缝
道路破损检测中很多裂缝在整张巡检图上只有十几像素,单靠加大输入分辨率很吃显存。正规的工程办法是先用切片推理:把原图切分成512x512的重叠块,每个块单独送入模型,把检测结果映射回原图坐标后做NMS合并。这样做的好处是裂缝、坑槽等小目标尺寸在原图占比变大,模型能生效。缺点是重叠部分会让同一目标被重复检测,合并时要用较低的NMS阈值。切片尺寸往往需要和训练时的输入尺寸一致,如果训练时用的640,推理切片用512会让物体尺寸比例偏移。将重叠率设置在15%左右是相对稳妥的起点。凡是提到“道路破损检测数据集”时,还要和后端的运营数据关联起来。单张图片的检测框不能直接用来判断道路损坏等级。后面应该加一个跟踪逻辑:同一位置多帧检测框置信度做均值,连续几帧都出现的坑槽才上报。YOLOv5单帧检测可以用于实时巡逻,但用于维修派单时,必须结合位置坐标和时间序列,否则路面反光造成的误检会夹在报告里。
6. 验证模型迁移效果时最值得做的3个动作
6.1 看你拿到的“训练好的权重”能不能直接用于当前路段
当更换城市或路面材质时,先不要重新训练。在本地用100张新场景图片做一次小规模评估。所谓“小规模评估”不是只看mAP,而是把结果按类别和距离分组。将图片按拍摄距离分成近距、中距、远距三组,再按光照分成白天/阴影两组,跑一次完整推理,统计每组的平均置信度。这个方法可以帮你在不标注新数据的情况下,定位当前权重在哪些场景下失效。
python detect.py --weights weights/road_damage.pt --source ./new_city/ \ --conf-thres 0.05 --save-txt --save-conf --project ./eval_newcity配合一个简单的bash脚本统计检测框数量与平均置信度,可以快速看出远距组是否全部在阈值以下。如果远距组全部漏检,说明当前模型训练数据里远距离负样本不足。这时你有两个选择:一个是收集并标注更多远距离数据继续微调;另一个是在推理侧增加切片逻辑,让远距离目标放大后再进入模型。选用“切片”或者“微调”的判断依据是,切片后能否明显恢复原先漏检的召回率,若能,说明模型特征是完整的,只是输入分辨率不够,此时优先改推理流程,微调的收益反而有限。
6.2 用混淆矩阵检查类别标签的错位
YOLOv5训练完会在runs/train/*/confusion_matrix.png生成混淆矩阵。道路破损检测数据集里,坑槽和龟裂在视觉上容易互相混淆,行人、井盖、路边栏杆经常被误判成破损。未标注目标被检测出来时会落到background列,这个列的值越高,说明模型学到的“破损”和“正常交通要素”的区别还没建立好。在这个阶段增加新类别没有用,应该人工检查漏检的样本,把引起误检的树干阴影、修补带框进训练数据,并让模型学会它应该忽略的东西。最后一步可以输出每一类精确率和召回率,而不是只看mAP;这对市政上报业务尤其重要,因为上报一次误检也要付出复核成本。
6.3 对权重做AB切换验证,而不是只改阈值
当发现旧的训练好的权重在某些场景下误检多时,最常见的操作是调低--conf-thres,把高置信度误检当成正常。这种操作会压制召回率,最终结果是OK但漏了不少早期裂纹。与其调阈值,不如把旧权重在难题集上的输出结果导成label,用这个label做伪标注,再训练一个新权重,形成旧模型指导新模型的迭代循环。这个“权重迭代”做法不是每次训练都从头开始,而是把上一版本的模型权重放在--weights里继续微调。将验证集固定不变,每次迭代后对比mAP曲线,比在已有模型上调参数更可靠。
最终的交付物不只是best.pt,而是一份记录数据来源、类别映射、训练参数和验证集划分的说明。再好的权重,如果没有配好类别映射和目标场景描述,换一个环境后大概率会被重新推倒。对于道路破损检测这种现场数据变动频繁的项目,持续记录每次数据更新后的模型变化,比追求最后那0.5%的mAP更有实际价值。
本文还有配套的精品资源,点击获取