简介:这份资源面向计算机视觉方向的学习者与工程开发者,聚焦墙面与水泥路面裂缝的自动检测任务,提供一套可直接用于目标检测模型训练与验证的标注数据集。数据覆盖单一类别crack,共8678张jpg图像,每张均配有对应的VOC格式xml标注与YOLO格式txt标注,标注框总数达11741个,采用labelImg以矩形框方式完成,可直接接入YOLO系列或Pascal VOC流程。压缩包共2000个文件,以1999个xml标注文件和1个说明txt为主,整体约622.83MB,目录结构便于按图像与标注对应检索。目前已有387人学习下载,适合需要快速搭建裂缝检测基线、验证数据增强策略或开展迁移学习实验的读者参考使用。
1. 裂缝检测数据集到底解决什么问题:从 8678 张 VOC+YOLO 标注说起
拿到「墙面水泥路面裂缝检测数据集8678张VOC+YOLO格式.zip」这个标题,很多人第一反应是「又一个数据集」,但真正做过裂缝检测项目的人知道,这类数据的价值不在数量,而在标注粒度和格式兼容性。裂缝检测是典型的小目标、细长目标、低对比度场景,墙面和水泥路面的裂缝往往只有几个像素宽,背景纹理却极其杂乱——水泥的颗粒感、墙面的污渍、光照不均的阴影,都会让模型把非裂缝区域误判成裂缝。8678 张这个量级,配合 VOC 和 YOLO 双格式,意味着你可以直接跳过最耗时的标注环节,把精力放在模型选型和训练策略上。这个数据集适合三类人:一是做建筑结构健康监测的工程师,需要快速验证裂缝分割或检测方案;二是刚接触目标检测的开发者,想找一个真实场景练手 YOLO 训练全流程;三是做边缘部署的团队,需要评估轻量模型在裂缝这种细长目标上的实际表现。VOC 格式提供 XML 标注,适合做数据分析和格式转换;YOLO 格式提供归一化坐标的 txt 文件,可以直接喂给 YOLOv5/v8/v11 甚至最新的 YOLO 系列训练脚本。两者并存,省去了自己写转换脚本的麻烦,但也带来一个常见问题:两套标注是否完全对齐?这个后面会专门讲怎么验证。
2. 拆开压缩包先看什么:VOC 与 YOLO 双格式的结构与对齐检查
2.1 VOC 格式的目录树与 XML 字段含义
VOC 格式的标准结构是VOCdevkit/VOC2007/下分Annotations、JPEGImages、ImageSets、SegmentationClass等目录。裂缝检测数据集通常只用到前三个。Annotations里每张图对应一个 XML,核心字段是<object>下的<name>(类别名,一般是crack)、<bndbox>里的xmin/ymin/xmax/ymax。注意裂缝的边界框往往很扁或很窄,比如一张 640×480 的图,裂缝框可能是xmin=120, ymin=300, xmax=580, ymax=310,高度只有 10 像素。这种框在 YOLO 训练时如果 anchor 设置不当,召回率会非常低。
先跑一段脚本统计框的宽高分布,这一步能帮你判断后续要不要改 anchor 或改用分割方案:
import os import xml.etree.ElementTree as ET import numpy as np ann_dir = "VOCdevkit/VOC2007/Annotations" widths, heights, ratios = [], [], [] for xml_file in os.listdir(ann_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(ann_dir, xml_file)) root = tree.getroot() for obj in root.findall("object"): bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) w, h = xmax - xmin, ymax - ymin widths.append(w) heights.append(h) ratios.append(w / h if h > 0 else 0) print(f"总框数: {len(widths)}") print(f"宽度 中位数/均值: {np.median(widths):.1f} / {np.mean(widths):.1f}") print(f"高度 中位数/均值: {np.median(heights):.1f} / {np.mean(heights):.1f}") print(f"宽高比 中位数: {np.median(ratios):.2f}") print(f"高度小于 20px 的框占比: {sum(1 for h in heights if h < 20) / len(heights) * 100:.1f}%")这段代码遍历所有 XML,提取每个裂缝框的宽高和宽高比。重点看两个指标:高度中位数和高度小于 20px 的占比。如果高度中位数低于 30px,说明大部分裂缝在特征图上经过 32 倍下采样后只剩不到 1 个像素,YOLO 的 P3 检测头(8 倍下采样)勉强能覆盖,但 P4/P5 基本失效。这时候要么把输入分辨率提到 1280,要么在 P3 上增加更小的 anchor。宽高比中位数如果大于 5,说明裂缝是极端长条形,默认 anchor 的宽高比(通常是 1:1、1:2、2:1)完全不匹配,需要自定义 anchor 或改用旋转框检测。
2.2 YOLO 格式的 labels 目录与归一化坐标验证
YOLO 格式每张图对应一个 txt,每行是class_id x_center y_center width height,全部归一化到 0~1。转换逻辑本身简单,但坑在于 VOC 的xmax/ymax是包含边界的,而 YOLO 的宽高计算是(xmax - xmin) / img_w,如果转换脚本没做像素对齐,框会整体偏移 1 像素。对于大目标无所谓,对于裂缝这种细长目标,1 像素偏移可能让框完全错开裂缝区域。
验证对齐的脚本如下:
import os import xml.etree.ElementTree as ET from PIL import Image img_dir = "VOCdevkit/VOC2007/JPEGImages" ann_dir = "VOCdevkit/VOC2007/Annotations" label_dir = "labels" # YOLO 格式 txt 所在目录 mismatch = 0 for xml_file in os.listdir(ann_dir): if not xml_file.endswith(".xml"): continue base = xml_file.replace(".xml", "") img_path = os.path.join(img_dir, base + ".jpg") txt_path = os.path.join(label_dir, base + ".txt") if not os.path.exists(txt_path): print(f"缺失 YOLO 标注: {base}") continue img = Image.open(img_path) iw, ih = img.size tree = ET.parse(os.path.join(ann_dir, xml_file)) root = tree.getroot() voc_boxes = [] for obj in root.findall("object"): bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) voc_boxes.append((xmin, ymin, xmax, ymax)) with open(txt_path) as f: yolo_lines = f.readlines() if len(voc_boxes) != len(yolo_lines): print(f"框数不一致: {base}, VOC={len(voc_boxes)}, YOLO={len(yolo_lines)}") mismatch += 1 continue for (xmin, ymin, xmax, ymax), line in zip(voc_boxes, yolo_lines): parts = line.strip().split() xc, yc, w, h = map(float, parts[1:]) # 反归一化 xc_, yc_, w_, h_ = xc * iw, yc * ih, w * iw, h * ih xmin_, ymin_ = xc_ - w_ / 2, yc_ - h_ / 2 xmax_, ymax_ = xc_ + w_ / 2, yc_ + h_ / 2 if abs(xmin_ - xmin) > 2 or abs(ymin_ - ymin) > 2: print(f"坐标偏差过大: {base}, VOC=({xmin},{ymin}), YOLO反算=({xmin_:.1f},{ymin_:.1f})") mismatch += 1 break print(f"检查完成,不一致样本数: {mismatch}")这段代码做三件事:检查每张图是否有对应的 YOLO txt、检查框数量是否一致、反归一化后对比坐标偏差。阈值设 2 像素是因为 JPEG 压缩和浮点转换本身会有 1 像素左右的误差,超过 2 像素就说明转换脚本有问题。如果发现大量不一致,不要急着训练,先修标注。裂缝检测里标注错误比模型选错更致命,因为模型会把错误标注当成正样本学进去,导致在真实场景中疯狂误检。
3. 从 VOC 到 YOLO 训练:转换脚本、数据划分与配置文件
3.1 写一个带校验的 VOC 转 YOLO 脚本
虽然数据集号称双格式,但实际项目中经常遇到 YOLO 格式缺失或类别映射错误的情况。自己写一个转换脚本,顺便做完整性校验,比直接信任压缩包更稳妥。下面这个脚本处理类别映射、坐标归一化,并输出转换日志:
import os import xml.etree.ElementTree as ET from PIL import Image from tqdm import tqdm voc_root = "VOCdevkit/VOC2007" out_label_dir = "datasets/crack/labels/all" os.makedirs(out_label_dir, exist_ok=True) # 类别映射:根据实际 XML 里的 name 字段调整 class_map = {"crack": 0} img_dir = os.path.join(voc_root, "JPEGImages") ann_dir = os.path.join(voc_root, "Annotations") log = [] for xml_file in tqdm(os.listdir(ann_dir)): if not xml_file.endswith(".xml"): continue base = xml_file.replace(".xml", "") img_path = os.path.join(img_dir, base + ".jpg") if not os.path.exists(img_path): log.append(f"图片缺失: {base}") continue img = Image.open(img_path) iw, ih = img.size tree = ET.parse(os.path.join(ann_dir, xml_file)) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in class_map: log.append(f"未知类别: {name} in {base}") continue bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 裁剪到图像边界,防止越界坐标 xmin, ymin = max(0, xmin), max(0, ymin) xmax, ymax = min(iw, xmax), min(ih, ymax) if xmax <= xmin or ymax <= ymin: log.append(f"无效框: {base}") continue xc = (xmin + xmax) / 2 / iw yc = (ymin + ymax) / 2 / ih w = (xmax - xmin) / iw h = (ymax - ymin) / ih lines.append(f"{class_map[name]} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") with open(os.path.join(out_label_dir, base + ".txt"), "w") as f: f.write("\n".join(lines)) with open("convert_log.txt", "w") as f: f.write("\n".join(log)) print(f"转换完成,异常记录 {len(log)} 条,详见 convert_log.txt")关键参数说明:class_map必须和 XML 里的<name>完全一致,大小写敏感;坐标裁剪到[0, iw]和[0, ih]是防止标注时框超出图像边界,YOLO 训练时越界坐标会导致 loss 计算异常;保留 6 位小数是因为裂缝框很小,归一化后精度不够会放大误差。转换完成后,convert_log.txt里的异常记录要逐条看,尤其是「无效框」和「未知类别」,前者说明标注质量有问题,后者说明类别名不统一。
3.2 按 8:1:1 划分训练集、验证集和测试集
裂缝检测的数据划分不能随机打散,因为同一面墙或同一段路面的照片可能被分到不同集合,导致验证集和训练集高度相似,指标虚高。正确做法是按拍摄场景或图像来源分组,同一组只出现在一个集合里。如果数据集没有提供分组信息,至少按文件名前缀或拍摄时间做粗略分组。
import os import random from sklearn.model_selection import GroupShuffleSplit all_labels = sorted(os.listdir("datasets/crack/labels/all")) # 假设文件名前缀代表拍摄场景,如 "wall_001.jpg" 的前缀是 "wall" groups = [name.split("_")[0] for name in all_labels] gss1 = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, temp_idx = next(gss1.split(all_labels, groups=groups)) train_files = [all_labels[i] for i in train_idx] temp_files = [all_labels[i] for i in temp_idx] temp_groups = [groups[i] for i in temp_idx] gss2 = GroupShuffleSplit(n_splits=1, test_size=0.5, random_state=42) val_idx, test_idx = next(gss2.split(temp_files, groups=temp_groups)) val_files = [temp_files[i] for i in val_idx] test_files = [temp_files[i] for i in test_idx] for split_name, files in [("train", train_files), ("val", val_files), ("test", test_files)]: with open(f"datasets/crack/{split_name}.txt", "w") as f: for name in files: f.write(f"images/{name.replace('.txt', '.jpg')}\n") print(f"{split_name}: {len(files)} 张")GroupShuffleSplit保证同一组的图片只出现在一个集合里。如果文件名没有分组信息,可以退而求其次用train_test_split随机划分,但要在验证集指标异常高时警惕数据泄漏。划分完成后,train.txt、val.txt、test.txt里写的是图片相对路径,YOLO 训练脚本会去对应目录找同名的.txt标注文件。
3.3 YOLOv8 配置文件的关键参数怎么设
以 YOLOv8 为例,crack.yaml的内容如下:
path: datasets/crack train: train.txt val: val.txt test: test.txt nc: 1 names: ['crack']nc是类别数,裂缝检测通常只有一类,设 1。names的顺序必须和转换脚本里的class_map一致。训练命令:
yolo detect train data=datasets/crack/crack.yaml model=yolov8s.pt epochs=200 imgsz=1024 batch=8 patience=30参数选择理由:imgsz=1024而不是默认的 640,因为裂缝目标太小,1024 输入下 P3 特征图是 128×128,裂缝还能保留几个像素;batch=8是显存和梯度的折中,V100 上可以开到 16,消费级显卡 8GB 显存建议 4;patience=30是早停耐心值,裂缝检测的 loss 下降很慢,耐心值太小会提前终止。如果训练时发现cls_loss震荡严重,把lr0从默认的 0.01 降到 0.001,裂缝检测的类别极度不平衡(正样本框少,负样本背景多),学习率太大会导致模型偏向预测背景。
4. 裂缝检测训练避坑:从 anchor 到标注噪声的 5 个翻车现场
4.1 现象:模型把所有纹理都预测成裂缝,误检率极高
原因:裂缝的视觉特征和水泥颗粒、墙面污渍高度相似,模型没有学到「细长」这个几何先验。默认 anchor 的宽高比是 1:1、1:2、2:1,而裂缝的宽高比经常是 10:1 甚至 20:1,anchor 匹配阶段正样本就很少,模型只能靠背景纹理瞎猜。
解决:在 YOLOv5/v8 的配置里自定义 anchor。先对训练集的框做 K-means 聚类,得到适合裂缝的 anchor 尺寸。YOLOv8 虽然是无 anchor 的,但可以通过调整reg_max和增加 P2 检测层来提升小目标召回。更直接的办法是把输入分辨率提到 1280,让裂缝在特征图上占更多像素。
4.2 现象:验证集 mAP 很高,但测试集一塌糊涂
原因:数据划分时同一面墙的图片被分到了训练集和验证集,模型记住了这面墙的纹理,换一面墙就失效。裂缝检测的场景相关性极强,同一段路面的裂缝走向、光照、背景几乎一致。
解决:按拍摄场景分组划分,用GroupShuffleSplit而不是随机划分。如果数据集没有场景标签,至少按文件名前缀或 EXIF 拍摄时间做分组。验证集 mAP 超过 0.9 时不要高兴太早,先检查有没有数据泄漏。
4.3 现象:训练到一半 loss 突然变成 NaN
原因:裂缝框的宽高归一化后非常小,比如w=0.002, h=0.015,在计算 CIoU loss 时分母接近零,浮点溢出。另外,如果标注里有xmax=xmin或ymax=ymin的无效框,也会导致 loss 爆炸。
解决:在转换脚本里过滤掉宽或高小于 2 像素的框,并在训练配置里加梯度裁剪grad_clip=10.0。YOLOv8 默认开启 AMP 混合精度,如果 NaN 频繁出现,先关掉 AMP 用 FP32 跑几个 epoch 确认不是数值问题。
4.4 现象:模型对横向裂缝检测很好,纵向裂缝全漏
原因:卷积核的感受野是正方形,对水平方向的边缘响应强于垂直方向。裂缝如果是纵向的,在特征图上经过多次下采样后,垂直方向的梯度信息被池化操作抹掉了。
解决:在数据增强里加入随机旋转(degrees=90),让模型见过各个方向的裂缝。另外,把fliplr和flipud都打开,水平翻转和垂直翻转同时用。如果还是漏检,考虑改用旋转框检测(如 YOLOv8-OBB),但需要重新标注成旋转框格式。
4.5 现象:推理时框的位置整体偏移,裂缝在框外
原因:VOC 转 YOLO 时没有做像素对齐,xmax和xmin的差值多算或少算 1 像素。对于大目标无所谓,对于宽度只有 3~5 像素的裂缝,1 像素偏移就是 20% 的误差。
解决:用 2.2 节的验证脚本逐张检查坐标偏差,超过 2 像素的样本重新转换。转换时用(xmax - xmin + 1) / img_w还是(xmax - xmin) / img_w取决于标注工具,VOC 标准是包含边界,所以宽高计算要加 1。但很多标注工具实际输出的是不包含边界的坐标,最稳妥的办法是对比几张图的标注和原图,肉眼确认框是否贴合裂缝。
5. 把裂缝检测推到可用:从指标验证到边缘部署的最后一公里
训练完模型,看mAP50和mAP50-95只是第一步。裂缝检测的落地场景通常是巡检人员拿着手机或边缘设备拍照,要求模型在 1 秒内给出结果,并且漏检率要低——漏掉一条裂缝可能意味着错过一次结构安全隐患。所以验证阶段要重点看召回率,而不是只看 mAP。在val模式下加--conf 0.1跑一遍,统计不同置信度阈值下的召回和误检曲线,找到业务能接受的平衡点。我一般会把conf设到 0.15~0.25,宁可多报几条让巡检人员复核,也不要漏掉真实裂缝。
边缘部署时,YOLOv8n 在 Jetson Nano 上跑 640 输入大概 15 FPS,但裂缝检测需要 1024 输入才能保证小目标召回,帧率会掉到 5 FPS 以下。折中方案是用 YOLOv8s 做 1024 输入的 TensorRT FP16 推理,Jetson Orin 上能到 20 FPS 左右。导出命令:
yolo export model=runs/detect/train/weights/best.pt format=engine imgsz=1024 half=True device=0导出后一定要用val模式再跑一遍验证集,确认 TensorRT 引擎的精度和 PyTorch 原模型差异在 1% 以内。我遇到过 FP16 量化后小目标召回下降 8% 的情况,后来改成 FP32 才恢复。如果设备算力实在不够,可以把图像切成 512×512 的块分别推理,再合并结果,但切块边界处的裂缝会被截断,需要在后处理里做框合并。
最后说一个我踩过的坑:裂缝检测模型上线后,巡检人员反馈「有时候框住了裂缝,有时候框住了裂缝旁边的阴影」。排查发现是训练集里阴影样本太少,模型把阴影当成了裂缝的一部分。后来在数据增强里加了随机亮度调整和对比度扰动,让模型学会区分裂缝和阴影。裂缝检测没有银弹,数据质量、anchor 匹配、输入分辨率、后处理阈值,每个环节都要根据实际场景调。希望帮到你。
本文还有配套的精品资源,点击获取