简介本资源为道路裂缝检测方向的Pascal VOC格式目标检测数据集面向从事道路病害识别、路面巡检算法研发的学生与工程师可用于训练和验证裂缝检测模型。数据集共包含12988张jpg图像及一一对应的12988个xml标注文件标注类别仅roadcrack一类累计标注框数达35440个全部由labelImg工具按矩形框规则人工标注标注准确合理。压缩包为zip格式文件总数2000内含13054个jpg、12988个xml及1个使用说明txt整体约895.19MB目录结构清晰便于直接接入YOLO、Faster R-CNN等主流检测框架。目前已有4720人学习下载适合需要大规模单类别裂缝样本、快速搭建训练与评测流程的读者参考使用。1. 道路裂缝数据集VOC格式-12988张从标注结构到训练落地的完整拆解手里有一份 12988 张的道路裂缝数据集VOC 格式第一反应往往不是“数据量够大”而是“这批标注到底能不能直接喂进检测网络”。道路裂缝检测在养护巡检、路面病害普查、自动驾驶高精地图更新里都是高频需求但公开可用的裂缝数据要么类别单一要么标注质量参差要么格式转换后框全歪了。这份数据集的价值在于它把裂缝目标按 VOC 的 XML 结构组织好了省掉了从零标注的时间但 VOC 本身不是训练格式真正落地还得走一遍转换、校验、划分、增强的流程。适合做道路病害检测的算法工程师、做巡检系统落地的开发者以及需要快速搭一个裂缝检测 baseline 的学生。下面按我实际拆包跑通的顺序把结构、转换、训练和坑一次讲清。2. VOC 标注结构拆解12988 张图里到底存了什么2.1 VOC 目录树与 XML 字段含义拿到 VOC 格式的数据集第一件事不是急着转 YOLO而是先确认目录结构是否符合标准 VOC 布局。标准 VOC 通常长这样RoadCrack_VOC/ ├── JPEGImages/ # 12988 张 jpg 原图 ├── Annotations/ # 对应的 xml 标注文件 ├── ImageSets/ │ └── Main/ # 训练/验证/测试划分文件 └── SegmentationClass/ # 裂缝数据集一般没有可忽略JPEGImages和Annotations的文件名必须一一对应只是扩展名不同。我一般先跑一条命令核对数量ls JPEGImages | wc -l ls Annotations | wc -l两个数字必须都是 12988。如果 Annotations 少了说明有图没标训练时这些图会被当成纯背景反而拉低召回。XML 里真正影响训练的是这几个字段annotation filenameroad_00001.jpg/filename size width1280/width height720/height depth3/depth /size object namecrack/name difficult0/difficult bndbox xmin312/xmin ymin204/ymin xmax498/xmax ymax356/ymax /bndbox /object /annotationname是类别名裂缝数据集常见就一个类crack也可能细分transverse、longitudinal、alligator。difficult为 1 的目标在 VOC 评测里会被忽略但转 YOLO 时如果不处理它照样参与 loss容易让模型学到模糊样本。bndbox的四个值是绝对像素坐标且必须满足xmin xmax、ymin ymax否则转换后会得到负宽高。2.2 类别分布与长尾问题排查12988 张听起来多但裂缝目标本身细长单图目标数可能只有 1 到 3 个。先统计类别和框数import os import xml.etree.ElementTree as ET from collections import Counter ann_dir RoadCrack_VOC/Annotations cls_counter Counter() box_per_img [] for f in os.listdir(ann_dir): if not f.endswith(.xml): continue tree ET.parse(os.path.join(ann_dir, f)) objs tree.findall(object) box_per_img.append(len(objs)) for obj in objs: cls_counter[obj.find(name).text] 1 print(类别分布:, cls_counter) print(平均每图目标数:, sum(box_per_img) / len(box_per_img)) print(零目标图数量:, box_per_img.count(0))如果零目标图数量占比超过 5%这批图要么是负样本要么是漏标。负样本可以保留但要在训练配置里确认框架是否把空标注图计入。类别分布如果出现某个类只有几百框而另一个类上万框直接训练会让少类召回崩掉常见做法是对少类做过采样或调cls损失权重。提示VOC 的difficult字段在转 YOLO 时建议直接过滤掉别指望训练框架自动识别多数转换脚本不认这个字段。3. VOC 转 YOLO 格式转换脚本与坐标归一化细节3.1 转换脚本逐行说明YOLO 格式要求每张图对应一个.txt每行class_id x_center y_center width height全部归一化到 0 到 1。转换脚本如下import os import xml.etree.ElementTree as ET # 类别映射按你的实际类别顺序改 classes [crack] ann_dir RoadCrack_VOC/Annotations img_dir RoadCrack_VOC/JPEGImages out_dir labels os.makedirs(out_dir, exist_okTrue) for f in os.listdir(ann_dir): if not f.endswith(.xml): continue tree ET.parse(os.path.join(ann_dir, f)) 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 classes: continue # 过滤 difficult 目标 if obj.find(difficult) is not None and obj.find(difficult).text 1: continue bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) # 裁剪越界坐标 xmin max(0, min(xmin, w)) xmax max(0, min(xmax, w)) ymin max(0, min(ymin, h)) ymax max(0, min(ymax, h)) xc (xmin xmax) / 2.0 / w yc (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h if bw 0 or bh 0: continue lines.append(f{classes.index(name)} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) txt_name f.replace(.xml, .txt) with open(os.path.join(out_dir, txt_name), w) as fw: fw.write(\n.join(lines))逻辑上分四步读 XML 拿原图宽高、遍历 object 过滤类别和 difficult、把绝对坐标转成归一化中心点加宽高、写 txt。参数上classes的顺序决定class_id必须和训练时的data.yaml完全一致否则类别名对不上。坐标裁剪那两行是血泪经验有些标注框会超出图像边界不裁的话归一化后出现大于 1 的值训练时直接报错或产生异常梯度。3.2 转换后校验与数据集划分转完不能直接开训先做一致性校验import os img_dir RoadCrack_VOC/JPEGImages lbl_dir labels img_set {os.path.splitext(f)[0] for f in os.listdir(img_dir)} lbl_set {os.path.splitext(f)[0] for f in os.listdir(lbl_dir)} print(有图无标:, len(img_set - lbl_set)) print(有标无图:, len(lbl_set - img_set))有标无图必须为 0否则训练时找不到图会中断。有图无标如果是负样本可以保留但要确认框架支持空 txt。划分训练验证集用train_val_split思路按 8:2 或 7:3import random import os random.seed(42) names sorted(os.listdir(labels)) random.shuffle(names) split int(len(names) * 0.8) train_names names[:split] val_names names[split:] with open(train.txt, w) as f: for n in train_names: f.write(os.path.join(images, n.replace(.txt, .jpg)) \n) with open(val.txt, w) as f: for n in val_names: f.write(os.path.join(images, n.replace(.txt, .jpg)) \n)random.seed(42)是为了可复现别小看这个换台机器跑出不同划分会让指标对不上。划分文件里写的是图片路径不是标签路径YOLO 系列读的是图标签按同名规则去找。注意裂缝目标细长划分时如果按随机切可能出现某类裂缝只在验证集里导致验证指标虚高或虚低。更稳的做法是按类别分层抽样但 12988 张量级下随机划分通常够用。4. 训练配置与增强策略裂缝细长目标的参数怎么设4.1 data.yaml 与模型输入尺寸以 YOLOv8 为例data.yaml长这样path: ./RoadCrack_YOLO train: train.txt val: val.txt nc: 1 names: [crack]nc是类别数裂缝单类就写 1。names顺序必须和转换脚本里的classes一致。输入尺寸方面裂缝在图中占比小且细长imgsz设 640 是常规起点但如果原图是 1280 宽裂缝宽度可能只有几个像素缩到 640 后直接糊掉。我一般会先跑一次 640看验证集召回如果漏检集中在细裂缝就把imgsz提到 960 或 1024代价是显存和训练时间上升。yolo detect train datadata.yaml modelyolov8s.pt epochs100 imgsz960 batch8batch8是 960 尺寸下 8G 显存的保守值显存够可以往上加。epochs100对 12988 张单类数据通常够收敛但要看早停别硬跑。4.2 针对细长目标的增强参数裂缝检测的增强不能照搬通用配置。默认的mosaic会把四张图拼一起对细长目标容易把裂缝截断但也能提升小目标召回我一般保留mosaic1.0但把close_mosaic设到 10让最后 10 个 epoch 关掉 mosaic 稳定收敛。mixup对裂缝不太友好容易把裂缝和背景混成模糊区域建议设 0 或 0.1。mosaic: 1.0 close_mosaic: 10 mixup: 0.0 copy_paste: 0.1 degrees: 5.0 translate: 0.1 scale: 0.5 flipud: 0.0 fliplr: 0.5degrees旋转角度别开太大裂缝有方向性旋转 90 度会让横向裂缝变纵向如果类别里区分了横向和纵向这个增强会直接制造错标。flipud上下翻转对道路场景通常不真实设 0。fliplr左右翻转可以保留道路左右对称性较强。4.3 训练过程监控与指标解读训练时重点看三个指标box_loss、cls_loss、mAP50。裂缝单类任务cls_loss通常很快降到接近 0如果一直不降多半是类别映射错了。mAP50到 0.6 以上算可用但裂缝检测更该看召回因为漏检一条裂缝比误检一条代价大。yolo detect val modelruns/detect/train/weights/best.pt datadata.yaml imgsz960验证时把imgsz和训练保持一致否则指标不可比。如果验证集mAP50高但实际看图漏检多多半是验证集和训练集分布太近需要重新划分或补一批不同路况的图。提示12988 张里如果大部分是同一路段、同一光照模型泛化会很差。训练前先按拍摄场景粗分一下别让验证集全是训练集同款。5. 避坑与排查VOC 转 YOLO 训练中最容易翻车的五件事5.1 现象训练报错 “negative width/height”原因XML 里xmin xmax或ymin ymax转换脚本没做校验归一化后宽高为负。解决在转换脚本里加if xmax xmin or ymax ymin: continue并在转换后统计负值数量。5.2 现象类别名对不上训练时 nc 报错原因data.yaml的names顺序和转换脚本classes不一致或者 XML 里出现了未登记的类别名。解决先跑类别统计脚本把所有name值列出来再统一映射表别靠猜。5.3 现象mAP 虚高但实际漏检严重原因验证集和训练集同源或者difficult目标没过滤模型在模糊样本上过拟合。解决重新按场景划分验证集过滤difficult1并单独统计细裂缝的召回。5.4 现象训练到一半 loss 变 NaN原因学习率过大或增强过猛裂缝细长目标在 mosaic 拼接后出现极端宽高比。解决降学习率到 0.001关掉 mixup把close_mosaic提前。5.5 现象推理时框偏移或框不全原因训练imgsz和推理imgsz不一致或者原图有 EXIF 旋转信息PIL 读图和标注时方向不一致。解决统一推理尺寸读图时用ImageOps.exif_transpose校正方向。6. 进阶技巧用切片推理把细裂缝召回再拉一截12988 张数据训出来的模型在整图推理时对细裂缝仍然容易漏因为下采样到 960 后裂缝可能只剩一两个像素宽。一个实操有效的办法是切片推理把原图切成带重叠的小块逐块推理再合并。以 1280×720 的图为例切成 640×640、重叠 128 像素import cv2 import numpy as np from ultralytics import YOLO model YOLO(best.pt) img cv2.imread(road_00001.jpg) h, w img.shape[:2] tile, overlap 640, 128 step tile - overlap all_boxes [] for y in range(0, h, step): for x in range(0, w, step): x2 min(x tile, w) y2 min(y tile, h) x1 max(0, x2 - tile) y1 max(0, y2 - tile) crop img[y1:y2, x1:x2] results model(crop, imgsz640, conf0.25) for box in results[0].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 合并重叠框 def nms(boxes, iou_thres0.5): boxes sorted(boxes, keylambda b: b[4], reverseTrue) keep [] while boxes: best boxes.pop(0) keep.append(best) boxes [b for b in boxes if iou(best, b) iou_thres] return keep def iou(a, b): xx1, yy1 max(a[0], b[0]), max(a[1], b[1]) xx2, yy2 min(a[2], b[2]), min(a[3], b[3]) inter max(0, xx2 - xx1) * max(0, yy2 - yy1) area_a (a[2] - a[0]) * (a[3] - a[1]) area_b (b[2] - b[0]) * (b[3] - b[1]) return inter / (area_a area_b - inter 1e-6) final nms(all_boxes) print(切片推理后框数:, len(final))切片推理的关键参数是tile和overlap。tile设 640 是为了匹配训练尺寸overlap设 128 是为了避免裂缝正好落在切缝上被截断。合并时用 NMS 去重iou_thres设 0.5 比较稳太低会误删相邻裂缝太高会留下重复框。这套流程跑下来细裂缝召回通常能比整图推理高 5 到 10 个百分点代价是推理时间翻几倍适合离线巡检报告生成不适合实时视频流。验证切片推理是否有效别只看框数变多要拿一批人工确认过的细裂缝图做对比统计漏检率。我一般会固定 50 张难例图整图推理和切片推理各跑一遍人工数漏检数字说话。从那以后我每次拿到裂缝数据集都强制先跑一遍切片推理对比确认整图推理的召回边界在哪再决定线上用哪种。希望帮到你。本文还有配套的精品资源点击获取