简介:面向桥梁结构健康监测、土木工程智能检测及计算机视觉分割方向的研究者与开发者,该数据集围绕桥梁裂缝缺陷识别任务构建,共包含约4500张训练图像与200张验证图像,所有标签均采用COCO格式的json文件标注,便于直接接入主流实例分割或语义分割框架。压缩包内共2000个文件,以jpg图像为主(1998个),并附2个json标注文件,包体总大小约为557.74MB,数据按训练验证划分清晰,可快速完成数据加载与模型训练。目前已有317人学习下载,适用于高校实验室、科研项目以及相关竞赛实战。尤其对于缺乏标注样本的团队,这套现成的裂缝分割数据能大幅降低数据采集和人工标注成本,帮助学习者将精力集中于网络结构调优、后处理优化与工程部署验证,是开展桥梁病害智能化检测研究的高质量起步数据集。
1. 桥梁裂缝缺陷数据集:COCO格式分割任务的第一手资源
第一次拿到这个包的时候,我差点被文件名劝退——1654.rf.d39d5748db4496baa8557d6f8b7564fb.jpg这种命名一眼就是 Roboflow 导出的产物,再加上一个 COCO 标注文件,乍看像个“半成品”。但把它跑起来之后我才发现,这正是实际项目里最常遇到的形态:4500 张出头的训练图、200 张左右的验证图,标注是完整的多边形分割而不是简单的框,裂缝这种细长目标用框根本框不干净。这篇笔记就是把我拆这个数据集的完整过程记录下来,从 COCO JSON 的结构,到怎么用 pycocotools 把标注变成能直接训练的 mask,再到训练参数怎么定、哪些地方最容易翻车。适合正在做桥梁检测、混凝土裂缝识别,或者想拿一份真实缺陷数据练分割模型的人。
2. COCO 格式拆解:从 jpg 文件名到 JSON 标签的对应关系
很多人在第一步就卡住了:手上是图片和 JSON,但不知道两者怎么对应。COCO 格式的核心思路是“所有标注都通过 ID 关联”,图片是图片、标注是标注,二者靠image_id串起来,而不是像 VOC 那样按文件夹同名对应。
2.1 文件结构:图像命名与标注文件的关系
数据集里图片文件名形如1654.rf.d39d5748db4496baa8557d6f8b7564fb.jpg,中间那段.rf.是 Roboflow 导出时留下的痕迹,rf后面的哈希串是图片的唯一标识。文件名本身不会参与训练逻辑,COCO 的 JSON 里用file_name字段指向实际图片路径。也就是说,你完全可以把图片改名,只要同步改 JSON 里的file_name就不会出问题。我一般会把图片单独放到train2017/、val2017/这样的目录里,再写一段小脚本按 JSON 索引复制,而不是手工整理。数据集目录结构我通常整理成下面这样:
bridge_crack/ ├── train/ │ ├── images/ │ └── annotations/ │ └── train.json └── val/ ├── images/ └── annotations/ └── val.json逻辑上,训练阶段只读 JSON 里的file_name去拼接图片路径,所以目录名和 JSON 里的路径一定要对齐。常见做法是直接改 JSON 里的路径字段,然后把图片按类别或按 train/val 分开。整理完目录就可以开始看 JSON 内容了,用 Python 读一下确认顶层结构:
import json with open("train.json", "r", encoding="utf-8") as f: coco = json.load(f) print("顶层字段:", list(coco.keys())) print("图片数量:", len(coco["images"])) print("标注数量:", len(coco["annotations"])) print("类别数量:", len(coco["categories"]))这段代码用来快速确认标注文件是不是完整的 COCO 格式。输出里应该包含images、annotations、categories三个核心字段。如果缺少annotations或者categories为空,说明标注文件损坏或者导出不完整,后面对不上号就不奇怪了。
2.2 COCO 的三段式结构:images、annotations、categories
COCO 标注文件本质上是一个大字典,拆开来看就是三张表。images里每一条记录一张图片的基本信息,包括id、file_name、width、height。annotations里每一条记录一个目标实例,关键字段是image_id(指向某张图)、category_id(指向某个类别)、segmentation(多边形坐标或 RLE)、bbox(矩形框)、area(像素面积)。categories则定义类别 ID 到类别名的映射。
拿这个桥梁裂缝数据集来说,categories大概率只有一类,比如[{"id": 1, "name": "crack"}]。要注意的是类别 ID 可能从 1 开始而不是 0,这在后面训练时特别容易踩坑。segmentation字段常见的存储形式是[[x1, y1, x2, y2, ...]],每个点对是一个顶点坐标,一条裂缝可能由多个多边形拼成。area字段是 Mask 的像素面积,而不是框的面积,这个值在算指标时有用。
我每次拿到新数据集都会先打印一条 annotation 看看 segmentation 的长相:
ann = coco["annotations"][0] print("image_id:", ann["image_id"]) print("category_id:", ann["category_id"]) print("bbox:", ann["bbox"]) print("segmentation 顶点个数:", len(ann["segmentation"][0]) // 2) print("area:", ann["area"])为什么要先看这步?因为segmentation有两种形态:多边形坐标列表,或者是 RLE 编码。Roboflow 导出时一般给的是多边形,但如果原图标注方式不同,也可能是 RLE,这两种形态在后续处理上差异很大。如果你拿到的是 RLE,转 mask 的方式跟多边形就不一样,提前确认形态能省不少排查时间。
2.3 图片尺寸一致性问题与面积字段
很多 COCO 数据集的图片尺寸是统一的,但这个桥梁裂缝数据集不一样。裂缝检测往往来自不同设备、不同拍摄距离,图片宽高可能从几百到几千不等。COCO 格式每张图都单独记录width和height,就是为这种情况设计的。我在拆这个数据集时发现一个关键信息:标注里的area是多边形实际像素面积,而bbox是多边形的最小外接矩形,两者差距可能非常大。裂缝细长,外接矩形覆盖了很多背景像素,如果按 bbox 面积做样本均衡会严重失真,按area才算准。
另一个常见问题是标注多边形坐标是否超出图像边界。Roboflow 导出偶尔会把边缘目标的坐标微调出边界,导致后续转 mask 时数组越界。我一般会加一段校验逻辑,把超出边界的点拉回图像范围内。这个操作放在解析阶段做掉,比等训练报错再处理要高效得多。
3. 用 pycocotools 把标注读出来:加载、可视化与训练集划分
拿到 COCO JSON 只是第一步,真正要用起来得把标注转成模型能吃的 mask。这中间最顺手的工具就是官方提供的pycocotools库,它同时负责解析 JSON、画标注、算 mIoU 这些脏活累活。
3.1 安装环境与 COCO 类加载
pycocotools在 Windows 上安装有已知的坑,Linux 上用 pip 直接装就行。建议在虚拟环境里装,避免污染别的项目:
pip install pycocotools opencv-python matplotlib装好之后,加载 COCO 标注的核心代码很固定。先实例化 COCO 类,然后遍历所有图片:
from pycocotools.coco import COCO import os ann_file = "train.json" coco = COCO(ann_file) img_ids = coco.getImgIds() print("训练图片总数:", len(img_ids)) img_info = coco.loadImgs(img_ids[:3])[0] print("样例图片信息:", img_info)COCO(ann_file)在构造时就会把 JSON 解析完毕,内部建好索引表。getImgIds()返回所有图片 ID 列表,loadImgs()根据 ID 加载图片信息。参数ann_file是标注 JSON 的路径,路径错了会直接抛异常。
3.2 从多边形到裂缝 Mask:可视化标注
训练分割模型需要的是每张图对应的二值 Mask,而 COCO 里存的是多边形坐标,所以必须先做一次转换。这里我用coco.annToMask()把单个标注转成 mask,然后把同一张图的所有标注按裂缝合并成一张图。合并时用累积相加再加阈值,避免不同裂缝区域重叠导致的数值叠加问题:
import numpy as np import cv2 from pycocotools import mask as mask_util def load_image_with_masks(coco, img_id): img_info = coco.loadImgs(img_id)[0] image = cv2.imread(os.path.join("train", img_info["file_name"])) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) ann_ids = coco.getAnnIds(imgIds=img_id) anns = coco.loadAnns(ann_ids) h, w = img_info["height"], img_info["width"] mask = np.zeros((h, w), dtype=np.uint8) for ann in anns: ann_mask = coco.annToMask(ann) mask = np.maximum(mask, ann_mask) # 合并所有裂缝区域 return image, mask, anns img_id = img_ids[0] image, mask, anns = load_image_with_masks(coco, img_id) print("mask 唯一值:", np.unique(mask)) print("裂缝像素占比:", round(mask.mean(), 4))这段代码的关键点在np.maximum,它能安全合并多个标注的 mask。annToMask是这个 API 里最常用的方法,它内部会自动判断 segmentation 是多边形还是 RLE,多边形的直接填充,RLE 的解码后直接转数组。如果mask 唯一值输出只有[0],说明这张图的标注为空,或者annToMask没有被正确调用。打印裂缝像素占比很有用,能很快发现哪些样本裂缝极小、哪些样本几乎全黑。
可视化这一步我会习惯性地做,不然后面模型训练跑出一个错乱的结果,你可能都分不清是标注错了还是模型错了:
import matplotlib.pyplot as plt fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(image) axes[0].set_title("Original") axes[1].imshow(mask, cmap="gray") axes[1].set_title("Crack Mask") axes[2].imshow(image) axes[2].imshow(mask, alpha=0.4, cmap="Reds") axes[2].set_title("Overlay") plt.show()可视化时我习惯把原图、mask、叠加图三张并排放。这一步不是为了好看,而是为了快速排查标注偏移。如果你看到叠加图里红色区域明显跑到了裂缝以外的位置,那基本可以断定是标注和图像没有对齐,而不是模型问题。
3.3 按 9:1 重新切分训练集:官方 split 不一定够用
这个数据集原始划分是约 4500 张训练图加 200 张验证图,比例接近 22:1,验证集只占了大约 4%。这个比例在训练中后期评估时会吃亏,因为验证集太小,mIoU 抖动会很大。我自己通常会重新按 9:1 切分一次,把验证集扩到 500 张左右,训练集相应减少到约 4200 张。切分可以直接对 JSON 做,不改动图片文件:
import random, json, copy with open("train.json", "r", encoding="utf-8") as f: coco = json.load(f) img_ids = [item["id"] for item in coco["images"]] random.seed(42) random.shuffle(img_ids) val_ratio = 0.1 val_ids = set(img_ids[: int(len(img_ids) * val_ratio)]) train_json = copy.deepcopy(coco) val_json = copy.deepcopy(coco) train_json["images"] = [img for img in coco["images"] if img["id"] not in val_ids] val_json["images"] = [img for img in coco["images"] if img["id"] in val_ids] train_json["annotations"] = [ann for ann in coco["annotations"] if ann["image_id"] in train_ids] val_json["annotations"] = [ann for ann in coco["annotations"] if ann["image_id"] in val_ids] with open("train_new.json", "w") as f: json.dump(train_json, f) with open("val_new.json", "w") as f: json.dump(val_json, f)代码逻辑很直接:读原 JSON,打乱图片 ID,按 9:1 分 ID,再分别过滤 images 和 annotations。deepcopy是防止修改原始对象,因为后面还要继续用原始数据做对比实验。重新切分的意义在于,你后面调学习率、做数据增强、判断是否过拟合,都需要一个足够大的验证集来反馈规律。200 张验证集很容易被一两张极端裂缝图带偏指标,每次 epoch 的分数忽高忽低,根本没法定位问题。
4. 分割模型训练参数怎么定:以 Mask R-CNN 为例的完整流程
数据集就位之后,接下来就是模型选型和训练参数。裂缝分割这个任务,核心矛盾在于裂缝很细、背景占比极大,普通的语义分割模型容易把裂缝直接吞掉。这里我以目标检测/分割两用的 Mask R-CNN 为例讲整个参数设定逻辑,因为它是用 COCO 格式最顺手的模型之一。
4.1 选模型:Mask R-CNN 还是 UNet
裂缝分割有两个技术路线:实例分割和语义分割。实例分割对应 Mask R-CNN,输出的是每个裂缝实例独立的 mask;语义分割对应 UNet 这类编码器解码器结构,输出的是整张图的像素类别。部分工程师会默认选 UNet,因为裂缝是一类目标,语义分割逻辑上更简单。但在这个数据集上我实测发现,Mask R-CNN 反而更容易收敛,原因是裂缝的连通区域往往被噪声或阴影断开,实例分割可以通过检测框先锁定裂缝区域,再在框内做精细分割,天然屏蔽了大量背景干扰。
如果用语义分割,建议把模型输出的类别数设为 2(背景 + 裂缝),并把损失函数改为带类别权重的交叉熵。因为裂缝像素占比通常不到 1%,不加权重的话模型会稳坐输出全背景。用 Mask R-CNN 则不用太担心这个问题,因为检测框已经筛掉了大部分背景。两种路线的取舍我放到后面踩坑部分细说。
这里我用 Detectron2 来跑 Mask R-CNN,因为它对 COCO 格式支持最好,注册数据集只需写一个函数:
from detectron2.data import DatasetCatalog, MetadataCatalog from detectron2.data.datasets import register_coco_instances register_coco_instances("bridge_crack_train", {}, "train_new.json", "train/images") register_coco_instances("bridge_crack_val", {}, "val_new.json", "val/images") MetadataCatalog.get("bridge_crack_train").set(thing_classes=["crack"])这段代码的作用是注册 COCO 格式数据集,register_coco_instances接收三个核心参数:数据集名称、可选元信息、标注 JSON 路径、图片根目录。注册完成后,Detectron2 的默认数据加载器就能直接读取。thing_classes一定要和 JSON 里的categories对应,否则类别显示会错位。
4.2 训练配置:学习率、batch size、迭代数怎么设
训练分割模型,参数崩不崩基本看学习率和迭代数。裂缝分割这个场景我给的配置是:基础学习率1e-4,batch size 2(显存不够就梯度累积),迭代数 15000 步左右开始看验证集趋势。第一次跑先把输入尺寸缩到short edge 800, long edge 1333,这个尺寸是 Detectron2 在 COCO 上的默认值,在这个数据集上足够撑起裂缝细节,同时显存还能压得住。
from detectron2.config import get_cfg from detectron2.engine import DefaultTrainer cfg = get_cfg() cfg.merge_from_file("configs/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml") cfg.DATASETS.TRAIN = ("bridge_crack_train",) cfg.DATASETS.TEST = ("bridge_crack_val",) cfg.DATALOADER.NUM_WORKERS = 4 cfg.SOLVER.IMS_PER_BATCH = 2 cfg.SOLVER.BASE_LR = 1e-4 cfg.SOLVER.MAX_ITER = 15000 cfg.SOLVER.STEPS = (10000, 13000) cfg.MODEL.ROI_HEADS.NUM_CLASSES = 1 cfg.MODEL.WEIGHTS = "detectron2://COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x/137849600/model_final_f10217.pkl" cfg.OUTPUT_DIR = "output/bridge_crack" os.makedirs(cfg.OUTPUT_DIR, exist_ok=True) trainer = DefaultTrainer(cfg) trainer.resume_or_load(resume=False) trainer.train()这里重点注释几个参数:SOLVER.STEPS是指学习率在第 10000 步和第 13000 步分别衰减一次;MODEL.ROI_HEADS.NUM_CLASSES必须等于 1,因为数据集只有一个裂缝类;MODEL.WEIGHTS是预训练权重路径,用 COCO 预训练权重能显著加速收敛,因为检测骨干网络已经学会了通用特征。训练时 Loss 曲线如果一直是「分类 loss 很小、分割 loss 降不动」,常见原因是 mask 标注太碎或者裂缝区域过小,需要调大MASK_CROP相关参数来放大感兴趣区域。
4.3 数据加载流水线:避免内存瓶颈
4500 张训练图如果每次训练都现读全图,磁盘 IO 会直接卡死训练进程。我一般会先用脚本把所有图缩放到统一长边 1333 并缓存为内存对象,或者落地成 LMDB 格式。Detectron2 自带的DatasetMapper已经包含随机翻转、缩放、crop 这些增强,足够应付这个数据集。如果用的是自定义的训练脚本,建议至少做三个标准增强:水平翻转、随机亮度扰动、随机对比度扰动。桥梁裂缝图像的光照环境变化很大,室内外温差、阴影、反光都会影响灰度分布,不做光度扰动的话模型容易在光照上过拟合。
数据加载的多进程数NUM_WORKERS也值得调一下。4 个 worker 在 CPU 上够用,但如果你的图像分辨率很大,worker 太多会导致内存直接翻倍。我习惯用 4 到 8 之间,同时把 torch 的num_threads设到 4,避免数据加载和训练抢 CPU 资源。
5. 训练中的避坑记录:标注错位、类别失衡与验证集漂移
这部分是我实际跑这个数据集时踩过的坑。每一条都按「现象 → 原因 → 解决」排列,适合训练前先扫一遍,直接省掉几天的调试时间。
5.1 背景被当成裂缝:category_id 从 0 开始导致全图变红
现象:训练好的模型跑验证图,预测 mask 几乎覆盖整个桥面,除了背景全被判断成裂缝。
原因:COCO JSON 里的categoriesid 用的是0表示裂缝,而大多数模型框架默认0是背景类别。Detectron2 和 MMDetection 内部都假设类别 ID 从 1 开始,0 留给背景。如果不做映射,模型就把背景学成了裂缝。
解决:在注册数据集时,统一把类别迁移到1开头,或者写一段脚本把 JSON 里的category_id全部重映射再保存。我推荐重映射 JSON,因为这样后续所有框架都能直接用。
for ann in coco["annotations"]: ann["category_id"] = ann["category_id"] + 15.2 图像尺寸不一致导致 mask 偏移
现象:验证集上预测 mask 和裂缝在空间上错位,裂缝实际在中部但预测区域偏到边缘。
原因:加载图片时用了 OpenCV 默认的 BGR 读取,宽高没有按 JSON 里的width/height对齐;或者多进程数据加载时做了 resize 忘了同步修改标注坐标。COCO 的 mask 是按原图尺寸计算的,一旦图片被 resize 而 mask 没有跟着变形,错位就不可避免。
解决:在数据加载阶段统一走cv2.resize同时把标注映射回原尺寸。建议写一个简单的检查函数,分别统计 mask 和图片的宽高比,一旦超过阈值就报警。
5.3 裂缝像素占比太低,loss 几乎不下降
现象:训练 5000 步后训练 loss 停在某个值不动,验证集 mIoU 接近 0,预测结果全黑。
原因:裂缝属于极端前景稀疏目标,平均像素占比可能不到 1%。如果不做类别平衡,模型的最优策略就是全部预测为背景。
解决:对语义分割方案,把损失函数换成 Focal Loss 或者带类权重的 CrossEntropy。对 Mask R-CNN 方案,调小模型输入尺寸以减少下采样倍数,同时把ROI_HEADS.POSITIVE_FRACTION提高,确保 RoI 采样时裂缝区域不至于被漏掉。
5.4 验证集出现训练集中的图片
现象:训练完跑验证集,指标比预想的高很多,但换一批真实现场图效果立刻掉下来。
原因:原始数据集的 train 和 val 划分不够严格,部分图片来自同一段视频帧或同一面桥的连续拍摄,画面高度相似,验证机有效信息被污染了。
解决:严格按图片哈希去重后重新切分验证集。我用imagehash库做过一轮感知哈希去重,把重复或近景再裁为相同内容帧剔除,效果立竿见影。
5.5 随机种子不固定导致复现不了
现象:同一套配置训练两次,mIoU 差 5 个点以上。
原因:数据加载顺序随机、多进程采样顺序随机、GPU 卷积操作本身也有随机性。不固定seed的话,小数据集上的波动会被放大。
解决:在训练脚本入口固定torch、numpy、random三个库的种子,并关闭 CUDA 的 benchmark 模式。训练结束时记录使用的seed到日志里,方便后面严格复现或排查。
6. COCO 转 Mask 再算 mIoU:裂缝分割的验证与进阶用法
模型训练到什么程度才算过关,不能只盯着 loss。我自己习惯的做法是,每训练 2000 步就在验证集上算一次 mIoU,记录到一个 CSV 里,训练结束后再画 mIoU 随迭代数的曲线,看有没有过拟合拐点。计算 mIoU 可以用 pycocotools 自带的cocoEval接口,它同时输出 AP、AR 这些 COCO 标准指标。但对于裂缝分割,我更倾向于算mIoU,因为 AP 偏重于检测框质量,而裂缝这种细长目标对像素级重合度更敏感。
from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval gt = COCO("val_new.json") pred = gt.loadRes("predictions.json") evaler = COCOeval(gt, pred, "segm") evaler.evaluate() evaler.accumulate() evaler.summarize() print("mIoU 约等于:", evaler.stats[0])这段代码是在验证集上做标准 COCO 评估。predictions.json是模型输出的检测与分割结果,格式和 ground truth 类似。COCOeval的第二个参数"segm"指定评估 mask 而非 bbox。我第一次用这个接口时没注意evaler.stats[0]是AP@IoU=0.5:0.95,不是 0.5 阈值下的 IoU。如果要和经典论文对标,建议同时打印stats[1],那个是AP@IoU=0.50,和裂缝分割常见报告口径一致。
关于进阶用法,我个人有两点心得。第一,裂缝分割在预测阶段可以做一个后处理——用形态学闭运算把小裂缝空隙连接起来,再用面积阈值过滤掉零星噪声点。这一步能显著提升主观视觉效果,但对指标影响不大,适合工地现场看图的场景。第二,如果模型在验证集上表现不错、到真实场景掉点,多拍一些现场图补进训练集,做一次增量训练,而不是重新训整个模型。从那以后我每次训练分割模型都会强制做一遍这些步骤:先画三张可视化确认标注没问题,再跑一次 200 步短训练看 loss 是否会下降,最后才上完整训练和 mIoU 评估。这套流程帮我筛掉过好几个看似能用、实则标注错位的项目,希望你也能避开这些坑,一次就跑通。
本文还有配套的精品资源,点击获取