简介:本资源为道路裂缝检测方向的Pascal VOC格式目标检测数据集,面向从事道路病害识别、路面养护巡检及计算机视觉算法训练的学生、研究人员与工程开发者,可用于裂缝检测模型的训练、验证与对比实验。压缩包共约2000个文件,包含12988张jpg图像与12988个同名xml标注文件,另附1个使用说明txt,整体约895.19MB,图像与标注一一对应,便于直接接入主流检测框架。标注由labelImg完成,采用矩形框方式,仅设roadcrack一个类别,累计标注框数达35440个,覆盖多种路面裂缝形态,标注准确合理。目前已有4720人学习下载,适合需要大规模单类别裂缝样本的检测任务,可省去自行采集与标注成本,快速构建训练集并开展模型迭代与效果评估。
1. 道路裂缝数据集VOC格式-12988张:从标注文件到训练管线的落地拆解
手里拿到一个标注好的道路裂缝数据集,12988 张图像,VOC 格式,第一反应往往不是兴奋而是犯愁——XML 散落在 Annotations 目录里,JPEGImages 里图像命名五花八门,直接喂给检测器大概率在数据加载阶段就报错。道路裂缝检测本身是路面养护、市政巡检、自动驾驶感知里的高频需求,裂缝目标细长、对比度低、背景纹理复杂,对数据管线的鲁棒性要求比常规目标检测更高。这个数据集的价值不在于数量,而在于它把「裂缝」这个难样本类别做成了可直接复用的标注资产。适合谁用:做路面病害检测的算法工程师、想跑通自定义数据集训练的学生、需要快速验证裂缝分割或检测方案的产品团队。接下来我会按「先看清数据长什么样、再转成训练框架吃得下的格式、然后跑通训练、最后处理裂缝特有的坑」这条线,把整个流程拆开讲。
2. 先摸清 VOC 目录结构与 12988 张的分布底细
拿到数据集别急着写 DataLoader,先花二十分钟把目录结构和标注分布摸清楚。VOC 格式看似标准,实际交付时经常出现文件名大小写不一致、XML 字段缺失、图像与标注数量对不上这三类问题。12988 张这个量级,如果前期不做统计,训练到一半才发现某个类别只有几十个样本,返工成本很高。
2.1 VOC 标准目录与常见变体
标准 VOC 目录长这样:
VOCdevkit/ └── VOC2007/ ├── Annotations/ # XML 标注文件 ├── JPEGImages/ # 原始图像 ├── ImageSets/ │ └── Main/ # train.txt / val.txt / trainval.txt └── SegmentationClass/ # 分割任务才用但实际拿到的数据集经常是扁平结构,Annotations 和 JPEGImages 直接放在根目录,ImageSets 缺失。这时候需要先确认两件事:图像扩展名是否统一(.jpg / .JPG / .png 混用很常见),XML 里的 filename 字段是否和实际文件名完全一致。我一般会先跑一段统计脚本:
import os import xml.etree.ElementTree as ET from collections import Counter img_dir = "JPEGImages" ann_dir = "Annotations" img_files = set(os.path.splitext(f)[0] for f in os.listdir(img_dir)) ann_files = set(os.path.splitext(f)[0] for f in os.listdir(ann_dir)) print("图像数量:", len(img_files)) print("标注数量:", len(ann_files)) print("有图无标注:", len(img_files - ann_files)) print("有标注无图:", len(ann_files - img_files)) # 统计类别分布 cls_counter = Counter() for ann in os.listdir(ann_dir): tree = ET.parse(os.path.join(ann_dir, ann)) for obj in tree.findall("object"): cls_counter[obj.find("name").text] += 1 print("类别分布:", cls_counter)这段脚本做三件事:比对图像与标注的文件名集合,找出缺失配对;遍历所有 XML 统计每个类别的目标数量;输出结果供后续决策。参数上只需要改img_dir和ann_dir两个路径。如果「有图无标注」数量超过总数的 2%,说明交付质量有问题,需要联系数据方补齐或直接剔除这些图像。类别分布如果出现长尾——比如「横向裂缝」有 8000 个而「网状裂缝」只有 200 个——训练时就要考虑重采样或类别权重。
2.2 12988 张的划分策略与统计口径
12988 张不是小数目,划分 train/val 时不能简单 8:2 随机切。道路裂缝图像往往来自连续采集,相邻帧高度相似,随机划分会导致验证集里出现和训练集几乎一样的图像,指标虚高。常见做法是按采集批次或路段划分,如果没有批次信息,至少要用图像哈希去重后再切分。
import hashlib from sklearn.model_selection import train_test_split def file_hash(path): with open(path, "rb") as f: return hashlib.md5(f.read()).hexdigest() names = sorted(img_files) hashes = [file_hash(os.path.join(img_dir, n + ".jpg")) for n in names] # 按哈希去重后再划分 unique = {} for n, h in zip(names, hashes): unique.setdefault(h, n) unique_names = list(unique.values()) train, val = train_test_split(unique_names, test_size=0.15, random_state=42) with open("ImageSets/Main/train.txt", "w") as f: f.write("\n".join(train)) with open("ImageSets/Main/val.txt", "w") as f: f.write("\n".join(val))哈希去重能干掉重复或近乎重复的图像,test_size=0.15留出约 1900 张做验证。random_state固定后结果可复现。注意这里用的是文件级 MD5,只能去掉完全相同的图,如果采集设备有轻微抖动导致像素级差异,还需要用感知哈希(pHash)做近似去重。划分完成后检查一下 train 和 val 的类别分布是否接近,偏差过大就换随机种子重切。
3. 把 VOC 转成 YOLO 格式:坐标归一化与目录重组
VOC 用绝对坐标存 bounding box,YOLO 用归一化的中心点加宽高。转换本身不复杂,但裂缝目标细长,坐标取整和边界裁剪稍不注意就会产生零宽或零高的无效框。12988 张里只要有几十个这种框,训练时就会报 NaN loss。
3.1 VOC 到 YOLO 的转换脚本与参数说明
import os import xml.etree.ElementTree as ET from PIL import Image classes = ["crack"] # 根据实际类别修改 cls_map = {c: i for i, c in enumerate(classes)} def convert(xml_path, img_path, out_label_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in cls_map: continue bbox = obj.find("bndbox") x1 = float(bbox.find("xmin").text) y1 = float(bbox.find("ymin").text) x2 = float(bbox.find("xmax").text) y2 = float(bbox.find("ymax").text) # 边界裁剪,防止越界 x1, y1 = max(0, x1), max(0, y1) x2, y2 = min(w, x2), min(h, y2) bw, bh = x2 - x1, y2 - y1 if bw < 2 or bh < 2: # 过滤无效框 continue cx = (x1 + x2) / 2 / w cy = (y1 + y2) / 2 / h nw = bw / w nh = bh / h lines.append(f"{cls_map[name]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") with open(out_label_path, "w") as f: f.write("\n".join(lines))关键参数有三个:classes列表要和 XML 里的 name 字段完全对应,大小写敏感;bw < 2 or bh < 2这个阈值过滤掉细到无法学习的框,裂缝本身可能只有几个像素宽,但小于 2 像素的框在特征图上基本消失;坐标保留 6 位小数足够,YOLO 官方实现内部会做浮点运算,精度损失可忽略。转换后目录结构应该是 images/train、images/val、labels/train、labels/val 四个文件夹,图像和标签文件名一一对应,扩展名不同。
3.2 生成 data.yaml 与训练前自检
YOLO 系列训练需要一个 data.yaml 描述数据路径和类别:
path: /data/crack_dataset train: images/train val: images/val nc: 1 names: ["crack"]path用绝对路径避免相对路径解析歧义,nc是类别数,names顺序必须和转换脚本里的classes一致。写完后跑一个自检脚本,随机抽 20 张图把标注框画出来看一眼:
import cv2 import random for name in random.sample(train, 20): img = cv2.imread(f"images/train/{name}.jpg") h, w = img.shape[:2] with open(f"labels/train/{name}.txt") as f: for line in f: c, cx, cy, nw, nh = map(float, line.split()) x1 = int((cx - nw/2) * w) y1 = int((cy - nh/2) * h) x2 = int((cx + nw/2) * w) y2 = int((cy + nh/2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(f"check_{name}.jpg", img)这一步是血泪经验——转换脚本写错一个除法方向,框会全部偏移到图像角落,但训练 loss 照样下降,只是 mAP 永远上不去。可视化检查能在训练前拦住这类低级错误。
4. 用 YOLOv8 跑通裂缝检测训练:参数配置与显存权衡
数据管线通了之后,训练本身反而是最标准的一步。但裂缝检测有几个特殊性:目标细长导致正样本匹配困难,背景占比极高导致正负样本失衡,图像分辨率通常较大导致显存吃紧。这一章把训练命令、关键参数和显存优化讲清楚。
4.1 训练命令与裂缝场景的参数调整
yolo detect train \ data=/data/crack_dataset/data.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=1024 \ batch=8 \ lr0=0.01 \ lrf=0.01 \ warmup_epochs=5 \ mosaic=0.5 \ mixup=0.0 \ copy_paste=0.0 \ degrees=0.0 \ translate=0.1 \ scale=0.3 \ fliplr=0.5 \ flipud=0.0 \ patience=30 \ device=0逐项说明:imgsz=1024是因为裂缝在低分辨率下会丢失细节,640 训练出来的模型对小裂缝召回明显偏低,但 1024 会让显存占用翻倍,batch=8是 8GB 显存下的保守值,显存够可以加到 16。mosaic=0.5比默认的 1.0 低,因为 mosaic 四图拼接会引入大量非裂缝背景,裂缝本身占比就小,过度拼接反而稀释正样本。degrees=0.0关闭旋转增强,道路裂缝有明确的方向语义,随机旋转会制造不存在的裂缝朝向。flipud=0.0同理,上下翻转在道路场景中不自然。patience=30是早停轮数,裂缝数据集容易过拟合,验证集 mAP 连续 30 轮不升就停。
4.2 显存不够时的三个降级方案
1024 分辨率加 YOLOv8s 在 8GB 卡上跑 batch=8 已经是极限,如果显存报 OOM,按优先级试这三个方案。第一,换yolov8n.pt,参数量从 11M 降到 3M,显存占用降约 40%,代价是 mAP 掉 2 到 4 个点。第二,开梯度累积,batch=4配合nbs=64,等效 batch 不变但显存峰值降低。第三,用imgsz=768训练,推理时再用 1024 测试,这种 train-test resolution discrepancy 在裂缝检测里通常能接受,因为裂缝的纹理特征在 768 下已经可见。
# 梯度累积方案 yolo detect train data=data.yaml model=yolov8s.pt batch=4 nbs=64 imgsz=1024 epochs=150nbs是名义 batch size,YOLOv8 内部会自动做梯度累积来匹配。注意batch必须是nbs的约数,否则会有警告。训练过程中用nvidia-smi -l 1监控显存,如果看到显存缓慢增长直到 OOM,大概率是 dataloader 的 worker 泄漏,把workers从默认 8 降到 4 试试。
5. 裂缝检测训练中的避坑与排查记录
这一章记录我在裂缝数据集上实际翻过的车,每条按现象、原因、解决三段写。有些坑在通用目标检测里不明显,但裂缝的细长特性会把它放大。
5.1 现象:loss 正常下降但 mAP 始终低于 0.1
原因:VOC 转 YOLO 时坐标归一化用错了分母。有人用x2 - x1做宽度的归一化分母,正确应该是图像宽度w。这种错误不会让训练崩溃,因为 YOLO 的损失函数对框的尺度有一定容忍度,但预测框会系统性地偏大或偏小,IoU 永远达不到阈值。解决:用 3.2 节的可视化脚本抽查,如果框的位置对但大小明显不对,就是归一化分母问题。重新检查转换脚本里nw = bw / w和nh = bh / h这两行。
5.2 现象:验证集 mAP 波动极大,相邻两轮能差 0.15
原因:验证集里混入了和训练集同批次采集的近似图像,模型在验证时相当于看到了训练数据的变体,指标虚高且不稳定。12988 张如果来自连续视频抽帧,这个问题几乎必然出现。解决:回到 2.2 节的哈希去重,把 MD5 换成 pHash,汉明距离小于 5 的视为重复。去重后验证集 mAP 会下降几个点,但那是真实水平,后续调优才有意义。
5.3 现象:训练到 50 轮左右 loss 突然变 NaN
原因:标注里有零宽或零高的框,或者坐标超出图像边界。VOC 标注工具在标注细长裂缝时,如果用户拖拽幅度很小,可能产生 x1 等于 x2 的框。YOLO 在计算宽高损失时会对零值取对数,直接 NaN。解决:转换脚本里的bw < 2 or bh < 2过滤能拦住大部分,但还要加一道坐标裁剪,确保 x1 < x2 且 y1 < y2。如果 NaN 已经出现,检查最近一个 epoch 的 batch,用torch.autograd.set_detect_anomaly(True)定位到具体样本。
5.4 现象:推理时小裂缝全部漏检,大裂缝正常
原因:训练时的 anchor 或特征图尺度不匹配。YOLOv8 是 anchor-free 的,但特征金字塔的 stride 决定了最小可检测目标。1024 输入下 P3 特征图的 stride 是 8,意味着小于 8 像素的裂缝在特征图上不足一个点。解决:如果小裂缝是重点,把imgsz提到 1280 或 1536,或者改用分割方案把裂缝当作细长区域处理。另一个方向是切片推理,把大图切成 512 的小块分别检测再合并,这对路面巡检的工程落地更实用。
5.5 现象:模型在验证集上表现好,实际路段测试一塌糊涂
原因:数据集里的裂缝类型和实际路段不匹配。12988 张如果集中来自某几种路面材质(比如沥青),模型学到的是沥青裂缝的纹理,遇到水泥路面就失效。解决:先统计数据集的采集场景分布,如果材质单一,要么补数据,要么在训练时加强颜色和纹理增强(hsv_h=0.05、hsv_s=0.7、hsv_v=0.4),让模型对材质变化更鲁棒。实际部署前一定要用目标路段的数据做一次零样本测试,别只看验证集指标。
6. 从 12988 张到可部署模型:切片推理与阈值调优
训练完拿到 best.pt 只是中间产物,裂缝检测的落地难点在推理阶段。整图推理对小裂缝不友好,而切片推理又面临切片边界处的裂缝被切断的问题。这一章讲一套我常用的切片加合并策略,以及置信度阈值的调法。
6.1 切片推理的实现与重叠区域合并
思路是把原图按固定步长切成有重叠的小块,每块单独推理,再把所有预测框映射回原图坐标,最后用 NMS 去重。重叠区域的作用是避免裂缝正好落在切片边界时被截断。
import cv2 import numpy as np from ultralytics import YOLO model = YOLO("best.pt") def sliced_inference(img, slice_size=640, overlap=128, conf=0.25): h, w = img.shape[:2] stride = slice_size - overlap all_boxes = [] for y in range(0, h, stride): for x in range(0, w, stride): x2 = min(x + slice_size, w) y2 = min(y + slice_size, h) x1 = max(0, x2 - slice_size) y1 = max(0, y2 - slice_size) patch = img[y1:y2, x1:x2] results = model.predict(patch, conf=conf, verbose=False) for r in results: for box in r.boxes: bx1, by1, bx2, by2 = box.xyxy[0].tolist() all_boxes.append([ bx1 + x1, by1 + y1, bx2 + x1, by2 + y1, box.conf[0].item() ]) # 全局 NMS if not all_boxes: return [] boxes = np.array(all_boxes) indices = cv2.dnn.NMSBoxes( boxes[:, :4].tolist(), boxes[:, 4].tolist(), score_threshold=conf, nms_threshold=0.5 ) return boxes[indices]slice_size=640是推理时的切片大小,可以和训练时的imgsz不同,但不要差太多。overlap=128是重叠像素数,经验值是 slice_size 的 20% 左右,太小会在边界漏检,太大则推理时间线性增长。conf=0.25是初始置信度阈值,切片推理会产生大量低置信度的重复框,全局 NMS 的nms_threshold=0.5负责合并。注意cv2.dnn.NMSBoxes的输入格式是 [x, y, w, h],而 YOLO 输出是 [x1, y1, x2, y2],代码里需要做一次转换,上面为了简洁直接传了 xyxy,实际使用时记得改。
6.2 置信度阈值的分场景调法
裂缝检测的置信度阈值没有万能值,取决于你的应用场景。市政巡检追求高召回,宁可误报不可漏报,conf设 0.15 到 0.2,后续人工复核。自动化养护决策追求高精度,conf设 0.4 到 0.5,只保留确信度高的裂缝。我一般会画一条 precision-recall 曲线来选阈值:
from ultralytics import YOLO model = YOLO("best.pt") metrics = model.val(data="data.yaml", conf=0.001, iou=0.5) # metrics.box.p 和 metrics.box.r 是不同置信度下的数组 # 找 F1 最大的点作为推荐阈值 f1 = 2 * metrics.box.p * metrics.box.r / (metrics.box.p + metrics.box.r + 1e-6) best_idx = f1.argmax() print("推荐置信度阈值:", metrics.box.conf[best_idx])conf=0.001是为了让验证时输出所有可能的预测,这样才能画出完整的 PR 曲线。iou=0.5是匹配阈值,裂缝的 IoU 普遍偏低,如果发现 mAP@0.5 本身就不高,可以试试iou=0.3看指标是否合理。这套流程跑下来,从 12988 张 VOC 数据到可部署的切片推理管线就完整了。我自己的习惯是每次换数据集都先跑一遍第 2 章的统计脚本,哪怕数据方说「格式没问题」——裂缝数据的标注质量方差很大,信脚本不信承诺。希望帮到你。
本文还有配套的精品资源,点击获取