☰
道路裂缝数据集VOC格式12988张:从标注解析到YOLO训练与切片推理全流程
2026/10/2 18:38:37 网站建设 项目流程

简介:本资源为道路裂缝检测方向的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> <filename>road_00001.jpg</filename> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>crack</name> <difficult>0</difficult> <bndbox> <xmin>312</xmin> <ymin>204</ymin> <xmax>498</xmax> <ymax>356</ymax> </bndbox> </object> </annotation>

name是类别名,裂缝数据集常见就一个类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_ok=True) 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:3:

import 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 data=data.yaml model=yolov8s.pt epochs=100 imgsz=960 batch=8

batch=8是 960 尺寸下 8G 显存的保守值,显存够可以往上加。epochs=100对 12988 张单类数据通常够收敛,但要看早停,别硬跑。

4.2 针对细长目标的增强参数

裂缝检测的增强不能照搬通用配置。默认的mosaic会把四张图拼一起,对细长目标容易把裂缝截断,但也能提升小目标召回,我一般保留mosaic=1.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.5

degrees旋转角度别开太大,裂缝有方向性,旋转 90 度会让横向裂缝变纵向,如果类别里区分了横向和纵向,这个增强会直接制造错标。flipud上下翻转对道路场景通常不真实,设 0。fliplr左右翻转可以保留,道路左右对称性较强。

4.3 训练过程监控与指标解读

训练时重点看三个指标:box_loss、cls_loss、mAP50。裂缝单类任务cls_loss通常很快降到接近 0,如果一直不降,多半是类别映射错了。mAP50到 0.6 以上算可用,但裂缝检测更该看召回,因为漏检一条裂缝比误检一条代价大。

yolo detect val model=runs/detect/train/weights/best.pt data=data.yaml imgsz=960

验证时把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目标没过滤,模型在模糊样本上过拟合。解决:重新按场景划分验证集,过滤difficult=1,并单独统计细裂缝的召回。

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, imgsz=640, conf=0.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_thres=0.5): boxes = sorted(boxes, key=lambda b: b[4], reverse=True) 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 张难例图,整图推理和切片推理各跑一遍,人工数漏检,数字说话。从那以后我每次拿到裂缝数据集,都强制先跑一遍切片推理对比,确认整图推理的召回边界在哪,再决定线上用哪种。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询