道路坑洞目标检测实战:VOC与YOLO格式转换及YOLOv8训练

道路坑洞目标检测实战:VOC与YOLO格式转换及YOLOv8训练 简介道路坑洞目标检测数据集面向计算机视觉初学者与智慧交通、道路养护等应用场景提供665张标注清晰的坑洞图片统一为Pascal VOC与YOLO两种主流格式可直接接入YOLO、Faster R-CNN等常见检测框架无需额外转换。包体共2000个文件其中665个xml标注文件与1332个txt标签文件分别对应两种格式的标注信息另有少量jpg示例压缩包仅23.87MB结构精简便于快速下载与训练迭代。数据集中全部1740个目标框均按矩形框规范标注类别聚焦pothole使用labelImg工具完成标注质量准确一致可支撑模型训练、验证与数据增强实验。已有405人浏览学习适合用来测试模型调参、完成课程设计或搭建道路病害识别原型。借助该数据集可省去手动采集与标注的繁琐流程专注模型结构与参数优化快速获得可验证的检测效果。1. 拿到“道路坑洞目标检测数据集VOCYOLO格式665张1类别.zip”先做什么压缩包解压之后你会看到 665 张道路实拍图外加两套指向同一批图片的标注文件一套是 PASCAL VOC 的 XML一套是 YOLO 的 txt。对做目标检测的人来说这个 zip 最有价值的地方不是“有数据”而是它同时给好了两套格式省掉了我日常里最烦的标签转换环节。665 张单类别不算大但道路坑洞这个任务的特殊性在于目标小、背景杂、光照变化大跑迁移学习完全够用。这篇文章就顺着“先看清目录再完成转换最后把训练跑通”这条线覆盖数据校验、参数调整、标签排错和增广策略。2. 先拆开看VOC 与 YOLO 两套标注的目录结构和坐标基准2.1 VOC 目录为什么是“三件套”JPEGImages、Annotations、ImageSets打开压缩包后常见做法是看到一个VOCdevkit/VOC2007或者平铺的VOC2007目录里面至少有三个文件夹JPEGImages放原始图片Annotations放每个图片对应的 XML 描述文件ImageSets/Main放训练集、验证集、测试集的图片文件名清单。缺少任何一个Pascal VOC 协议就不完整。目录作用判断标准JPEGImages原始 JPG/PNG 图片数量与 XML 数量一致Annotations每张图一个 XMLXML 内包含object节点ImageSets/Maintrain.txt、val.txt、test.txt文件名不带扩展名一行一个labelsYOLO 侧每张图一个 txt行数等于该图目标数classes.txt / names 文件类别名只有pothole或0 pothole如果压缩包里没有ImageSets也不代表数据集不能用。YOLO 训练时更常见的是直接用train.txt、val.txt把图片路径带进去很多数据集作者会额外生成一个split文件夹。标准情况是两套都有但只有JPEGImages和Annotations也能通过脚本补齐。VOC 格式里最核心的 XML 结构是每个目标对一个object节点里面有name和bndboxbndbox的四个值xmin、ymin、xmax、ymax是像素坐标原点在图片左上角单位是像素。这几个值超没超出图片宽高是后面排错的第一个检查点。2.2 YOLO 格式为什么只存一个 txt归一化坐标的原点与范围YOLO 的标签文件不叫 XML而是和图片同名的.txt每一行表示一个目标格式固定为「类别 id、中心点 x、中心点 y、宽、高」五个数字全部是归一化后的比例取值范围在 0 到 1 之间图片宽为w、高为h时中心点坐标计算方式是x_center / w、y_center / h。有人会把 YOLO 前两个数字理解成左上角坐标这是最大的误区。中心点坐标的定位逻辑直接决定了预测框的回归目标模型的 bbox 分支输出就是基于中心点偏移量的。第二点是宽度和高度也是归一化的不是像素值。宽高如果大于 1说明坐标换算时把分子分母写反了。KITTI 转 YOLO 的脚本大家在网上常见KITTI 标注的前两个值是左上角、后两个值是右下角转 YOLO 时要先求中心点。这套道路坑洞数据集如果作者已经把两套格式都生成好理论上不需要你再转但为了保险拿一张图用下面的脚本验证一下是值得的。2.3 坐标换算从 XML 里的像素框到 YOLO 的归一化框假设我只拿到 VOC 标注需要生成 YOLO 格式脚本核心就这一段import xml.etree.ElementTree as ET from pathlib import Path def voc_xml_to_yolo(xml_path, img_w, img_h): root ET.parse(xml_path).getroot() boxes [] for obj in root.iter(object): cls_name obj.findtext(name) # 1 类别数据集里通常直接映射成 0 cls_id 0 bndbox obj.find(bndbox) xmin int(bndbox.findtext(xmin)) ymin int(bndbox.findtext(ymin)) xmax int(bndbox.findtext(xmax)) ymax int(bndbox.findtext(ymax)) # 中心点和宽高全部除以图片尺寸得到 0~1 的比值 x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h boxes.append((cls_id, x_center, y_center, box_w, box_h)) return boxes # 使用示例img_w1280, img_h720 # result voc_xml_to_yolo(0001.xml, 1280, 720) # 每行格式0 0.5123 0.4472 0.0821 0.0714逻辑说明xml.etree.ElementTree解析bndbox时拿到的是字符串必须转 int除以图片宽高得到的是比例这样图片分辨率变化了标签也不用重画。参数上要注意img_w和img_h必须和实际图片尺寸一致如果 XML 里存在宽大于高的竖图而你传反了宽高生成的框会整体偏移。如果你要写回 VOC 格式反方向变换是xmin (x_center - box_w / 2) * img_w记得结果四舍五入到整数边界小于 0 时截断为 0。这套转换逻辑是所有数据格式转换的基础Road pothole 数据集的验证阶段我一般会抽查第 1、第 100、第 500 张图转完之后拿可视化脚本画出来看一遍。3. 用 YOLOv8 跑通这套 665 张数据的完整流程3.1 先把目录理顺images/labels 与三份路径清单现代 YOLO 版本YOLOv5/v8/ultralytics不强制要求 VOC 那种ImageSets/Main的 txt 清单但要求图片和标签的组织方式是images/train、labels/train、images/val、labels/val。从数据集本身已有 VOC 格式的前提下推荐用脚本拆一次目录而不是手动拖拽。mkdir -p dataset/images/train dataset/images/val dataset/images/test mkdir -p dataset/labels/train dataset/labels/val dataset/labels/test python split_and_convert.pysplit_and_convert.py负责三件事读取ImageSets/Main/train.txt得到用于训练的文件名列表然后去JPEGImages里复制对应图片到images/train再把对应 XML 转成 YOLO txt 放到labels/train。如果压缩包里没有主清单按 8:1:1 随机切分即可import random from pathlib import Path random.seed(42) image_paths list(Path(JPEGImages).glob(*.jpg)) random.shuffle(image_paths) total len(image_paths) val_cut int(total * 0.8) test_cut int(total * 0.9) paths { train: image_paths[:val_cut], val: image_paths[val_cut:test_cut], test: image_paths[test_cut:], } for split_name, paths in paths.items(): with open(f{split_name}.txt, w) as f: for p in paths: f.write(str(p.resolve()) \n)逻辑说明固定random.seed保证每次切分结果可复现后续训练结束后对比两次实验时不会被数据划分干扰。val_cut取 80% 的位置test_cut取 90% 的位置剩下 10% 做测试集。665 张图的实际结果是 532 训练、66 验证、67 测试这样的比例对单类别小数据集是够用的。3.2 写一个 dataset.yaml类别只有一行也不能错YOLO 训练需要一份 YAML 配置文件里面写训练集、验证集的 txt 路径以及类别名。不要在这里手敲绝对路径除非你确定服务器上的目录不会变。train: /data/pothole/dataset/images/train val: /data/pothole/dataset/images/val test: /data/pothole/dataset/images/test nc: 1 names: 0: pothole参数说明nc必须和names里的数量一致写错的话训练还没开始就会报mismatch错误names的顺序必须和 txt 里的第一列 class id 对应如果数据集作者用的是 1 而不是 0需要在转换阶段减去 1。我见过有人直接把 VOC 的类别名Pothole大写YOLO 训练不会报错但后续推理输出名字会影响日志阅读。3.3 训练命令参数选择与首次训练YOLOv8 用 ultralytics 包启动训练GPU 显存不够时直接换小模型先跑通再考虑精度。推荐的起步命令是这样yolo detect train \ modelyolov8m.pt \ datapothole.yaml \ epochs100 \ imgsz1280 \ batch8 \ patience20 \ device0 \ projectpothole_runs参数含义与选择理由yolov8m是中等规模模型比s精度高比l显存占用低对 665 张数据来说不会欠拟合也不会严重过拟合imgsz1280是道路坑洞检测里特别值得提的一个点坑洞在原图中往往只占几十个像素默认的 640 输入会把小目标细节抹掉batch8考虑 12GB 显存下 1280 分辨率的稳定训练patience20意思是验证集 mAP 连续 20 轮不上升就提前停止避免在 665 张这种规模的数据集上死磕到第 100 轮。如果显存只有 8GB把模型换成yolov8s.pt、imgsz960batch8一般也能跑1280 分辨率下yolov8m会比较吃力。首次训练时不要先做太多改动默认优化器auto和默认学习率在大多数数据集上表现已经很稳定增广也保持默认跑通之后再调。3.4 训练日志里哪几个指标值得看训练结束后pothole_runs目录下会有results.png和weights/best.pt。我会按顺序看三个地方第一个是train/cls_loss是否持续下降如果损失在波动甚至上升先怀疑标签转换出错第二个是val/box_loss这个指标比 mAP 更能反映 bbox 回归质量第三个是metrics/mAP50(B)坑洞检测里因为目标小mAP50-95 通常比 mAP50 低不少不要因此怀疑模型坏了。指标含义坑洞场景常见区间mAP50IoU 阈值 0.5 的平均精度0.75 ~ 0.92mAP50-950.5 到 0.95 多阈值平均0.55 ~ 0.75Precision预测框中真阳性的比例0.85 以上Recall真实目标被找回的比例0.80 左右665 张如果划分合理、标签质量没问题第一次训练跑完 mAP50 在 0.8 以上是正常水平。低于 0.6 就先别调参回去画标签找问题。4. 坑洞目标检测的参数边界与标签质量排错4.1 分辨率、batch size、模型大小怎么选道路坑洞和行人、车辆检测最大的区别在于目标尺寸分布。一个路面裂缝宽度可能只有 20 像素如果按 640 输入等比缩放后变成 6 个像素特征提取器很难捕捉到边缘和纹理信息。这就是为什么imgsz在坑洞任务里是第一优先级参数。参数建议值变更方向imgsz1280 优先最低 960显存不够降 960不要低于 800batch8 ~ 16显存允许时调大稳定梯度modelm 起s 兜底数据少不要直接上 xlr00.01 默认第二轮微调可降到 0.001mosaic1.0增强时可调 0.5 观察效果模型大小方面yolov8x在这种数据量下很容易过早过拟合val_loss上升而train_loss继续下降就是过拟合信号。yolov8s速度最快但是小目标召回偏低。我的通常做法是先在yolov8m上跑通拿一个 baseline确认标签没问题再试l或x。4.2 用可视化脚本检查标注是否对准了坑洞标签错误直接决定了最终模型的天花板。YOLO 训练本身不校验语义坐标写错它照样学只是学出一堆错误模式。检查方法是用 OpenCV 把 bbox 画回原图import cv2 import numpy as np from pathlib import Path def draw_yolo_label(image_path, label_path): img cv2.imread(str(image_path)) h, w img.shape[:2] with open(label_path) as f: for line in f: parts line.split() if not parts: continue cls_id, xc, yc, bw, bh map(float, parts) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(int(cls_id)), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) return img # 示例检查 dataset/images/train 下前 20 张 # for img_path in sorted(Path(dataset/images/train).glob(*.jpg))[:20]: # vis draw_yolo_label(img_path, img_path.with_suffix(.txt))逻辑说明txt 里的坐标是归一化比值要乘回h和w得到像素位置。with_suffix(.txt)把图片路径的.jpg换成同名的.txt这是 YOLO 标签命名规则的核心。把画好框的图片拼接成格子图一眼扫过去重点看两类问题框框住了整条裂缝而不是单个坑洞、框明显偏移只框住一半目标。批量输出成单个视频或网格图会让检查效率更高我一般把 100 张图拼成 10×10 的grid.png一屏能看完。如果发现某张图完全没有框但原图里明显有坑洞那就是漏标漏标会直接拉低 recall。4.3 XML 缺目标、路径带中文、bbox 越界这三个坑第一个坑是 XML 里没有object节点。233 张图里只要有十几张空标注训练时这些图片会被跳过但测试时模型输出空框导致 recall 异常偏低。快速找出这些文件find Annotations -name *.xml | xargs -I {} grep -L object {}第二个坑是图片路径带中文。YOLO 训练时 OpenCV 读取中文路径会静默失败模型训练跨过这些图但不报错损失曲线会莫名抖动。处理方式是把数据集整体移动到纯英文路径或者在复制阶段用uuid重命名。第三个坑是 bbox 越界。def check_labels_out_of_bounds(label_dir, img_dir): for txt in Path(label_dir).glob(*.txt): img cv2.imread(str(Path(img_dir) / (txt.stem .jpg))) h, w img.shape[:2] for line in txt.read_text().splitlines(): _, xc, yc, bw, bh map(float, line.split()) message if xc bw / 2 1.0 or xc - bw / 2 0.0: message fx 越界: {txt} if yc bh / 2 1.0 or yc - bh / 2 0.0: message fy 越界: {txt} if message: print(message)逻辑说明归一化后的坐标中心点加减宽高的一半必须落在 0 到 1 区间内否则模型推理阶段裁剪特征图时产生索引越界或信息丢失。这个脚本比训练时报错更早暴露问题建议放在数据集校验的第一步执行。还有一种极端情况是bw或bh等于 0通常是标注时点了同一个点直接删除这一行即可。5. 只有 665 张迁移、增广与伪标签自检5.1 从预训练权重开始不全是从零训练YOLOv8 的modelyolov8m.pt会自动加载 COCO 预训练权重。Coco 里有traffic light、stop sign这类与道路场景相关的类别模型已经具备路面背景的底层特征。迁移学习下665 张单类别数据的训练轮数不用太多epochs80通常就够多了反而在验证集上抖动。加载预训练权重时留意日志里是否出现Transferred 342/352 items from pretrained weights如果只有一百多项说明模型结构被改过检查nc是否写成了非 1 的数字。5.2 坑洞场景更推荐留哪些增广YOLOv8 默认开启的增广里对坑洞最有用的是 Mosaic 和 HSV 变换。Mosaic 能把四个小目标拼进一张图相当于模拟了坑洞密集路段。但是mosaic1.0全程开启会让小目标被裁剪出图训练后期建议下降到 0.5。HSV 的饱和度变化对路面阴影和柏油颜色差异很有帮助保持默认即可。比较有争议的是fliplr水平翻转。坑洞没有左右语义水平翻转是安全的垂直翻转要谨慎因为道路照片通常有固定的地平线位置。实际操作是在pothole.yaml同目录放一个augment.yaml用yolo detect train ... cfgaugment.yaml指定只改degrees: 5轻微旋转模拟车辆颠簸角度和mosaic: 0.5。5.3 用“伪标签”找出漏标样本做第二轮训练665 张数据集的提升空间往往不在模型结构而在标注召回率。第一轮训练完用best.pt对训练集本身做推理置信度设低一点yolo detect predict \ modelpothole_runs/weights/best.pt \ sourcedataset/images/train \ conf0.25 \ save_txtTrue \ save_confTrue生成的 txt 会和原标注文件混在一起先备份原labels/train目录再用脚本逐个比对如果预测框中心和人工标注框中心距离大于 0.3归一化且置信度大于 0.5就把它视为疑似漏标。人眼确认后把这个框追加到原标注 txt 里重新训练一轮。这种“模型找候选、人工做确认”的伪标签自检循环在只有几百张的坑洞数据集上通常能把 mAP50 提升 3 到 5 个点比换更大的模型更有效。整理完了这份 665 张单类别数据集之后整个流程留一个检查清单就好坐标归一化、目录切分、路径无中文、bbox 无越界、预训练加载正常、增广不过度最后用伪标签循环把漏标补回来。本文还有配套的精品资源点击获取