简介:面向道路病害检测与计算机视觉目标检测模型训练,提供一套道路坑洞(pothole)标注数据集,同时给出Pascal VOC与YOLO两种主流标注格式,可直接接入YOLOv5、YOLOv8、Faster R-CNN等常见训练框架,减少数据格式转换成本。压缩包共2000个文件,类型以txt标注、xml标注及jpg图像样本为主,整体大小约23.87MB,标注文件与图片同名对应,方便按需划分训练集、验证集与测试集。数据集包含665张道路场景图像,标注框总数达1740个,覆盖单一类别pothole,由labelImg工具画矩形框完成,标注信息准确规范。该数据集既适合目标检测入门者练习完整训练与评估流程,也可作为道路坑洞识别、路面健康监测等课题的补充数据来源。目前已有400余人浏览学习,整体内容紧凑、格式统一,下载后即可开展实验。
1. 道路坑洞目标检测数据集为什么值得自己先过一遍标注格式
道路坑洞目标检测看起来场景单一,但真正动手做时,光线变化、沥青裂缝、阴影积水都会让标注边界很难定。这次拿到的VOC+YOLO双格式数据集整理得相当干净:665张jpg对应665个xml和665个txt,唯一类别pothole,标注框总数1740,平均下来每张图约2.6个框。它没有提供任何训练好的模型权重,也没有做train/val划分,所以更适合用来练手完整的目标检测流程,而不是直接拿来出结果。对于做巡检系统的工程师、刚接触YOLO目标检测的读者,或者想研究Pascal VOC和YOLO标签差异的人,这份数据都能当作可靠的起点。接下来我会从标签字段、统计校验、训练参数和部署过滤几个层面拆开讲。
2. VOC与YOLO两种标注格式的结构差异与互相转换
目标检测训练框架对标签要求不是完全一样。Pascal VOC用xml保存绝对像素坐标,YOLO保存的是归一化中心坐标与宽高。两者记录的是同一个矩形,只是坐标系和精度习惯不同。这个数据集同时保留两种格式,正好可以用一个文件去校验另一个文件,也可以让你在切换框架时不至于重新画框。
2.1 XML与TXT字段怎么对应
VOC格式的每个xml文件里,外层是annotation,内部有folder、filename、size、object等节点。其中size下必须包含width、height、depth,object下必须有name和bndbox,而bndbox里的xmin、ymin、xmax、ymax就是矩形框在原始图像中的绝对像素坐标。对于坑洞这种单类别任务,name基本都是pothole,但格式本身并不限制类别数量。
YOLO的txt格式则完全不同的,每一行代表一个目标,格式是class_id x_center y_center width height,后四个值全部除以图像宽高做归一化,所以取值范围通常在0到1之间。class_id从0开始编号,这个数据集只有pothole一个类别,所以txt里的第一列永远是0。为了直观对比,把常见字段列成下表:
| 信息 | VOC XML | YOLO txt | 说明 |
|---|---|---|---|
| 类别名 | <name>pothole</name> | 0 | YOLO只认类别id,需要names映射 |
| 图像宽 | <width>640</width> | 不存 | 转YOLO前必须知道原图宽高 |
| 边界框 | <xmin>100</xmin>等 | x_center y_center w h | 单位分别是像素和归一化比例 |
| 多目标 | 有多个<object> | 每行一个目标 | 同一图片不同框逐行写入 |
上表里用宽度640做例子,是因为很多公开数据集默认到这个尺寸,你解压后看到的数值要以实际xml为准。转换公式是:x_center = (xmin + xmax) / 2 / width,y_center同理用height计算,w = (xmax - xmin) / width,h = (ymax - ymin) / height。这里有个常见误区:不要把xmin、xmax直接当作中心点,必须先求中心,再除以宽高。
2.2 用Python脚本批量转换并控制坐标精度
虽然这个数据集已经同时给出了xml和txt,但在实际项目里,你仍然经常需要自己转换。例如从labelImg标注完只得到xml,或者需要把矩形框旋转框转换到OBB检测框架,再到后来把yolo格式转回VOC给MMDetection用。我一般会保留一个转换脚本,而不是依赖在线工具。下面这段代码可以逐个读取xml并输出txt:
import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') w = float(size.find('width').text) h = float(size.find('height').text) stem = os.path.splitext(os.path.basename(xml_path))[0] out_lines = [] for obj in root.iter('object'): cls_name = obj.find('name').text # 如果只保留pothole,默认类别id为0 if cls_name != 'pothole': continue box = obj.find('bndbox') xmin = float(box.find('xmin').text) ymin = float(box.find('ymin').text) xmax = float(box.find('xmax').text) ymax = float(box.find('ymax').text) cx = (xmin + xmax) / 2.0 / w cy = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h out_lines.append(f"0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_path = os.path.join(out_dir, stem + '.txt') with open(out_path, 'w', encoding='utf-8') as f: f.write('\n'.join(out_lines))这段代码先把w和h转成float,避免Python在整数除法时丢失小数点。之后遍历object节点,读取bndbox里的四个坐标值,再按前面提到的公式归一化累积到out_lines,最后写文件。保留6位小数是YOLO训练里的常见做法,精度已经远超图像像素误差。cls_name != 'pothole'这一行是保险:如果xml里混入其他类别,你不会把错误类别写成0,而是直接跳过。如果未来加到多类别,你需要先定义类别到id的映射字典,例如{'pothole': 0, 'crack': 1}。
使用时可以按目录批量执行:
mkdir labels python -c "import os; from voc_to_yolo import voc_to_yolo; [voc_to_yolo(os.path.join('annotations', f), 'labels') for f in os.listdir('annotations') if f.endswith('.xml')]"这个命令适合目录不大、临时验证的场景。项目文件较多时,我还是建议把脚本写成argparse版本,支持--xml_dir、--out_dir参数,否则到了下一个数据集还要改路径。
2.3 用txt反向还原XML核对一致性
转换完成之后,不能只看生成了几行txt就认为没问题。我见过不少标签错位,就是因为源xml里有个框的xmax写成了小于xmin的数值,或者图像宽高读取错误,导致归一化坐标超界。下面这段反向还原函数可以直接把yolo格式的某一行还原成像素坐标:
def yolo_to_voc_line(line, w, h): cid, xc, yc, bw, bh = map(float, line.split()) xmin = (xc - bw / 2) * w ymin = (yc - bh / 2) * h xmax = (xc + bw / 2) * w ymax = (yc + bh / 2) * h return int(cid), xmin, ymin, xmax, ymax逻辑并不复杂:先读入类别id和归一化中心坐标、宽高,然后用中心点减去一半宽高得到左上角,加上一半得到右下角。校验时拿这个结果和xml里的bndbox做差值,如果差值绝对值在2像素以内,通常认为转换一致。数据集的txt和xml是同一个标注工具生成的,一般不会差太多,但如果你用在线工具转换过,这一步仍然值得做。
需要提醒的是,YOLO txt本身不保存图像尺寸,所以反向还原时必须传入和原图一致的w、h。如果原始包是平铺结构,建议在转换脚本里同时读取同名jpg的宽高,而不是相信固定尺寸。这里项目描述里没有给出图像分辨率,我建议你解压后先用Python的PIL.Image读一遍尺寸,把异常宽高记录下来,再做后续训练。因为不同来源的监控相机可能输出不同分辨率,混在一起会带来隐性问题。
3. 数据集标注统计和边界框质量分析方法
许多目标检测项目失败,不是模型结构不对,而是标注数据在输入训练脚本之前就已经有残缺。拿到这个数据集后,第一步不是着急跑训练,而是先复算描述里的关键数字:图片665、xml 665、txt 665、pothole框1740。这些数字对得上,说明文件完整性没问题,后续训练才能安心。
3.1 统计每个类别的框数和每图框数
用xml遍历是统计最直接的方式,因为xml里既有filename又有完整的object列表。写一个几十行的脚本,就能把类别框数、平均每图框数都打出来:
import glob import xml.etree.ElementTree as ET total_xml = 0 total_boxes = 0 class_box = {} per_image_box = [] for xml_path in glob.glob('annotations/*.xml'): total_xml += 1 root = ET.parse(xml_path).getroot() objs = root.findall('object') total_boxes += len(objs) per_image_box.append(len(objs)) for obj in objs: name = obj.find('name').text class_box[name] = class_box.get(name, 0) + 1 print(f'xml files: {total_xml}') print(f'total boxes: {total_boxes}') print(f'class boxes: {class_box}') print(f'avg boxes/image: {sum(per_image_box) / len(per_image_box):.2f}')这段脚本用glob收集所有xml,root.findall('object')可以直接拿到一张图里所有标注目标,不需要遍历整棵树的子节点,速度上也比iter更快。跑完应该看到xml files: 665、total boxes: 1740、class boxes: {'pothole': 1740},avg boxes/image约2.62。这说明平均每张图里有2到3个坑洞,不少图是多个框重叠或贴近排列的,这对NMS和后处理会有一定压力。如果统计出的框数不是1740,先从标注工具导出设置找原因,比如labelImg的自动保存是否覆盖了旧文件。
类别统计里只出现pothole是符合预期的。如果某个xml出现第二种类别,你就要看是不是画错或者混入了其他数据集。类别的纯净度直接影响后续names配置,yaml里写nc: 1没问题,但如果混入第二种,整个训练验证标准就要重做。
3.2 检查边界坐标是否越界和框面积是否合理
VOC转YOLO时最常见的错误,是xml里的xmax大于图像宽度,或者ymax大于图像高度。原因可能是标注时拖出了画布,也可能是图像被resize但标签没同步更新。越界标签转出来的YOLO坐标会大于1,训练时虽然不会报错,但会误导损失函数。下面这个函数专门检查单个xml是否存在越界或非法框:
def check_annotation(root): size = root.find('size') w = int(size.find('width').text) h = int(size.find('height').text) problems = [] for idx, obj in enumerate(root.findall('object')): box = obj.find('bndbox') xmin = int(box.find('xmin').text) ymin = int(box.find('ymin').text) xmax = int(box.find('xmax').text) ymax = int(box.find('ymax').text) if xmin < 0 or ymin < 0 or xmax > w or ymax > h: problems.append((idx, 'out_of_bound')) if xmax <= xmin or ymax <= ymin: problems.append((idx, 'invalid_area')) if (xmax - xmin) * (ymax - ymin) < 16: problems.append((idx, 'too_small')) return problems对每个object,先检查坐标是否落在图像范围内,再判断宽高是否反转,最后看框面积。16像素这个阈值只是示例,对普通监控画面来说,小于16像素的坑洞基本无法通过检测器稳定识别,就算标出来也大概率是噪声。如果图像分辨率很高,比如1920x1080以上,可以把阈值提高到50或100,避免把小墨点当作坑洞。运行后把problems统一写进日志文件,不要只print,否则665张图会刷屏。
我一般还会统计一张图里面积最大的框和最小的框的比例。道路坑洞在远景时会很小,近景时很大,如果同一张图里出现面积相差两个数量级的框,训练时YOLO的尺度适应会更有挑战。这时可通过imgsz=1280提升小目标的分辨率,但代价是显存占用显著上升。
3.3 常见标签质量问题和排查顺序
整理成一张表,方便新手对照:
| 现象 | 判断方法 | 对训练的影响 |
|---|---|---|
| xml和txt框位置不一致 | 用2.3的反向还原对比 | 边界框偏移,loss曲线震荡 |
| txt行数多于xml对象数 | 逐行统计每个txt行数 | 空标签或重复目标,mAP失真 |
| 小目标框过多 | 计算框面积与图像面积比 | mAP50-95偏低,需要更高输入分辨率 |
| 个别图没有标注 | 对比jpg文件名和xml文件名 | 图片被跳过,评估集数据量减少 |
| 文件后缀大小写不一致 | 检查.JPG和.jpg | 文件路径匹配失败,训练报错 |
这套数据里文件名是firc_pothole_356这种风格,既有jpg也有txt和xml,命名上相对统一。但要注意不要随手改成中文名,YOLO按文件名匹配图片和标签,中文或空格容易导致路径解析问题。上一章提到的反向还原脚本,在这里就能派上用场:把所有txt还原成矩形框,和xml对比,数值差超过2像素的文件单独列出来。处理完之后再进入训练,才能保证后面所有实验都可以复现。
4. 用YOLOv8训练坑洞检测模型时的数据划分与训练参数
665张图片属于小型数据集,直接拿全部数据训练容易过拟合,盲目增加数据增强也可能掩盖标签问题。这里推荐用YOLOv8作为讲解对象,因为它把标注格式、数据划分和训练命令统一做得比较顺,环境安装也比旧版v5简单。数据集的xml在这里可以当作备份,真正训练时用的是txt和对应的jpg。
4.1 数据目录组织与yaml配置
YOLO系列通常要求目录结构是images/train、images/val、labels/train、labels/val。原始zip如果平铺在同一个目录,需要先建结构再移动文件。下面这段脚本把图片和txt按比例分到训练集和验证集:
import random from pathlib import Path root = Path('pothole_dataset') for split in ['train', 'val']: (root / 'images' / split).mkdir(parents=True, exist_ok=True) (root / 'labels' / split).mkdir(parents=True, exist_ok=True) imgs = sorted((root / 'images').glob('*.jpg')) random.seed(42) random.shuffle(imgs) val_num = int(len(imgs) * 0.2) for i, img in enumerate(imgs): split = 'val' if i < val_num else 'train' label = root / 'labels' / (img.stem + '.txt') img.rename(root / 'images' / split / img.name) if label.exists(): label.rename(root / 'labels' / split / label.name)脚本先用Path.mkdir(parents=True, exist_ok=True)创建四层目录,再按20%比例把图片放入val。random.seed(42)是为了固定随机划分,换别的随机数会导致训练集不同,实验对比就不公平。移动时用rename而不是复制,因为665张图复制一份更快,但如果你还要保留xml,建议先把整个zip解压后的目录做一次备份再进行划分。如果原始目录里images下没有图片,而是在annotations和labels之外单独放着图片,先把它们统一归入pothole_dataset/images再执行这段脚本。
划分之后写pothole.yaml:
path: pothole_dataset train: images/train val: images/val nc: 1 names: ['pothole']path是相对当前命令运行目录的根路径,也可以写绝对路径。train和val相对于path填写,不要写成/images/train。nc: 1因为只有pothole一个类别,names里的顺序就是txt类别id的映射,所以第一个元素必须是pothole。很多新手在names里加一个background,这在YOLOv8里是错误的,背景不需要占类别位。
4.2 训练超参数选择与显存控制
小数据集训练,关键是控制模型容量和训练时长。我建议先用yolov8n起步,而不是直接yolov8x,因为665张图不足以让大模型充分收敛。预训练权重能提供通用特征,坑洞在纹理上和普通路面有较强区分性,n或s的容量已经够用。命令如下:
yolo detect train data=pothole.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16 patience=20 cache=Falseepochs=100对这类数量级的数据是足够的,如果早停触发早,也可以节省时间。patience=20表示连续20轮验证集指标不上升就停止,这是小数据集防过拟合的重要手段。batch=16是默认值,如果GPU显存不到8GB,建议降到8或4,因为坑洞检测的输入尺寸如果保持640,显存占用会比理论数值高。cache=False表示不把图片一次性载入内存,虽然会慢一些,但避免内存不足导致进程被杀。
如果原图分辨率明显高于640,比如很多道路巡检相机的输出是1920x1080,可以先试imgsz=960。坑洞在图像里通常只占几十到几百像素,放大输入能保留更多细节,但训练时间和显存都会上升。我一般先跑一次640,确认流程没问题后再试1280,而不是一开始就追求最高分辨率。注意yolov8默认开启马赛克增强,对小数据集是多阶段训练中比较关键的一项,不要为了“看得清”把它关掉。
4.3 单类目标的训练输出怎么看
训练结束后,重点看results.png、weights/best.pt和last.pt。results.png里有train/box_loss、val/box_loss、metrics/mAP50、metrics/mAP50-95。由于是单类别,mAP50就是pothole类别本身的检测精度,不需要再做平均。坑洞目标小且外形接近椭圆,mAP50-95通常低于mAP50很多,如果两者差距过大,说明模型对边框精度的拟合还不够,后期可以微调损失函数的box IoU权重,或者提高输入分辨率。训练loss曲线如果还在下降,说明epoch不够,可以继续用last.pt训练;训练loss下降但验证loss上升,就是过拟合信号,此时可以增加验证集比例,或把patience调小。
5. 部署时对坑洞检测结果做置信度和尺度过滤的实用技巧
模型训练完成之后,直接拿默认参数推理往往不够稳。道路环境中井盖、排水格栅、沥青裂缝和阴影都很容易被误判成坑洞,所以部署时要做两层过滤。
5.1 用conf和iou控制坑洞误检
推理命令里最值得调的就是conf和iou。下面这段命令适合检测单张图片:
yolo detect predict model=runs/detect/train/weights/best.pt source=road.jpg conf=0.30 iou=0.45 imgsz=640conf=0.30表示只保留置信度高于30%的框,能去掉大量低置信度误检。如果发现坑洞漏检,把conf调到0.15,但你要接受更多阴影误报。iou=0.45是NMS的合并阈值,道路视频里几个坑洞挨得很近,太高会把相邻框合并掉,太低又会产生同一个目标多个框,0.45是一个比较折中的起点。如果最终要部署在Jetson等边缘设备,imgsz可以降到480或320,但小坑洞召回率会明显下降,需要拿验证集做一次最佳conf和imgsz组合搜索。
5.2 根据坑洞宽高比做后处理
透视关系下,坑洞通常不会出现特别夸张的宽高比。有些裂缝会被模型标成一个高瘦矩形,实际坑洞更多是横向或方形。在onnx或tensorrt的推理后处理里加一个简单的宽高比过滤:
def filter_aspect(boxes, scores, min_ratio=0.4, max_ratio=2.5): result_box = [] result_score = [] for box, score in zip(boxes, scores): x1, y1, x2, y2 = box w = x2 - x1 h = y2 - y1 ratio = w / h if h > 0 else 999 if min_ratio <= ratio <= max_ratio: result_box.append(box) result_score.append(score) return result_box, result_score这段函数先计算w/h,再判断是否落在0.4到2.5之间。数值范围需要根据你部署摄像头的安装角度调整,可以用一批验证集ground truth统计宽高比分布,再设上下分位数。过滤掉的长条框大部分是裂缝和栏杆阴影,对坑洞检测的误报抑制非常有效。把这段函数放在NMS之后、输出结果之前,每条框只多几行Python计算,不会影响帧率。后续接入巡检流程时,我一般还会把检测结果对齐到图像GPS位置,用连续几帧的确认来降低单帧误报。
本文还有配套的精品资源,点击获取