简介面向道路养护与自动驾驶场景的坑洞检测需求这套数据集提供665张真实道路图像及对应的Pascal VOC与YOLO双格式标注类别统一为pothole共包含1740个矩形标注框。所有标注由labelImg工具完成框选边界准确、类别信息完整可直接接入YOLOv5、YOLOv8等主流目标检测框架进行训练与验证。压缩包共包含2000个文件其中1332个txt为YOLO格式标注665个xml为VOC格式标注另有3个jpg预览图片整体大小23.87MB目录结构简洁便于按需取用。目前已有408人下载学习适合计算机视觉初学者练习目标检测全流程也可作为道路病害识别项目的训练数据补充。数据集虽不保证模型精度但标注质量可靠可省去大量人工采集与标注时间。1. 道路坑洞目标检测数据集665张单类别为什么比几万张网图更靠谱市政道路养护、自动驾驶路测、保险定损拍照这三个场景每年要产生海量路面图像但能直接喂给目标检测模型的标注数据却一直稀缺。拿到“道路坑洞目标检测数据集VOCYOLO格式665张1类别.zip”这个名字时懂行的人第一反应不是“图太少”而是“格式齐、类别纯”。665张图听起来不大但做的是单类别坑洞检测任务边界极窄模型不需要区分几十个类别的复杂度训练反而容易收敛。更难得的是压缩包里同时给了VOC的XML标注和YOLO的txt标注省掉了从零标注和格式转换的大量重复劳动。这篇笔记把这类数据集从拆包、格式核对、转码到训练落地的完整路径讲清楚适合正在跑yolov8训练自己的数据集、又不想在数据上耗太多时间的同学。2. 先看清包里的东西VOC与YOLO双格式标注的文件结构拿到zip包别急着解压丢进训练脚本先把目录结构、标注格式和类别定义摸清楚。双格式数据集的坑往往不在内容而在你对标注的假设和实际文件对不上。2.1 XML和TXT两套标签同一张图两套坐标体系VOC格式的标注文件是XML核心信息用object标签包着每个object代表一个目标框。常见结构里能看到name是类别名bndbox里四个值是xmin、ymin、xmax、ymax表示目标框在原始图片像素坐标系下的左上角和右下角。一路从业界流传习惯看voc格式的标注文件里还会带folder、filename、path这些元信息有的标注工具还会塞segmentation等无关字段解析时别只盯着object部分其余字段不影响训练。YOLO格式的标注文件是txt每一行对应一个目标框格式固定为“class_id x_center y_center width height”。关键区别是后四个数字全部做了归一化除以图片的宽和高值域在0到1之间。class_id从0开始编号单类别数据集里就只会出现0一旦出现1就说明标注混入了别的类别训练时会直接报错或吞掉样本。两套格式的换算关系要刻在脑子里VOC的xmin映射成YOLO的x_center需要做 (xmin xmax) / 2 / width宽是 (xmax - xmin) / width高同理。转换脚本写错一个乘除方向出来的框就是歪的训练时loss能降但mAP永远是0。这种训练过程“看着正常、结果全废”的翻车我在坑洞检测项目里碰到过两次都是坐标换算少除了一个width。2.2 zip包内的目录布局JPEGImages、Annotations、labels各归其位这类双格式数据集的目录结构通常是约定俗成的解压后应该看到三个核心目录JPEGImages放原始图片Annotations放XML标注labels放YOLO格式的txt标注。有的包会再套一层ImageSets/Main目录里面放train.txt、val.txt这种划分清单但很多直接出包的数据集省略了这一步需要自己划分。我拿到包的第一件事是跑一条命令核对图片和标注是否一一对应别偷懒ls JPEGImages | wc -l ls Annotations | wc -l ls labels | wc -l三条命令的数字应该完全一致。数字对不上的时候优先看是不是有隐藏文件、空文件或文件名后缀不一致的问题。常见情况是图片是.jpg而标注文件名还带着.peg或者Windows解压时把文件名改了大小写。文件名对不上后面所有脚本都在处理残缺数据模型效果崩了都不知道崩在哪一步。2.3 665张单类别数据怎么撑起训练类别少反而省心单类别数据集的价值容易被低估。道路坑洞检测这类任务背景复杂度远高于类别复杂度。一张路面图里可能有井盖、裂缝、阴影、水渍、落叶这些干扰项视觉上和坑洞很像但训练目标只需要回答“是不是坑洞”。类别少意味着模型的注意力可以全花在区分前景和背景上不用在学习多个类别间的区分上浪费参数。665张图配单类别在迁移学习的设定下完全够用。训练时加载COCO预训练权重模型已经具备基础的纹理提取能力坑洞检测只是在这个基座上做微调。我做这类小样本训练时一般会把训练轮数提到120到150轮配合mosaic和随机翻转增强最后在验证集上mAP50能到0.85左右。图不在多关键在每一张框得准、切得干净。提示压缩包解压后先检查每张图片的尺寸是否统一。坑洞数据集常混着不同相机拍的原图YOLO训练会自动resize到640x640但如果某几张图特别小或特别横归一化坐标在resize后会出现拉伸变形直接拉低验证分数。3. 处理数据集用于yolov8训练转换脚本、数据划分与标签校验这是整个流程里最磨人的一环也是决定训练能不能顺利跑通的关卡。VOC和YOLO双格式意味着你现在有两条路直接吃VOC格式让Ultralytics帮你转或者自己先把XML统一转成txt。我的建议是自己转因为Ultralytics内置的转换脚本虽然省事但遇到坑洞这类不规则目标时XML里可能残留空的bndbox或重复object内置转换容易静默跳过你根本不知道丢了多少标注。3.1 VOC转YOLOXML坐标归一化脚本以下脚本是处理这种双格式数据集最常用的做法把Annotations目录下所有XML统一转成labels目录下的txt。脚本用xml.etree.ElementTree解析XML按每个object提取归一化坐标并写入对应txt文件文件名保持与图片同名。import os import xml.etree.ElementTree as ET from pathlib import Path # 注意改成你自己解压后的相对路径 annotations_dir Annotations labels_dir labels # 只处理坑洞这一类class_id固定为0 class_name pothole class_id 0 os.makedirs(labels_dir, exist_okTrue) for xml_file in Path(annotations_dir).glob(*.xml): tree ET.parse(xml_file) root tree.getroot() # 从XML的size节点读取原始图宽高归一化必须用它不能猜 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) out_lines [] for obj in root.findall(object): name obj.find(name).text if name ! class_name: continue # 非目标类别直接跳过避免混入脏标注 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) # 坐标越界防护坑洞标注里偶尔会出现负数或超过图宽的像素值 xmin max(0, xmin) ymin max(0, ymin) xmax min(img_w, xmax) ymax min(img_h, ymax) # 跳过宽或高为0的空框 if xmax xmin or ymax ymin: continue # YOLO格式要求中心点坐标和宽高都除以原图宽高 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h out_lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) if out_lines: txt_path Path(labels_dir) / f{xml_file.stem}.txt txt_path.write_text(\n.join(out_lines) \n)脚本的核心逻辑分四层第一层从XML的size节点读真实宽高这是坐标归一化的基准用错的话所有框整体偏移第二层按类别名过滤忽略不是坑洞的object第三层做边界裁剪把落在图像外的像素值拉回有效范围第四层才是换算和写盘。参数上唯一要改的就是class_name和两个路径变量其它按默认跑就行。注意输出浮点精度保留6位小数足够满足YOLO训练需求写成4位会累积极小的框偏移。3.2 训练集验证集划分固定随机种子避免数据分布翻车数据集小划分比例的影响会被放大。665张图按82划分验证集只有133张每删掉一张对验证指标的扰动都是1%级别的。所以我不用随机抽而是用带固定随机种子和分层策略的脚本保证每张图只出现在训练集或验证集中其一且图片和它的txt标签文件同步移动。#!/bin/bash # 划分脚本随机种子固定为42输出train.txt和val.txt清单 python3 - EOF import os import random from pathlib import Path random.seed(42) images sorted(Path(JPEGImages).glob(*)) random.shuffle(images) val_ratio 0.2 val_count int(len(images) * val_ratio) val_set set(images[:val_count]) train_set set(images[val_count:]) with open(train.txt, w) as f: for img in sorted(train_set): f.write(str(img.resolve()) \n) with open(val.txt, w) as f: for img in sorted(val_set): f.write(str(img.resolve()) \n) print(ftrain: {len(train_set)}, val: {len(val_set)}) EOF这个脚本用顶层路径的绝对路径作为清单内容好处是后面换dataloader时不用二次拼接路径。random.seed(42)让每次划分结果可复现这是很多项目容易忽略的细节。实际项目里如果数据集里有连续帧拍摄的序列图随机划分会把同一段路的相邻帧分到训练和验证里造成数据泄漏坑洞数据集常有这种连拍情况。遇到这种问题按图片文件名前缀分组再划分更稳妥文件名前缀相同的归到同一集合。3.3 三个必做的标签校验越界、空框、类别id异常转换完成不等于数据能用我每次都会跑一段校验脚本把异常标签提前暴露出来这比训练到一半再排查省事得多。校验脚本做的事情很机械但能挡住80%的翻车事故。import os from pathlib import Path labels Path(labels) for txt in sorted(labels.glob(*.txt)): lines txt.read_text().strip().splitlines() if not lines: print(fEMPTY: {txt.name}) continue for line in lines: parts list(map(float, line.split())) if len(parts) ! 5: print(fFORMAT: {txt.name} - {line}) cls_id, x_c, y_c, w, h parts if cls_id ! 0: print(fCLASS_ID: {txt.name} - {cls_id}) if w 0 or h 0: print(fSIZE: {txt.name} - {line}) if not (0 x_c 1 and 0 y_c 1): print(fNORMALIZED: {txt.name} - {line})校验分三档空文件直接报空格式不对和数值越界分开打印。特别注意normalized检查x_c和y_c在0到1之间是基本前提一旦出现负数说明XML解析时尺寸字段读取失败默认值0导致归一化炸了。这种方式能一次性把全部异常筛选出来避免训练时日志里随机蹦出几条警告根本定位不到是哪张图的问题。校验通过后数据准备阶段才算真正结束。4. 用YOLOv8训练坑洞检测模型data.yaml与训练命令的取舍数据准备好了接下来进入模型训练。环境安装不在这次讨论范围重点讲那些决定坑洞检测效果的关键决策data.yaml怎么写、训练参数怎么设、训练过程怎么判断有没有跑偏。yolov8训练自己的数据集这套流程很多人栽在参数设置上不是模型不行是参数和数据集规模不匹配。4.1 写data.yaml路径、类别名与train/val指向Ultralytics训练时需要一个data.yaml内容极其简单但路径和类别名错一个字符整个训练就起不来。方式上我建议在项目根目录放相对路径少用绝对路径方便迁移到别的机器重跑。# data.yaml path: . # 配置文件所在目录为根 train: train.txt # 训练集清单内容为图片绝对路径 val: val.txt # 验证集清单内容为图片绝对路径 # 单类别数据集的names列表索引0对应pothole names: 0: pothole这里的path决定了train和val是相对谁的路径。写成path: . 表示当前目录train.txt里的绝对路径不受影响。names的索引必须从0开始连续编号如果写成names: 1: pothole那么训练时会强制把所有标签的class_id当作1处理单类别数据集里class_id是0的标注全部变成背景模型准确率直接崩到个位数。这是低错高频的坑偶发在从规范文档抄袭配置时。4.2 训练命令与必调参数epochs、imgsz、batch、mosaic的取舍665张图的小数据集训练命令我会这样写yolo detect train \ datadata.yaml \ modelyolov8s.pt \ epochs150 \ imgsz640 \ batch16 \ patience30 \ projectruns \ namepothole_train \ seed42 \ mosaic1.0参数说明要结合坑洞数据的特性来理解。epochs设150是因为小数据集需要多轮次才能把坑洞的纹理特征学充分少于80轮时验证集mAP通常还没到平台期。另一套常见做法是接上早停机制patience30的意思是验证集指标连续30轮不提升就提前停止训练过程如果第90轮就停了那说明数据量下模型已收敛不必硬跑满150轮。mosaic保留1.0因为坑洞检测里目标在整图中的尺寸变化很大坑洞很大时占半个画面mosaic增强能模拟更丰富的尺度变化。batch大小主要看显存16是个保险值只要显存不爆就不动它。想快就换yolov8n.pt想更准就上yolov8l.pt但665张图用s级是性价比最稳的选择。4.3 监控训练过程loss曲线和验证指标的早期判断训练启动后别只是盯着终端等结束前30轮就能看出模型有没有跑偏。YOLOv8的训练日志会在每个epoch后打印box_loss、cls_loss、dfl_loss和验证集的mAP50、mAP50-95。我比较关注的信号有两个一是box_loss是否在连续下降二是mAP50在第20到第30轮之间是否开始出现非零值。loss在降但mAP一直是0这是最典型的异常指向标签数据和图片不对应。另一个值得警惕的是第一个epoch的mAP就跳到0.9以上这种“开局即巅峰”反而说明验证集和训练集有重叠或者数据集划分时连拍帧泄漏。正常趋势是mAP在30轮左右爬到一个峰值然后小幅震荡而loss持续走低。损失函数和mAP的相关性在小数据集上没那么直接以损失曲线判断是否欠拟合以验证mAP判断最终效果两个维度各自把关。注意依赖Ultralytics默认的学习率设置在小数据集上是够用的但如果你换用了优化器或调了lr0比如把lr0从默认的0.01改大到0.1训练中断或loss变成nan的概率会陡增。坑洞数据集小梯度更新方向本身就敏感学习率翻车后第一个epoch的loss就会异常这时候调lr0比调模型结构更快见效。5. 小样本坑洞训练常见问题5个最容易翻车的点小数据集训练最磨心态因为图少每一个微小错误都会被放大成训练结果的全面失败。下面几条是这类坑洞检测项目里反复出现的踩坑记录按现象、原因、解决三层拆开方便你对照排查。5.1 训练跑完mAP0loss却正常现象loss曲线持续下降训练过程没有任何报错但验证集mAP50一直是0推理时一个框都不出。原因最可能在转换脚本阶段埋下数据划分或标签里的坐标归一化出错框的位置偏了但loss还在下降模型学到的是“背景区域没有目标”。第二个常见根因是验证集里恰好没有坑洞或者坑洞样本的标注全被之前校验脚本误删。解决先回看校验脚本输出确认txt里的坐标分布是否合理再用下面一段代码暴力可视化几个框画在图上对照原图。from PIL import Image import matplotlib.pyplot as plt import matplotlib.patches as patches img Image.open(JPEGImages/000001.jpg) w, h img.size with open(labels/000001.txt) as f: line f.readline().split() x_c, y_c, bw, bh map(float, line[1:]) # 反归一化还原像素坐标 xmin (x_c - bw / 2) * w ymin (y_c - bh / 2) * h fig, ax plt.subplots(1) ax.imshow(img) rect patches.Rectangle((xmin, ymin), bw * w, bh * h, linewidth2, edgecolorr, facecolornone) ax.add_patch(rect) plt.show()这个逻辑仿照上面代码块前半段跑一遍框如果画得和坑洞边缘落差超过15个像素就去查转换脚本里的归一化公式。5.2 验证集里全是假阳性划分时复制粘贴覆盖了原目录现象训练效果“好得离谱”训练集mAP接近0.99验证集mAP也超过0.95。原因直接复制图片文件时把同一批文件的另一个副本也复制过去或者在脚本里用shutil.copy把训练集图片复制到了验证集目录导致两个集合共享同一批样本。解决严格按3.2节的清单方式划分首先确认train.txt和val.txt里的路径没有重复。去重检查就在训练前跑一句comm -12 train.txt val.txt有输出就说明两边存在交集。5.3 训练中途loss突变为nan进程直接崩现象训练到第40轮左右loss突变成nan日志开始报错训练进程退出。原因最常见是学习率过大导致梯度爆炸另一个坑洞数据专项原因是mosaic增强时四张图拼接后某张图的标注映射坐标计算溢出生成超大的loss值。解决控制变量法先试关闭mosaicmosaic0.0如果稳定了就是数据增强的问题如果还在nan把lr0调小一个数量级再跑。优化器层面还可以加梯度裁剪参数但yolov8里更实际的做法是降学习率——小数据集普遍对学习率更敏感。5.4 训练启动就报错标签类别和配置对不上现象日志里出现“class index out of range”或者反复打印warning说某张图的txt里浮点数字超出预期。原因转换脚本的class过滤没有生效单片图上存在background、crack等非坑洞类别的objectclass_id被统一设成0以外的数值。解决不要只用肉眼扫txt跑一遍3.3节的校验脚本把所有class_id不等于0的行全部打印出来对照图片看是标注混入其他类别了还是XML里的category字段没填pothole。如果是标注混入把该object直接过滤掉最稳妥。5.5 混淆矩阵总合不唯一验证阶段类别映射错位现象训练完成后验证时混淆矩阵的行列加起来和样本总数对不上类别0的recall看起来莫名其妙地低。原因验证时data.yaml的names列表顺序用的是自定义类别顺序而训练时权重里的类别映射是按另一套顺序写入的或者是迁移了之前某个模型的缓存。解决重新validate时带上同一个data.yaml并清掉run目录下的缓存文件再跑一遍。混淆矩阵总合不唯一大概率是data.yaml里只换了一个类别名但忘了同步模型输出层的类别索引。6. 留一个可复用的推理验证脚本用真实视频帧检验模型泛化训练出的模型最终要到真实路面上接受检验写推理脚本是个技术活也是个体力活。我会在训练结束后用一批没进过训练集的真实拍摄视频帧做验证而不是只看验证集数值。视频帧往往是鱼眼镜头或行车记录仪拍出来的光照条件复杂路面纹理和训练集分布有明显差异这正是检验泛化能力的考场。推理脚本的核心逻辑是调用训练好的best.pt权重对一张新图做检测并输出带框的结果图from ultralytics import YOLO import cv2 model YOLO(runs/pothole_train/weights/best.pt) img cv2.imread(test_frames/road_007.jpg) # conf: 置信度阈值坑洞场景建议0.25起步 results model.predict(img, conf0.25, iou0.5, imgsz640, verboseFalse) for box in results[0].boxes: x1, y1, x2, y2 box.xyxy[0].tolist() conf box.conf[0].item() cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2) cv2.putText(img, fpothole {conf:.2f}, (int(x1), int(y1)-8), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imwrite(test_frames/road_007_out.jpg, img)这段代码值得注意的两个参数是conf和iou。conf0.25是个比较宽松的阈值推理时宁多勿缺后续可以再通过跟踪算法做时序筛选iou0.5是非极大值抑制的阈值用它去掉重叠框。初版推理结果如果发现坑洞漏检多就降conf到0.15代价是树叶阴影和污渍会变成误检如果误检多就往0.4以上调。跑推理脚本时我习惯同时把置信度分布打印出来看正样本置信度集中在0.5到0.7还是0.9以上这能侧面反映模型的确定性水平。有一次我给甲方跑测试视频模型在正常路面上框出了十几处“坑洞”后来发现那些全是积水反光。这类场景分布偏移问题是单类别检测的固有边界常见的做法是专门拍一个小时的雨夜路面视频筛选出误检帧做二次标注补进训练集再迭代一版。模型效果的提升最后往往不在网络结构改动上而是一次一次把真实误检样本喂回数据集里。这套双格式数据集的处理流程走完一遍回头再看665张图真正消耗时间最多的是数据校验不是训练本身。我第一次跑坑洞检测时在数据划分上偷了懒直接随机复制文件结果验证集泄漏导致模型在测试视频上全线翻车后来老实写了校验脚本才算稳住。做目标检测数据集宁可慢一步把清单、标签格式、类别映射全部核清楚也不要急着开训练数据这一关通过之后后面的路通常就顺畅了希望帮到你。本文还有配套的精品资源点击获取
