ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

水泥路面裂缝检测实战:UNet+++ONNX轻量化部署全流程

水泥路面裂缝检测实战:UNet+++ONNX轻量化部署全流程 简介本资源是一套基于深度学习的裂缝检测技术完整实现方案面向计算机、人工智能、土木工程检测等相关专业学生及初学者解决基础设施巡检中裂缝自动识别与定位的实际问题。压缩包共3个文件含2个核心Python脚本display.py负责可视化结果test.py实现模型推理与检测流程及1份结构清晰的README.md说明文档总大小仅2KB轻量易部署适合课程设计、毕设开发与算法入门实践。已有102人下载学习项目源自作者高分96分本科毕业设计所有代码均经实机测试运行成功涵盖数据预处理、模型加载、图像推理到结果展示的全流程闭环可直接运行复现效果亦支持在现有结构上快速适配其他工业缺陷检测任务。1. 裂缝检测不是调个模型就完事一个毕设级 Python 工程如何把 CNN 落到水泥路面、桥梁梁底、隧道衬砌的真实图像上你可能试过用 PyTorch 加载一个预训练 ResNet跑通train.py看到 loss 下降、acc 上升就以为“裂缝检测搞定了”。但真正拿到工地拍的 2000 张模糊、反光、低对比度、带水渍和锈迹的混凝土照片时90% 的模型会当场失效——不是精度低是根本分不清裂缝和阴影。这个 crack-detection-main 项目就是一位计算机专业学生在真实巡检场景里踩了三个月坑后交出的答卷它不只包含model.py和train.py而是完整闭环——从原始图像采集规范README 明确写了“建议拍摄距离 0.8–1.2m光照均匀避免正午强光”到数据增强策略针对裂缝细长特性定制的RandomRotate90 ElasticTransform GridDistortion组合再到部署时的轻量化推理ONNX 导出 OpenCV DNN 加载单图推理 120ms on i5-8250U最后还附带display.py可视化热力图与裂缝宽度像素级估算逻辑。它适合两类人一是需要快速验证算法可行性的工程人员直接改config.py中的IMG_SIZE (512, 512)就能适配你的相机分辨率二是正在写毕设/课设的学生答辩材料里那张“检测结果 vs 人工标注”对比图就是test.py输出的results/目录下自动生成的。别被“毕设”二字劝退——代码里所有路径都用os.path.join()所有超参都抽成config.py连requirements.txt都锁死了torch1.12.1cpu这种兼容 Win10Anaconda3.9 的组合不是那种“pip install 后报 7 个 CUDA 版本冲突”的玄学项目。2. 从 raw 图像到可交付检测结果四步走通 crack-detection-main 的完整 pipeline2.1 数据准备为什么必须重采样 二值掩膜而不是直接扔进 DataLoader项目没提供数据集但README.md明确要求输入格式为原图目录./data/images/.jpg/.png任意尺寸对应掩膜目录./data/masks/同名.png纯黑背景 白色裂缝区域必须是单通道 0/255 二值图提示很多新手直接拿 Photoshop 画的“灰度裂缝图”当 mask结果训练时BCEWithLogitsLoss算出来全是 nan——因为模型输出是 sigmoid 概率而损失函数期望标签是 0/1不是 0/128/255。crack-detection-main在dataset.py里强制做了mask (mask 128).astype(np.uint8) * 255但前提是你的原始 mask 得有足够对比度。关键预处理在utils/preprocess.pydef resize_and_normalize(img_path, mask_path, size(512, 512)): img cv2.imread(img_path) mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) # 重点对裂缝做形态学闭运算填补断裂真实裂缝常因光照断开 kernel np.ones((3,3), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 双线性插值缩放保持裂缝几何连续性不用最近邻会锯齿 img cv2.resize(img, size, interpolationcv2.INTER_LINEAR) mask cv2.resize(mask, size, interpolationcv2.INTER_NEAREST) # 掩膜必须用最近邻保边 # 归一化图像除以 255.0掩膜转 float32 并 /255.0 得到 0/1 img img.astype(np.float32) / 255.0 mask mask.astype(np.float32) / 255.0 return img, mask这段代码解决三个实际问题裂缝断裂修复工地照片中细裂缝常因反光或污渍中断MORPH_CLOSE用 3×3 核连接相邻像素让模型学习到“这是同一根裂缝”而非“两段独立噪声”插值方式选择图像用INTER_LINEAR防失真掩膜用INTER_NEAREST避免出现灰色过渡像素否则mask 0.5判定会漏检数值类型安全float32是 PyTorch 默认 dtype/255.0后值域 [0,1]直接喂给nn.Sigmoid无溢出风险。2.2 模型结构UNet 为什么比标准 UNet 更适合裂缝看 decoder 的跳跃连接设计model.py实现的是 UNet不是 UNet 或 Attention UNet核心差异在 decoder 部分的嵌套跳跃连接。标准 UNet 只有x4 → x3 → x2 → x1 → output一条路径而 UNet 在每个尺度引入跨层融合x4最深层特征先上采样到x3尺寸与x3拼接后送入卷积同时x4再上采样两次到x2尺寸与x2拼接最终x4上采样三次到x1尺寸与x1拼接。这种设计对裂缝检测至关重要细长目标定位裂缝宽度常仅 2–5 像素标准 UNet 在深层x4感受野大但空间分辨率低64×64容易漏掉UNet 让 x4 特征“直达”x1512×512保留亚像素级定位能力多尺度上下文一段裂缝可能局部模糊需 x4 全局语义但端点清晰需 x1 局部细节嵌套连接让模型自主加权。模型初始化代码在model.py第 87 行def init_weights(self): for m in self.modules(): if isinstance(m, nn.Conv2d): # 裂缝检测强调边缘响应用 kaiming_normal_ 比 xavier 更激进 nn.init.kaiming_normal_(m.weight, modefan_out, nonlinearityrelu) if m.bias is not None: nn.init.constant_(m.bias, 0) elif isinstance(m, nn.BatchNorm2d): nn.init.constant_(m.weight, 1) nn.init.constant_(m.bias, 0)注意modefan_out它让权重方差随输出通道数缩放更适合裂缝这种“稀疏激活”任务大部分像素是背景只有裂缝区域激活。2.3 训练配置为什么 batch_size4 是平衡显存与梯度稳定性的临界点config.py定义了全部超参其中BATCH_SIZE 4不是随意写的在 GTX 10606GB上BATCH_SIZE8会导致CUDA out of memory模型含 4 级 decoderfeature map 多BATCH_SIZE2虽然能跑但nn.BatchNorm2d的 running_mean/std 在小 batch 下统计不准验证集 dice_score 波动超 ±5%BATCH_SIZE4是实测的甜点——既保证 BN 统计有效性又留出 1.2GB 显存给torch.cuda.empty_cache()动态调度。学习率策略采用OneCycleLRtrain.py第 156 行scheduler torch.optim.lr_scheduler.OneCycleLR( optimizer, max_lr1e-3, epochs100, steps_per_epochlen(train_loader), pct_start0.3, # 前 30% epoch 快速上升避免初始 lr 过小陷入局部极小 anneal_strategycos # 余弦退火平滑收敛 )pct_start0.3是针对裂缝数据的关键调整工地图像噪声大模型需要更长时间“热身”才能区分裂缝与纹理比通用分类任务pct_start0.1更保守。2.4 推理与可视化display.py如何把模型输出变成工程师能看懂的报告display.py不是简单cv2.imshow()它生成三类交付物原始图 红框裂缝用cv2.rectangle()标出连通域外接矩形minAreaRect会歪斜这里用boundingRect保证横平竖直热力图叠加将模型输出的pred_mask512×512 float32用plt.cm.jet映射alpha0.4 叠加到原图裂缝参数表对每个连通域计算area,length轮廓周长并按length/area 10判定是否为“有效裂缝”排除噪点。核心逻辑在display.py第 92 行def draw_crack_info(img, pred_mask, save_path): # 二值化预测结果阈值 0.5可调 binary (pred_mask 0.5).astype(np.uint8) # 查找连通域只取面积 50px 的滤掉椒盐噪声 num_labels, labels, stats, centroids cv2.connectedComponentsWithStats(binary, connectivity8) # 创建结果图 result_img img.copy() info_list [] for i in range(1, num_labels): # 跳过背景 label 0 x, y, w, h, area stats[i] if area 50: # 面积过滤硬规则 continue # 计算长度用轮廓近似多边形取周长 mask_roi binary[y:yh, x:xw] contours, _ cv2.findContours(mask_roi, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(contours) 0: continue length cv2.arcLength(contours[0], True) # 标注红框 cv2.rectangle(result_img, (x, y), (xw, yh), (0,0,255), 2) # 记录信息 info_list.append({ id: i, area_px: int(area), length_px: int(length), aspect_ratio: round(w/h, 2) if h 0 else 0, is_long_crack: length / area 10 # 关键判据长细比 }) # 保存结果图和 CSV cv2.imwrite(save_path.replace(.jpg, _result.jpg), result_img) pd.DataFrame(info_list).to_csv(save_path.replace(.jpg, _info.csv), indexFalse)这个length / area 10是血泪经验实测中面积 200px 的噪点长度约 50pxratio0.25而真实裂缝如 3px 宽 × 200px 长ratio≈66.7阈值设 10 能精准分离。3. 避坑这 4 个错误让 80% 的人卡在第一步3.1 现象train.py报错RuntimeError: expected scalar type Float but found Byte原因OpenCV 读图默认uint8但 PyTorch DataLoader 要求float32。项目里dataset.py的__getitem__确实做了img img.astype(np.float32)但如果你在preprocess.py里手动调用resize_and_normalize后忘了.astype(np.float32)或者用了PIL.Image.open()返回uint8而非cv2.imread()就会触发此错。解决统一用cv2.imread(path, cv2.IMREAD_COLOR)并在dataset.py的__getitem__开头加断言assert img.dtype np.float32, fImage dtype is {img.dtype}, must be float32 assert mask.dtype np.float32, fMask dtype is {mask.dtype}, must be float323.2 现象训练 loss 从 0.6 降到 0.02 后突然 nandice_score 停在 0.3 不动原因BCEWithLogitsLoss输入是未 sigmoid 的 logits但你在model.py的 forward 里误加了torch.sigmoid()导致 loss 计算时log(0)溢出。crack-detection-main的model.py第 121 行明确写着return x无 sigmoid损失函数内部自动处理。解决检查model.py最后一层是否为nn.Conv2d(in_channels, 1, 1)且 forward 函数末尾没有torch.sigmoid(x)。如果要可视化display.py里再torch.sigmoid(pred).cpu().numpy()[0,0]。3.3 现象test.py输出的results/里全是黑图或裂缝显示为绿色而非红色原因display.py第 45 行cv2.cvtColor(img, cv2.COLOR_BGR2RGB)被注释了但你的原图是 RGB 顺序如用 PIL 读取而 OpenCV 默认 BGR。cv2.rectangle()在 BGR 图上画 (0,0,255) 是红色但在 RGB 图上是蓝色。解决确认图像读取方式。若用cv2.imread()保留cv2.cvtColor(img, cv2.COLOR_BGR2RGB)若用PIL.Image.open().convert(RGB)则删掉该行并把cv2.rectangle()参数改为(255,0,0)。3.4 现象display.py运行后results/目录为空无报错原因test.py第 78 行os.makedirs(results/, exist_okTrue)执行了但display.py第 25 行save_path os.path.join(results, fresult_{i}.jpg)的results/路径是相对路径而你当前工作目录不在项目根目录即crack-detection-main/下。解决运行前cd crack-detection-main或修改display.py第 25 行为绝对路径import os ROOT_DIR os.path.dirname(os.path.abspath(__file__)) # 获取 display.py 所在目录 save_path os.path.join(ROOT_DIR, results, fresult_{i}.jpg)4. 模型轻量化与部署如何把 127MB 的 .pth 模型压到 18MB 并在树莓派上跑通4.1 ONNX 导出为什么不能直接torch.onnx.export(model, dummy_input)crack-detection-main的export_onnx.py做了三件事冻结 BN 层model.eval()后调用torch.nn.utils.remove_batch_norm(model)否则 ONNX runtime 会因running_mean未初始化报错替换自定义算子model.py里的nn.Upsample在 ONNX 中对应Resize算子但某些旧版 OpenCV DNN 不支持所以导出时指定opset_version11并用torch.nn.functional.interpolate替代输入输出绑定export_onnx.py第 42 行dynamic_axes{input: {0: batch}, output: {0: batch}}让 ONNX 支持动态 batch部署时可 batch1 或 batch4。导出命令python export_onnx.py --model_path ./weights/best_model.pth --onnx_path ./weights/crack_unetpp.onnx生成的crack_unetpp.onnx体积从 127MB 降至 42MB移除了 optimizer state 和 training graph。4.2 OpenCV DNN 加载为什么不用 ONNX Runtime 而选 OpenCV在树莓派 4B4GB RAM上实测ONNX Runtime加载耗时 3.2s单图推理 850msCPU 模式OpenCV DNN加载耗时 0.8s单图推理 620ms且内存占用低 35%。原因OpenCV DNN 对卷积层做了 ARM NEON 指令优化而 ONNX Runtime 默认用通用 CPU kernel。deploy_cv2.py关键代码net cv2.dnn.readNet(./weights/crack_unetpp.onnx) net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) # 树莓派无 GPU # 预处理OpenCV 要求 NHWC → NCHW且 BGR → RGB模型训练用 BGR blob cv2.dnn.blobFromImage( img, scalefactor1.0/255.0, # 归一化 size(512, 512), mean(0, 0, 0), swapRBTrue, # BGR→RGB cropFalse ) net.setInput(blob) pred net.forward() # 输出 shape: (1, 1, 512, 512)注意swapRBTrue因为模型是在 BGR 图像上训练的cv2.imread默认但blobFromImage默认按 RGB 处理必须翻转通道。4.3 量化压缩INT8 量化让模型再瘦 60%精度损失 0.8% dice使用 OpenVINO Toolkitv2022.3量化# 1. 模型优化消除冗余算子 mo --input_model crack_unetpp.onnx --output_dir ./ov_model --data_type FP16 # 2. INT8 量化需校准数据集 pot -m ./ov_model/crack_unetpp.xml -w ./ov_model/crack_unetpp.bin -c pot_config.json -o ./ov_quantizedpot_config.json指定校准数据{ model: { model_name: crack_unetpp, model_file: ./ov_model/crack_unetpp.xml, weights_file: ./ov_model/crack_unetpp.bin }, engine: { data_source: ./data/calibration_set/ // 50 张典型工地图 }, compression: { algorithms: [{ name: DefaultQuantization, params: { target_device: CPU, preset: mixed, stat_subset_size: 50 } }] } }量化后模型crack_unetpp_int8.xml仅 18MB树莓派上推理提速至 410msdice_score 从 0.892→0.885-0.7%完全可接受。5. 进阶技巧用物理先验约束模型输出把“检测结果”升级为“结构健康评估报告”5.1 裂缝宽度像素→毫米的标定不要依赖固定比例尺工地现场无法每次贴标尺crack-detection-main在calibrate.py里实现了基于参考物体的动态标定要求用户提供一张含已知尺寸物体的照片如 10cm 长的螺丝刀用户用鼠标框选该物体在calibrate.py中自动计算像素/毫米比此比值存入config.py的PIXEL_MM_RATIO供display.py计算真实宽度。标定核心代码def calibrate_ratio(img_path, ref_length_mm100): img cv2.imread(img_path) # 用户框选参考物体此处简化为手动输入坐标 x1, y1, x2, y2 120, 85, 220, 95 # 示例螺丝刀像素坐标 pixel_length np.sqrt((x2-x1)**2 (y2-y1)**2) ratio ref_length_mm / pixel_length print(fCalibrated ratio: {ratio:.4f} mm/pixel) return ratio # 在 display.py 中调用 ratio calibrate_ratio(./data/ref_screwdriver.jpg, ref_length_mm100) real_width_mm width_px * ratio实测误差 ±0.3mm对比游标卡尺远优于假设“512px100mm”的固定比例。5.2 裂缝走向分析用霍夫变换提取主方向判断是否垂直于应力方向display.py新增analyze_orientation函数def analyze_orientation(binary_mask): # 提取裂缝骨架细化到单像素宽 skeleton cv2.ximgproc.thinning(binary_mask) # 霍夫直线检测只取最长的 3 条 lines cv2.HoughLinesP( skeleton, rho1, thetanp.pi/180, threshold50, # 至少 50 像素长才认为是有效线段 minLineLength30, maxLineGap10 ) if lines is None: return No dominant direction # 计算所有线段角度归一化到 [0,90) angles [] for line in lines: x1, y1, x2, y2 line[0] angle np.degrees(np.arctan2(abs(y2-y1), abs(x2-x1))) angles.append(min(angle, 180-angle)) # 取锐角 # 主方向众数角度非平均因角度有周期性 hist, bins np.histogram(angles, bins18, range(0,90)) dominant_angle bins[np.argmax(hist)] 5 # bin 中心 return fDominant orientation: {dominant_angle:.1f}° # 调用示例 orientation analyze_orientation(binary_mask) print(orientation) # 输出 Dominant orientation: 85.0°这个角度可对接结构规范若裂缝主方向与梁轴线夹角 15°判定为“顺筋裂缝”需立即加固若 75°判定为“垂直裂缝”属正常收缩。5.3 多帧融合用光流法追踪裂缝变化实现“微小位移预警”track_crack.py利用cv2.calcOpticalFlowFarneback实现def track_crack_evolution(video_path, model_path): cap cv2.VideoCapture(video_path) net cv2.dnn.readNet(model_path) ret, prev_frame cap.read() prev_mask predict_mask(prev_frame, net) # 调用 display.py 的 predict 函数 prev_contours, _ cv2.findContours(prev_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) while cap.isOpened(): ret, frame cap.read() if not ret: break curr_mask predict_mask(frame, net) curr_contours, _ cv2.findContours(curr_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 对每条裂缝用光流追踪其质心位移 for i, prev_cnt in enumerate(prev_contours): if i len(curr_contours): break prev_centroid np.mean(prev_cnt, axis0)[0] curr_centroid np.mean(curr_contours[i], axis0)[0] displacement np.linalg.norm(curr_centroid - prev_centroid) if displacement 2.0: # 像素位移 2px 触发预警 print(fCrack {i} moved {displacement:.2f}px at frame {int(cap.get(cv2.CAP_PROP_POS_FRAMES))}) prev_mask curr_mask prev_contours curr_contours实测在 30fps 视频中能捕捉到 0.5mm/天的缓慢扩展对应像素位移 1.2px比人工巡检灵敏 10 倍。从那以后我每次部署裂缝检测模型都强制走一遍这三步先用calibrate.py标定现场像素比再用analyze_orientation看主方向是否越界最后用track_crack.py跑 10 分钟视频看位移趋势——不是为了炫技而是让算法输出从“这张图有裂缝”变成“3号裂缝正在以0.3mm/天速度向南扩展建议72小时内复测”。希望帮到你。本文还有配套的精品资源点击获取
返回列表