YOLOv5s+BiFPN隧道裂缝检测实战:特征融合优化与部署全流程解析
2026/9/19 7:30:45 网站建设 项目流程

接隧道检测项目那阵子,我先把市面上能跑的目标检测模型都过了一遍,最后定下YOLOv5s加BiFPN的组合。原因很简单:现场要求能在工控机或嵌入式设备上跑实时推理,不能动不动就要A100;但隧道里的裂缝又偏偏是那种又细又长、和背景融在一起的小目标,只靠YOLOv5s默认的PANet特征融合,漏检率高得没法看。这篇文章就把整个项目从数据准备、模型改造到落地部署的过程完整拆给你看,包括数据集怎么找、怎么标、怎么排坑。适合正在做隧道结构检测、路面病害检测,或者其他小目标检测项目的朋友参考。

先说结论:YOLOv5s+BIFPN这套组合,在隧道裂缝检测里确实比原版YOLOv5s的PANet要稳,mAP@0.5能提升3到5个百分点,速度损失可以接受。但真正决定项目成败的不是模型结构,而是数据集。裂缝样本的采集、筛选和标注,占了整个项目七成以上的工作量。下面按项目推进的顺序写。

1. 隧道裂缝检测的痛点:为什么这个任务没那么简单

1.1 隧道现场环境与裂缝形态的特殊性

隧道裂缝和普通路面裂缝、墙体裂缝最大的区别在于环境极度不友好。首先是光照,隧道内部几乎只有人工光源,很多区段光照不均匀,中间亮、边墙暗,裂缝在暗部的对比度极低。其次是背景干扰,隧道内壁不是干净的混凝土,而是带着模板缝、施工冷缝、渗水痕迹、锚杆孔、管线支架、电缆沟槽等各种纹理,这些背景特征和裂缝在视觉上经常高度相似。

再说裂缝本身的形态。隧道裂缝通常是细长条,宽度可能只有3到5个像素,长度却有几十甚至上百像素。用目标检测的水平框去框它,框里大部分区域都是背景,这会严重干扰模型学习。而且裂缝形态变化很大,有横向的、纵向的、网状的,还有沿着施工缝边界开裂的,训练阶段如果数据分布不全,泛化性就会很差。

  • 光照不均:夜间采集的隧道图像经常出现局部过曝或过暗
  • 背景噪声多:渗水、污渍、钢筋外露、模板纹理都可能被误判
  • 目标尺寸极端:长宽比大、像素占比小,属于典型的小目标

这些因素叠加在一起,决定了隧道裂缝检测不能照搬通用目标检测的思路。很多通用模型在城市道路裂缝数据集上表现不错,一到隧道现场就严重掉点,原因就在数据分布差异上。

1.2 传统巡检手段的局限与YOLOv5s的机会

传统隧道裂缝检测主要靠人工目检,检测人员搭着平台车或升降车一寸一寸地看,效率低、主观性强,还容易漏检。后来有了探地雷达、超声波、红外热成像这些无损检测仪器,虽然能发现内部缺陷,但对表面裂缝的定位和尺寸评估仍然不够直观,而且设备贵、操作门槛高。

深度学习目标检测模型切入这个场景后,最大的优势是可以把高清相机采集的隧道衬砌图像直接喂进去,端到端输出裂缝的边界框和类别。和图像分割相比,目标检测不需要逐像素标注,标注成本低很多,工程落地也更简单。和传统Canny边缘检测、形态学处理相比,深度模型对复杂背景的鲁棒性好得多,能适应隧道内的多种光照和纹理条件。

YOLOv5在工程界的普及度极高,推理速度、部署生态、文档完整度都很成熟。但它默认的PANet特征融合对多尺度小目标的处理还有优化空间,所以我没直接用它开跑,而是先考虑怎么在不显著增加计算量的情况下把特征融合增强一下。BiFPN就是在这个背景下引入的。

1.3 选型分析:为什么是YOLOv5s+BIFPN

项目初期我对比过几个路线:YOLOv8n、YOLOv5s、RT-DETR,以及YOLOv5s+PANet换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再传到P3;BiFPN里P3可以直接和P4、P5建立融合关系,信息传递路径更短。

加权融合的公式也不复杂,就是把不同输入特征按可学习的权重做加权平均。权重经过ReLU确保非负,然后除以所有权重之和做归一化,这样训练更稳定。公式可以写成:

输出 = (w1 * 输入1 + w2 * 输入2) / (w1 + w2 + ε)

其中w1和w2是网络自动学习的参数,ε是一个很小的常数防止除零。这个机制的好处是不再默认不同层特征贡献相同,模型可以根据训练数据自动调整,比如当光照暗的时候,可能更依赖底层边缘特征;当背景噪声多的时候,更依赖高层语义特征。这种自适应能力对隧道裂缝这种环境多变的任务非常实用。

2.3 在YOLOv5s代码里加一个BiFPN需要动哪几个文件

我在YOLOv5 6.0版本上做的改动,主要涉及三个文件:models/common.pymodels/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_grad=True) 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 关键训练超参数怎么定

训练YOLOv5s+BIFPN时,超参数设置比网络结构更影响最终效果。我用的配置如下:

参数数值说明
image size1280裂缝太细,640下细节丢失严重
batch size16受显存限制,可配合梯度累积
epochs150前50轮冻结骨干,后100轮全量微调
optimizerSGD动量0.937,初始lr 0.01
lr decaycosine配合warmup 3轮
mosaic1.0前80轮开启,后20轮关闭
mix_up0.1太低容易过拟合
fliplr0.5仅水平翻转,避免垂直语义破坏

输入尺寸是我踩过最大的坑。最初用640训练,模型对独立裂缝的检测还行,但遇到宽度只有2像素的细微裂缝,召回率很低。后来把输入提到1280,mAP@0.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张,全部来自不同隧道段落。测试结果如下:

模型mAP@0.5mAP@0.5:0.95参数量推理耗时(ms)
YOLOv5s+PANet76.8%43.2%7.2M8.3
YOLOv5s+BiFPN80.9%46.7%7.8M9.1
YOLOv5m+BiFPN82.1%48.5%21.4M14.6

从结果看,YOLOv5s+BIFPN在mAP@0.5上比PANet提升了4.1个百分点,推理耗时只增加了不到1毫秒,性价比非常高。YOLOv5m+BIFPN精度更高,但参数量翻了接近三倍,边缘设备上并不划算。

我还统计了不同裂缝宽度下的召回情况。对于宽度大于5像素的裂缝,三种模型差别不大;对于宽度小于3像素的细微裂缝,YOLOv5s+BiFPN比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,关闭动态shape,builder耗时虽然长了一点,但推理速度确实快了不少。对于定期巡检这种场景,固定输入尺寸不是什么大问题。

最后想分享一个小技巧:不管模型调得多好,现场运行时要留一个“人工复核”的接口。我会在一个界面上同时展示原始图像、检测框和置信度,置信度在0.3到0.6之间的检测结果单独标黄,提醒检测人员重点确认。这套机制帮我快速收集了一批难例,每两周迭代一次模型,效果越跑越稳。隧道裂缝检测不是一个静态模型能搞定的,数据迭代和用户反馈才是精度持续提升的发动机。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询