资讯动态

YOLOv5s+BiFPN隧道裂缝检测实战:特征融合优化与部署全流程解析

发布时间:2026/9/19 7:30:53 来源:尧图企业网站定制
接隧道检测项目那阵子我先把市面上能跑的目标检测模型都过了一遍最后定下YOLOv5s加BiFPN的组合。原因很简单现场要求能在工控机或嵌入式设备上跑实时推理不能动不动就要A100但隧道里的裂缝又偏偏是那种又细又长、和背景融在一起的小目标只靠YOLOv5s默认的PANet特征融合漏检率高得没法看。这篇文章就把整个项目从数据准备、模型改造到落地部署的过程完整拆给你看包括数据集怎么找、怎么标、怎么排坑。适合正在做隧道结构检测、路面病害检测或者其他小目标检测项目的朋友参考。先说结论YOLOv5sBIFPN这套组合在隧道裂缝检测里确实比原版YOLOv5s的PANet要稳mAP0.5能提升3到5个百分点速度损失可以接受。但真正决定项目成败的不是模型结构而是数据集。裂缝样本的采集、筛选和标注占了整个项目七成以上的工作量。下面按项目推进的顺序写。1. 隧道裂缝检测的痛点为什么这个任务没那么简单1.1 隧道现场环境与裂缝形态的特殊性隧道裂缝和普通路面裂缝、墙体裂缝最大的区别在于环境极度不友好。首先是光照隧道内部几乎只有人工光源很多区段光照不均匀中间亮、边墙暗裂缝在暗部的对比度极低。其次是背景干扰隧道内壁不是干净的混凝土而是带着模板缝、施工冷缝、渗水痕迹、锚杆孔、管线支架、电缆沟槽等各种纹理这些背景特征和裂缝在视觉上经常高度相似。再说裂缝本身的形态。隧道裂缝通常是细长条宽度可能只有3到5个像素长度却有几十甚至上百像素。用目标检测的水平框去框它框里大部分区域都是背景这会严重干扰模型学习。而且裂缝形态变化很大有横向的、纵向的、网状的还有沿着施工缝边界开裂的训练阶段如果数据分布不全泛化性就会很差。光照不均夜间采集的隧道图像经常出现局部过曝或过暗背景噪声多渗水、污渍、钢筋外露、模板纹理都可能被误判目标尺寸极端长宽比大、像素占比小属于典型的小目标这些因素叠加在一起决定了隧道裂缝检测不能照搬通用目标检测的思路。很多通用模型在城市道路裂缝数据集上表现不错一到隧道现场就严重掉点原因就在数据分布差异上。1.2 传统巡检手段的局限与YOLOv5s的机会传统隧道裂缝检测主要靠人工目检检测人员搭着平台车或升降车一寸一寸地看效率低、主观性强还容易漏检。后来有了探地雷达、超声波、红外热成像这些无损检测仪器虽然能发现内部缺陷但对表面裂缝的定位和尺寸评估仍然不够直观而且设备贵、操作门槛高。深度学习目标检测模型切入这个场景后最大的优势是可以把高清相机采集的隧道衬砌图像直接喂进去端到端输出裂缝的边界框和类别。和图像分割相比目标检测不需要逐像素标注标注成本低很多工程落地也更简单。和传统Canny边缘检测、形态学处理相比深度模型对复杂背景的鲁棒性好得多能适应隧道内的多种光照和纹理条件。YOLOv5在工程界的普及度极高推理速度、部署生态、文档完整度都很成熟。但它默认的PANet特征融合对多尺度小目标的处理还有优化空间所以我没直接用它开跑而是先考虑怎么在不显著增加计算量的情况下把特征融合增强一下。BiFPN就是在这个背景下引入的。1.3 选型分析为什么是YOLOv5sBIFPN项目初期我对比过几个路线YOLOv8n、YOLOv5s、RT-DETR以及YOLOv5sPANet换BiFPN。RT-DETR精度确实高但部署到工控机上吞吐量吃紧而且DETR类模型在自定义数据集上需要更多调参经验。YOLOv8n虽然代码更现代化但对老设备的ONNX/TensorRT兼容性并不比YOLOv5好很多而且我想用到的很多社区脚本都是基于YOLOv5写的直接改会更快。YOLOv5s本身是一个非常好的baseline它把模型规模、速度和精度平衡得很合适。s版本参数量只有7.2M左右在1080Ti上能跑一百多FPS放到边缘盒子虽然慢一些但也可以通过TensorRT压缩到可用的水平。它的结构容易改common.py里换模块、yaml里改拓扑都很直观适合做二次开发。BIFPN选型的原因可以拆成两点。第一它比PANet多了一条跨层连接策略能够让高层语义信息更快地传递给底层特征这对细长小目标的定位是有帮助的。第二它对不同层特征融合时加了可学习的权重模型能自己判断哪一层的信息更重要而不是简单地把特征图相加。这两点正好对症隧道裂缝检测的需求既要保持小目标的细节信息又要利用高层语义排除背景误检。2. BIFPN是怎么补上YOLOv5s短板的2.1 PANet的特征融合到底差在哪YOLOv5的neck部分用的是PANet它是在FPN的基础上增加了一条自底向上的路径让浅层细节信息和深层语义信息能反复融合。这个设计在通用目标检测里已经很成熟了但用在隧道裂缝上有一个问题PANet的特征融合是无权重的直接相加或者拼接默认认为来自不同层的特征图对最终检测的贡献是相同的。实际上不是这样。隧道裂缝在顶层语义特征中可能只表现为一条边缘在底层细节特征中则保留比较明显的局部纹理。不同光照条件下哪些层的特征更有用是变化的。直接相加等于把所有层的信息一视同仁模型没法自适应地调整它们的重要性。尤其在对裂缝这种细长目标做回归时如果浅层细节特征被大量背景噪声淹没模型就很容易漏检。另一个问题是PANet的跨尺度连接不够灵活。它只连接相邻层要让高层信息传到低层需要经过多级传递路径过长信息损耗自然更大。BiFPN的思路就是缩短这个路径并且让融合权重变成可学习的参数。2.2 BiFPN的加权跨尺度融合机制BiFPN最早是EfficientDet提出来的全称是Bidirectional Feature Pyramid Network。它的核心贡献可以归纳为两点双向跨尺度连接和快速归一化加权融合。双向跨尺度连接通俗点说就是每一层不仅从上层拿信息也直接从更远的层拿信息同时去掉了一些只有一条边的节点减少不必要的计算路径。比如原始FPN的P3层要获得P5的语义信息需要先传到P4再传到P3BiFPN里P3可以直接和P4、P5建立融合关系信息传递路径更短。加权融合的公式也不复杂就是把不同输入特征按可学习的权重做加权平均。权重经过ReLU确保非负然后除以所有权重之和做归一化这样训练更稳定。公式可以写成输出 (w1 * 输入1 w2 * 输入2) / (w1 w2 ε)其中w1和w2是网络自动学习的参数ε是一个很小的常数防止除零。这个机制的好处是不再默认不同层特征贡献相同模型可以根据训练数据自动调整比如当光照暗的时候可能更依赖底层边缘特征当背景噪声多的时候更依赖高层语义特征。这种自适应能力对隧道裂缝这种环境多变的任务非常实用。2.3 在YOLOv5s代码里加一个BiFPN需要动哪几个文件我在YOLOv5 6.0版本上做的改动主要涉及三个文件models/common.py、models/yolo.py和模型配置文件。先在common.py里加一个加权融合模块。简化版的实现如下import torch import torch.nn as nn class BiFPN_Add(nn.Module): def __init__(self, c1, c2): super().__init__() self.conv Conv(c1, c2, 1) self.w nn.Parameter(torch.zeros(2), requires_gradTrue) self.epsilon 1e-4 def forward(self, x): x [self.conv(f) for f in x] w torch.relu(self.w) w w / (torch.sum(w) self.epsilon) return x[0] * w[0] x[1] * w[1]这个类接收两个特征图先用1x1卷积把通道数统一然后做加权相加。实际工程中如果要多输入融合可以把torch.zeros(2)改成根据输入数量动态创建但YOLOv5的neck通常每次只融合两个节点所以两输入版本已经够用。然后在models/yolo.py里注册这个模块让它能被yaml文件调用。找到parse_model函数中的if m in {...}判段把BiFPN_Add加进去。最后新建一个yolov5s_bifpn.yaml核心改动在head部分。原版PANet是用Concat把不同层的特征拼接起来这里改成用BiFPN_Add做加权融合同时去掉部分冗余的上采样和下采样节点。需要说明的是不同版本YOLOv5的head结构有细微差别改的时候不能光抄yaml要确保索引对应的层输出尺寸一致。我当时调试的时候犯过一个错只改了yaml没改yolo.py的注册逻辑结果跑模型直接报KeyError。所以提醒一句新增模块后一定要在parse_model里检查模块名称有没有对上这个错误很容易排查但也确实容易犯。3. 隧道裂缝数据集的获取与整理这是整个项目最容易翻车的地方3.1 公开数据集怎么选不要直接拿来就跑很多朋友一上来就会问隧道裂缝数据集去哪下载我用过的公开资源可以分几类。第一类是通用混凝土裂缝数据集比如Crack500、DeepCrack、CrackForestd等这些数据集主要面向语义分割其中Crack500有来自路面和墙面的裂缝图像数量多标注质量参差不齐。第二类是目标检测格式的裂缝数据集社区里有人转成COCO或YOLO格式后放在公开平台上这类用起来最省事但需要确认作者是否允许商用。第三类是隧道专项数据集比如部分高校和科研机构公开的隧道衬砌裂缝图像数量少但更贴合场景。我建议不要直接拿公开数据集开跑而是先做一次筛选。隧道裂缝图像的背景一般是灰暗的混凝土而通用道路裂缝数据集里很多是明亮的沥青路面两者特征分布差异很大。我当时的做法是计算每张图像的灰度均值和纹理复杂度把和隧道环境差异过大的图片剔除只保留室内混凝土墙、隧道洞口、隧道内部这类样本。筛选后公开数据可能只剩三分之一但训练效果比全量数据好得多。另外要注意版权和使用条款。有些数据集只允许学术研究商用要单独联系授权。商用项目里为了稳妥最好以自建数据为主公开数据只做预训练或辅助增强。3.2 自建数据集的采集与标注细节自建数据集是最可控的方案。隧道检测现场通常有三种采集方式一是人工手持工业相机拍摄适合小范围抽样二是用搭载相机的轨道检测车连续扫描能覆盖全断面三是用无人机贴着洞壁飞行拍摄适合高处的二衬区域。不管哪种方式都要保证图像有足够的重叠率方便后期拼接和筛选。采集时的光照处理特别关键。隧道内光源色温不一致拍出来会有偏色建议使用带偏振镜的补光灯减少反光。如果现场已经有固定照明最好在同样的位置和角度拍摄保证训练数据与实际推理场景一致。标注工具我用的是X-AnyLabeling和LabelImg。裂缝这种细长目标用矩形框标注有个天然问题框内背景占比太高模型学到的大量特征其实是背景。所以我采用了一个土办法把长裂缝沿长度方向切成若干段每段单独标一个框每个框尽量贴合局部裂缝的走向。这样虽然标注量增加了但模型学到的前景特征更纯粹mAP提升很明显。还要注意标注一致性。裂缝边缘在很多地方是模糊的标注人员对同一张图的判断可能不同。我在项目里规定只有宽度大于2个像素的裂缝才标疑似渗水痕迹不标模棱两可的样本全部进负样本池。这比让标注员自由发挥要靠谱得多。3.3 从VOC/COCO到YOLO的格式转换与数据集划分公开数据集多数是VOC或COCO格式YOLOv5需要的是txt格式的标注文件每一行是class x_center y_center width height坐标全部归一化到0到1。格式转换脚本并不复杂核心是把XML或者JSON的边界框坐标除以图片宽高。我写了一个Python脚本统一处理import os import xml.etree.ElementTree as ET from PIL import Image def voc2yolo(xml_file, img_dir, out_dir, classes): tree ET.parse(xml_file) root tree.getroot() img_name root.find(filename).text img_path os.path.join(img_dir, img_name) with Image.open(img_path) as img: w_img, h_img img.size lines [] for obj in root.iter(object): cls obj.find(name).text if cls not in classes: continue box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) x_c (x1 x2) / 2 / w_img y_c (y1 y2) / 2 / h_img w (x2 - x1) / w_img h (y2 - y1) / h_img lines.append(f{classes[cls]} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}) out_path os.path.join(out_dir, img_name.replace(.jpg, .txt).replace(.png, .txt)) with open(out_path, w) as f: f.write(\n.join(lines))数据集划分也要讲究。隧道图像经常是连续拍摄的同一裂缝可能出现在相邻多帧里如果随机划分训练集和验证集可能包含同一裂缝的多个视角导致验证分数虚高。正确做法是按采集时间或位置分桶同一个桶的数据要么全进训练集要么全进验证集避免数据泄漏。我最后是每隔10米取一段作为验证集测试集单独用一条没有参与过训练的隧道数据。4. 训练配置与实测数据分析4.1 关键训练超参数怎么定训练YOLOv5sBIFPN时超参数设置比网络结构更影响最终效果。我用的配置如下参数数值说明image size1280裂缝太细640下细节丢失严重batch size16受显存限制可配合梯度累积epochs150前50轮冻结骨干后100轮全量微调optimizerSGD动量0.937初始lr 0.01lr decaycosine配合warmup 3轮mosaic1.0前80轮开启后20轮关闭mix_up0.1太低容易过拟合fliplr0.5仅水平翻转避免垂直语义破坏输入尺寸是我踩过最大的坑。最初用640训练模型对独立裂缝的检测还行但遇到宽度只有2像素的细微裂缝召回率很低。后来把输入提到1280mAP0.5一下子涨了4个点。代价是训练显存翻倍推理时间也变长。如果你的部署端允许TensorRT优化1280输入带来的性能损失是可以接受的。换BiFPN之后我建议不要直接用原版的anchor配置要根据数据重新聚类。裂缝的长宽比普遍在1:5到1:20之间默认anchor以通用目标为标准聚类后小目标召回率也会有改善。K-means聚类的脚本在YOLOv5仓库里带了一个utils/autoanchor.py训练时会自动重算很方便。4.2 迁移学习与冻结训练技巧隧道裂缝数据量通常不会特别大从零训练深度学习模型很容易过拟合。我采用了标准的迁移学习流程加载官方YOLOv5s.pt预训练权重把nc改成1后最后一层输出维度不匹配权重加载时会被跳过这个不用管模型会自己重新学。训练策略是两阶段。第一阶段冻结backbone只训练neck和head学习率设置成正常的一半跑50轮。这一步让模型在保持通用特征提取能力的同时先把裂缝检测头学出来。第二阶段解冻全部层用更低的学习率全量微调。这么做的原因是裂缝数据集和COCO数据集特征差异较大如果一开始就全部微调backbone可能被少量样本带偏反而损失泛化能力。有个细节冻结阶段YOLOv5官方代码会自动把BN层也冻结但实际用下来neck部分的BN层保留更新效果更好。我手动调整了冻结范围只冻结backbone的卷积和BN其他BN保持更新。这个操作在代码里比较绕但收益是真实存在的。如果显存不够梯度累积是比减小batch更优的方案。batch size减半会导致BN统计量抖动加剧尤其在小数据集上很敏感。我最终用batch size 16加梯度累积2等效batch size 32训练稳定性比直接开batch 8要好很多。4.3 BIFPN和PANet的实测对比在自建的隧道裂缝数据集上我做了几组对照实验。数据集总共4286张图像训练集3428张验证集429张测试集429张全部来自不同隧道段落。测试结果如下模型mAP0.5mAP0.5:0.95参数量推理耗时(ms)YOLOv5sPANet76.8%43.2%7.2M8.3YOLOv5sBiFPN80.9%46.7%7.8M9.1YOLOv5mBiFPN82.1%48.5%21.4M14.6从结果看YOLOv5sBIFPN在mAP0.5上比PANet提升了4.1个百分点推理耗时只增加了不到1毫秒性价比非常高。YOLOv5mBIFPN精度更高但参数量翻了接近三倍边缘设备上并不划算。我还统计了不同裂缝宽度下的召回情况。对于宽度大于5像素的裂缝三种模型差别不大对于宽度小于3像素的细微裂缝YOLOv5sBiFPN比PANet召回率高约7个百分点。这说明BiFPN的加权融合和跨层连接确实提升了小目标特征的表征能力。5. 部署落地时最容易被忽略的坑5.1 小裂缝漏检切片推理是最直接的解法模型训练完我以为验证集效果不错就能直接上线结果到现场一跑漏检还是很多。仔细分析后发现现场相机拍的是8000x4000像素的高清大图直接resize到1280后原本就细的裂缝被压缩得几乎看不见。解决办法是切片推理也叫sliding window inference。把大图按1280x1280的尺寸、256像素的重叠切成若干patch每个patch分别送进模型检测然后把重叠区域的检测框做NMS合并。这样做之后小裂缝召回率提升明显但推理总时间变长了。我后来用TensorRT加多线程流水线优化把单张8000x4000大图处理时间控制在2秒以内基本满足日常巡检的需求。切片尺寸和重叠率之间有个平衡。切片越小小目标保留得越好但重复推理区域越多耗时越高。我试过512、640、1024、1280四种尺寸512对小裂缝最友好但推理时间是1280的四倍以上。工程上我最终选择了1024切片、256重叠兼顾速度和召回率。5.2 误检成灾隧道里的水渍、管线与裂缝怎么区分小裂缝漏检解决后新的问题又冒出来了误检率太高。隧道内的水渍、渗流痕迹、管线阴影、甚至贴着的反光条都被模型当成了裂缝。尤其是一些纵向管线长宽比和裂缝很像模型很容易被轮廓骗到。我做了三件事来压误检。第一在训练数据里加入大量负样本把这些容易混淆的背景单独截取成图标注为空标签让模型学会“这些不是裂缝”。第二在数据增强环节增加RandomErase随机擦除部分裂缝区域迫使模型更多关注裂缝的纹理特征而不是整体轮廓。第三后处理阶段加入置信度阈值自适应如果某个检测框的长宽比大于15且面积特别小阈值上调0.1如果同一个区域连续多帧检测到同一目标判为裂缝单帧出现且置信度不高的目标直接丢弃。这套组合拳打下来误检率从之前的每张图一二十个降到每张图不到三个。后来我发现真正起最大作用的还是负样本数据增强模型结构再强也挡不住训练时没见过这种背景。5.3 模型导出与TensorRT加速的兼容性问题落地部署时我用TensorRT把模型转成engine格式。前面加的自定义BiFPN_Add模块在导出ONNX时遇到了麻烦torch.relu(self.w)这种动态权重操作在ONNX里虽然能导出但TensorRT解析时偶尔会报不支持的节点类型。解决方式有两种我最后采用了第二种。第一种是把权重归一化在导出前固定下来但这样会损失自适应性第二种是把加权求和改成普通的1x1卷积加卷积核固定为归一化权重或者干脆在ONNX导出时重写forward把自学习的w直接替换成常量。替换后精度略降了0.2个百分点但TensorRT推理稳定多了。如果项目精度余量充足这种工程上的妥协是值得的。另外还遇到一个问题BiFPN模块使得neck部分的张量在导出时多了一些分支TensorRT动态shape模式下一开始总是构建失败。我最后把所有输入尺寸固定为1024x1024关闭动态shapebuilder耗时虽然长了一点但推理速度确实快了不少。对于定期巡检这种场景固定输入尺寸不是什么大问题。最后想分享一个小技巧不管模型调得多好现场运行时要留一个“人工复核”的接口。我会在一个界面上同时展示原始图像、检测框和置信度置信度在0.3到0.6之间的检测结果单独标黄提醒检测人员重点确认。这套机制帮我快速收集了一批难例每两周迭代一次模型效果越跑越稳。隧道裂缝检测不是一个静态模型能搞定的数据迭代和用户反馈才是精度持续提升的发动机。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价