简介一份以路面裂缝检测系统为实战对象的计算机视觉与深度学习项目案例教程采用MATLAB和Python作为开发工具面向从事图像处理、道路养护及交通工程研究的工程技术人员与相关专业学生。资源为一份PDF格式的电子文档大小约1.46兆字节内容出自《计算机视觉与深度学习实战——以MATLAB、Python为工具》一书的第21章目前已有726人学习或下载。教程从传统人工检测效率低、主观性强且存在安全风险等痛点切入系统阐述裂缝线状目标检测的理论基础包括图像灰度化的分量法、最大值法、均值法与加权平均法以及均值滤波、中值滤波等噪声抑制手段并指出裂缝图像因窄小、低对比度、自然间断等特点对传统算法的挑战。随后深入讲解利用卷积神经网络进行裂缝特征学习与自动识别的方法完整覆盖数据集准备、模型选择与训练、系统测试与优化等环节并给出基于MATLAB与Python深度学习框架的实现思路。读者可通过该案例掌握从图像预处理到模型部署的完整流程为低对比度、复杂背景下的线状目标识别项目提供可复用的设计参考。1. 路面裂缝检测不是图像分类而是一条像素级分割流水线路面裂缝检测系统这个标题看起来像是一个通用的计算机视觉大作业但真正动手时你会发现它跟你平时做的猫狗识别、MNIST 手写数字完全不在一个难度层级。裂缝检测的落地本质是像素级语义分割——模型不但要判断“这张图有没有裂缝”还要在 512×512 的图像里把每一条龟裂、横向裂缝、纵向裂缝的轮廓精确抠出来误差超过几个像素就可能让后续的裂缝宽度计算和等级评估彻底失效。为什么这个项目开发案例教程要同时用 MATLAB 和 Python 两套工具链我的理解是MATLAB 适合做图像预处理、形态学后处理和快速原型验证Image Processing Toolbox 里一堆现成函数能把连通域分析、骨架提取这类活儿压到几行代码Python 则胜在深度学习生态完整PyTorch 训练 U-Net 的效率比 MATLAB 的 Deep Learning Toolbox 顺畅不少。如果是课程设计或者研究生阶段的项目入门用 Python 走通训练流程、再用 MATLAB 做结果分析和报告出图是一条性价比最高的路线。这篇笔记就是沿着这条双工具链路线把路面裂缝检测从数据准备到模型部署的完整链路拆开讲清楚包括参数取值、运行命令和踩过的坑。2. 裂缝数据集决定模型上限标注规范、样本均衡和切图策略2.1 公共数据集与自采数据怎么选路面裂缝检测的公开数据集不少常见的有 CFDCrackForest Dataset、Crack500、DeepCrack另外还有一个比较经典的是 GAPs 数据集。这些数据集各有各的脾气CFD 分辨率低但有像素级标注Crack500 是从 500 张 2000×1500 左右的原始路面图里切出来的训练样本量大但标注质量参差不齐。实际做系统设计的时候我一般建议别直接拿某一个数据集一口气训到底先把几个数据集的标注格式统一成 8 位单通道 PNG0 是背景、1 是裂缝再做一个灰度分布直方图对比看看标注的裂缝占整张图面积的比例大概是多少。裂缝这种目标在整张图里通常只占 1% 到 5% 的像素神经网络如果直接在这种极度不平衡的数据上训练loss 会被背景类完全绑架最后预测结果全黑一条裂缝都出不来。如果项目要求用自采数据手机拍摄的路面图要先做透视校正和光照归一化。我踩过的坑是用手机在中午顶光下拍的路面裂缝阴影偏重而且沥青纹理的高光区域局部过曝直接喂进网络训练Pretrained 权重反而成了干扰。自采数据建议在三个时间段采集——清晨低角度光、阴天漫射光、傍晚斜射光用三组覆盖同一路段这样模型对光照变化的鲁棒性会好不少。所有原始图片统一压到 512×512 或 448×448 再做训练不要图省事用原图全分辨率显存会先爆掉。2.2 标注格式转换从 JSON 到 PNG 掩码的一个小脚本很多开源裂缝数据集用的是 JSON 多边形标注而 U-Net 训练要的是掩码图。转换这一步有个常见做法是 OpenCV 的fillPoly把多边形光栅化成 0/255 的单通道图但注意原始 JSON 里的坐标如果是相对坐标比如取值在 0~1 之间的归一化坐标必须乘以图像宽高再取整否则多边形画出来全部偏移到左上角。import json import numpy as np import cv2 def json2mask(json_path, img_width, img_height, mask_path): with open(json_path, r, encodingutf-8) as f: data json.load(f) mask np.zeros((img_height, img_width), dtypenp.uint8) # 一张图里可能有多个裂缝区域全部用多边形填充 for shape in data[shapes]: points np.array(shape[points], dtypenp.float32) # 归一化坐标还原到像素坐标注意先乘后取整 points[:, 0] * img_width points[:, 1] * img_height points points.astype(np.int32) cv2.fillPoly(mask, [points], color255) cv2.imwrite(mask_path, mask) print(fsaved mask: {mask_path}, unique values{np.unique(mask)})这段脚本有三个地方值得注意。第一shapes字段是 LabelMe 标注工具的常用输出格式如果你用的标注工具是其他家的字段名要对应改第二归一化坐标转换时不能直接astype(np.int32)之后再乘宽高那样精度丢失很严重二值掩码锯齿会非常明显第三最终写出的掩码里只有 0 和 255 两种值训练代码里通常要再mask / 255转成 0 和 1。2.3 正负样本失衡的两个补救手段路面裂缝特有的样本失衡可以拆成两个层面。第一层是像素级失衡裂缝像素占比太少这个用 Dice Loss 或者 Weighted Cross Entropy 解决后面第四章会展开讲函数怎么设计。第二层是区域级失衡有些 512×512 的切图里整张图都是干净沥青一条裂缝都没有这种纯背景图在训练集里如果占比太高模型会倾向于输出全背景。我的处理方式是控制纯背景图比例不超过总训练集的 15%多余的背景图从训练列表里踢掉。另一个补救手段是负难例挖掘第一轮模型训练完之后用模型去预测训练集的一部分挑出预测错误的上图——也就是存在裂缝但模型漏检的图——把它们单独放到一个文件夹里做第二轮 Fine-tune。这个操作能比较明显地提升裂缝较细或者对比度较低那部分样本的召回率值得作为系统流程写进课设报告里。2.4 切图策略滑动窗口切图与重叠率取值原始路面图尺寸通常超过 1000×1000不能直接整张塞进网络。滑动窗口切图是 U-Net 类模型做遥感或路面检测的标配方案。窗口大小我取 512×512步长 stride 取 256也就是 50% 重叠。这样做的好处是裂缝在窗口边缘被切断的概率会降低训练样本量也能翻倍。预测阶段不用重叠直接非重叠切图即可这是训练和推理的一个常见差别。提示切图不要用 OpenCV 的crop循环加imwrite直接存盘速度慢而且会写出一堆磁盘碎片。更推荐做法是在__getitem__里实时切配合 PyTorch 的 DataLoader 多进程读取训练时 IO 吞吐明显快很多。3. 用 U-Net 搭裂缝分割模型Python 训练和 MATLAB 验证的两种打开方式3.1 为什么选 U-Net 而不是 VGG16 或者 ResNet裂缝检测有人尝试过用 VGG16 做分类先判断有缝没缝再对“有缝”的图做二次处理。这个方案的问题在于裂缝宽度只有几个像素经过 VGG16 的池化层之后细节全没了。U-Net 用编码器-解码器结构编码器下采样提取语义特征解码器上采样恢复空间分辨率中间通过 skip connection 把每一层的细节特征直接拼到解码器对应层所以输出层的裂缝轮廓能保留到接近原图分辨率。路面裂缝又细又长又连续这种拓扑结构恰好是 U-Net 这类带跳跃连接模型擅长处理的。对于裂缝检测场景U-Net 的另一个优点是数据需求量相对友好。用 ImageNet 预训练的 ResNet 做 Encoder 确实能加快收敛但裂缝是纹理类目标跟 ImageNet 里的物体语义差异很大预训练权重帮助有限。我做过的对比里从零训练 U-Net 和用 ResNet34 编码器做迁移学习的最终 mIoU 差距不到 2%但训练时间差了接近一倍。所以课程设计级别的时间预算下直接训练 U-Net 是性价比最高的选择。3.2 Python 端最小 U-Net 实现从数据加载到训练主循环import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader import cv2 import numpy as np import os class CrackDataset(Dataset): 读取 img_dir 下的原始图和 mask_dir 下的二值掩码 def __init__(self, img_dir, mask_dir, size512): self.img_dir img_dir self.mask_dir mask_dir self.names sorted(os.listdir(img_dir)) self.size size def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img cv2.imread(os.path.join(self.img_dir, name)) mask cv2.imread(os.path.join(self.mask_dir, name.replace(.jpg, .png)), cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (self.size, self.size)) mask cv2.resize(mask, (self.size, self.size), interpolationcv2.INTER_NEAREST) img img.astype(np.float32) / 127.5 - 1.0 mask mask.astype(np.float32) / 255.0 img torch.from_numpy(img).permute(2, 0, 1) mask torch.from_numpy(mask).unsqueeze(0) return img, mask class ConvBlock(nn.Module): 两个卷积 BN ReLU 的标准块 def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x) class UNet(nn.Module): 轻量 U-Net通道数按 [32, 64, 128, 256] 递增 def __init__(self, in_ch3, out_ch1): super().__init__() self.enc1 ConvBlock(in_ch, 32) self.enc2 ConvBlock(32, 64) self.enc3 ConvBlock(64, 128) self.pool nn.MaxPool2d(2) self.bottleneck ConvBlock(128, 256) self.up3 nn.ConvTranspose2d(256, 128, 2, stride2) self.dec3 ConvBlock(256, 128) self.up2 nn.ConvTranspose2d(128, 64, 2, stride2) self.dec2 ConvBlock(128, 64) self.up1 nn.ConvTranspose2d(64, 32, 2, stride2) self.dec1 ConvBlock(64, 32) self.out nn.Conv2d(32, out_ch, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) b self.bottleneck(self.pool(e3)) d3 self.dec3(torch.cat([self.up3(b), e3], dim1)) d2 self.dec2(torch.cat([self.up2(d3), e2], dim1)) d1 self.dec1(torch.cat([self.up1(d2), e1], dim1)) return self.out(d1)这个实现里有两个参数值得注意。第一是通道数从 32 起步而不是经典的 64内存占用降低一半在 6GB 显存的卡上也能带起来而精度损失不大。第二是torch.cat拼接 skip connection 时放在up上采样之后、dec卷积之前顺序不能反这是官方 U-Net 的原始设计。mask在读取时用cv2.INTER_NEAREST做 resize不能用线性插值否则掩码边缘会出现灰阶过渡值训练时会让 loss 原地抖。3.3 训练脚本的 Loss 函数与评价指标裂缝分割的 Loss 不建议用纯 Cross Entropy正负样本差距太大。常见做法是 Dice Loss 与二分类交叉熵的加权组合。Dice Loss 的公式是 1 减去两倍交集除以并集它的梯度天然地放大“裂缝区域没预测对”那一部分的权重在多轮训练中能稳住稀疏目标的优化方向。def dice_loss(inputs, targets, smooth1.0): inputs torch.sigmoid(inputs) inputs inputs.contiguous().view(inputs.size(0), -1) targets targets.contiguous().view(targets.size(0), -1) intersection (inputs * targets).sum(dim1) dice (2.0 * intersection smooth) / (inputs.sum(dim1) targets.sum(dim1) smooth) return 1.0 - dice.mean() class CombinedLoss(nn.Module): BCE 加 Dice 的混合损失适合裂缝这种极稀疏目标 def __init__(self, weight_dice0.7): super().__init__() self.bce nn.BCEWithLogitsLoss() self.weight_dice weight_dice def forward(self, pred, target): return self.bce(pred, target) * (1 - self.weight_dice) dice_loss(pred, target) * self.weight_dice为什么 Dice 的权重取 0.7试过 0.5 和 0.90.5 时背景类 loss 占比高训练初段模型输出整体偏暗0.9 时早期训练震荡厉害Dice 的梯度在目标极稀疏时会出现不稳定的跳变。0.7 是收敛曲线相对平滑的取值。另外这个inputs.sum用的是遍历所有像素不是只求和目标像素写的时候注意别混。3.4 MATLAB 侧验证加载训练好的权重做后处理模型训练完论文里通常需要跑一组实验结果对比图。MATLAB 在这块的体验比 Python 好很多——读取 PyTorch 导出的 ONNX 模型做前向推理然后接形态学后处理和可视化代码量很短% 加载 PyTorch 导出的 ONNX 格式裂缝分割模型 net importNetworkFromONNX(crack_unet.onnx); % 读取测试图并缩放到 512x512数值范围转为 [-1, 1] img imresize(imread(test_crack.jpg), [512, 512]); img im2single(img) * 2 - 1; % 前向推理输出是 1 通道的 logitssigmoid 转成概率 prob predict(net, img, Outputs, output); prob 1 ./ (1 exp(-prob)); % 简单阈值分割 开运算去孤立噪声点 骨架提取 bw prob 0.5; bw imopen(bw, strel(disk, 2)); skel bwskel(bw); % 叠加在原图上并设置灰度亮度和透明度 figure; imshow(img); hold on; vis imshow(repmat(uint8(bw) * 255, [1 1 3])); set(vis, AlphaData, bw * 0.6);注意 MATLAB 的predict对 ONNX 输出的维度顺序可能跟 PyTorch 不一致中间可能需要一次permute。另外bwskel得到的骨架线可以直接喂给后续的裂缝宽度统计模块这是 MATLAB 相比 Python 的工程便利之处。4. 训练配置和参数调优学习率、Batch Size、数据增强的定量取值4.1 学习率与 Batch Size 的联动取值裂缝分割在这个模型配置下的训练参数量大约在 780 万到 900 万之间通道数 32 起步的轻量 U-Net不算是大模型但学习率选不好照样会loss 震荡。初始学习率1e-3配合 ReduceLROnPlateau 是我做这类项目常用的配置具体触发逻辑是连续 5 个 epoch 验证集 Dice 没有上升学习率乘以 0.5。Batch Size 的设置要结合显存来算。512×512 输入、通道 32单卡 6GB 显存时 Batch Size 取 8 是安全值取 16 大概率 OOM。如果你用的是 8GB 以上的显卡Batch Size 可以提到 12 到 16。注意 Batch Size 翻倍的同时学习率也要跟着调大一些常见做法是线性缩放比如 Batch Size 从 8 翻到 16学习率从1e-3提到1.4e-3这样收敛速度不会因为步数减少而降太多。4.2 数据增强的顺序和参数裂缝检测的数据增强有两个原则不能改变裂缝的拓扑形态不能把裂缝“抹掉”。旋转、翻转、缩放这类几何增强直接用亮度、对比度调整要注意幅度Gamma 取值在 0.8 到 1.2 之间太亮或太暗会让本来就细的裂缝对比度进一步恶化。import imgaug.augmenters as iaa # 按概率组合增强算子顺序固定几何变换 - 颜色扰动 - 高斯噪声 aug iaa.Sequential([ iaa.Sometimes(0.7, iaa.Affine(scale(0.9, 1.1), rotate(-30, 30), shear(-8, 8))), iaa.Sometimes(0.5, iaa.LinearContrast((0.9, 1.15))), iaa.Sometimes(0.3, iaa.AdditiveGaussianNoise(scale(0, 0.03 * 255))), ], random_orderFalse)这套增强里有一个隐藏参数容易踩雷rotate设为 ±30 度以上时图像四角会出现黑色填充区域这些区域不是路面纹理模型会学会“角落里黑块不是裂缝”测试时如果真实图上恰好有阴影就会漏检。解决方法是同时开iaa.Affine的cval参数把填充值设为路面纹理的平均灰度而不是 0。4.3 训练轮数和早停策略从零训练 U-Net 在这个数据集规模假设 2000 张 512×512 切图下60 到 80 个 epoch 能收敛。判断收敛不能只看训练 loss我用验证集 Dice 做早停监控保存验证集上表现最好的权重不保存最后一次迭代的结果。训练完毕后对照训练集 Dice 和验证集 Dice如果两者差距超过 8%要检查是不是数据增强过强引入太多样本噪声或者训练集和测试集分布差异太大。5. 训练和推理中的五个常见绊脚石现象、原因和处置5.1 模型预测全黑一条裂缝都检测不到现象验证集 Dice 到 0预测结果全部是背景。原因这个坑九成出在标签预处理。掩码图在Dataset.__getitem__里被mask.astype(np.float32) / 255.0转成了 0 到 1但如果原始掩码不是 0 和 255 而是 0 和 1除以 255 之后裂缝像素值变成 0.004几乎等于背景梯度直接消失。解决在数据准备脚本里做一次np.unique(mask)检查确保掩码只有两个值训练代码里不除以 255直接读原始值。5.2 训练 loss 下降但预测结果碎成雪花状现象验证 loss 曲线正常下降但预测的裂缝图里散布大量孤立像素噪声点裂缝形态不连续。原因模型把沥青路面上的细纹理当成了裂缝。这类误检大多来自训练集里对比度高的路面纹理区域没有被标注为负样本模型学到了错误的纹理特征。解决检查训练集图像凡是路面本身有清晰纹理的图要么在标注时把非裂缝纹理单独建一个类别要么用 Mask R-CNN 这种基于区域的模型替代 U-Net 做实例级过滤。另一个偏工程的手段是在后处理时用bwareaopen删除小于 500 像素的连通域配合imclose连接断裂的裂缝段。5.3 裂缝细线在中途断裂宽度不连续现象预测的裂缝线中间出现断点一条 200 像素长的裂缝被切成了三四段。原因模型对裂缝与背景交界处的置信度不够高。裂缝在图像里常常只有 2 到 5 个像素宽经过编码器的两次下采样后特征图的空间分辨率只剩 128×128裂缝的细长结构信息严重丢失上采样恢复出来就断了。解决优先加宽输入分辨率到 768×768代价是显存占用上升其次把编码器中 MaxPool 改成 Stride 卷积整体池化次数减一次最省事的方法是后处理里做一个形态学闭运算imclose(bw, strel(line, 5, angle))角度参数可以根据裂缝的主方向设定。5.4 验证集随机切图导致同一裂缝重复统计现象验证集 Dice 明显虚高换了另一组测试图后性能大幅下滑。原因滑动窗口切图时验证集与训练集的切图窗口重叠同一裂缝可能既出现在训练窗口又出现在验证窗口导致数据泄漏。解决切图之前先按整图分配保证同一张原始路面图的所有切块只出现在训练集或只出现在验证集中不在切图后做随机划分。这是工程上避免数据泄漏的最常用做法。5.5 用 MATLAB 输出报告时图像尺寸对不上现象在 MATLAB 里把预测掩码叠加到原图上发现裂缝位置偏移了几个像素。原因imresize默认用双三次插值将掩码缩放到原始尺寸时边界被平滑裂缝边缘偏移半像素以上。解决MATLAB 侧对掩码做 resize 时指定最近邻插值mask imresize(mask, [H, W], nearest)如果要做亚像素级别的宽度测量就不应该在原图上直接测量预测掩码而应该把模型输出的概率图上采样到更高分辨率再测量。6. 验证方法论除了 Dice 之外用裂缝连续性指标判断模型可不可用很多课设和论文止步于展示几个 Dice 分数和几张预测图但真实的路面裂缝检测系统要评判能不能落地Dice 远远不够。裂缝检测的最终用户关心的是两件事有没有漏掉一条宽度超过阈值的真实裂缝、以及检测出的裂缝位置能不能支撑后续修复决策。这两个诉求对应的指标是每张图的裂缝级召回率和预测骨架线的端到端连通度。我做验收时有一个固定流程先对验证集每张图计算整图级召回率——如果模型在 512×512 的切图里漏掉了超过 30% 的标注裂缝片段不管 Dice 多高都判定不合格然后用bwskel提取真实标注和预测结果的骨架把骨架分成若干个 20 像素的段统计预测骨架与真实骨架在 3 像素范围内匹配上的段的比例。这个比例低于 0.6 时说明预测结果虽然像素覆盖率还行但裂缝是断断续续的无法支撑宽度测量。关于部署参数还有一个容易被忽视的细节阈值的选择不能固定 0.5。训练结束后我会在验证集上扫描 0.3 到 0.7 的阈值画出 Precision-Recall 曲线选 PR 曲线拐点的阈值作为推理默认值。对裂缝检测这个场景漏检的代价是裂缝从 1 毫米发展到 3 毫米才被发现所以我通常偏向低阈值允许小幅误报靠后处理的面积过滤把噪声去掉。如果后续要跑 FPGA 或者用 TensorRT 做加速ONNX 导出的模型里需要把后处理逻辑剥离干净只在模型里保留卷积输出阈值和连通域过滤放到加速框架的外部算子去做。这套双工具链方案从批处理脚本到验证指标大致覆盖了路面裂缝检测从零到交付的整个路径——Python 负责训练和调参MATLAB 负责后处理和报告可视化中间用 ONNX 作为模型交换格式。作为收尾提醒一句裂缝检测模型的“能跑”跟“能用”之间差一个完整的验证闭环我的习惯是每次换数据集都重新算一遍阈值和骨架连通度不沿用旧参数这个习惯替我挡过不少次测试翻车。希望这套流程对你做路面裂缝检测系统的课程设计或工程落地有参考价值。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?