简介:面向混凝土墙面与道路裂缝检测场景,这一基于Unet++网络模型的图像分割Python项目,适合具备一定深度学习基础的研究者与工程人员,用于路面、墙体等病害区域的自动化识别。项目提供完整数据集与代码,其中约2300张裂缝图像及对应标注,压缩包内共有2000个文件,以1877个PNG图像为主要数据与掩码,116个JPG为原始图片,5个Python脚本和2个TXT说明组成训练与推理模块。代码实现灵活,训练时可选Adam、SGD、RMSProp优化器,损失函数采用BCE逻辑损失,学习率支持恒定、余弦退火与Step衰减三种策略;训练自动保存最优与最后权重,并输出预处理可视化、Dice/Loss曲线及日志,便于分析调参。已有222人学习下载,资源整体约320.54MB,内容组织清晰,适合作为裂缝分割任务从数据准备到模型训练的完整实战参考。
1. 基于 Unet++ 做混凝土裂缝分割:难点不在网络而在像素细节
墙面上的细裂缝常常只有两三个像素宽,道路裂缝却又宽又粗,还拖着阴影和修补痕迹。同样是“裂缝”,这两类目标放进同一次分割里,会让普通 UNet 顾此失彼:网络一深,细节跟着下采样丢掉;网络一浅,粗裂缝的上下文又不够。所以做这个场景才会选 Unet++ 而不是直接套 UNet。Unet++ 的关键不在于层数更多,而是把编码器和解码器之间的跳跃连接重新编排成嵌套结构,让每个解码层都能拿到不同分辨率的组合特征,墙面细裂纹不至于断线,道路粗裂缝也不至于糊成一片。整套做完,你会得到一份能跑通的 Python 训练代码、一套带重叠裁剪的裂缝数据集处理流程,以及从分割结果反推裂缝宽度的量化方法,而不只是输出一张掩膜。
2. 为什么选 Unet++:从 UNet 到密集嵌套结构的改进逻辑
2.1 UNet 分割混凝土裂缝时的两个典型问题
UNet 的经典设计是编码器下采样提取语义、解码器上采样恢复细节,再用跳跃连接把同分辨率的低级特征拼回来。这套结构做器官分割、车牌分割问题不大,但放到墙面和道路裂缝上,会暴露两个典型问题。
第一个问题是细裂纹被下采样吃掉了。编码器每下采样一次,分辨率就减半,两三个像素宽的墙面裂缝在深层特征图里只剩一个不连续的点。到解码阶段再上采样,UNet 只恢复一次空间精度,跳跃连接又只能补回同层的单份信息,最终预测结果自然把细裂缝连成虚线。很多人反复加深网络、加注意力,效果提升仍不明显,根子其实在特征融合方式而不在卷积宽度。
第二个问题是道路裂缝的阴影和油渍会骗过浅层特征。浅层编码器对边缘敏感,但分不清裂缝边缘和阴影边缘;深层编码器语义强,但分辨率低。UNet 的跳跃连接是单条直连,相当于让解码层只信任同一个尺度来源的信息,多尺度之间缺少互相校正的环节。裂缝数据里一旦同时混了墙面和道路两类背景,这种单条连接的误检率就会明显上升。这也是为什么有人先去套 YOLO 检测框,但检测框给不了像素边界,最后还得绕回分割。
2.2 Unet++ 密集跳跃连接:每个解码节点都做二次卷积融合
Unet++ 最早在医学图像分割场景里提出,思路不是换 backbone,而是把编码器和解码器之间的“一条直路”改成“一组嵌套路径”。把编码器不同层的输出记为 x00、x10、x20,再逐层生成 x01、x11、x02 这些中间节点。生成规则是:同一分辨率的前序输出,与来自下一层上采样的输出做通道拼接,过一组卷积,再传给下一个节点。
理解这个结构最直接的方式是看节点之间的运算。忽略 batch 维度和通道维度后,一个节点可以写成下面这样。
# 伪代码:Unet++ 单个节点的拼接与卷积 x01 = conv3x3_bn_relu(cat([x00, upsample(x11)], dim=1)) x02 = conv3x3_bn_relu(cat([x00, x01, upsample(x12)], dim=1))而 UNet 的同层解码输入是cat([skip_x00, upsample(decoder_up)]),差别就在于多了 x01、x02 这些中间节点。每多一个节点,当前层就多一次对高分辨率特征的重新卷积,裂缝细节是被“接力”保留的,而不是进入下采样后彻底丢掉。细裂缝的高分辨率特征在每个解码节点里都会被重新加权一次,粗裂缝的上下文也能回传到浅层节点,用来压住阴影误检。
工程上比较省心的实现是直接调用 segmentation_models_pytorch 里的 UnetPlusPlus 接口,配合 encoder_depth 参数调嵌套深度,第 4 章会给出可执行代码。
2.3 网络深度选择:L2、L3 还是 L4
Unet++ 的嵌套深度决定了中间节点的数量,也从根上影响显存占用和分割表现。实际项目里不会一上来就上最深的配置,而是先看手头数据量。
| 嵌套深度 | 解码节点规模 | 常用场景 |
|---|---|---|
| L2 | 较小 | 小数据集、快速验证,墙面细裂纹优先 |
| L3 | 中等 | 常规默认,路面与墙面混合数据 |
| L4 | 较大 | 裂缝形态差异大、背景复杂,需要强上下文 |
经验是:切完片后如果样本量低于 1000 张,L2 往往足够,L4 在这种规模下反而容易过拟合;如果项目要顺带处理桥墩、隧道衬砌这类背景复杂的病害照片,直接上 L4 比较稳。
2.4 深度监督与推理期裁剪
Unet++ 还提供了深度监督选项,训练时每个解码节点都接一个 1×1 卷积计算损失,梯度能同时到达浅层和深层。工程上有一个非常实用的技巧:L4 训练完成后,推理阶段可以把最后一个嵌套块裁掉,只保留 L3 的预测头,速度能上来一截,而精度损失往往在 1 个百分点以内,对裂缝这种二分类分割任务非常划算。
损失函数层面,裂缝正样本占整张图的比例经常低于 3%,直接用交叉熵会让模型偏向预测背景。常见做法是 BCE 加 Dice 的组合损失,形式记为loss = 0.5 * BCE + Dice,两个分量量级接近,不用单独调权重。如果阴影背景误检仍然高,可以再并入 0.4 权重的 Focal Loss,让网络更关注难分类的裂缝像素。
3. 裂缝数据集与预处理:墙面、道路分开做的三个环节
3.1 数据源与标签格式:公开裂缝数据集与自建桥墩病害数据补充
做裂缝分割的第一步是统一掩膜格式。公开能拿到的裂缝数据集主要是 CrackForest 这类城市道路数据集和 DeepCrack 系列,前者偏向沥青路面纹理,后者包含墙面和地面混合场景。两者的标签格式不完全一致,有的是边缘线,有的是区域掩膜。处理时统一转成 0/255 的单通道 PNG,再二值化为 0/1 标签,这是后续所有流程的前提。
如果项目范围还包含桥墩、隧道衬砌等构筑物,可以补充桥墩病害数据集,这类数据背景纹理更复杂,能明显提高模型在真实巡检照片上的泛化能力。真实项目里还有一个常见操作:把厂家巡检视频按间隔抽帧后手动标注,间隔按拍摄速度定,避免连续帧高度相似导致训练集冗余。
3.2 图像切片函数:重叠裁剪与镜像补齐
墙面和道路原图通常在 1000×1000 以上,直接进网络要么撑爆显存,要么把裂缝缩得太小。常见做法是切成 256×256 或 320×320 的补丁,重叠率取 50%,也就是 stride 等于 patch 的一半。0.5 重叠率的作用是让跨切片的裂缝同时在两个补丁里完整出现,减少边界处的断裂误判。
def crop_with_overlap(image, mask, patch_size=256, stride=128): h, w = mask.shape[:2] patches = [] for y in range(0, h - patch_size + 1, stride): for x in range(0, w - patch_size + 1, stride): img_p = image[y:y + patch_size, x:x + patch_size] mask_p = mask[y:y + patch_size, x:x + patch_size] patches.append((img_p, mask_p)) # 尾行尾列不足一个 patch 时,用镜像补齐后单独取块 if y + patch_size < h or x + patch_size < w: pad_img = np.pad(image, ((0, patch_size), (0, patch_size), (0, 0)), mode="reflect") pad_mask = np.pad(mask, ((0, patch_size), (0, patch_size)), mode="reflect") for yy in range(0, h, stride): for xx in range(0, w, stride): if yy + patch_size > h or xx + patch_size > w: patches.append((pad_img[yy:yy + patch_size, xx:xx + patch_size], pad_mask[yy:yy + patch_size, xx:xx + patch_size])) return patches代码逻辑:先用步长遍历生成完整补丁;图高宽不能被步长整除时,剩余区域用 reflect 镜像补齐后单独取尾块。参数上,patch_size=256 是计算量与感受野的折中,320 能保留更多上下文但显存占用增加约一半;stride 与 patch_size 相等时不重叠、无冗余,但边界裂缝容易断;重叠率越高训练样本重复度越高,0.5 是精度与过拟合风险的平衡点。
3.3 裂缝场景专属增强策略
第三个环节是数据增强。通用分割里的随机裁剪、翻转可以直接用,但要额外加三类针对裂缝的增强:亮度扰动、阴影模拟、椒盐噪声。真实墙面和路面常有光照不均,亮度乘性扰动在 0.8 到 1.2 之间比较合适,gamma 变换则用来模拟逆光。
这里有一个容易忽略的点:做增强时不能对图像做随意缩放,或者缩放后必须同步等比例变换标签。裂缝宽度是后续评估的核心指标,图像一旦被放大而标签没跟着放大,训练出的模型在推算真实宽度时会系统性偏大。常用参数按下面这张表配置。
| 增强操作 | 代表参数 | 适用场景 |
|---|---|---|
| 水平/垂直翻转 | p=0.5 | 墙面裂缝,道路裂缝同样可用 |
| 随机旋转 | 15 度以内 | 保留裂缝长轴方向 |
| 亮度对比度 | 0.8~1.2 | 模拟阴天、逆光巡检照片 |
| 高斯噪声 | sigma 不超过 5 | 模拟路面颗粒纹理 |
| gamma 变换 | gamma 0.8~1.5 | 修补强阴影干扰 |
3.4 标签质量检查:骨架化看连通性
标签画得太粗、断点太多是裂缝分割训练失败的首要原因。训练前我会做一轮快速检查:对每一张掩膜做骨架化并统计连通域数量,如果一条真实裂缝被切成超过 3 段,说明标签断点过多,模型的预测结果也会跟着断。用 scikit-image 的 skeletonize 配合 scipy 的 label 就能完成统计,跑完全部标签只需要半分钟左右,能赶在训练前把数据问题暴露出来,比事后反复调损失函数高效得多。
4. 基于 Python 的 Unet++ 训练:从模型定义到参数调优
4.1 模型定义:优先用现成库,自己手写反而容易错
Unet++ 的工程实现细节不少,节点间通道数、上采样衔接、深度监督的损失计算都要处理,手写容易出 bug。我一般直接用 segmentation_models_pytorch 封装好的 UnetPlusPlus,它支持预训练编码器,换 backbone 只改一个参数。完整代码的工程结构通常拆成 train.py、predict.py、utils.py 三个文件,模型定义独立成 model.py。
import segmentation_models_pytorch as smp model = smp.UnetPlusPlus( encoder_name="resnet34", # 主干网络 encoder_weights="imagenet", # 预训练权重 in_channels=3, # 输入图像通道数 classes=1, # 二分类,只分割裂缝 decoder_attention_type="scse", # 解码器注意力 encoder_depth=5, # 编码器深度,对应嵌套 L4 )参数说明:encoder_name 决定主干网络,resnet34 是速度和精度的常见折中;encoder_weights 加载 ImageNet 预训练可以少训练很多轮;classes=1 表示输出单通道 logits,后面接 sigmoid 得到裂缝概率;decoder_attention_type 里的 scse 是类似压缩激励的注意力,会增加少量计算量,但对阴影区域抑制有帮助。如果显存紧,把 resnet34 换成 resnet18,或者将 encoder_depth 从 5 改成 4。
4.2 Dataset 与数据加载写法
训练前要把切片和标签组装成 torch Dataset。掩膜是单通道 PNG 的话,需要先灰度化再二值化,下面这段是核心部分。
class CrackDataset(Dataset): def __init__(self, img_files, mask_files, aug=False): self.img_files = img_files self.mask_files = mask_files self.aug = aug def __len__(self): return len(self.img_files) def __getitem__(self, idx): img = cv2.imread(self.img_files[idx]) # BGR 读取 img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 转 RGB mask = cv2.imread(self.mask_files[idx], 0) # 单通道 _, mask = cv2.threshold(mask, 127, 1, cv2.THRESH_BINARY) img = img / 255.0 img = torch.from_numpy(img.transpose(2, 0, 1)).float() mask = torch.from_numpy(mask).unsqueeze(0).float() return img, mask注意掩膜二值化放在__getitem__里而不是离线处理阶段,好处是每次读取都走同一套清理逻辑,不会被预处理脚本的中间状态干扰。用了 ImageNet 预训练编码器的话,图像归一化建议配合通道标准化,而不是简单除以 255,否则预训练权重的统计口径对不上。训练时跨 batch 随机翻转这类轻增强也应加在这个类里,增强对象必须同时包含图像和掩膜。
4.3 训练参数表:照着这套起步,再根据显存调整
训练参数直接决定能否训得动。下表是一套我常用的起步配置,显存不够时优先减 batch size,再减 patch size,不要一上来就降学习率。
| 参数名 | 起步值 | 调整建议 |
|---|---|---|
| patch_size | 256 | 320 以上需要更大显存 |
| batch_size | 8 | 显存不足时降到 4,配合梯度累积 |
| 学习率 | 3e-4 | AdamW 常用区间 1e-4~3e-4 |
| 优化器 | AdamW | 比 Adam 更不容易跑飞 |
| 最大 epoch | 100 | 可以设 200,但要配早停 |
| 早停 patience | 15 | 验证集 Dice 连续 15 轮不升则停 |
| 混合精度 | AMP | 省一半显存,开启梯度缩放 |
训练循环里有个细节经常被忽略:掩膜是 0/1 二值,模型输出是未过 sigmoid 的 logits,损失函数要选带 logits 的版本。PyTorch 的 BCEWithLogitsLoss 在内部做了数值保护,Dice Loss 则要在计算前自己做一次 sigmoid,两者不要混用,否则损失曲线会反复抖动。
4.4 训练中的三个坑:数据不平衡、验证集选择、标签漏标
第一个坑是正样本像素占比过低。统计后如果裂缝像素占比低于 1%,建议在 Dataset 里做加权采样,对含裂缝的补丁提升读取概率,而不是在损失函数里硬调权重。
第二个坑是验证集和训练集来自同一段巡检视频的邻近帧,导致验证指标虚高。切分数据集时要按拍摄区域来分,不要随机打乱帧级文件,否则模型遇到水沟边、桥墩这类新背景时性能会明显跳水。
第三个坑是标签漏标。手工标注经常漏掉细裂缝,这会让模型在“漏标区域”给出高响应,验证 IoU 反而下降,表面看像过拟合,实际是真值错误。遇到这种情况,把预测结果和原图叠加、逐张看难例,比调参数更解决问题。
5. 从预测掩膜到裂缝宽度:推理拼接与量化评估
5.1 切片推理的拼接方式
推理阶段把 256 切片送进网络后,重叠区域不能简单丢弃。常见做法是每个像素保留它在所有覆盖它的补丁中的预测均值;追求质量时,往重叠区加一个高斯权重,中心像素权重最高、边缘像素权重最低,这样拼接出来的掩膜在补丁边界不会出现块状接缝。全图掩膜出来后先做 0.5 阈值二值化,再做一次开运算去掉孤立噪点,规则很简单:面积小于 20 像素的连通域直接删除。
5.2 评估指标的优先级
语义分割里 IoU 和 Dice 都要看,但对裂缝来说,更重要的是预测宽度的相对误差。IoU 对细裂缝太严格,两像素宽的裂缝上错位一个像素,IoU 就会掉到 0.5 以下,让人误以为模型很差。所以我通常同时看三个指标:Dice 衡量断裂程度,IoU 用来与别人的结果横向对比,宽度均方误差才是业务指标。项目报告里这三个分开写,后面算裂缝等级时只看第三个。
5.3 估计真实裂缝宽度:骨架化加距离变换
这个环节最值得做。把二值掩膜转成骨架,用欧氏距离变换算出骨架上每个像素到最近背景的距离,距离乘以 2 就是局部的裂缝宽度像素值。
from scipy import ndimage from skimage.morphology import skeletonize dist = ndimage.distance_transform_edt(mask) # 每个像素到最近背景的距离 sk = skeletonize(mask) # 提取中轴骨架 width_px = 2 * dist[sk] # 骨架上每个点的局部宽度 avg_width_px = width_px.mean() # 平均宽度,单位像素 median_width_px = np.median(width_px) # 中位数宽度,抗噪更强这段代码的含义很直接:裂缝可以看作一条在背景中挖出的细长通道,通道中轴上的点到两侧边界的距离之和就是宽度。距离变换给出的是到最近背景的欧氏距离,乘 2 就是完整宽度。骨架化默认按连通性处理,掩膜断点多的要先做闭运算,否则骨架会分裂,宽度统计也会失真。
5.4 像素当量标定与开工前检查
像素当量的标定方法:在墙面贴一张已知尺寸的标定纸,与裂缝拍进同一张照片,算出标定纸在图像里占据的像素数,物理长度除以像素数就是毫米每像素。巡检距离不固定时,把拍摄距离和像素当量做成对应表,比硬编码一个常数可靠。
开工前做一次快速自检就够了:预测结果里单像素噪声比例是否低于 5%、细裂缝是否连续贯穿、宽度分布是否明显偏离人工抽检结果。把这三个阈值写进配置文件,每次训练完自动打印,跑三五轮后你就能一眼看出到底是哪一环出了问题。
本文还有配套的精品资源,点击获取