做工业视觉检测的人都知道,大型铸件从砂型里倒出来、清完砂、切掉浇冒口之后,摆在你面前的是一个动辄几百公斤、表面还带着氧化皮和残余砂粒的“铁疙瘩”。在这样一类场景里做缺陷分类识别,和拍几个小零件跑通一个demo完全是两码事。今天这篇东西,就围绕一个我们实际落地过的方案来聊:基于 yolov10n-EUCB-SC 模型的大型铸件缺陷分类识别。我会把模型结构怎么设计、数据集怎么折腾、训练和部署中踩过哪些坑,尽量完整地记录下来,给正在做同类项目的朋友一个能拿去参考的路线。
这个模型的定位很明确:在不把推理帧率拖垮的前提下,提升大型铸件表面气孔、缩孔、裂纹、夹渣、冷隔这几类常见缺陷的识别率。基线选的是 YOLOv10n,nano 版本参数量小、速度快,适合产线工控机这种算力受限的部署环境。EUCB 是我们在颈部网络中加入的增强统一卷积模块,SC 是一个轻量上下文注意力模块。整套改完以后,模型参数量只增加不到 0.4M,但 mAP50 比原版 v10n 提升了接近四个点。下面从需求拆解开始,把整个设计逻辑和实操过程捋一遍。
1. 项目背景与需求拆解
1.1 大型铸件的缺陷分布与检测难点
大型铸件的缺陷和普通小尺寸铸造件不太一样。比如气孔和缩孔,在大型件上经常混在一起,形成复合型缺陷,单靠轮廓很难区分。夹渣则会以极不规则的团块形状出现,有的甚至藏在氧化皮下面,肉眼都很难一次性看出来。最头疼的是微裂纹,纹路细、走向随机,在 640x640 的输入尺寸下往往只占二十几个像素,属于典型的小目标检测问题。
从产线流程来说,缺陷检测通常安排在后处理工序之后,铸件表面已经经过抛丸或粗磨,环境粉尘大、光照不稳定,相机和工件之间还会存在一定振动。这些外部因素都会压缩图像质量,让特征提取更困难。再加上缺陷类别之间存在严重的样本不均衡,缩孔和夹渣出现频率高,冷隔和微裂纹则很少见,模型容易对少数类产生强烈的偏向性。
1.2 为什么基线选 YOLOv10n
做目标检测的都知道,YOLO 系列在工业界的普及程度几乎是“默认选项”。我们当时在 YOLOv8n 和 YOLOv10n 之间做了对比,最终选择 yolov10n 有两点关键原因。第一是 n 版本在 640 分辨率下参数量只有 2.7M 左右,计算量约 8.4G FLOPs,在普通的 RTX 显卡或者高性能工控机上能跑出非常可观的帧率,这对产线节拍非常重要。第二是 YOLOv10 引入了无 NMS 的推理机制,去掉了传统的 NMS 后处理环节,直接输出最终预测结果,这一步在 C++ 部署时会省掉大量排序和抑制计算的代码,也减少了推理延迟的随机抖动。
但 yolo v10n 为了追求极致轻量,特征表达上做了很多压缩。大型铸件表面背景复杂、缺陷尺度跨度大,n 版本在 neck 部分只有一层简单的多尺度融合,对小目标语义信息和深层目标位置细节的保留都不够充分。实际测试中,微裂纹在低对比度区域的召回率非常低,这是选择在它基础上做改进的直接原因。
1.3 确定改进方向:EUCB-SC 的两个切入点
模型改进不能拍脑袋,必须从数据反馈的问题出发。第一批实验里,我用原版 yolov10n 跑了完整训练,统计了每一类缺陷的召回情况,发现三组现象:第一,细长型裂纹漏检最多,原因在于感受野不够,上下文信息不足;第二,气孔和缩孔容易混淆,原因是深层特征中空间细节丢失太多,形状特征不完整;第三,检测头在前景和背景极不均衡的表面上容易产生误检,说明通道维度的注意力机制没有起到应有的筛选作用。
于是我们把改进方向收敛到两点:一是增强多尺度上下文特征,让模型在感受野上既有细粒度裂纹所需要的局部精度,又有识别复合型缺陷所需的全局视野;二是加入轻量注意力机制,在几乎不增加参数量的前提下对通道和空间进行重新校准。EUCB 模块解决第一点,SC 模块解决第二点。
2. 模型结构设计与核心模块详解
2.1 整体网络架构
yolov10n-EUCB-SC 的改动范围集中在 Backbone 输出的 P3、P4、P5 三层特征之后,具体是在 Neck 部分每个尺度的特征融合前先经过一个 EUCB 模块完成多尺度上下文提取,再通过 SC 注意力模块对融合后的特征进行通道重标定。整体结构可以理解为:
- Backbone 部分保持 YOLOv10n 原结构,使用 v10 自带的 C2f 模块和 SPPF 模块;
- Neck 的每个上采样或下采样分支末端插入 EUCB;
- 在每个检测头输入特征图之前插入 SC 模块;
- Head 部分保持 YOLOv10 无 NMS 的 one-to-one 检测头不变。
这样改的好处是 Backbone 完全没有动,预训练权重可以直接复用大半,训练时间显著缩短,收敛也更稳定。实际测试中,这种“只动颈部和头部入口”的改动方式,在迁移学习时比整体换结构要稳得多。
2.2 EUCB 模块:怎么做多尺度特征增强
EUCB 全称是 Enhanced Unified Convolution Block。它的设计目标是在一个轻量模块内同时获得不同感受野的特征。实现上采用并行双分支结构:一个分支使用普通 3x3 卷积,负责常规细节;另一个分支使用膨胀率为 2 的 3x3 空洞卷积,并不额外增加参数量,却能把感受野扩大一倍,专抓裂纹和缩孔这类需要看“周边关系”的缺陷。
模块前先用 1x1 卷积把通道压缩一半,控制整体计算量,双分支输出后在通道维度拼接,再用 1x1 卷积恢复原通道数。这样做和很多主流轻量模块的思路一致:中间瓶颈化,两头收敛通道。
下面是模块的核心代码,方便直接复现:
import torch import torch.nn as nn class Conv(nn.Module): def __init__(self, in_c, out_c, k=1, s=1, d=1, act=True): super().__init__() self.conv = nn.Conv2d(in_c, out_c, k, s, padding=d if k > 1 else 0, dilation=d, bias=False) self.bn = nn.BatchNorm2d(out_c) self.act = nn.SiLU() if act else nn.Identity() def forward(self, x): return self.act(self.bn(self.conv(x))) class EUCB(nn.Module): def __init__(self, c1, c2): super().__init__() self.shortcut = Conv(c1, c2, 1, act=False) self.conv_reduce = Conv(c1, c2 // 2, 1) self.branch_d1 = Conv(c2 // 2, c2 // 2, 3, d=1) self.branch_d2 = Conv(c2 // 2, c2 // 2, 3, d=2) self.conv_fuse = Conv(c2, c2, 1) def forward(self, x): identity = self.shortcut(x) y = self.conv_reduce(x) y1 = self.branch_d1(y) y2 = self.branch_d2(y) y = torch.cat([y1, y2], dim=1) y = self.conv_fuse(y) return y + identity这里我加了残差连接,主要考虑是在同等通道数情况下保证梯度流通顺畅,不然颈部模块堆叠之后,小模型容易出现深层梯度消失。
2.3 SC注意力模块:用更低的算力找回空间上下文
SC 模块是一个简化版的上下文注意力机制。传统 SE 注意力用两个全连接层来建模通道依赖,参数量会随着通道数平方增长,在浅层网络或轻量模型中非常不划算。SC 模块借鉴 ECA 的思路,用一维卷积替代全连接层,核大小固定为 3,能够在通道维度上建模局部依赖关系,参数量几乎可以忽略不计。
同时它比 ECA 多一些空间上下文处理:在注意力权重计算之前,对特征图做一次 3x3 分组卷积,让高响应区域在空间上互相激活,这对细长裂纹这种“局部断断续续”的目标有正面作用。实现比较简单:
class SC(nn.Module): def __init__(self, c, k_size=3): super().__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) self.conv1d = nn.Conv1d(1, 1, kernel_size=k_size, padding=k_size // 2, bias=False) self.conv_spatial = nn.Conv2d(c, c, kernel_size=3, padding=1, groups=c, bias=False) self.sigmoid = nn.Sigmoid() def forward(self, x): b, c, _, _ = x.size() y = self.avg_pool(x).view(b, c) y = self.conv1d(y.view(b, 1, c)).view(b, c) w = self.sigmoid(y).view(b, c, 1, 1) x = x * w.expand_as(x) return x + self.conv_spatial(x)这里的conv_spatial走的是深度卷积路线,参数量只是c * 3 * 3,放在颈部完全能接受。实践中这个模块给裂纹类缺陷的直接召回带来了明显提升,后面评估数据里能看出来。
2.4 计算量与参数量变化分析
我在同一份数据上分别统计了原版 yolov10n 和 yolov10n-EUCB-SC 的参数规模和计算量,输入统一为 640x640x3,结果如下:
| 模型 | 参数量(M) | 计算量(GFLOPs) | 单张推理耗时(RTX 4090, ms) |
|---|---|---|---|
| yolov10n | 2.71 | 8.42 | 1.98 |
| yolov10n-EUCB-SC | 3.08 | 9.45 | 2.36 |
参数量增加约 13.6%,计算量增加约 12.2%,推理耗时增加不到 0.4ms。对于工业检测这种场景,一台相机通常每分钟需要处理几十张图像,这个速度损耗完全可以接受。而得到的收益是几个关键缺陷类别的 AP 提升了三到六个百分点,后面评估小节里会给出具体数字。
3. 数据集构建与预处理
3.1 缺陷类别定义与标注规范
做缺陷识别最忌讳标准不统一。我们和现场质检工程师反复确认后,把缺陷类型定为五类:气孔、缩孔、裂纹、夹渣、冷隔。每类都写入了详细的标注规范,比如“气孔直径小于 3mm 且边缘光滑”“裂纹必须为连续性断裂且长度大于 5mm”“夹渣区域呈现不规则块状且与基体有明显灰度差异”。没有这样的规范,标注员和模型都会陷入混乱。
标注工作使用了开源的矩形框标注工具,但实际过程中三分之二的图像需要二次返工。原因在于大型铸件表面存在大量假性缺陷,比如抛丸残留造成的暗斑、砂眼、氧化皮边缘阴影,这些在标注初期很容易被标成气孔或夹渣。后来我们增加了一条规则:标注时必须同时参考多角度拍摄的同一区域图像,保证框选区域确实对应缺陷。这条规则直接降低了训练样本中的噪声水平。
3.2 数据增强策略
缺陷样本总量不小,但分布非常不均衡。总共约 14000 张图像,其中夹渣占比超过 45%,气孔占比约 25%,缩孔约 15%,冷隔约 8%,真正的裂纹样本只有不到 7%。在这样的分布下,直接训练出来的模型对裂纹几乎是“睁眼瞎”。
所以数据增强在设计上重点照顾少数类。对裂纹和冷隔图像,我采用了水平翻转、垂直翻转、随机旋转 15 度、随机裁剪,以及 Mosaic 拼接。另外还针对裂纹的线性特征加了随机透视变换和随机亮度对比度扰动,模拟不同光线条件下裂纹的可见程度变化。需要强调的是,Mosaic 在样本量不够大的时候确实有效,但不要用于每一轮训练,绝大多数场景下前 100 个 epoch 开启就够了,后期关闭能避免过度破坏真实图像统计分布。
3.3 样本分配与验证集划分
训练集、验证集、测试集的划分不能简单地随机切分。我有一个教训:第一版实验直接按文件列表随机划分,结果测试集和训练集中可能包含同一铸件不同角度的图像,导致验证指标虚高。后来改成按“工件ID”划分,同一个铸件所有图像必须归入同一个集合,从根本上杜绝了数据泄露。
最终划分是训练集 11200 张,验证集 1400 张,测试集 1400 张。测试集专门从一段连续时间采集的图像中抽取,模拟真实现场光照变化、振动模糊和表面清洁度的波动,保证评估结果能反映生产的实际情况。
4. 训练配置与实操过程
4.1 环境与依赖
训练环境用了一台单机八卡的机器,但实际上四卡就够,因为这只是一个 nano 级别的模型。软件环境如下:
Python 3.10 PyTorch 2.1.0 CUDA 11.8 ultralytics (基于 v10 分支的自定义代码)建议把 torch 和 CUDA 版本锁定,不要盲目追新。我们一开始在 CUDA 12.1 环境上训练,出来后导出 TensorRT 时反而出现了算子兼容问题,白白折腾了一周。稳定压倒一切,训练和部署环境版本尽量保持一致。
4.2 模型配置文件修改
模型的 yaml 配置文件是在 yolov10n.yaml 基础上改的。Neck 部分每个尺度的检测头输入之前新增了 EUCB 和 SC 模块,核心改动如下:
# yolov10n-EUCB-SC.yaml backbone: # 保持原有 YOLOv10n backbone 不变 neck: # GPSA 等原结构保持不变,但在每个输出分支前加入以下模块 - [-1, 1, EUCB, [128]] - [-1, 1, SC, [128]] # P4 分支 - [-1, 1, EUCB, [256]] - [-1, 1, SC, [256]] # P5 分支 - [-1, 1, EUCB, [512]] - [-1, 1, SC, [512]]这里 EUCB 后的通道数需要和该层实际输入通道保持一致,不能随意改。我一开始在 P4 分支上强行把通道翻倍,结果参数量涨了一倍多,训练 loss 反而震荡得厉害。轻量模型讲究“够用即可”,不要盲目加宽。
4.3 超参数设置与训练命令
训练超参数可以给一组经过多次实验验证的数值:
python train.py \ --weights yolov10n.pt \ --cfg yolov10n-EUCB-SC.yaml \ --data cast_defect.yaml \ --hyp hyp.scratch-low.yaml \ --epochs 300 \ --batch-size 32 \ --imgsz 640 \ --optimizer SGD \ --lr0 0.01 \ --lrf 0.01 \ --warmup-epochs 3 \ --device 0,1SGD 配合 warmup 在这种中小数据集上表现很稳。AdamW 前期收敛快,但后期边界框回归精度往往不如 SGD 精调,这点在多个同类项目里都验证过。
训练时使用了两卡并行,总 batch 32,每张卡 16。学习率初始 0.01,到了第 250 个 epoch 后我手动下调到 0.002,作为最后一轮精细调整。整个过程大约耗时 18 小时,比预想中快,因为 Backbone 沿用了预训练权重。
4.4 训练过程监控与调参记录
训练过程中最值得记录的是 loss 曲线波动。前 30 个 epoch 比较正常,box loss 稳定下降。但到了第 120 个 epoch 左右,验证集上的召回率出现平台期,尤其裂纹类几乎不动。我先检查了数据增强是否过强,把 Mosaic 在第 100 个 epoch 关闭之后,裂纹的召回有了一点提升,但依旧不理想。 后续我把 SC 模块从“检测头前”挪到“EUCB 后”,相当于让注意力机制先完成通道重标定,再送入 EUCB 做多尺度上下文扩充,这个顺序变化让裂纹类 AP 提升了 1.8 个点。如果你们复现时发现某个类上不去,不妨留意一下模块间的顺序,这种细节很影响结果。
5. 评估结果与对比分析
5.1 核心指标
整个项目以 mAP50、mAP50:95、参数量、单张推理耗时作为主要评估指标。最终结果如下:
| 模型 | mAP50 | mAP50:95 | 参数量(M) | 推理耗时(ms) |
|---|---|---|---|---|
| yolov10n | 89.4 | 68.7 | 2.71 | 1.98 |
| yolov10n-EUCB-SC | 93.1 | 74.5 | 3.08 | 2.36 |
| yolov10n-EUCB-SC + TTA | 93.6 | 75.2 | 3.08 | 8.10 |
可以看到,改进后的模型在 mAP50 上提升了 3.7 个点,mAP50:95 提升了 5.8 个点。TTA 虽然进一步提升精度,但推理耗时翻了几倍,工业场景一般不会在产线上用,只适合离线抽检确认。
5.2 各类缺陷的检测表现
分项的 AP50 数字更能说明问题:
| 缺陷类型 | yolov10n AP50 | EUCB-SC AP50 |
|---|---|---|
| 气孔 | 92.1 | 94.3 |
| 缩孔 | 90.5 | 93.8 |
| 裂纹 | 83.2 | 89.6 |
| 夹渣 | 93.8 | 95.1 |
| 冷隔 | 87.6 | 91.2 |
裂纹和冷隔的提升幅度最大,这验证了最初的判断:多尺度上下文和注意力机制对细长型、低对比度缺陷有明显帮助。气孔和夹渣本身已经是易识别类,提升幅度不大,但也没有出现负优化。
5.3 与YOLOv10n基线对比
除了精度,还比较了误检率和漏检率。原版 yolov10n 在测试集上的漏检总数为 214 个缺陷框,改进后降到了 131 个,下降了约 38.8%。误检框从 97 个降到了 74 个,下降了约 23.7%。这个数据在实际现场很有意义,因为漏检直接关系到客户投诉和产品报废,误检则增加人工复检工作量。38.8% 的漏检下降意味着每千件产品能少流出去几十个有问题的大型铸件。
值得一提的是,在推理耗时增加不到 0.4ms 的前提下换来的精度收益,对于产线节拍的影响微乎其微。如果产线采用“触发式拍照”而不是连续高速拍摄,这 0.4ms 几乎体现不出来。
6. 部署与产线落地的经验
6.1 模型导出与量化
训练完成后,导出部署格式是重头戏。我用 ONNX 作为中间格式,再转到 TensorRT FP16。命令参考如下:
python export.py \ --weights best.pt \ --include onnx \ --opset 12 \ --dynamic True trtexec \ --onnx=best.onnx \ --fp16 \ --saveEngine=best.engine这里要特别注意,YOLOv10 的模型结构和 v8 不同,不能直接套旧版导出脚本。我最初用网上通用的 v8 导出脚本,结果输出的 ONNX 里多了一套 NMS 子图,导致 TensorRT 转换时算子报错。后来基于 YOLOv10 官方仓库的导出代码调整,把end2end参数打开,才得到不带 NMS 的干净引擎。
6.2 实际部署的工程问题
现场用的工控机是一台配置很普通的设备,显卡是 RTX 3060,内存 16G。整机跑起来 TensorRT FP16 的推理耗时约为 6.5ms,加上相机曝光、图像传输和预处理,单件检测周期可以稳定在 350ms 以内,对产线节拍来说足够用了。
实际部署中最大的坑来自预处理。训练时用的是 BGR 图像、归一化到 0-1,但工业相机 SDK 往往默认输出 RGB 或 YUV 格式。我在 C++ 推理代码里忘记调整通道顺序,导致前几次试跑时模型输出框的位置和分数看着正常,但类别置信度全偏了。这种问题几乎无法通过调整阈值解决,最终通过逐像素对比预处理结果定位到通道顺序问题。建议所有做过模型转换的人,一定要在部署前准备十张图像,对比 PyTorch 输出和 TensorRT 输出的差异,逐通道验证预处理链路。
6.3 性能调优建议
部署后的性能优化可以按以下顺序做:
- 将图像缩放从双线性插值改为最近邻插值,推理精度损失很小,但耗时能低 0.3ms 左右;
- 开启 CUDA Stream,让 GPU 推理和相机取图重叠执行,吞吐量能有明显提升;
- 使用固定尺寸输入,避免动态 shape 在 TensorRT 中反复重绑定导致的延迟尖峰;
- 检测结果的可视化叠加不要放在 GPU 端处理,直接放到 CPU 线程异步处理。
这四条都做完之后,整体检测节拍比最初版本快了不少。产线实测下来,误检导致的停机报警频率也明显降低。
7. 常见问题与排查技巧
7.1 漏检问题
漏检是缺陷检测项目里最需要优先解决的问题。排查思路要按照“数据-特征-阈值”三层走。先看漏检图像是否在训练集中有相似样本,如果完全没有,那就是数据覆盖问题,需要补样本;如果样本存在但模型没检出,再检查特征层面,确认是否因为缺陷对比度过低,需要在预处理阶段加入自适应直方图均衡化;最后才是降低置信度阈值,一般从 0.25 降到 0.15 左右,再配合 NMS 参数调整。
千万不要一上来就调阈值。我曾经为了压低漏检率,把阈值一路降到 0.05,结果误检框爆炸式增长,后端的复检工人直接在工位上开骂。
7.2 误检问题
误检的典型原因是铸件表面纹理被当作缺陷。在我这里,最大的误检来源是氧化皮边缘在侧光条件下产生的强阴影。解决方法是调整光源布局,采用低角度环形光替代直射光,阴影明显减轻。除了硬件手段,算法上还可以增加一类的后处理规则,比如“候选框内灰度方差小于一定阈值则忽略”,因为真实缺陷区域通常具有明显的内部灰度变化。
7.3 训练不收敛问题
如果训练时 loss 出现周期性震荡甚至发散,优先检查几个方面:学习率是不是过高;Backbone 预训练权重是否被正确加载;模型配置文件中的通道数是否和预训练权重匹配;batch size 是否过小导致 BN 统计量不稳定。我遇到过最隐蔽的情况是修改 yaml 时不小心把 P5 分支的通道数写成了 256 而不是 512,模型照样能跑,但 loss 一直在高位抖动,排查了很久才发现是通道数量级不一致导致的。
8. 最后的一点个人心得
大型铸件缺陷检测不像普通目标检测那样可以有一百个花哨的改进点,实际项目中真正起作用的往往是最朴素的策略:把类别定义定死,把数据分布摸清,把模块顺序调对。yolov10n-EUCB-SC 这个方案目前在我们这边已经稳定运行了一段时间,漏检率控制在可接受范围内,推理速度和精度权衡也符合预期。
如果你们也正在做类似方向,我建议先在原版模型上跑一版完整指标,把每一类缺陷的基线数字记录下来,再动手改进。不要一开始就追求复杂结构,先找到瓶颈类,再有针对性地加强特征。这条路走起来也许慢,但每一步都算数,现场的准确率也会给你最诚实的反馈。