ARTICLE DETAIL

资讯详情

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

工业质检中的图像分割:钢铁缺陷检测实战

工业质检中的图像分割:钢铁缺陷检测实战 简介面向国际数据科学竞赛学习者的钢缺陷检测项目源自谢韦尔钢铁缺陷识别赛题主要任务是利用高频摄像机拍摄的钢板图像对表面缺陷进行准确定位与分类。该方案在全部2427支队伍中排名第345位准确率达89.5%而冠军准确率为90.8%差距较小可作为图像分割与竞赛调优的实战参考。资源包仅12KB共14个文件以5个Python脚本为实现主体辅以4个Markdown说明文档、2个CSV数据文件以及示例图片、NPY数组和Git配置文件结构相当精简。代码按照utilities、tasks、networks、datasets等模块组织覆盖从数据读取、损失函数设计、U型网络与密集连接卷积网络构建到训练评估的完整流程配合说明文档可快速理顺复现思路。当前已有2270人学习浏览适合希望切入缺陷检测竞赛、参考系统化代码组织的中高级数据科学爱好者。1. 项目在做什么一场工业质检的像素级挑战提到 Kaggle 上的 Severstal 钢铁缺陷检测很多做过 CV 方向的朋友应该不陌生。这个比赛的任务用一句话说清楚就是给定一批钢板表面灰度图我们需要把上面的划痕、麻点、压入氧化皮和轧制气泡这四类缺陷用像素级的 mask 标出来。听起来就是经典的分割任务但实际动手之后你会发现工业场景下的数据远比学术数据集要野得多。比赛是 Severstal 公司和 Kaggle 合办的目标很明确——用机器视觉替代传统的人工质检。钢材在热轧、冷轧、运输过程中表面难免会出现各种瑕疵如果靠人眼盯产线效率低不说漏检率还容易波动。所以这个项目本质上是在解决一个非常现实的问题如何在高速产线上自动、准确地找到缺陷位置。因为这是个人赛Kernel Only所有训练和推理都必须在 Kaggle 的 GPU 环境下完成所以策略上得更务实模型不能太复杂推理速度必须可控TTA、多折融合这些技巧的使用也得精打细算。我当时选这条赛道主要看中它麻雀虽小五脏俱全——数据量不大训练集一万两千多张图但该有的坑一个不少类别极度不均衡、标注有噪声、图像分辨率奇葩1600x256、需要 RLE 编解码。啃完这个项目对工业分割类任务的基本套路就算彻底摸清了。无论你是刚想入门 Kaggle 的 CV 比赛还是已经在做工业质检相关项目这篇文章里提到的数据预处理、损失函数设计、后处理策略和踩坑记录都值得码住。下面我按当时实际操作的顺序把整个项目的关键环节掰开揉碎讲一遍。2. 数据是这场比赛的最大Boss2.1 看明白那张1600x256的图第一次看到 Severstal 的训练数据时多数人的第一反应是这长条形的图像怎么回事1600像素宽、256像素高和印象里的方方正正的照片完全不一样。这其实是工业线扫相机的典型输出。钢材在产线上是连续运动的相机固定不动一行一行地扫过去自然就拼出来这种超宽幅的单通道灰度图。彩色信息对这个任务没什么用因为钢材表面的缺陷在灰度图里就已经表现得比较清楚了。另外要留个心眼的是图像虽然长但并不是所有位置都有缺陷。很多图整张都是干净的属于典型的负样本——这也直接导致后面类别不均衡的问题。我当时拿到数据后第一件事就是统计每张图里 mask 的像素占比发现缺陷像素占总像素的比例不到 1%。这就意味着哪怕你输出一张全黑的图模型正确率也能到 99%但没有任何实际价值。2.2 标注格式之谜RLE的编码与解码比赛给的标注不是那种常见的 PNG mask 图片而是 RLERun-Length Encoding格式。很多新手在这里就卡住了。RLE 的核心思想很简单把连续出现的像素段压缩成起点 长度的形式。具体到这比赛标注里给的是起始像素位置 像素长度但这个位置是从第 1 个像素开始计数的注意不是 0位置和长度都按一维数组来处理。图像是 1600x256 的展开成一维就是 409600 个像素位置按照行的顺序依次排列。解码的时候要注意一点RLE 解码出来后要 reshape 成 (256, 1600) 才能得到和原始图像对应的高宽坐标。不少人在这个环节翻车reshape 的维度顺序搞反导致 mask 全错位了还浑然不知。解码代码其实不复杂核心逻辑大概是def rle_decode(mask_rle, shape(256, 1600)): s mask_rle.split() starts list(map(int, s[0::2])) lengths list(map(int, s[1::2])) starts np.asarray(starts, dtypenp.int32) - 1 ends starts np.asarray(lengths, dtypenp.int32) img np.zeros(shape[0] * shape[1], dtypenp.uint8) for lo, hi in zip(starts, ends): img[lo:hi] 1 return img.reshape(shape)编码则反过来把二维 mask 展平找出像素值变化的点按起始位置 连续长度输出。Kaggle 的提交格式要求也是 RLE所以编码函数同样必须写对。建议自己写完之后先用训练集数据做一次编码-解码-再编码的往返测试确认数据没丢再放心往下走。2.3 为什么类别不均衡问题这么棘手Severstal 的比赛里四类缺陷的分布极不均匀。第一类缺陷样本量最多第四类相对较少有些类别在部分折里甚至只有几百张图。这就给训练带来两个直接影响模型很容易偏向样本多的类别少数类 mask 经常学不到位。验证集划分如果不小心某些类别可能在某一折里直接灭绝导致评估指标失真。我试过几种缓解策略逐个说下感受对少数类做过采样简单有效但要注意别让模型过拟合到那几张图上。损失函数加权给少数类更高的权重能解决一部分问题但权重调的不好反而会震荡。先从有没有缺陷这个二分类做起再定位缺陷类别这个思路我在后面细讲也是我最后采用的一阶段方案的基础。最终的方案其实是在图像级别做了一个有缺陷/无缺陷的二分类只有被判为有缺陷的图才做像素级预测。这样做的原因很简单把找不找得到和找得准不准两个目标拆开各自的难度都降低了不少。3. 模型选型与训练U-Net是主线但细节是魔鬼3.1 为什么选U-Net而不是直接堆Transformer分割任务里大家现在默认会想到 U-Net 这类编码器-解码器结构或者用分割 Transformer。实际在 Kaggle 这种有限算力 有限数据的环境下U-Net 的性价比远高于 Transformer 类大模型。原因有三个数据量只有一万两千张图Transformer 动辄上亿参数很容易陷入过拟合。U-Net 的跳层连接可以同时保留高分辨率细节和语义信息对小目标缺陷、细长划痕有天然优势。推理速度快TTA 和多折融合后仍然能控制在提交时间限制内。我在主干网络上试过 EfficientNet-B3、B4 和 SeResNext50最后综合精度和速度选了 EfficientNet-B3。分割头就是标准的 U-Net 解码路径中间加了 scSE 注意力模块用来增强对通道和空间上有响应区域的关注。这些小改动在验证集上大概能涨 0.002~0.004 的 Dice属于积少成多的优化。3.2 输入尺寸怎么定长条图直接推理还是切片这是这个项目最纠结的点之一。1600x256 的输入对常见分割网络来说太扁了直接 resize 到 256x256 会严重损失细节——本来就小的缺陷变得更模糊而且长宽比扭曲会让模型学到错误的形状先验。我尝试了两种方案第一种是直接 resize 到 512x160保持原有的宽高比例。训练速度比较快但缺陷的细节会有损失。第二种是切片。把 1600x256 裁成四段 400x256 的小图每段分别训练和推理最后拼回去。好处是分辨率几乎没有损失小缺陷更容易被捕捉训练时间略增但可控。缺点是需要额外处理边界处被切断的缺陷一个缺陷跨切片的情况。实测下来切片方案在 Dice 上比直接 resize 高 1% 左右而且边界拼接问题可以通过切的时候留 overlap推理完再对重叠区域取均值来缓解。我最终采用的是切片方案overlap 设了 32 个像素也就是每个切片之间重叠 32 像素。3.3 损失函数BCE和Dice怎么配合分割任务最常见的损失就是 BCE 和 Dice Loss 的组合但这个组合的具体实现方式很讲究。Dice Loss 直接优化 Dice 系数但收敛速度慢容易出现训练初期梯度不稳定BCE 的梯度更平滑收敛快但遇到极度不均衡的数据时它对负样本的惩罚力度过大。把两者加权相加可以让训练初期更快找到正确的方向后期精修 mask 边界。权重我调过很多组最终用 0.5 * BCE Dice Loss也就是两者各占一半。这个组合在验证集上表现最稳。Dice Loss 的实现要注意加 smooth防止除零一般设 1e-6 就够了。class BCEDiceLoss(nn.Module): def __init__(self, weight_bce0.5): super().__init__() self.weight_bce weight_bce def forward(self, pred, target): bce F.binary_cross_entropy_with_logits(pred, target) pred_sig torch.sigmoid(pred) inter (pred_sig * target).sum(dim(2, 3)) union pred_sig.sum(dim(2, 3)) target.sum(dim(2, 3)) 1e-6 dice 1 - (2 * inter / union).mean() return self.weight_bce * bce dice这里有个容易踩的坑如果你的 mask 里大部分是全黑的图无缺陷BCE 算出来的值会非常低模型会倾向于输出全黑。所以训练时我刻意把图像级别的有缺陷/无缺陷分类 loss 和分割 loss 分开计算避免这两个目标互相干扰。4. 训练策略与调参实录4.1 两阶段还是端到端我为什么选了端到端我前面提到可以把有无缺陷和缺陷位置分开处理实际操作里有两个方案方案A先训练一个图像分类模型判断有没有缺陷有缺陷的图再送入分割模型。这叫两阶段方案优点是分工明确但推理时间翻倍而且分类模型的误差会直接传导到分割阶段。方案B端到端训练一个模型输出 4 个通道的 mask每类一个通道同时在分割头之上接一个全局池化 分类头让模型同时学习有没有和在哪里。推理时分类头判定为无缺陷的图直接输出空 mask节省后处理时间。我最终选了方案B。原因很实际端到端训练时分割和分类可以共享特征提取器训练时间节省很多推理时只跑一次前向速度也更快。实测下来方案B在验证集上的 Dice 比方案A高一点点而且逻辑上更优雅。4.2 训练关键参数和验证策略训练参数这一块我直接给出可复现的配置优化器Adam学习率初始 3e-4配合 CosineAnnealing 调度器。Batch Size16四块 K80 跑的话要降到 8视显存调整。Epoch30 轮左右前面加 3 轮 warmup。图像增强水平翻转、垂直翻转、随机亮度对比度、随机裁剪。因为钢板纹理有方向性没有做随机旋转。验证方式5 折分组按 ImageId 分组防止同一张图的不同切片同时出现在训练和验证集里。我这里要特别强调一下验证集划分的坑。如果直接按行随机划分同一个钢板的不同视角图会同时出现在两个集合中验证分数会虚高看起来是 0.78实际上线只有 0.74。必须按 ImageId 或者按视频帧分组去重后再划分得到的分数才可信。另外Kernel Only 比赛还有个现实约束Kaggle 的 GPU 时间有限模型训练不能太贪。我最后只训练了 5 折中前 3 折的模型后两折用来做验证调参这样既保证时间够用又能有足够数据评估稳定性。4.3 TTA 和后处理的经验TTATest Time Augmentation几乎是这类分割比赛的必选项。我用了水平翻转 垂直翻转双重 TTA四个结果取平均。实测能提高 0.003~0.005 的 Dice代价是推理时间翻四倍。如果你的时间预算吃紧至少要加水平翻转这一项性价比最高。后处理这部分最常见的坑是预测的 mask 非常碎很多小噪点被误判为缺陷。我做了两个过滤根据连通域面积过滤面积小于 50 像素的区域直接丢掉。这种碎点大概率是噪声。根据类别做差异化阈值训练集里出现频率高的类别阈值可以稍微低一点比如 0.45频率低的类别阈值高一点比如 0.55减少误报。还有个小技巧是预测结果在提交前要转成 RLE这个过程不要用循环直接用 numpy 的扁平化操作速度能快几十倍不然推理 5000 张图光是编码环节就要跑十几分钟。5. 踩坑记录那些让人想砸键盘的瞬间这个比赛我前前后后跑了三版方案踩了不少坑。挑几个印象最深的说希望你看到的时候能直接避开。第一个坑是 RLE 解码时数组索引从 1 开始算。这个细节埋得很深因为你第一次跑通代码时模型输出的 Dice 看着挺正常的但其实 mask 整体往右偏了一个像素。这种偏差在小缺陷上影响不大但在细长划痕上会让 Dice 直接掉 3 个点。排查办法很简单随机抽几张图把解码后的 mask 叠在原始图上可视化肉眼看一眼对齐情况。第二个坑是训练时忘了做 mask 的数值归一化。PIL 打开标注图后像素值默认是 0 或 255如果忘了除以 255损失函数里的 BCEWithLogits 会怎么算都不对——因为 target 的取值范围不在 [0,1]。我这个低级错误耗了整整一个下午最后还是可视化时发现 mask 变成了奇怪的灰色才意识到。第三个坑是关于验证集分数的自信。我有一版模型验证集 Dice 0.79提交之后线上分数直接掉到 0.73。排查之后发现是图像预处理 pipeline 不一致训练时做了归一化减均值、除方差推理时忘了做同样的操作。这种低级错误在比赛高压下特别容易犯所以从头到尾保持一个统一的预处理函数非常重要最好把训练和推理都封装成同一个类。第四个坑是伪标签。我在比赛后期试着用测试集预测结果做伪标签试图提升模型泛化能力。结果发现因为测试集本身噪声不小伪标签的错误会随着训练放大Dice 反而降了。后来在别的比赛里我也试过伪标签除非你有非常确信的置信度阈值过滤机制否则不建议贸然用。第五个坑是 TTA 的时候忘了同步做 mask 的反变换。TTA 是把原始图翻转后预测再把预测 mask 翻转回来取平均。翻转 mask 这一步如果忘了做相当于预测结果整体空间错位指标直接崩盘。我当时写了自动化封装函数把图像增广和 mask 增广绑在一起才彻底杜绝了这个隐患。6. 最后的思考这个项目带来的不只是排名说实话这个比赛本身的排名我记得已经有点模糊了但这个项目在很多方面给我的收获比单纯排名更长久。第一是理解了工业质检的真实痛点。你有没有想过为什么产线上的钢材表面缺陷检测要用 1600x256 这种长条形图因为相机装的位置固定钢材是流动的采集到只能是这种横向连续的图像。这种物理约束直接决定了数据格式和预处理方式。比赛的价值就在于逼你去理解这些约束而不是天真地用 256x256 的正方形图片硬套。第二是学会了在有限资源下做取舍。Kaggle 比赛拼的不只是模型上限还有工程效率。你可以训练一个巨大的模型然后靠分布式推理苟到高名次但现实中产线上的推理设备往往很简陋模型必须轻量又高效。这个项目的很多决策EfficientNet-B3 而不是更大、切片而不是直接放大、TTA 和模型数量之间的权衡本质上都是资源受限下的优化问题。第三是验证策略的严谨性直接决定你能走多远。我见过很多朋友在本地验证集上跑出漂亮数字一提交就被打回原形然后开始疯狂调参。但真正的问题是数据泄漏——训练集和验证集的划分方式不合理。如果你能从项目第一天就把验证集划分方式做对后面的每一步都会更加踏实。如果你也想拿这个比赛练手建议不要只看我这篇复盘一定自己动手跑一遍数据处理流程把所有可视化做出来再上模型。Kaggle 上的 Elite 选手从来都不是靠模型打架赢的他们赢在理解和处理数据的功力上。等你把 RLE 编解码、类别不均衡、验证集划分这些小细节都吃透之后再去碰更大规模的分割任务你会发现很多套路其实是相通的。本文还有配套的精品资源点击获取
返回列表