简介:在工业视觉质检中,语义分割通过逐像素分类实现缺陷的精准定位,相比目标检测更适合裂纹、夹杂、划痕等不规则表面缺陷。面对背景像素占比极高的类别不平衡问题,单纯依靠准确率评估会严重失真,需引入IoU、Dice系数与召回率。本文以4100张钢材缺陷图像为样本,介绍U-Net与预训练编码器的基线搭建,详细讲解加权交叉熵与Dice Loss的组合策略,并针对细长缺陷漏检、背景纹理误检等工程难题给出可复用的调优方案。该方法可推广至钢板、纺织、半导体等表面质检场景,为工业视觉开发者提供从数据到模型落地的完整参考。 钢材缺陷分割这个方向,很多人一上来就奔着深度学习模型去,但真正决定项目成败的往往是数据本身。这次做的是4100张带像素级标签的钢材表面缺陷数据,任务很明确——多类别分割,把裂纹、夹杂、划痕三类缺陷从背景里逐像素抠出来,属于标准的语义分割框架下的工业缺陷检测项目。整套流程从数据划分、模型选型到损失函数调优,我踩了不少坑,也沉淀了一些可以复用的经验,这篇就把它完整拆开讲。
1. 项目全貌:任务定义与数据集解读
1.1 一个什么样的任务:多类别缺陷的像素级定位
工业缺陷检测最常听到的词是"目标检测",也就是用框把缺陷框出来,输出“这里有问题”。但钢材表面的裂纹、夹杂、划痕形状极不规则,裂纹是细长条、夹杂是不规则块、划痕是长度不等的线状区域,用矩形框去表达这些形态会产生大量背景噪音,漏检率和误检率都不好压。
所以这个项目选择了语义分割路线。语义分割做的事情是给图像里每一个像素分配一个类别标签,输出和输入同分辨率的掩膜图。在多类别分割的场景下,每个像素要么是背景,要么属于三类缺陷之一,这样就能得到缺陷的精确轮廓、面积、位置,后续还能拿这些掩膜去做缺陷评级。
这个任务对深度学习模型的考验在于:裂纹和划痕占整张图的像素比例非常低,常常只有百分之零点几,而背景占了绝大多数。模型稍有不慎,就会把所有像素都预测成背景,得到很高的“准确率”,却对缺陷毫无感知。这种极端类别不平衡,是工业分割和自然图像分割最大的差异点,也是整个项目里最需要花心思的地方。
1.2 4100张带标签数据:不是简单的“拍脑袋”规模
这次项目用的是钢材表面缺陷公开数据集(SEV数据集)的常见配置,总计4100张图像,每张都有对应的像素级标签。这里要特别说明一个容易混淆的点:4100张是总数据量,其中非缺陷样本占了大多数,真正带缺陷的样本反而是少数。按照常用的官方划分方式,训练集2500张,其中有缺陷样本约400张、无缺陷样本约2100张;验证集832张,其中有缺陷样本约112张、无缺陷样本约720张;测试集768张,其中有缺陷样本约46张、无缺陷样本约722张。
这个配置很典型,也很反映真实工业场景:产线上绝大部分钢板是正常的,真正需要模型揪出来的就是少数缺陷。如果按普通分类任务的习惯随机切分数据集,很可能出现测试集里某一类缺陷几乎没有的情况,评估结论完全失真。我的做法是分层切分,先按类别把带缺陷的样本单独拎出来,再按比例分配到训练、验证、测试三个集合里,保证每个集合都覆盖三类缺陷。
这种数据规模在工业分割项目里算“小而精”,既没有海量数据撑腰,又要求模型具备实用精度,反而是学习语义分割和工业检测的好素材。如果换成一个几十万张的工业数据集,很多经验反而不容易复现。
1.3 三类缺陷的形态差异与标注口径
标注口径直接影响模型的收敛上限。这个数据集里定义了三类缺陷标签,分别是裂纹、夹杂、划痕,各自有明确的物理含义:
- 裂纹:呈细长、扭曲的条状,表面带有分支或断续,灰度偏低,和背景有较明显的边缘对比。
- 夹杂:呈颗粒状或块状,边界相对清晰,灰度可能偏亮也可能偏暗,有点像异物压入钢板表面。
- 划痕:呈线状或带状,方向性比较强,宽度通常比裂纹大一些,灰度变化平缓。
这三类的形态差异给多类别分割带来了一个天然挑战:模型不仅要判断“是不是缺陷”,还要分清楚“是哪一种”。实际操作中,我见过不少模型把细长的划痕和裂纹混淆,尤其是当两者灰度接近时。所以在训练前,我习惯先把每类的样本数量、像素占比、形态特征统计一遍,做到心里有数,再决定数据增强和损失函数的配比。
2. 方案选型:为什么语义分割框架选择了U-Net系
2.1 类别不平衡:非缺陷样本占比84%意味着什么
把训练集里2100张非缺陷样本和400张有缺陷样本放在一起算,非缺陷图像占了大约84%。再往像素层面看,一张200×200的图里,背景像素几乎占据98%以上,缺陷像素常常只有几百到几千个。这意味着如果模型是个“懒汉”,把所有像素都预测为背景,它在像素准确率上也能拿到95%以上的分数。
这种假象在工业项目里非常坑人。只看准确率觉得模型已经很好,但放到产线上一跑,真正的缺陷一个都没揪出来。所以评估指标绝不能只看准确率,要看IoU(交并比)、Dice系数、以及每类缺陷的召回率。召回率在质检场景里优先级最高,漏掉一个缺陷可能意味着整批钢材流向客户端,损失远远大于误报。
针对类别不平衡,常规做法有两类:一是重采样,让模型在训练时更频繁地看到缺陷样本;二是改损失函数,给稀有类别更高的权重。这个项目里我用的组合是“加权交叉熵 + Dice Loss”,后面会专门展开。
2.2 疑似缺陷样本为什么难处理
SEV数据集里有个很典型的现象:很多背景纹理看起来跟缺陷很像,材料表面的氧化皮、水渍、辊印、灰度跳变,长得都像裂纹或者夹杂。这些被称为"疑似缺陷",它们的标签仍然是背景,但视觉特征和真实缺陷高度重叠。
这让模型非常纠结。如果模型把真实缺陷识别出来了,但同时也把疑似缺陷判成缺陷,那么精确率会下降;反过来,如果模型为了避开疑似缺陷变得保守,真实缺陷的召回率又会掉。这是一个典型的“误检”和“漏检”之间的跷跷板。
我的处理思路是:数据层面不回避这些样本,保留下来让模型见过足够多的“难背景”,同时在后处理阶段用形态学操作过滤掉面积过小的预测区域。模型层面则通过合理的损失权重,让模型在“背景上犯错”的代价足够高,从而学会区分细微差异。
2.3 从检测到分割:任务需求倒推技术路线
有些同事问过我,为什么不用目标检测或者实例分割?原因在于需求端。这个项目的最终目标是输出缺陷的面积、形态、分布密度,用于给钢板做质量评级。目标检测只能给出矩形框,无法准确量出裂纹的像素面积和形态走向;实例分割虽然能做到逐实例区分,但在工业界分割同一类缺陷时往往不需要区分“第1条裂纹、第2条裂纹”,反而增加了标注和模型的复杂度。
语义分割恰好落在“分类+定位+形状”的交集上,一图一掩膜,既直观又容易转换成后续的统计指标。泛化到别的工业场景时也是一样,先想清楚“下游要这个掩膜做什么”,再决定用哪一种范式,不要为了追新而追新。
2.4 基线模型怎么选:U-Net与Segmentation Models PyTorch
模型选型上,我第一版跑的是U-Net。U-Net的结构是编码器逐层提取特征、解码器逐步恢复分辨率,中间通过跳跃连接把浅层细节和深层语义拼起来。它训练参数少、收敛快,在中小规模数据集上表现非常稳,适合作为工业项目的第一版基线。
实现的时候我直接用了Segmentation Models PyTorch(SMP)这个库,它把很多主流分割模型打包成类似乐高积木的接口,一行代码就能实例化一个带预训练编码器的U-Net。我的做法是:先用ResNet34作为backbone,初始化权重用ImageNet预训练权重,虽然钢材表面图像和自然图像差异不小,但预训练权重在纹理和边缘特征上的先验仍然能明显加速收敛,尤其在数据量不够大的时候。
如果SMP库不方便装,也可以用PyTorch自己搭一个轻量U-Net,结构不复杂,但训练时间会长一些。我的建议是初期不要纠结模型结构有多花哨,先把U-Net的baseline跑通,拿到一组可信的指标,再谈升级。
3. 训练细节与损失函数调优
3.1 预处理与数据划分实操
数据预处理这一步,看起来平淡,实际影响很大。原始图像是灰度图,但读入时可以保留单通道,也可以复制成三通道配合ImageNet预训练权重。我实测下来,三通道输入配合预训练编码器整体更稳,虽然多了一点计算量,但收敛速度和最终精度都比单通道好。
数据划分的细节更不能忽略。先按缺陷类别对应文件列表进行分层划分,保证训练集、验证集、测试集中出现过的各类缺陷数量比例一致。然后做像素级别统计,计算每一类缺陷的像素占总像素的比重,这个比重直接用于加权交叉熵的权重设定。
预处理流程可以收敛成下面几步:
- 读取原图和对应掩膜,统一尺寸到256×256或384×384。
- 灰度图复制成三通道;掩膜标签按0、1、2、3映射为背景、裂纹、夹杂、划痕。
- 对输入做标准化,均值和方差用训练集的统计值,不要用ImageNet的默认值硬套。
- 在线增强时同步对图像和掩膜做同样的空间变换。
3.2 数据增强的控制与切图策略
数据增强是工业分割项目里最需要“克制”的一环。钢材表面缺陷的形态具有明确的物理意义,比如裂纹是细长条,划痕有方向性,如果无脑做大幅旋转和裁剪,可能造出不符合真实场景的样本,反而让模型学到错误模式。
我用了一组比较保守的增强策略:
- 水平翻转、垂直翻转、随机90度旋转,这几项可以放心用。
- 随机亮度对比度调整,幅度控制在0.8到1.2之间,钢材图像的灰度变化本来就不大。
- 随机裁剪,从原图中裁出更小的patch,相当于变相增加样本量。
- 不轻易用弹性形变或随机擦除,前者会扭曲缺陷的细长形态,后者容易把真实缺陷擦掉导致标签错误。
训练时还可以配合多尺度,比如随机把输入缩放到0.75倍到1.25倍,但只做轻微缩放,避免裂纹这类细长结构在缩放后断裂。
3.3 加权交叉熵与Dice Loss的配合
损失函数是这个项目里最值得调的部分。我先说结论:只用交叉熵在极端类别不平衡下基本不行,只用Dice Loss在小目标上又容易震荡,两者按比例相加是最稳的组合。
加权交叉熵的做法是给每个类别一个权重,背景类权重压低,缺陷类权重抬高。比如背景是0.05,裂纹是0.6,夹杂是0.15,划痕是0.2,具体数值根据像素占比反比调整。它的作用是让模型在“背景上犯错”的代价变大,从梯度上直接缓解类别不平衡。
Dice Loss则是从重叠度出发,直接优化预测掩膜和真实掩膜的交并比。它对小目标更友好,因为不管目标多小,只要预测和真实的重叠少了,损失就大。两者相加之后,加权交叉熵负责稳定梯度,Dice Loss负责拉高目标区域的重合度,在SEV这类数据上实测下来mIoU能提升3到5个点。
我推荐用如下组合方式:
import torch import torch.nn.functional as F def weighted_ce_dice_loss(pred, target, class_weights, dice_weight=0.5): ce_loss = F.cross_entropy(pred, target, weight=class_weights) pred_prob = F.softmax(pred, dim=1) # 计算多类别Dice Loss,忽略背景类 dice_loss = 0.0 for cls in range(1, pred.shape[1]): pred_cls = pred_prob[:, cls] target_cls = (target == cls).float() intersection = (pred_cls * target_cls).sum() union = pred_cls.sum() + target_cls.sum() + 1e-6 dice_loss += 1 - (2 * intersection / union) dice_loss /= (pred.shape[1] - 1) return ce_loss + dice_weight * dice_loss这里dice_weight我一般取0.5到1.0,如果发现模型过于激进、误检增多,就把dice_weight调低一点点。
3.4 训练配置与收敛节奏
训练配置直接给一组可复用的参数,基于单张入门级显卡就能跑起来:
- 优化器:AdamW,初始学习率1e-4,weight_decay设为1e-5。
- 学习率调度:CosineAnnealingWarmRestarts,周期T_0=10,T_mult=2,配合early stopping。
- Batch size:8到16,取决于显存,过小会导致梯度噪声大,缺陷样本少的类别难以稳定学习。
- Epochs:60到80,配合early stopping,patience设12。
- 评估频率:每个epoch结束跑一次验证集,记录mIoU、每类IoU、Recall。
收敛节奏上,前10个epoch指标可能涨得很慢,因为模型还在适应类别不平衡;到了20个epoch以后,随着学习率周期重启,指标会有一个明显的台阶式上涨。这时候最容易出现的情况是验证指标光涨不动,训练集指标继续下降,说明过拟合开始,需要及时早停,并考虑加一点轻量正则化。
4. 落地过程常见问题与排查实录
4.1 裂纹这类细长缺陷频繁漏检怎么办
裂纹是三类缺陷里最让人头疼的,因为它细长且占比极低,模型在深层的特征图上经常把它弄丢。我在第一次训练时就遇到了裂纹召回率只有0.3左右的情况,预测掩膜要么缺一段,要么整条都没出来。
排查之后定位到两个原因:一是深层特征图分辨率太低,小目标信息丢失;二是损失函数里裂纹类权重仍然偏小。解决方法是双管齐下,先把裂纹的class weight调高到0.7,让梯度更偏向裂纹;同时把模型输入尺寸从256×256上调到384×384,给裂纹多保留一些像素。
还有一个很实用的技巧,就是给解码器中间层加辅助监督。在U-Net解码器倒数第二层单独接一个分割头,计算它和真实掩膜的加权交叉熵损失,然后和主损失相加。这相当于让中间层也学习“找裂纹”的能力,对细长缺陷的恢复帮助明显。
4.2 背景纹理被误判成缺陷怎么处理
模型训练到中后期,最常见的现象是背景纹理大量被预测成缺陷,尤其氧化皮和辊印区域。这个问题本质上是模型没有足够强的“不是缺陷”的证据,而归因于数据里真实缺陷和疑似缺陷的样本量差距过大。
我的处理方式分三层:
- 数据层:把验证集里误检最严重的疑似缺陷图像挑出来,看它们的掩膜预测结果,如果确实和真实缺陷差异很大,就考虑用这些图像做一次额外训练,相当于hard example mining。
- 模型层:降低Dice Loss的权重,因为Dice Loss对小目标的敏感性会把一些纹理区域也当成目标拉高。
- 后处理层:对预测概率图做条件随机场或简单形态学过滤,把面积小于阈值的连通域直接置为背景。
形态学过滤是最快见效的手段。用OpenCV的connectedComponentsWithStats统计每个连通域的面积,面积小于50像素的直接删除,能在几乎不损失召回率的情况下显著降低误检率。
4.3 训练Loss不降、评估指标原地打转
有几次训练遇到loss卡在某个平台期不动,验证mIoU也纹丝不动。排查时先检查标签和输入是否对齐,其次检查类别权重是否设置正确,最后再怀疑优化器配置。
一个容易被忽略的坑是,标准化时如果直接用了ImageNet的均值方差,而钢材图像本身是灰度、均值方差差异很大,模型可能会花很长时间来适应这种分布偏移。后来我改成统计训练集自己的均值方差,loss平台的周期明显缩短。
另外一个坑是batch size太小。缺陷像素本来就少,如果batch size只有4,一个batch里可能只有一张带缺陷的图,梯度更新不稳定,指标会一直震荡。把batch size提到16之后,训练节奏稳了很多,收敛也更快。
4.4 一张速查表
| 常见问题 | 可能原因 | 排查/解决手段 |
|---|---|---|
| 裂纹漏检严重 | 缺陷像素占比太低、输入分辨率不足 | 提高裂纹类别权重、增大输入尺寸、增加中间层辅助监督 |
| 背景纹理误检为缺陷 | 疑似缺陷样本多、模型过于敏感 | 难例挖掘、降低Dice权重、对预测结果做连通域面积过滤 |
| Loss长时间不降 | 标签错位、标准化参数不合理、batch太小 | 检查数据加载逻辑、改用训练集均值方差、加大batch size |
| 验证指标震荡 | 学习率过大、验证集缺陷样本少 | 调低学习率、使用分层采样构建验证集 |
| 预测掩膜边缘粗糙 | 解码器上采样过于简单 | 换用带注意力模块的解码器,或增加深监督 |
5. 大模型在工业缺陷分割里的定位
最近SAM(Segment Anything Model)系列大模型很火,很多人问能不能直接拿来做钢材缺陷分割。我的观点是:SAM在工业场景里定位应该是“标注辅助工具”,而不是“最终推理模型”。
SAM的zero-shot分割能力在自然图像上很强,但在钢材表面这类灰度纹理图像上,直接推理的边界往往不精准,容易把疑似背景区域一起分割进来,而且它输出的是一类一类的掩膜,没有语义类别信息,你还需要自己判断这个掩膜是裂纹还是划痕。
如果要用,我建议把它用在数据标注阶段。比如先用SAM在无标签的钢材图像上生成候选掩膜,再人工筛选和标注,能显著减少逐像素标注的时间。至于模型推理环节,还是老老实实用自己训练的U-Net或者DeepLabV3这类轻量分割模型,稳定可控、推理快、容易部署。
大模型的价值在于降低数据生产门槛,而不是替代工业场景里的专用模型。这个认知想清楚,后面再做类似项目就不容易被热点带着跑偏。
整套项目跑下来,最深的体会是:工业缺陷分割的难点不在模型结构,而在数据理解。4100张图不算多,但把类别不平衡、疑似缺陷、细长目标这些老问题都暴露了一遍。先想清楚任务要什么,再决定技术方案,然后花耐心去调损失函数和数据策略,最后用合理的评估方式守住可靠性的底线——这套方法论放在其他工业视觉场景里,一样能用。
本文还有配套的精品资源,点击获取