道路裂缝检测数据集实操:VOC/YOLO格式转换与YOLOv8训练全流程

发布时间:2026/10/10 14:36:12
道路裂缝检测数据集实操:VOC/YOLO格式转换与YOLOv8训练全流程 简介面向道路检测研究者、计算机视觉算法工程师及智能交通方向学生这份资源提供了完整配套的航拍路面病害检测数据集。全部3302张jpg图像均经过人工标注覆盖Alligator crack、Longitudinal crack、Oblique crack、Pothole、Repair、Transverse crack等7类常见路面病害同时给出Pascal VOC格式xml与YOLO格式txt标注文件可直接用于YOLO系列、Faster R-CNN、SSD等主流目标检测模型的训练与验证。数据集中图像与标注一一对应不涉及分割路径解压即用能显著减少前期数据预处理和格式转换成本。压缩包共2000个文件以xml标注文件为主并附有txt使用说明整体约166.21MB目录按统一命名规则组织便于批量读取和划分。目前已有1197人学习下载适合需要航拍视角下道路裂缝、坑槽、修补区域等目标检测数据支撑的课题或工程项目可作为算法验证、模型微调与对比实验的可靠数据基础。1. 道路裂缝检测数据集到手3302张航拍图、7类病害、VOC与YOLO双格式一次讲清干道路裂缝检测这行最烦的不是模型收敛慢而是数据格式脏。下载一份数据集解压出来图片、xml、txt对不上号训练脚本跑一半就报错调两天才发现是标注坐标写错了。这份航拍路面病害检测数据集至少在格式上把脏活提前干完了3302张jpg、3302份Pascal VOC格式xml、3302份YOLO格式txt三者一一对应类别覆盖Alligator crack、Longitudinal crack、Oblique crack、Pothole、Repair、Transverse crack这些道路病害的常见类型。适合正在跑道路裂缝目标检测的从业者、做无人机巡检算法验证的人以及刚用YOLOv8训练自己数据集、想找一份现成数据练手的同学。压缩包里只有图片和标注没有train/val/test划分清单所以拿到手第一件事不是开训而是把目录结构、标注坐标和类别清单逐一验干净。2. 解压与标注解析从7z到xml/txt的落盘、坐标换算与一致性检查2.1 先验证再解压别让压缩包结构打乱你的工程目录很多人在Linux下拿到7z压缩包第一反应是tar -xf或者用unzip结果报错或者解出来一堆乱码文件。7z不是tar的变种得用专门的p7zip工具。我一般拿到压缩包会先例行两步7z l看一眼内部文件列表确认是散落的jpgxmltxt散文件还是外面套了一层目录再决定解压到哪。# 先安装工具Ubuntu/Debian系常用做法 sudo apt-get install -y p7zip-full # 查看压缩包内部文件列表不要直接解压 7z l 航拍路面病害检测数据集道路裂缝检测数据集VOCYOLO格式3302张7类别.7z | head -30 # 确认结构后解压到独立的目录注意-o参数 mkdir -p ./crack_dataset 7z x 航拍路面病害检测数据集道路裂缝检测数据集VOCYOLO格式3302张7类别.7z -o./crack_dataset这里-o后面没有空格是指定输出目录的关键写法。解压完成后建议继续跑一条find检查文件数量find ./crack_dataset -name *.jpg | wc -l # 期望 3302 find ./crack_dataset -name *.xml | wc -l # 期望 3302 find ./crack_dataset -name *.txt | wc -l # 期望 3302数量对不上就要警惕如果jpg少而xml多说明有图片缺失如果txt少于xml说明转换环节丢过框。这三个数字是这份数据集的第一道体检报告我在拿到任何目标检测数据集时都会先跑一遍宁可这里花两分钟也不要等训练时报FileNotFoundError。2.2 VOC文件里读什么xml的object节点与bndbox坐标VOC格式的xml核心不是那一长串annotation标签而是每个object节点里的name和bndbox。name决定类别bndbox的四个值决定像素坐标系里的目标位置。下面这段代码用于批量读取xml并记录类别和框信息建议放到任何训练脚本之前。import os import xml.etree.ElementTree as ET xml_dir crack_dataset stats {} # 类名 - 框数量 box_size [] # 记录每个框的宽高用于后续分析 for fname in os.listdir(xml_dir): if not fname.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, fname)) root tree.getroot() # 图片文件名在 annotation/filename 节点 img_name root.findtext(filename) for obj in root.iter(object): cls obj.findtext(name).strip() stats[cls] stats.get(cls, 0) 1 # bndbox 是像素坐标VOC 约定左上右下 bnd obj.find(bndbox) xmin float(bnd.findtext(xmin)) ymin float(bnd.findtext(ymin)) xmax float(bnd.findtext(xmax)) ymax float(bnd.findtext(ymax)) box_size.append((xmax - xmin, ymax - ymin)) print(stats) # 各类别框数量 print(总框数:, sum(stats.values()))几个容易看漏的细节filename字段的值不一定和实际文件名完全一致有的数据集会把路径写进去有的只写文件名xmin/ymin/xmax/ymax在VOC里是整数像素坐标但解析时最好转成float因为后面做归一化要参与除法。另外object节点可能缺失difficult这类可选字段读取用findtext并给默认值即可不要假设每个节点结构完全一样。2.3 YOLO的txt坐标与XML的换算关系YOLO格式txt每一行是class_id cx cy w h其中cx、cy是目标中心点相对图片宽高的比例w、h是目标宽高相对图片宽高的比例全部归一化到0~1区间。从VOC到YOLO的换算公式我每次都会默写一遍cx (xmin xmax) / 2 / Wcy (ymin ymax) / 2 / Hw (xmax - xmin) / Wh (ymax - ymin) / HW、H是图片真实像素宽高不是某个固定值。下面这段脚本验证同一张图在xml和txt里的框是否对应也是我刚拿到数据集时必做的一致性检查from PIL import Image import os def voc_to_yolo(xmin, ymin, xmax, ymax, W, H): cx (xmin xmax) / 2 / W cy (ymin ymax) / 2 / H w (xmax - xmin) / W h (ymax - ymin) / H return cx, cy, w, h jpg firc_lu_2945.jpg xml firc_lu_2945.xml txt firc_lu_2945.txt W, H Image.open(jpg).size # 读xml里的第一个框 import xml.etree.ElementTree as ET root ET.parse(xml).getroot() obj next(root.iter(object)) xmin float(obj.find(bndbox).findtext(xmin)) ymin float(obj.find(bndbox).findtext(ymin)) xmax float(obj.find(bndbox).findtext(xmax)) ymax float(obj.find(bndbox).findtext(ymax)) # 读txt的第一行 with open(txt) as f: cls_id, cx, cy, w, h f.readline().split() # 换算回来的框允许3像素误差 box_xmin (float(cx) - float(w) / 2) * W box_ymin (float(cy) - float(h) / 2) * H box_xmax (float(cx) float(w) / 2) * W box_ymax (float(cy) float(h) / 2) * H print(xml框:, xmin, ymin, xmax, ymax) print(txt框:, box_xmin, box_ymin, box_xmax, box_ymax)跑完这句你会立刻知道这份数据集的标注质量两者一致说明转换干净有偏差就要看偏差是固定偏移还是比例误差。固定偏移往往是某个转换脚本把坐标原点搞错了比例误差则可能是图片被resize过但标注没跟着缩放。这种问题在公开数据集里不少见早发现早省事。3. 类别对齐与数据集划分把名义7类清理成真实类别并生成训练清单3.1 类别统计与拼写差异Oblique crack和Obliquecrack怎么回事很多人在YOLO训练里翻车不是模型问题是类别列表本身就是脏的。这份摘要描述给的类别名称是[Alligator crack,Longitudinal crack,Oblique crack,Obliquecrack,Pothole,Repair,Transverse crack]仔细数一数7个字符串里其实藏着两副面孔——Oblique crack和Obliquecrack一个带空格一个不带极可能是同一类目标在不同标注批次里写法不一致。我建议用脚本统计xml里实际出现的所有name不要直接相信压缩包附带说明import os import xml.etree.ElementTree as ET xml_dir crack_dataset name_set set() for fname in os.listdir(xml_dir): if not fname.endswith(.xml): continue root ET.parse(os.path.join(xml_dir, fname)).getroot() for obj in root.iter(object): name_set.add(obj.findtext(name).strip()) print(len(name_set)) # 名义7类实际可能只有6个不同写法 for n in sorted(name_set): print(repr(n))Oblique crack和Obliquecrack这种就是典型的同名不同写。如果不做合并训练时模型被迫区分两个完全相同的视觉类别样本被摊薄推理时模型在两者之间随机横跳mAP直接被拉低一截。拿到任何数据集都要先跑这段统计花一分钟替后面省三个小时。3.2 重写txt类别索引统一类别名并重新编号YOLO的txt里存的是class_id数字不是类别字符串所以合并拼写差异本质上是把所有属于Obliquecrack的行的class_id统一改成Oblique crack对应的编号。假设你扫描完xml后得到如下映射关系Oblique crack - id 2 Obliquecrack - id 5和Oblique crack同一个东西合并逻辑是先把所有类别按字典序或出现频率排序建立统一的names列表再逐行改写txt。下面这段代码直接干这件事并且顺带检查每个txt的行数是否和对应xml的object数量一致import os xml_dir crack_dataset txt_dir crack_dataset # 人工确认后的合并映射把拼写不同但视觉相同的类别归并 merge_map { Obliquecrack: Oblique crack, # 如果还有别的变体继续往这里加 } # 第一步用xml扫描全部类别生成统一names # 这里用你上面3.1打印出来的结果手动填或用set自动收集 all_names [Alligator crack, Longitudinal crack, Oblique crack, Pothole, Repair, Transverse crack] name_to_id {n: i for i, n in enumerate(all_names)} for fname in os.listdir(txt_dir): if not fname.endswith(.txt): continue new_lines [] with open(os.path.join(txt_dir, fname)) as f: for line in f: parts line.split() if len(parts) ! 5: # 行格式异常记录下来 print(异常行:, fname, line) continue cls_id parts[0] # class_id - 类别名 - 合并 - 新class_id old_name all_names[int(cls_id)] if int(cls_id) len(all_names) else Unknown merged_name merge_map.get(old_name, old_name) new_id name_to_id[merged_name] new_lines.append(f{new_id} { .join(parts[1:])}\n) with open(os.path.join(txt_dir, fname), w) as f: f.writelines(new_lines)这段代码跑完整个数据集的类别编号就统一了。注意all_names的顺序一旦定了后面写YOLO数据yaml里的names顺序必须和这里完全一致因为txt第0列是数字索引不是字符串顺序错了等于所有类别张冠李戴。这是YOLO训练最常见的隐形坑比超参调不好致命得多。3.3 自己划分train/val/test固定随机种子提防数据泄漏摘要描述里写得很清楚这份资源不包含分割路径的txt文件只提供jpgxmltxt三件套。所以训练前必须自己划分数据集。划分方式直接决定验证集的可信度如果文件是无人机按航线连续拍摄的相邻编号的图片很可能来自同一段路面随机划分会把同一场景的几帧同时分进训练集和验证集造成数据泄漏val指标虚高模型实际泛化能力没那么好。我之前踩过这种坑随机划分后mAP跑到0.82换了一组真正独立的航拍视频测试直接跌到0.61。后来学乖了凡是带连续编号的文件名一律先排序再分段划分import os import shutil import random random.seed(42) files sorted([f for f in os.listdir(crack_dataset) if f.endswith(.jpg)]) n len(files) train_idx set(range(n)) val_idx set(range(int(n * 0.8), n)) # 尾部20%作为验证 test_idx set(range(int(n * 0.7), int(n * 0.8))) # 中间10%作为测试 for i, f in enumerate(files): src os.path.join(crack_dataset, f) if i in train_idx: dst os.path.join(crack_dataset, train) elif i in val_idx: dst os.path.join(crack_dataset, val) else: dst os.path.join(crack_dataset, test) os.makedirs(dst, exist_okTrue) shutil.move(src, os.path.join(dst, f))注意这里的关键不是把图片搬过去就完事而是要同步搬同名的xml和txt。建议把三个子目录各自再拆成images和labels两层YOLOv8的数据yaml更习惯这种结构。如果文件名确实没有明显的序列关联性用random.seed(42)做纯随机划分也可以但要保证seed固定否则每次跑脚本划分结果不同实验结果没法复现。4. YOLOv8训练航拍裂缝数据yaml、超参数选择与日志判读要点4.1 环境准备与数据yaml组织数据集清理干净后训练部分反而简单。这几年道路裂缝检测的落地项目里YOLO系是绝对主力YOLOv8的ultralytics库把训练流程封装得很完整不用从零写dataloader。环境安装用pip一行搞定pip install ultralytics然后准备好目录结构。我习惯把第3章整理好的数据按下面这种布局放好和后续所有YOLO项目统一crack_dataset/ ├── train/ │ ├── images/ │ │ ├── firc_lu_2945.jpg │ │ └── ... │ └── labels/ │ ├── firc_lu_2945.txt │ └── ... ├── val/ │ ├── images/ │ └── labels/ └── test/ ├── images/ └── labels/数据yaml写起来很简单但有一个原则必须守住names的顺序必须和第3章重写txt时用的all_names保持一致不能按自己的喜好调整。比如你把Pothole放在第0个位置那所有txt里class_id0的框就全变成Pothole了模型会把鳄鱼裂缝叫成坑洞# crack.yaml path: /absolute/path/to/crack_dataset train: train/images val: val/images test: test/images names: 0: Alligator crack 1: Longitudinal crack 2: Oblique crack 3: Pothole 4: Repair 5: Transverse crack我见过有人在yaml里写相对路径训练时换了一台机器跑就报Dataset not found。YOLO的path字段用绝对路径最稳或者用相对于yaml文件所在目录的路径写法具体可以看ultralytics的官方文档——但最省事的方案就是绝对路径一劳永逸。4.2 训练脚本与关键超参数解读训练命令用ultralytics的CLI或Python API都行。命令行方便快速跑通Python脚本方便做实验管理。这里直接给一份训练脚本并解释每个关键参数在这个航拍裂缝场景下的意义yolo detect train \ datacrack.yaml \ modelyolov8s.pt \ epochs200 \ imgsz640 \ batch16 \ workers4 \ patience50 \ projectruns_crack \ nameexp01 \ device0参数解析modelyolov8s.pt用小型模型权重做迁移学习而不是从yolov8n.pt开始。裂缝是细长目标尤其Oblique crack和Longitudinal crack宽高比经常超过1:10模型容量太小的话特征提取能力不够n模型在这种任务上容易欠拟合。如果显卡显存小于8G退一步用n也行但要做好mAP低一截的心理准备。imgsz640这个值不是越大越好。航拍图像里裂缝占的像素区域很小理论上用1024或1280能提升小目标召回但显存占用翻倍训练时间也翻倍。我的习惯是第一轮先用640跑通全流程确认数据没问题后再拿1280做对比实验而不是一上来就开大分辨率。batch16batch大小主要受显存限制。训练过程中如果报CUDA out of memory先把batch降到8或者把workers降到2。batch减半后学习率也该跟着调简单做法是保持默认的lr00.01看训练loss曲线来定要不要降。patience50早停耐心值50个epoch内val指标没提升就自动停。航拍裂缝数据集类别多、目标小收敛本来就比Pascal VOC这种通用数据集慢patience太低容易在模型还没学到位时就被打断。我一般设50~80之间。4.3 训练日志判读三个必看的信号训练跑起来之后不是盯着进度条干等就行。ultralytics会在runs_crack/exp01/下输出results.csv和一堆图表我每次必看三样东西第一是train/box_loss和val/box_loss的分离程度。两条曲线从始至终粘在一起说明模型欠拟合该加epoch或加大模型如果train的loss一路下降、val的loss在第50个epoch开始反弹这是过拟合信号早停机制一般会自动触发但你要是看到了就该考虑加augment或增大patience的代价。第二是metrics/mAP50(B)和metrics/mAP50-95(B)的差距。裂缝检测任务里这两个值通常差得很大因为目标太小IoU阈值从严之后很多预测框对不上。如果mAP50能到0.8但mAP50-95只有0.3说明框的位置精度不够常见解法是调高imgsz或者在推理阶段做多尺度TTAtest time augmentation。第三是验证集的混淆矩阵图ultralytics每次训练结束会自动保存confusion_matrix.png。看Oblique crack和Longitudinal crack是不是互相串——这两种裂缝形态本来就接近如果混淆矩阵里非对角线数值偏高说明类别定义本身对模型区分度不够需要回去检查标注样本而不是急着调模型。# 训练结束后直接查看验证集指标汇总 cat runs_crack/exp01/results.csv | tail -5这一行的输出会告诉你每个epoch的mAP、precision、recall拿来做多组实验之间的横向对比非常方便。我习惯每跑完一组实验就把这行记录复制进一个Excel几组对比下来哪个参数组合有效一目了然。5. 避坑与常见问题标注缺框、类别错位与数据泄漏的5个现场记录5.1 txt文件为空或行数少于xml的object数现象训练日志里频繁出现WARNING: zero labels found on ...某个epoch的loss数值异常偏大甚至直接变nan。原因这份标称3302份txt与3302份xml对应的数据集在实际使用中可能因为格式转换脚本的漏框逻辑让部分txt文件丢掉了一些object。xml里写着5个框txt里只有3行YOLO训练时那两张图就只学了3个目标另2个目标形同未标注模型推理时自然漏检。解决训练前写一个全量比对脚本统计每个同名xml和txt的object数量import os import xml.etree.ElementTree as ET xml_dir crack_dataset for fname in os.listdir(xml_dir): if not fname.endswith(.xml): continue base fname.replace(.xml, ) root ET.parse(os.path.join(xml_dir, fname)).getroot() xml_count sum(1 for _ in root.iter(object)) txt_path os.path.join(xml_dir, base .txt) with open(txt_path) as f: txt_count sum(1 for line in f if len(line.split()) 5) if xml_count ! txt_count: print(f不一致: {base} xml{xml_count} txt{txt_count})跑出不一致的文件列表后用第2章的VOC转YOLO脚本把xml重新转一次覆盖对应txt这是标准补救流程。5.2 训练时报类别数对不上或names列表错位现象训练启动时报错提示nc7但数据里统计到的class_id最大值是6或8或者训练能跑但loss异常高。原因txt里的class_id和yaml里的names顺序不一致。常见于有人用别的工具重新排了names顺序但没有同步重新编号txt。摘要描述里的Oblique crack和Obliquecrack若不合并会让实际类别数变7但视觉上只有6类模型被迫学一个无意义的类别边界。解决严格按第3章流程先扫xml定names再重写txt最后写yaml。三步的names顺序保持同一个变量切忌手工改yaml而不改txt。5.3 标注框越界xmax或ymax超出图片宽高现象训练时ultralytics报类似Unusual bbox width or height的警告或者loss曲线在前期剧烈震荡。原因航拍数据里有些病害目标在图像边缘标注人员没裁剪框直接画出图片边界。VOC格式允许这种越界但YOLO归一化后w或h会大于1模型训练时归一化处理不一致导致框坐标异常。解决写一段脚本读取每张图片的真实尺寸把xml里所有越界坐标clip回边界内然后重新生成txtfrom PIL import Image import xml.etree.ElementTree as ET for fname in os.listdir(crack_dataset): if not fname.endswith(.xml): continue img_path fname.replace(.xml, .jpg) W, H Image.open(img_path).size root ET.parse(fname).getroot() changed False for obj in root.iter(object): bnd obj.find(bndbox) for axis in [xmin, xmax]: if int(float(bnd.findtext(axis))) 0: bnd.find(axis).text 0 changed True # ymax和xmax同理clip到W/H if changed: tree ET.ElementTree(root) tree.write(fname)这段代码在clip之后还需要配合第2章脚本同步重新生成txt因为XML改完后YOLO标注还停留在旧的越界坐标。5.4 随机划分导致数据泄漏现象训练完在val集上报出mAP50超过0.85但你拿到现场拍的同类照片去预测效果明显不及验证集表现。原因航拍视频的相邻帧之间高度相似随机划分train/val时把同一段路面的连续帧拆开了模型见过的画面在val里又见了一遍。解决改用第3章按文件名排序后分段划分的方案保证同一条连续片段都落在一个集合里。判断依据很简单文件名编号连续且间隔不超过20就当作同一场景去处理。5.5 解压后目录嵌套混乱、文件名不匹配现象解压后发现所有文件都堆在一个叫使用前必读的文件夹下面或者jpg文件叫firc_lu_2945.jpg而xml里的filename写的是带路径的完整路径。原因7z的目录结构跟你预期的不一样或者标注工具在写filename节点时带了相对路径导致按xml读取图片时拼出的路径不对。解决用第2章的7z命令先看内部结构xml解析时用os.path.basename把filename字段规范化成纯文件名不要直接拿它拼接路径。我自己的准则是所有文件名映射都基于jpg文件的实际文件名xml里的filename字段只做参考不做拼接依据。6. 进阶验证用模型抽检标注质量把脏数据清理成可用训练集训练完一轮拿到best.pt之后不要急着用。把best.pt放到验证集上跑一遍批量预测然后把预测结果和ground truth做交叉比对这是清洗数据质量最有效的一步也是我第一次跑完这个数据集后才悟出来的流程。第一步对验证集图片生成批量预测结果保存为JSON格式yolo detect predict \ modelruns_crack/exp01/weights/best.pt \ sourcecrack_dataset/val/images \ save_jsontrue \ projectruns_crack_eval \ nameeval01save_jsontrue会输出一个predictions.json里面记录每张图每个预测框的类别、置信度和坐标。第二步用一段脚本把预测结果和val集里对应的txt标注做对比找出两类典型问题样本预测置信度极高但txt里没有对应目标的大概率是标注漏了框txt里有目标但模型在连续多个epoch里都检测不到说明标注框本身偏移严重或者目标太小超出模型分辨率。import json with open(runs_crack_eval/eval01/predictions.json) as f: preds json.load(f) # 统计每张图上高置信度预测的数量 from collections import defaultdict img_count defaultdict(int) for p in preds: if p[confidence] 0.8: img_count[p[image_id]] 1 # 输出高置信度预测但可能没标注的图片文件名 for img_id, cnt in sorted(img_count.items(), keylambda x: -x[1])[:20]: print(img_id, cnt)这些样本通常同时存在“标注缺漏”和“模型过拟合”两种可能单独看模型预测没法区分。我的习惯是把这些候选图片导出来用LabelImg或X-AnyLabeling打开人眼扫一遍确认是漏标注就补上确认是模型误检就记下来作为hard example后续训练中单独加权。补完标注后重新走一遍第3章的划分流程再训练第二轮mAP通常会有明显提升——这个提升不是调参调出来的是数据质量换来的。从那以后每次拿到新数据集我都强制自己先跑完标注一致性检查、类别合并、按序列划分这三步再谈训练。顺序乱了、跳过任何一步训练阶段的代价都会加倍还回来。希望你也能用这套流程把这3000多张航拍裂缝数据真正吃透少走我当年走的弯路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询