ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

路面裂缝检测识别系统设计:从数据准备到zip包交付全流程

路面裂缝检测识别系统设计:从数据准备到zip包交付全流程 简介基于深度学习与图像处理的路面裂缝检测识别系统完整MATLAB工程适合智能交通、道路检测领域的开发者及学习CNN图像分类的学生参考。系统以GUI操作平台集成图像增强、噪声去除、直方图均衡化、图像分割及CNN特征提取实现裂缝自动识别与定位。压缩包共18个文件含14个.m核心源码、1个.fig界面文件、测试图片与结果表格合计268KB脚本覆盖预处理、方向判断、裂缝判定等关键环节便于按模块阅读和二次开发。内容涉及深度学习建模、图像处理流程、MATLAB工具箱应用等完整方案可帮助理清从数据准备、模型训练到系统测试的全过程。已有1053人学习尤其适合课程设计或算法验证。通过源码可掌握图像处理工具箱、Deep Learning Toolbox的实际调用以及GUIDE界面搭建流程附带的样例图像与xls结果文件能直接运行验证帮助快速理解裂缝检测的实现细节。1. 路面裂缝检测识别系统设计先想清交付物再动手很多朋友看到“第 21 章 路面裂缝检测识别系统设计.zip”时下意识以为里面是一整套能开箱即用的代码其实这类压缩包通常只是项目骨架一份需求文档、若干样例图片、训练好的权重和一堆脚本。老实说我在实际交付过两次路面裂缝检测识别系统后最深刻的体会是真正决定项目成败的不是用哪个 SOTA 模型而是数据、模型、打包部署这三个环节能不能串起来。这篇就按我自己的落地顺序讲清楚路面裂缝检测识别系统从数据准备到 zip 包交付的完整设计路径适合正在做道路巡检项目、课程设计或者接手这类压缩包的工程师。2. 路面裂缝检测识别系统的数据链路模块拆分与掩膜数据集准备任何一个检测识别系统都绕不开“采集、预处理、检测、识别、输出”这条链路。我一般会先把模块边界画清楚再定数据格式否则后续代码改起来非常痛苦。尤其是路面裂缝这种纹理目标背景里的一点阴影、水渍都会影响结果所以数据环节常常比模型本身更花时间。2.1 系统模块拆分采集端、检测端、识别端与输出端先看一张我常用的模块分工表它能把“检测”和“识别”这两个经常混淆的概念拆开模块输入输出常用工具采集端路面照片/视频帧单帧图像、拍摄位置工业相机、车载相机、无人机预处理端原始图像增强灰度图OpenCV、PIL检测端预处理图像裂缝二值掩膜Canny、U-Net、DeepLabV3识别端裂缝掩膜裂缝类型、长度、宽度连通域分析、骨架化算法输出端结构化结果JSON/CSV 报告、标注图Pandas、FastAPI、ReportLab检测端只回答“哪些像素是裂缝”识别端回答“这条缝是横向、纵向还是龟裂”。把这两步分开后续如果只增加新的裂缝类型不需要重新训练分割模型只要在识别端加规则或分类器。近景手持拍摄和无人机俯拍的视场角差异很大建议统一缩放到模型输入尺寸避免焦点偏移。2.2 数据集选择与标注格式公开集和自标注的取舍公开的路面裂缝数据集有不少我在普通道路场景下用得比较多的是这几个数据集图片数标注方式特点Crack500500 张像素级掩膜背景杂含车道线和阴影DeepCrack537 张像素级掩膜裂缝边缘清晰CFD118 张像素级掩膜城市道路裂缝细如果只做分割演示Crack500 够用了但要输出裂缝类型公开数据集大多只提供二值掩膜没有“横向 / 纵向 / 龟裂”标签。这时候就要自建数据用 LabelMe 画多边形再用脚本转成掩膜。我一般用下面的函数处理 JSON 标注import json import numpy as np import cv2 def labelme_to_mask(json_path, target_size): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] mask np.zeros((img_h, img_w), dtypenp.uint8) for shape in data[shapes]: points np.array(shape[points], dtypenp.int32) if shape[label] in (crack, fracture): cv2.fillPoly(mask, [points], 1) if target_size: mask cv2.resize(mask, target_size, interpolationcv2.INTER_NEAREST) return mask这段代码先按原图宽高建空白掩膜再把标注为crack或fracture的多边形填充为 1。改变尺寸时必须使用INTER_NEAREST否则会让裂缝边缘出现介于 0 和 1 之间的模糊值。target_size建议和模型输入一致比如(512, 512)。还要注意标注人员的习惯有人写“裂缝”有人写“crack”最好在代码里做一个标签映射避免fillPoly把类别漏掉。2.3 图像预处理与数据增强降低光照干扰的关键步骤路面图片最常见的干扰是光照不均、树叶阴影和积水反光。我先把预处理固定成一个可复用函数训练和推理都调用它避免两边参数不一致import cv2 import numpy as np def preprocess(img, gamma1.2, clip_limit2.0, grid(8, 8)): if len(img.shape) 3: gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) else: gray img clahe cv2.createCLAHE(clipLimitclip_limit, tileGridSizegrid) gray clahe.apply(gray) table np.array([(i / 255.0) ** gamma * 255 for i in range(256)], dtypenp.uint8) return cv2.LUT(gray, table)CLAHE 在局部小块内做直方图均衡能压住“一边亮一边暗”的问题Gamma 校正则进一步调整亮度分布gamma1.2时会让暗部细节更明显。clip_limit太高会连路面颗粒都增强出来我一般从 2.0 起步根据验证集情况微调。数据增强方面水平翻转、旋转 ±15° 和轻微模糊都比较安全。裂缝是细长目标随机裁剪很容易把缝截成两段因此不要用太强的透视变换。可以用albumentations配置HorizontalFlip(p0.5)、Rotate(limit15)。这些变换会生成更多训练样本但要保证测试阶段不使用否则评估结果会虚高。提示预处理和增强的参数必须在训练、验证、推理三处保持一致否则线上效果的落差很容易被误判为模型过拟合。3. 裂缝检测识别算法怎么选传统CV、U-Net与超参数取舍数据准备好后才算真正进入算法选型。很多人觉得 2025 年了直接上深度学习就行但传统视觉方案在简单、均匀光照场景下速度极快仍然是轻量巡检设备的首选。深度学习适合背景复杂、裂缝细小、需要定量分析的任务。走哪条线取决于算力和部署环境。3.1 传统视觉路线Canny 形态学 连通域分析传统方法的核心是把裂缝看成“边缘响应的连续区域”。下面是一个可以直接跑的流程import cv2 import numpy as np def detect_canny(gray, sigma0.33, min_area50): v np.median(gray) lower int(max(0, (1.0 - sigma) * v)) upper int(min(255, (1.0 sigma) * v)) edges cv2.Canny(gray, lower, upper) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) edges cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel, iterations2) contours, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) result [] for cnt in contours: area cv2.contourArea(cnt) if area min_area: x, y, w, h cv2.boundingRect(cnt) result.append((x, y, w, h)) return edges, resultCanny 的高低阈值用“图像中位数 × sigma”计算这样不需要对每张图手调阈值闭运算可以把中间断掉的裂缝边缘连起来。min_area50是为了过滤路面颗粒带来的小边缘。这个方法最怕两类干扰车道线和积水反光它们的边缘响应同样强一旦背景复杂误检率会直线上升。如果只做传统方案识别类型可以用外接矩形长宽比宽高比大于 2 判为横向裂缝小于 0.5 判为纵向裂缝面积大且方向杂乱判为龟裂。这个规则很粗糙但作为兜底方案足够也能给深度学习输出做后处理参考。3.2 深度学习路线U-Net 与轻量化编码器选择路面裂缝形状细长、背景占比大用目标检测框会框进太多噪声分割模型更合适。U-Net 的下采样部分提取语义上采样部分恢复边缘跳跃连接把浅层细节传到深层。下面是一个轻量化版本import torch import torch.nn as nn class DoubleConv(nn.Module): 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 UNetSmall(nn.Module): def __init__(self, in_channels1): super().__init__() self.d1 DoubleConv(in_channels, 32) self.d2 DoubleConv(32, 64) self.d3 DoubleConv(64, 128) self.pool nn.MaxPool2d(2) self.up2 nn.ConvTranspose2d(128, 64, 2, stride2) self.d4 DoubleConv(128, 64) self.up3 nn.ConvTranspose2d(64, 32, 2, stride2) self.d5 DoubleConv(64, 32) self.out nn.Conv2d(32, 1, 1) def forward(self, x): p1 self.d1(x) p2 self.d2(self.pool(p1)) p3 self.d3(self.pool(p2)) x self.up2(p3) x self.d4(torch.cat([x, p2], dim1)) x self.up3(x) x self.d5(torch.cat([x, p1], dim1)) return torch.sigmoid(self.out(x))这个模型的初始通道是 32比经典 U-Net 小一半参数量更少适合 CPU 推理。in_channels1对应灰度图如果要用彩色图改为 3但第一层卷积参数会变多。注意torch.cat的维度x经过上采样后和跳跃连接的特征图分辨率必须一致否则会报尺寸不匹配。输出经过sigmoid后是 0 到 1 的概率图后处理用 0.5 作为阈值转成掩膜。3.3 损失函数与基础超参数训练裂缝分割模型的经验值裂缝像素占比经常不到 5%直接用 BCE 会让模型倾向于预测背景。我一般用 DiceLoss 加 BCE 的组合下面是我常用的一组训练参数超参数推荐值说明输入尺寸512 × 512太小裂缝断裂太大显存不够batch size8 ~ 16显存不足时配合梯度累积学习率1e-4AdamW 起步值epochs80 ~ 120用早停防止过拟合损失函数DiceLoss BCE权重比 0.5 / 0.5DiceLoss 的实现很直接import torch import torch.nn as nn class DiceLoss(nn.Module): def __init__(self, smooth1.0): super().__init__() self.smooth smooth def forward(self, pred, target): pred pred.view(pred.size(0), -1) target target.view(target.size(0), -1) intersection (pred * target).sum(dim1) dice (2.0 * intersection self.smooth) / (pred.sum(dim1) target.sum(dim1) self.smooth) return 1.0 - dice.mean()smooth是平滑项防止某一张图完全没有裂缝时分母为 0。DiceLoss 对裂缝这种极度不平衡的分割任务非常友好因为它是按类别重叠比例计算的而不是逐像素独立计算。训练时优化器用 AdamW 会比 Adam 更稳weight decay 设 1e-4。如果发现训练集 IoU 很高但验证集下降优先检查预处理是否一致而不是换更大模型。4. 路面裂缝检测识别系统的训练、评估与zip打包模型代码只占整个系统的一小部分剩下的是训练、评估、接口和打包。把实验产物变成别人能跑的 zip 包需要一套完整的流程。4.1 评估指标IoU、F1、裂缝宽度误差怎么算分割模型的排序指标通常看 IoU 和 F1但工程落地还要看裂缝宽度的计算误差。指标计算方式或者用途IoU预测掩膜与真实掩膜的交集比并集衡量空间重叠度F1Precision 和 Recall 的调和平均类别不平衡时更直观裂缝宽度误差掩膜骨架化后计算像素宽度再乘像素分辨率不要只报 IoU因为预测区域稍微偏移一点IoU 就会明显变化但道路养护业务更关心“这条缝到底有多宽”。宽度计算的常见做法是对掩膜做骨架化然后求每个骨架点到最近非零边缘的距离再取平均值乘分辨率。这个结果和人工用塞尺测量的数据对比才能判断系统是否满足检测精度要求。4.2 训练脚本与验证流程早停、保存最佳权重下面是最基础的训练循环只保留了核心步骤import torch import torch.nn as nn def train_one_epoch(model, loader, opt, criterion, device): model.train() total_loss 0 for imgs, masks in loader: imgs, masks imgs.to(device), masks.to(device) opt.zero_grad() pred model(imgs) loss criterion(pred, masks) loss.backward() opt.step() total_loss loss.item() * imgs.size(0) return total_loss / len(loader.dataset)实际训练时建议加torch.cuda.amp.autocast做混合精度减小显存占用。criterion可以直接传bce_loss dice_loss两者权重各 0.5。验证阶段计算验证集 IoU只在 IoU 提升时保存best_model.pth不要每次都保存最后 epoch 的权重。保存模型时最好同时把input_size、in_channels、预处理参数一起存成 JSON后面转 ONNX 或者写推理接口时不用回去翻训练代码。4.3 把模型封装成 CLI 和 API直接可运行的输入输出模型训练完第一步不是写界面而是先做成命令行工具方便巡检员跑单张图片再包装一个 API接入批量检测平台。CLI 示例import argparse import cv2 import torch def predict_single(model, img_path, devicecpu): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (512, 512)) / 255.0 img torch.FloatTensor(img).unsqueeze(0).unsqueeze(0) with torch.no_grad(): pred model(img.to(device)).squeeze().cpu().numpy() return (pred 0.5).astype(uint8) if __name__ __main__: parser argparse.ArgumentParser(descriptionCrack segmentation CLI) parser.add_argument(--img, typestr, requiredTrue) parser.add_argument(--weight, typestr, defaultbest_model.pth) args parser.parse_args() model UNetSmall(in_channels1) model.load_state_dict(torch.load(args.weight, map_locationcpu)) model.eval() seg predict_single(model, args.img) cv2.imwrite(args.img[:-4] _mask.png, seg * 255)--img是输入图片路径--weight是权重路径。注意这里的resize必须和训练时一致否则模型看到的分辨率不同准确率会下降。如果训练时做了 CLAHE 和 Gamma 校正推理时也要先调用同一个预处理函数。API 我一般用 FastAPI 写只要把predict_single封装进一个POST /predict接口上传图片后返回掩膜或者 base64 字符串即可。CLI 和 API 共用同一个预测函数可以保证离线批量和在线请求的结果一致。4.4 zip包目录设计与交付文档、权重、代码三件套打包不是简单的右键压缩目录设计直接影响使用者的上手速度。我通常把交付包整理成下面这样的结构project_root/ ├── docs/ │ ├── 系统设计说明.docx │ └── 使用手册.md ├── weights/ │ └── best_model.pth ├── src/ │ ├── preprocess.py │ ├── model.py │ ├── predict_cli.py │ └── api_server.py ├── requirements.txt ├── environment.yml └── README.md不建议把整个原始数据集放进去放 10 到 20 张示例图就够否则包体积会到几个 GB。requirements.txt要固定版本至少列出torch、opencv-python、fastapi、uvicorn。用 Python 打包的好处是可以在不同平台复用而且能排除指定目录import zipfile from pathlib import Path def make_zip(src_dir, out_zip, exclude_dirs(data/train, .git)): src Path(src_dir) with zipfile.ZipFile(out_zip, w, zipfile.ZIP_DEFLATED) as z: for f in sorted(src.rglob(*)): if f.is_dir(): continue rel f.relative_to(src) if any(rel.parts[:len(Path(d).parts)] Path(d).parts for d in exclude_dirs if d): continue z.write(f, arcnamerel)exclude_dirs里写的是相对项目根目录的路径rel.parts会按路径前缀匹配命中就跳过。ZIP_DEFLATED是默认压缩方式对代码和文档有效但对模型文件作用很小下一章会细说。提示README 里一定要写清三步安装依赖、用 CLI 预测、启动 API。否则 zip 包发过去使用者第一反应还是来问你“怎么运行”。5. 交付前必查zip包里的模型、依赖与运行脚本陷阱最后这部分是我被问得最多的场景也是 zip 包交付最容易翻车的地方。5.1 解压路径报错绝对路径、相对路径与中文目录很多解压后运行报错问题都不是模型而是代码里写死了C:/Users/xxx/...或者权重路径带了中文目录。修复方式是把所有路径改成基于脚本位置的相对路径from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent WEIGHT BASE_DIR / weights / best_model.pth__file__是当前 Python 文件的位置resolve()会去掉符号链接parent.parent返回项目根目录。这样 zip 解压到任何位置模型路径都跟着脚本走不会出现“本地能跑换台机器就崩”的问题。5.2 模型文件大、压缩慢怎么办.pth、.onnx这类权重文件内部已经是紧凑的二进制表示用ZIP_DEFLATED再压一遍不仅慢体积也几乎不变。遇到这种文件我建议在打包时直接用ZIP_STORED只存储不压缩还能保留 CRC 校验if f.suffix in (.pth, .onnx, .pt): z.write(f, arcnamerel, compress_typezipfile.ZIP_STORED) else: z.write(f, arcnamerel, compress_typezipfile.ZIP_DEFLATED)这样整个包生成速度快很多模型文件也没有损压缩带来的额外开销。如果网络传输平台限制了单包大小可以把模型单独分卷压缩再放到另一个目录但一定要在 README 里写清楚合并步骤。5.3 用sha256校验包完整性避免 could not find eocd“error read zip archive”和“invalid zip archive: could not find eocd”是我看到最多的解压报错本质是文件不完整或者传输损坏。发送包之前先算一个校验值sha256sum ../system_design.zip SHA256SUMS接收方解压前可以快速用 Python 校验不占太多内存import hashlib import sys def sha256_ok(file_path, expected): h hashlib.sha256() with open(file_path, rb) as f: for chunk in iter(lambda: f.read(1 20), b): h.update(chunk) return h.hexdigest() expected if __name__ __main__: print(sha256_ok(sys.argv[1], sys.argv[2]))1 20表示每次读 1MB分块读取避免大文件把内存占满。把这段脚本放进tools/目录下次解压前先跑一下能省掉很多来回确认的沟通成本。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进