☰
道路裂纹语义分割数据集实战:从格式校验到模型训练
2026/10/23 3:31:45 网站建设 项目流程

拿到这套道路裂纹语义分割数据集的时候,我最直观的感受是:资料包很小,但信息量很密。700张图像、二分类像素标注、像素值0和1、VOC格式、mask/json/txt三种格式并存——基本上是典型的“道路缺陷检测”方向的标准物料。但越是这种看起来“已经整理好”的数据集,越容易在细节上栽跟头。这篇博文不聊学术理论,就聊我实际拿到这种数据集之后,从解压、校验、格式转换,到训练、评估、部署的一整套经历,重点是那些文档不会写、但你必须知道的坑。

先说明一点,这类数据集的几个关键特征:裂纹是极细的线状目标,通常只占整张图的很小比例,标签是二分类,0为背景、1为裂纹。700张对于语义分割来说不算大,但也不是不能用,关键看你怎么组织训练策略、怎么处理增强、怎么设计损失、怎么评价结果。下面按我实际操作的顺序来写。

1. 拿到700张裂纹数据,我第一遍排查了什么

1.1 先看目录结构,别急着开训练脚本

很多人拿到数据集第一件事就是写训练代码,我的习惯是先解压、先看目录,把“图片、标注、索引”三者的对位关系搞清楚。这套道路裂纹数据集既然是VOC格式,目录通常就应该是这样:

VOC-like/ ├── JPEGImages/ # 原始路面图像 ├── SegmentationClass/ # 单通道mask,像素值为0/1 ├── Annotations/ # json标注(多边形坐标) └── ImageSets/ └── Segmentation/ ├── train.txt └── val.txt

但实际拿到手,目录名可能不完全标准,有的叫images、有的叫masks,txt也可能直接放在根目录下。我建议第一件事就是把所有文件名读取一遍,做一次对齐检查:每张jpg是否都能找到同名png/json,每个txt里的文件名是否都能在JPEGImages里对上。

import os img_dir = "JPEGImages" mask_dir = "SegmentationClass" ann_dir = "Annotations" imgs = {os.path.splitext(f)[0] for f in os.listdir(img_dir)} masks = {os.path.splitext(f)[0] for f in os.listdir(mask_dir)} anns = {os.path.splitext(f)[0] for f in os.listdir(ann_dir)} print("缺少mask:", imgs - masks) print("缺少json:", imgs - anns) print("多余mask:", masks - imgs)

这一步很重要,因为标注文件一旦缺一张,训练脚本会在dataloader里直接崩掉,而且崩的位置往往不是第一条数据,而是跑到一半才报错,浪费不少时间。

1.2 统计像素分布——裂纹占比到底有多悬殊

检查完文件对位,我做的第二件事是统计整份数据集的像素分布。语义分割不比分类,分类只要看样本数量,分割必须看类别像素占比。我在一个500多张的旧路面上收集过一次类似数据,裂纹像素占全图比例低到0.2%到3%,这种不均衡程度会直接决定训练时的损失函数选择。

import cv2 import numpy as np from glob import glob total_0, total_1, total_px = 0, 0, 0 for p in glob("SegmentationClass/*.png"): m = cv2.imread(p, cv2.IMREAD_UNCHANGED) if m.ndim == 3: # 有些mask被存成三通道彩色,必须降维 m = m[..., 0] total_0 += int((m == 0).sum()) total_1 += int((m == 1).sum()) total_px += m.size print("背景像素占比:", total_0 / total_px) print("裂纹像素占比:", total_1 / total_px)

我这里特意写了cv2.IMREAD_UNCHANGED,因为标注mask经常被保存成带调色板的索引PNG,如果用默认参数读取,可能得到三通道BGR,统计就会出错。另外,如果你发现mask里的标签不是0/1而是0/255,也别慌,很多标注工具导出时会默认显示黑白图,其中255就是前景,读出来后直接归一化到1即可。

这种像素分布统计的意义不只是写论文用,它直接影响后面几个选择:要不要用带权重的CrossEntropy?需不需要Dice Loss?验证指标到底看背景的IoU还是看裂纹的IoU?在开始训练前,脑子里要有一个大致的预期。

2. mask、json、txt三种格式,怎么用、怎么转

2.1 三种文件格式的角色分工

这套数据集同时提供了mask、json、txt三种格式,很多人不理解为什么需要三份,实际上它们是不同工作流里用的:

文件类型典型内容使用场景
mask/png单通道像素图,0/1逐个像素标注语义分割模型直接当标签用
json标注软件导出的矢量多边形坐标边框精修、可视化、重新渲染mask
txt训练/验证集图像名列表,或类别名列表训练集划分、类别索引读取

简单说,mask是“最终成品”,json是“可编辑原稿”,txt是“目录索引”。如果只给mask,你很难做细颗粒度的纠错;如果只给json,你又得每次训练前临时转mask。三份都提供,说明整理这份数据的人是真在工程里用过它的。

2.2 从json多边形转成mask的常规操作

json通常是标注工具导出时留下的矢量信息。以我常用的标注工具导出格式为例,它的结构大概是:

{ "version": "0.1", "imageWidth": 1024, "imageHeight": 512, "shapes": [ { "label": "crack", "points": [[x1, y1], [x2, y2], ...], "shape_type": "polygon" } ] }

转换成mask的核心思路就是:创建一个全零的单通道图,把所有多边形的内部填充成1。这里直接给一个我自己常用的转换脚本:

import json import cv2 import numpy as np with open("1.json", "r", encoding="utf-8") as f: data = json.load(f) h, w = data["imageHeight"], data["imageWidth"] mask = np.zeros((h, w), dtype=np.uint8) for shape in data["shapes"]: pts = np.array(shape["points"], dtype=np.int32) cv2.fillPoly(mask, [pts], 1) cv2.imwrite("1_mask.png", mask)

这个脚本看着简单,但我第一次转的时候翻过车,原因是json里的imageHeight和imageWidth和实际图片尺寸不一致。有些标注软件在缩放预览后导出的json,保存的还是原始分辨率,但有些工具导出的是你屏幕预览的尺寸,这会导致mask画出来整幅图错位。可靠的做法是直接用原图去读取宽高,替代json里的字段:

img = cv2.imread("1.jpg") h, w = img.shape[:2]

这是我在转标注时最常强调的一点:永远以原图为基准,json里的尺寸只作参考。

2.3 txt划分表的读取与陷阱

txt文件的用途,这套数据集里最常见的是两类:一类是train.txt、val.txt,里面一行一个不带后缀的文件名,用于数据集划分;另一类是classes.txt或labels.txt,用来声明类别顺序。这里有个常见的低级错误,就是txt里可能带路径前缀,比如JPEGImages/001.jpg整行写进去。处理的时候建议统一做strip,避免换行符混进文件名。

with open("ImageSets/Segmentation/train.txt", "r") as f: train_ids = [line.strip() for line in f if line.strip()]

如果txt里给的是带后缀的完整文件名,而你读取jpg时习惯拼os.path.join(img_dir, f"{img_id}.jpg"),那就会找不到文件。我的建议是在读取txt后立刻做归一化处理,全部剥掉后缀,统一成纯ID,后面怎么拼都不会出错。

3. 700张图能不能训练一个能用的裂纹分割模型

3.1 不加载预训练权重,收敛是真的难

不少人拿这种小数据集上来就从头训练一个分割网络,结论基本都是“Loss降不下去”。原因不复杂:道路裂纹这种细线目标,背景占绝对主导,700张图的信息量不足以让一个随机初始化的深层网络自己学会“什么是裂缝的纹理特征”。

我的做法是优先考虑迁移学习。选一个在ImageNet上预训练过的Encoder,比如ResNet50的权重,再接一个轻量Decoder,构成类似DeepLabV3+、UNet、SegNet这种结构。这样模型一开始就已经具备基础的纹理感知能力,后续训练只需要把“道路裂缝”这种特定模式微调进来。

整套训练配置我大致是这样:

  • 输入尺寸:不要用224,裂纹太细,建议512或640,至少也要384
  • 骨干网络:ResNet50或更轻的ResNet34,优先加载ImageNet预训练权重
  • Batch Size:根据显存来,一般8到16
  • 迭代轮数:50到100轮,看验证集IoU是否收敛
  • 优化器:SGD加动量,或AdamW,学习率1e-4左右,配合poly衰减

在这种配置下,700张是能训出一个“能看”的模型的,前提是你别指望它处理所有场景,路面光照、阴影、水渍这些干扰项,会让泛化有明显上限。

3.2 数据增强怎么做才不把裂纹“增强没了”

这是我认为最核心的一块。裂纹是线状目标,宽度常常只有三五个像素,如果采用目标检测里常见的随机旋转、大尺度缩放、随机擦除,很容易把本来就细的裂纹搞断、搞变形,或者让mask和图像内容失真。我不是说不能用增强,而是要用对增强。

我常用的增强组合是:

增强方式参数建议使用原因
随机水平翻转概率0.5翻转不改变几何结构
随机垂直翻转概率0.5路面纹理对翻转基本不敏感
随机裁剪从512裁出384~512的小块增加样本数量,保留局部细节
轻微旋转±10°以内超过15°线状裂纹会明显变形
亮度/对比度扰动幅度0.8~1.2模拟不同光照条件
随机高斯噪声非常轻增强对路面颗粒感的鲁棒性

这里特别提一下随机裁剪。如果原图是1024×1024,直接全图缩放到512,裂纹宽度可能就被压缩掉一半;但如果你从原图上随机裁出512×512的区域,裂纹还保持原来的尺度,等于额外获得了多张虚拟样本。这个操作对小目标、细线目标极其有效。在线增强和离线增强的选择上,我更推荐在线增强,因为每轮epoch看到的数据都不一样,变相扩充了700张的覆盖范围。

3.3 损失函数:CrossEntropy之外的几个备选项

裂纹分割的损失函数,我一开始用的普通CrossEntropy,训练过程给人的感觉是Loss在降,但可视化输出几乎全是黑的,预测全部是背景。这就是典型的不均衡问题,背景占比太高,模型发现只要全预测成背景,Loss就足够低。

后面我换成了一组组合损失,效果立刻不一样:

  • Dice Loss:直接优化交集占比,天然缓解正负样本不均衡
  • Focal Loss:降低易分类样本的权重,让模型更关注裂纹和背景交界处的难例
  • 两者按比例相加,比如Dice为主、Focal为辅
  • 也可以配合带权重的CrossEntropy,权重按背景和裂纹的像素比例倒数设置

公式我给个简单版本,Dice Loss常用形式是:

DiceLoss = 1 - (2 * |P ∩ Y| + smooth) / (|P| + |Y| + smooth)

Focal Loss可以理解为:

FocalLoss = -α * (1 - p)^γ * log(p)

当模型对某个像素越自信(p接近1),(1-p)^γ就越小,该样本对Loss的贡献就越低,于是模型被迫去关注那些分不清的像素。对细裂纹来说,边缘像素才是决定分割质量的关键,所以这种机制是实用的。

我在实践中的组合是Dice Loss加上Focal Loss,权重各一半,再配合一个极小的辅助CrossEntropy监督,整体训练过程稳定很多。如果你用的是现成框架,直接选DeepLabV3+配DiceLoss是起步配置。

4. 训练、评估和上路的落坑记录

4.1 我踩过的三个坑,每个都值得记下来

第一个坑是训练输出全背景。我在前面已经提到,一上来用普通CrossEntropy,模型学成一个“全都预测0”的摆烂状态。排查链路是这样的:先看训练集mask里是否真的存在像素值为1的区域,统计确认有;再看模型的输出Logits分布,发现模型对所有像素打分都偏向背景;然后看Loss曲线,发现一直在降但验证指标不变。这时候基本可以断定是类别不均衡压过了干净信号,换DiceLoss或者给背景类降权是最直接的解法。

第二个坑是验证时把原图一律缩放到256×256。我为了省时间,验证阶段图方便,把1024的图缩成256去算指标,结果测出来的mIoU还挺高。后来我仔细看预测图,发现小裂缝在缩小后的图上基本消失了,模型输出的是一片黑的“无裂纹”图,mIoU却很高,因为背景像素多。这个教训很直接:验证阶段必须保持和训练一致的尺度,分辨率不能用越低的图骗自己。

第三个坑是mask和原图尺寸不一致。有一条数据,原图是1024宽,但json里写的imageWidth却是1016,转换后整个mask偏移了一个固定量。排查时一个特征很明显:预测图上的裂纹位置和原图对不上,甚至出现在路肩、路面上错位的位置。处理方式前面提过,以实际图片shape为准,读取json字段时先和原图对齐。

4.2 评估指标该怎么看,别只盯整体mIoU

语义分割的常规评价指标是mIoU,也就是每个类别IoU取平均。但对裂纹这种极端不均衡场景,整体mIoU会很有迷惑性——背景类IoU通常高达99%,裂纹类IoU可能只有30%,两者一平均也能拿到65%左右的mIoU,看起来还行,实际上裂纹分割效果并不好。

所以我建议评估时,把每一类的IoU分开看,重点盯class 1也就是裂纹类的IoU。同时看F1分数和精确率、召回率,因为对道路养护来说,漏检风险比误检风险更值得关注。一张路面图如果有一米裂缝没检出,比多标了半米虚影更麻烦。可视化也不能少,直接把预测结果叠加在原图上,看裂纹位置的连续性、毛刺、断线情况,比只看数字更直观。

4.3 部署和后续应用的一点补充

训练只是第一步,真正把模型用在道路巡检里,还有几个细节要考虑。第一是输入分辨率,推理时尽量用训练时的尺寸,如果设备算力不够,宁可切块推理也不要把整图压得太小。第二是后处理,裂纹输出的mask通常会有一些椒盐噪点,做一个简单的中值滤波、开运算,把面积过小的连通域去掉,干净很多。第三是结果落库,建议对mask做连通域分析,把每条裂纹转换成“起始坐标+长度+平均宽度”的描述,这样对接养护系统更友好。

我个人的体会是,这套700张道路裂纹数据集虽然规模不大,但胜在标注干净、格式齐全,非常适合用来做一次完整的语义分割流程验证。从目录校验、格式转换,到迁移训练、损失调整,再到评估和后处理,每一步都有值得打磨的细节。尤其是损失函数和增强策略,直接影响最终模型能不能从“只会预测背景”进化到“真的找到裂缝”。如果你也在做类似的方向,建议先拿这套数据把全流程跑通,再去扩展更多数据,你会明显感觉到后续的模型迭代顺畅很多。

最后再分享一个小技巧:训练结束后,不要只看验证集指标,随机抽二三十张没参与训练的路面照片做一次盲测,用你的眼睛去判断模型输出的裂纹是否连续、是否和路面阴影混淆。指标是给人看的,但模型最终是要给真实路面用的。

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

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

立即咨询