☰
道路坑洞目标检测数据集:VOC与YOLO格式转换及训练避坑指南
2026/9/28 1:18:33 网站建设 项目流程

简介:这份道路坑洞目标检测数据集聚焦路面病害识别场景,共包含665幅真实道路jpg图像,类别统一为pothole,并提供Pascal VOC与YOLO双格式标注文件,适合刚接触目标检测的学习者快速体验从数据组织到模型训练的全流程,也能用于道路养护、自动驾驶辅助感知等方向的前期算法验证。压缩包内含2000个文件,以txt标签、xml标注和jpg图像为主体,图像与标注一一对应;所有框均由labelImg按矩形框规则完成,pothole类别合计1740个标注框,可为训练提供可靠的监督信号。整包仅23.87MB,下载与迁移都很轻便,尤其适合在本地或云环境快速跑通检测基线。目前已有408人学习下载,数据格式规范、目录结构简洁,既便于直接输入YOLO系列训练,也可随时转换成其他检测框架所需格式,省去大量标注与格式转换时间,是路面坑洞检测项目起步阶段的实用数据集。

1. 665张单类别道路坑洞数据集:为什么它够用,又为什么有人翻车

道路坑洞检测这几年被反复拿出来做,场景倒是很明确:市政养护、路面巡检、自动驾驶的路面预瞄,甚至外卖配送三轮车的避震系统都想前置一个“前面有坑”的提醒。做这类任务最卡人的不是模型选型,而是数据本身。市面上公开的路面破损数据集要么是实验室内的小样张,要么是无人机俯拍,跟装在车上平视路面的真实分布差得太远。而标题里这个“道路坑洞目标检测数据集VOC+YOLO格式665张1类别.zip”,本质上是把采集好的路面图片整理成了两种主流标注格式,单类、数量不大,但胜在干净、好上手、能用标准工具链直接吃进去。

665张单类别到底够不够用?这要看你对“够用”的定义。如果目标是拿去做施工验收那种拍脑袋判断,随便一个分类器都能过。但如果是要在真实路面视频里把坑的位置框出来,665张作为冷启动基线完全够,作为最终模型则多半要配合数据增强和自采数据迭代。很多人在这个数据上翻车,不是因为图片少,而是因为根本没搞清楚VOC和YOLO两种格式的对应关系——训练时要么读到一堆空白标签,要么类别对不上。这篇就把格式拆开、把训练跑通、把常见坑填平。

2. 拆开 .zip 看两种标注格式:VOC 的 XML 和 YOLO 的 TXT 如何一一对应

拿到zip的第一反应不要是直接解压扔给训练脚本。先看一眼目录结构,能省掉后面至少两小时的排错时间。这类数据集的常规组织方式是 images / labels / annotations 三件套,或者简单粗暴地一个images一个labels,VOC的XML和YOLO的TXT同时给你,等于把“换工具链”的成本提前付掉了。

2.1 文件结构与命名规则

常见文件结构长这样:

pothole_dataset/ ├── images/ # 全部原始图片 │ ├── 001_pothole_01.jpg │ ├── 001_pothole_02.jpg │ └── ... ├── annotations/ # VOC格式的XML标注 │ ├── 001_pothole_01.xml │ ├── 001_pothole_02.xml │ └── ... └── labels/ # YOLO格式的TXT标注 ├── 001_pothole_01.txt ├── 001_pothole_02.txt └── ...

命名规则是整个数据集最容易出猫腻的地方。理想情况下每张图对应一个同名的 .xml 和一个同名的 .txt,图片是 .jpg 那标注后缀也要跟着同名。有些打包方会用不一样的前缀,比如图片叫 road_001.jpg,XML却叫 001.xml,这种错位在训练时不会立刻报错,而是表现为“图像找不到标签”或者“这个batch的loss异常低”。

拿到手先做一步:数一下三种文件的数量是否一致,检查有没有孤儿文件。

# 在数据集根目录下执行 ls images/*.jpg | wc -l ls annotations/*.xml | wc -l ls labels/*.txt | wc -l

如果三个数字不一致,别急着训练,先找出多出来的那几张图,多半是标注时漏标了。漏标的处理方式不是删图,而是给一个空标签文件——稍后会在避坑部分展开。

2.2 同一张坑洞图的两种标注长什么样

打开同一张图的XML和TXT,你会看到两种完全不同的信息组织方式。VOC格式把一张图里所有的目标对象嵌套在<annotation>根节点下,每个<object>里带类别名和矩形框的四个像素坐标。YOLO格式则是一行一个框,五个数字分别是类别ID、归一化的中心点x、中心点y、归一化宽度、归一化高度。

拿一张 1920x1080 的图,坑洞左上角在 (510, 380),右下角在 (690, 540) 举例,VOC的XML片段:

<annotation> <folder>images</folder> <filename>001_pothole_01.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>pothole</name> <bndbox> <xmin>510</xmin> <ymin>380</ymin> <xmax>690</xmax> <ymax>540</ymax> </bndbox> </object> </annotation>

同一目标在YOLO格式的TXT里就是一行:

0 0.3125 0.4259 0.09375 0.1481

类别ID这里写成0,对应的是类别列表里的第一个类,也就是 pothole。四个小数分别是归一化后的中心坐标和宽高。之所以要归一化,是因为YOLO训练时会把图缩放到固定尺寸(比如640x640),如果标注存的是绝对像素,缩放后坐标就全错了。

2.3 从VOC XML到YOLO TXT的换算公式与边界坑

如果数据集只给了VOC格式,你需要自己做转换。公式不复杂,核心是四行:

# 假设 xmin, ymin, xmax, ymax 是整数像素坐标,W, H 是图像宽高 x_center = (xmin + xmax) / 2.0 / W y_center = (ymin + ymax) / 2.0 / H box_w = (xmax - xmin) / W box_h = (ymax - ymin) / H

换算本身不坑,坑在两个地方。第一个是 xmax 用不用加1。VOC标注里,矩形框是像素的包裹范围,像素坐标从左上角开始计,所以严格的框宽度应该是xmax - xmin + 1。这个 +1 在1920宽的大图上影响微乎其微,但对那种只有40像素宽的小坑洞,差值能达到2.5%。不少人转完格式后发现mAP总差一点,查半天就是这里。

第二个坑是归一化后的数值精度。TXT里的浮点数建议保留6位小数,直接拿python的字符串格式化%.6f就行。别用print(x_center)默认输出,默认精度在某些情况下会截断到小数点后4位,对小目标来说等于给标签加了噪声。

还要确认类别ID从0开始还是从1开始。YOLO官方的要求是0-based,也就是唯一的类别写0。一些野路子转换脚本习惯从1开始,对应到训练配置里names: {0: 'pothole'}就完全对不上,表现为训练loss正常但预测结果全是空框。

3. 把665张数据喂给YOLO训练:数据划分与YAML配置的实操套路

格式理顺了,下一步就是把数据切成训练集和验证集,写好YAML,启动训练。这一步的细节决定你整个训练过程是丝滑还是反复重启。

3.1 数据划分脚本:8:1:1还是9:0.5:0.5

665张图不多,划分比例上建议训练集:验证集取9:1,测试集可以不要。理由很实际:单类检测模型的验证集主要用于看loss曲线和精度的动态变化,不需要像多类别那样留额外的测试集做类别均衡评估。如果你打算做超参对比,验证集还能临时充当测试集。真到了要出报告的时候,再单独留一份现场采集的图做盲测更靠谱。

import os import random import shutil random.seed(42) image_dir = "pothole_dataset/images" label_dir = "pothole_dataset/labels" # 建输出目录 for sub in ["train/images", "train/labels", "val/images", "val/labels"]: os.makedirs(os.path.join("pothole_split", sub), exist_ok=True) images = sorted(os.listdir(image_dir)) random.shuffle(images) val_count = int(len(images) * 0.1) val_images = images[:val_count] for img in images: stem = os.path.splitext(img)[0] src_img = os.path.join(image_dir, img) src_txt = os.path.join(label_dir, stem + ".txt") if img in val_images: dst_img = f"pothole_split/val/images/{img}" dst_txt = f"pothole_split/val/labels/{stem}.txt" else: dst_img = f"pothole_split/train/images/{img}" dst_txt = f"pothole_split/train/labels/{stem}.txt" shutil.copy(src_img, dst_img) shutil.copy(src_txt, dst_txt) print(f"train: {len(images) - val_count}, val: {val_count}")

这段脚本做了三件事:固定随机种子、按9:1切分、把图片和同名标签一起拷贝到新目录。固定种子这步很多人嫌麻烦,但它在复现实验时极其重要——同样的参数跑两次,如果切分集合不同,结果差0.05个mAP都是正常的,到时候你根本分不清是调参效果还是数据分布变化。

3.2 YOLOv8的YAML配置和路径坑

切分完成后,写一个 YAML 配置文件指向这些目录。用 YOLOv8 作演示,YOLOv5 的字段也兼容,只是路径写法略有差别。

path: /home/yourname/pothole_split # 改成你的绝对路径 train: train/images val: val/images names: 0: pothole

三个字段别写错。path是数据集根目录,train和val是相对path的图片目录,YOLO会自动去找同级的 labels 目录——也就是你把 images 和 labels 并存于同一父目录下,它不需要你显式声明labels路径。这里最常翻车的点是path写成相对路径。YOLOv8训练时如果工作目录不在数据集旁边,相对路径会解析失败,报AssertionError: train: ... does not exist,看着像数据丢了,其实是路径解析问题。直接用绝对路径最稳。

关于类别ID,如果你的TXT里写的是0,那names的第一项必须是pothole。如果TXT里是1,names里就得放一个占位的background或空字符串在0的位置上。这点在避坑章会再展开。

3.3 训练启动命令与640分辨率下的锚框问题

启动训练就一行:

yolo detect train data=pothole.yaml model=yolov8s.pt epochs=150 imgsz=640 batch=16 device=0

几个参数说明一下。model=yolov8s.pt是加载COCO预训练权重复训,不是从零开始。对665张的小数据来说,从零初始化的收敛速度和精度都会差很多。imgsz=640是YOLO系列一贯输入尺寸,不要轻易改到320或1280。320对小坑洞太不友好,1280则直接把显存需求翻四倍,训练时间也跟着翻倍。batch=16是综合考虑显存后的默认值,如果你的卡是8G显存,降到8。

锚框在这里不需要手动设置。YOLOv5时代还要跑kmeans_anchor算自定义锚框,YOLOv8已经是anchor-free的检测头设计,锚框玄学基本退场了。不过坑洞这种目标有个特点——它宽高比的分布不像行人和车辆那么固定,路面视角下有的坑是细长裂缝,有的是近圆形,所以如果你的模型对长条坑漏检严重,先别怀疑锚框,先查输入尺寸和目标像素大小。

4. 从0.6到0.9:调参、迁移学习和数据增强

模型跑起来不代表模型好用。单类、665张,前期的主要矛盾是过拟合:训练集loss掉得很快,验证集mAP在0.6附近打转。这章讲怎么用现有手段把精度往上推。

4.1 预训练权重怎么选、要不要冻结主干

谈YOLO训练必谈预训练。905万张带标签的COCO预训练权重对坑洞这种专业场景,真正的价值在于它已经把“边缘、纹理、深浅色块”这些底层特征学好了。你在这665张坑洞图上要做的是把输出头重新校准到“路面上的黑色/深色凹坑”这个语义上。

实操中我倾向于这样:前20个epoch冻结主干,只训练检测头,然后再解冻全模型微调。原因很直接——道路坑洞图像的背景(柏油路、水泥路、裂缝纹理)和COCO里的日常场景差异极大,如果一开始就全量训练,yolov8s这种小模型很容易被背景里的纹理细节带偏,出现训练集loss很低、验证集不动的症状。冻结主干的做法相当于让检测头先学会在COCO特征上找坑洞,再松开主干去适应路面纹理。

# 以ultralytics为例,在训练脚本里做两段式 from ultralytics import YOLO model = YOLO("yolov8s.pt") # 第一阶段:冻结backbone,只训head model.train(data="pothole.yaml", epochs=20, imgsz=640, batch=16, freeze=10, # yolov8s主干10层 name="pothole_stage1") # 第二阶段:解冻全模型,小学习率微调 model.train(data="pothole.yaml", epochs=80, imgsz=640, batch=16, lr0=0.0005, name="pothole_stage2")

注意第二阶段里我把lr0调到了0.0005,比默认的0.01低了一个量级。这是血泪经验:前期已经收敛得差不多,突然解冻主干并恢复默认学习率,很容易把主干里学好的底层特征冲掉,验证集mAP不升反降。

4.2 数据增强策略对坑洞场景的作用

YOLOv8默认开启mosaic增强,对665张的小数据集来说,mosaic几乎是必需品。它把四张图拼成一张,等于变相扩大了训练样本的多样性。但坑洞场景有个特殊点:坑洞一般分布在地面中下部,mosaic随机拼接后,坑洞可能出现的位置被强行搬到画面上方或边缘,和真实路况分布不一致。这个偏差在训练早期帮助很大,在后期反而会拖后腿。所以一个常见的做法是训练后半段关闭mosaic。

yolo detect train data=pothole.yaml model=runs/detect/pothole_stage2/weights/last.pt \ epochs=30 imgsz=640 batch=16 lr0=0.0003 mosaic=0.0 name=pothole_finetune

mosaic=0.0就是关掉。另外两个值得调的参数是hsv_h和degrees。路面图像颜色单调,HSV色相抖动给不了太多增益,反而可能把柏油路的颜色弄成奇怪的紫色,干扰模型对“正常/坑洞”的颜色判断。旋转角度也别开太大,路面上的坑洞在鸟瞰或平视视角下基本都是水平或近水平的,你把它旋转±30度训练,等于在教模型认识一些现实中不存在的姿态。

4.3 训练日志怎么读,哪些loss崩溃是翻车信号

训练日志里一堆指标,新手很容易只看最后的mAP。实际上中间那些loss变化才暴露问题。

看三组关键数据:box_loss、cls_loss、dfl_loss,以及验证集的对应项。正常情况是三者单调下降,到中后期进入平台期。如果出现下面两种异常,基本可以断定有问题:

第一种,cls_loss训练中后期突然反弹然后振荡。原因多半是数据里混入了错误的类别标注——比如某几张图的TXT里类别ID写了1而不是0。解决方法是重新跑一遍标签检查脚本。

第二种,box_loss降不下去,一直卡在1.4以上。这时候去翻训练前几个epoch的图片样本,看你标注的框是不是有大面积错位。坑洞的边缘在画面上往往不明显,光照不良时人眼都会标偏几个像素,这个偏差会导致box_loss下不去。

还有个常见误区:训练时盯着loss曲线看,看到它降到接近0就认为模型好。其实针对小数据集,训练loss接近0往往是过拟合的第一信号,判断标准是val的box_loss是否同步下降。val loss不掉反升,就是过拟合,马上停掉,回到4.1的冻结策略或加数据增强。

5. 避坑:道路坑洞数据集最常见的5个坑

单类数据集看着简单,实际跑起来能翻车的点一个不少。下面这五条是按出现频率排的,每条都是现象和原因讲清楚,给出直接能落地的解决办法。

5.1 空标签文件导致的loss异常

现象:训练loss在某几个epoch突然跳变,验证集mAP在0.3左右飘忽不定,且日志里没有显式报错。

原因:数据集中有若干张图对应的TXT文件是空的。这类文件产生的原因是标注时偶然打开又没有保存,或格式转换脚本遗漏。空标签在YOLO训练里不会报错,它把这个batch的loss贡献变成0,让模型的梯度方向被其他样本左右,训练噪声被放大。

解决:训练前扫一遍所有TXT的行数。

find pothole_split/train/labels -name "*.txt" -empty | wc -l

统计出空文件数量。空文件不能直接删,因为对应的图片还在训练集里,模型会拿一张无目标图硬学。正确处理是保留图片但把空TXT补上“不存在目标”的语义——对单类数据集最简单的办法是把这张图分到验证集而不是删除,因为YOLO的验证集允许图片没有标签。

5.2 类别编号不一致,模型永远输出空框

现象:训练过程正常,loss下降也顺利,但用训练好的模型做预测,置信度不高、框的位置全偏,甚至一个框都出不来。

原因:数据集里的TXT是别人用脚本转的,类别ID从1开始,而你的YAML里names只有下标0。比如TXT里写1 0.35 0.42 0.1 0.1,模型会认为这是类别1,但配置里没有类别1,于是推理时对这个目标直接按背景处理。还有一种情况是多个类别ID混用,这通常是原始标注文件拼接造成的。

解决:写一段检查脚本,统计所有TXT里第一列数字的集合。

cat pothole_split/train/labels/*.txt | awk '{print $1}' | sort | uniq -c

如果输出的ID包含除了0之外的值,要么改TXT,要么在names里补上相应的类名。注意改TXT的时候要全量替换,别只替换训练集漏了验证集。

5.3 图片和标签文件名错位

现象:训练日志每个epoch都提示WARNING: found x missing images,或者某个epoch的loss明显偏低,验证集mAP波动剧烈。

原因:之前的脚本按文件名排序后硬切,但数据集内部图片命名是1.jpg、2.jpg……10.jpg,字典序排序会变成1、10、100、2、20。标注文件序列和图片序列顺序不同,拷贝时错位,导致一部分图配了别人的标签。

解决:别依赖人眼核对,写脚本用文件名做key,逐个对比图片和标签是否存在。

for img in $(ls pothole_split/train/images/); do name="${img%.*}" if [ ! -f "pothole_split/train/labels/$name.txt" ]; then echo "missing label: $name" fi done

跑完如果输出为空,才表示该目录下图片和标签是严格对应的。这个坑在批量整理多个来源的数据时特别常见,切分脚本里加一段同样的校验逻辑能省很多事。

5.4 EXIF旋转导致标注框错位

现象:训练loss曲线正常,但预测时模型给出的框位置比实际坑洞位置偏了大约90度或180度。

原因:手机或部分行车记录仪拍摄的照片带有EXIF方向信息,图片本身存的是倒着的,显示时由软件自动转正。标注工具在显示时看到的是正向图片,保存的坐标也是正向的,但训练框架读图时如果直接用原始像素,不理会EXIF方向,模型看到的图是倒的,标签坐标也跟着错。YOLO的数据加载默认不做EXIF矫正。

解决:训练前统一转正。

# 用ImageMagick批量转正 for img in pothole_dataset/images/*.jpg; do convert "$img" -auto-orient "$img" done

注意转正之后要重新生成一遍标签,因为像素坐标已经变了。如果不想重标,更省事的办法是训练前先确认所有图片的EXIF方向一致。大部分车载相机的原始输出不带旋转标记,但混杂来源数据集里很容易混入手机照片。检查方法:

identify -verbose pothole_dataset/images/001_pothole_01.jpg | grep Orientation

输出为Orientation: TopLeft表示无旋转,其他值都要处理。

5.5 小目标漏检:标注框太小和imgsz的关系

现象:大坑洞检测得不错,但远处的小坑洞全漏,验证集mAP@0.5和mAP@0.5:0.95的差距越来越大。

原因:坑洞类别的目标尺寸跨度极大。近距离的大坑可能占画面三分之一,远距离的可能只有30x30像素。当输入缩放到640x640后,30x30的坑缩成了约10x10,特征图上一两层就把它抹平了。

解决:优先在保持宽高比前提下裁切,而不是直接resize全图。或者把imgsz从640提到960,显存允许的话这是最直接的解法。另外要看标注统计分布:

python -c " import os sizes=[] for f in os.listdir('pothole_dataset/labels'): with open(os.path.join('pothole_dataset/labels',f)) as fp: for line in fp: _, cx, cy, w, h = map(float, line.split()) sizes.append(w*h) print('min:', min(sizes), 'max:', max(sizes), 'mean:', sum(sizes)/len(sizes)) "

如果最小框面积占比低于0.02,说明确实有大量小目标,这种数据去做检测不能只看整体mAP,要单独统计小目标这个子集的AP。YOLOv8的验证输出里本身就按尺寸拆分了metrics,训练完记得看small那一列。

6. 让这665张值回票价:验证集的三种用法和一个经验技巧

数据集本身是死的,怎么把它的价值榨干才是重点。这里说三种验证集的高级用法,以及一个我在单类检测里特别依赖的经验技巧。

6.1 验证集的三种用法

第一,拿验证集当“临时测试集”做置信度阈值的调优。mAP是模型在所有置信度下的匀速表现,但工程上你需要的是一个确定的阈值。用训练好的模型跑验证集预测,输出每张图的置信度和框,然后扫一遍0.2到0.6之间的置信度阈值,找到精确率和召回率交叉点,那个值就是部署时的默认阈值。这个操作会让误报率直观下降。

第二,验证集做早停的裁判。不看训练loss,只看验证集mAP@0.5:0.95,连续20个epoch没有提升就截断训练。665张的数据训练到150轮左右基本平台期,再硬train只会过拟合。

第三,用验证集做“回归测试”。每次调参改代码后,固定验证集重训或重推理,对比新旧mAP。长期迭代的项目里,这个固定验证集就是你的后悔药——防止越改越差而不自知。

6.2 网格化裁切这个经验技巧

单类检测最烦的是小目标和密集目标叠在一起,YOLO的全局预测对这类问题天生吃亏。我的做法是推理阶段把原图按50%重叠切成四块,每块分别预测,再把框坐标映射回原图合并。坑洞数据集因为目标集中在路面中下方,这种方法能显著提升小坑召回。

代码不复杂,核心是记住裁切后坐标要加回偏移量:

import cv2 from ultralytics import YOLO model = YOLO("runs/detect/pothole_stage2/weights/best.pt") img = cv2.imread("road_001.jpg") H, W = img.shape[:2] crop_size = 640 # 和训练尺寸一致 # 50%重叠:x方向步进320 for y in range(0, H, crop_size // 2): for x in range(0, W, crop_size // 2): crop = img[y:y + crop_size, x:x + crop_size] results = model.predict(crop, conf=0.25) for box in results[0].boxes.xyxy.cpu().numpy(): # 坐标还原到原图 x1, y1, x2, y2 = box print(f"pothole at ({x1 + x:.1f}, {y1 + y:.1f})")

注意最右和最下的边缘裁切会超出图像边界,需要做min(x + crop_size, W)的截断。这个技巧不是YOLO官方的标准姿势,但对单类小而多的检测任务非常有效。

说到最后,想起之前做路面巡检项目时的一个教训:当时拿到类似数据直接开训,调了一周参数精度都卡在0.7上不去,后来发现是验证集里混了十几张空标签图,把mAP硬生生拉低了。从那以后我拿到任何数据集第一件事永远是跑数据校验,而不是急着启动训练。数据集的665张只是个起点,把格式吃透、把校验跑好、把增强调对,它就能顶得上平时两三千张的效果。希望这篇能帮你少走这些弯路,把时间花在真正有用的调参和模型迭代上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询