
简介针对钢材表面缺陷检测与分割竞赛这套Python项目源码从数据增强、模型设计到训练评估提供了完整闭环面向参赛选手和工业视觉开发者帮助快速复现赛题方案。作者重点分享了对B、C数据集的剖析过程识别出对角线拼接、左右/上下拼接、椒盐噪声、左上角不规则矩形覆盖以及亮暗度变化等多种增强手法并敏锐指出C榜mIoU普遍高于A榜背后的数据泄露现象——B、C中的原始图像同样出现在A榜训练与测试集中这一结论对理解榜单差异、合理设计验证策略有直接帮助。资源共7个文件5个Python脚本分别实现模型结构、在线数据增强、lovasz损失、训练逻辑和带增强的评估流程1个预训练权重pth可直接加载使用1份Markdown文档说明整体思路。压缩包仅3.5MB轻量易部署。目前已有197人学习下载适合正在备战钢材表面缺陷分割比赛或从事工业视觉检测的开发者参考资源虽小但覆盖建模、训练、评估全流程性价比很高。1. 钢材表面缺陷检测与分割从“框出缺陷”到“画出轮廓”传统机器视觉处理钢材表面缺陷时靠的是光照控制、边缘算子和人工设计的纹理特征换一条产线几乎就要重调整个检测流程。深度学习把这个过程压缩成“标注加训练”但目标检测输出的是矩形框而实际质检需要的是缺陷的精确轮廓——划痕的延展范围、麻点的密集区域、裂纹的走向矩形框远远表达不了这些信息。分割任务对每个像素做类别判断让模型能够输出任意形状的缺陷区域这也正是表面缺陷检测竞赛从纯检测转向像素级分割的原因。围绕钢材表面缺陷检测与分割的赛题评分通常看 mIoU 与 Dice跑通完整方案涉及数据预处理、分割模型训练、后处理与调参。下面按一条可复现的路径把 Python 代码、配置参数和文档说明里容易被忽略的细节一起过一遍适合准备在缺陷赛道快速出分的开发者。2. 数据预处理从六类缺陷样本到训练掩码2.1 缺陷数据集的分布特点最常见的公开钢材表面缺陷数据集是 NEU-DET包含六类缺陷裂纹crazing、夹杂inclusion、斑块patches、麻点pitted_surface、轧制氧化皮rolled-in_scale、划痕scratches每类 300 张共 1800 张灰度图单张分辨率 200×200 像素。竞赛主办方有时会扩展分辨率、另增未标注测试集或附带一批弱标注子集。这六类缺陷的形态差异非常大裂纹是细长线状且对比度低麻点是密集分布的小圆坑氧化皮表现为大面积的低灰度差块。这种分布直接决定了后续模型选择——要在细线召回和大面积边界精度之间做平衡。样本层面最先要处理的问题是类别像素不平衡。缺陷像素往往只占一张图的 2%10%大多数像素是背景。直接用交叉熵训练网络会快速收敛到“全背景”预测。解决方向通常有两个设计对不平衡不敏感的损失函数以及在数据划分时保持各折类别比例一致。不少初学者一上来就调模型结构回头观察训练曲线才发现问题出在类别不均衡上白白浪费时间。注意划分训练集/验证集时不要直接按文件名的顺序随机 split。原始数据集常常把同类缺陷连续存放简单随机划分可能导致某一折缺失某个类别的样本验证指标失真。先按图像的主缺陷类别做分层抽样再在各折内部打乱是我在竞赛里的稳定做法。2.2 从 COCO 多边形标注生成训练掩码竞赛的标注格式通常是 COCO JSON 或 VOC XML。就算赛题名称里带分割许多时候 JSON 里也只有 segmentation 的多边形坐标。把多边形解析出来填充成单通道掩码是最稳妥的路径。我习惯把转换逻辑单独写成脚本放在项目 preprocess 目录下因为相比反复读取大型 JSON事先把所有掩码落盘成 png训练时加载速度会成倍提升。import json import numpy as np import cv2 from pathlib import Path def coco_polygon_to_masks(json_path: str, out_dir: Path) - None: 读取COCO格式JSON将segmentation中的多边形逐图绘制到掩码。 掩码像素值等于category_id背景固定为0。 with open(json_path, r, encodingutf-8) as f: data json.load(f) img_map {img[id]: img for img in data[images]} masks {} for ann in data[annotations]: img_info img_map[ann[image_id]] h, w img_info[height], img_info[width] mask masks.setdefault(ann[image_id], np.zeros((h, w), dtypenp.uint8)) if isinstance(ann[segmentation], list): for raw_poly in ann[segmentation]: poly np.array(raw_poly, dtypenp.float32).reshape(-1, 2) cv2.fillPoly(mask, [poly.astype(np.int32)], colorann[category_id]) else: # RLE编码用pycocotools解码竞赛里偶尔出现 from pycocotools import mask as mask_utils decoded mask_utils.decode(ann[segmentation]) mask[decoded 0] ann[category_id] for img_id, mask in masks.items(): filename img_map[img_id][file_name] cv2.imwrite(str(out_dir / (Path(filename).stem .png)), mask)这里的 masks 字典按 image_id 缓存每一张大图掩码处理过程中不断填充最后统一写盘避免反复分配大数组。segmentation 为 list 时坐标是展平的一维数组reshape 成 (-1, 2) 后交给 cv2.fillPoly如果同一个目标带内外环每个闭合轮廓会作为 list 中一项逐段填充即可。segmentation 为 dict 时表示 RLE 编码需要 pycocotools 的 mask_utils.decode 还原。填充颜色直接用 category_id意味着掩码图就是 CrossEntropyLoss 所需的 target 格式训练时不需要额外编码。2.3 数据增强参数与缺陷形态适配分割的数据增强和分类不同图像变换必须同步作用于掩码。albumentations 以组合方式管理这种绑定最方便import albumentations as A train_transform A.Compose([ A.RandomResizedCrop(height256, width256, scale(0.7, 1.0), p0.5), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.15), A.ShiftScaleRotate(shift_limit0.08, scale_limit0.15, rotate_limit45, p0.7), A.ElasticTransform(alpha24, sigma3, alpha_affine2, p0.2), A.ColorJitter(brightness0.15, contrast0.15, saturation0.05, p0.5), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ])解释几个容易改错的参数。RandomResizedCrop 的 scale 下限定在 0.7太低会让裂纹在裁剪时被切断造成掩码语义不完整。VerticalFlip 只给 0.15因为钢材轧制方向让缺陷大多沿水平延伸频繁上下翻转会破坏这种先验。ShiftScaleRotate 的 rotate_limit 设 45 度而不是 180 度同理是为了保留缺陷的自然方向分布。ElasticTransform 对裂纹这类细长目标能模拟真实应力形变但对麻点这类密集小目标要降低概率否则圆点被拉成椭圆后网络学到的形态和真实标注不一致验证时反而掉点。增强操作推荐概率对缺陷分割的影响HorizontalFlip0.5几乎无负面影响基础增强VerticalFlip0.15保留轧制方向先验ElasticTransform0.2模拟应力形变利于裂纹类ColorJitter0.5改善光照泛化亮度范围以 0.15 为宜3. 分割模型选型与损失函数设计3.1 编解码结构应对的两类难点当前主流的语义分割模型几乎都是编码器加解码器结构。钢材表面缺陷的自动分割要同时处理两种难点一是缺陷与背景在灰度分布上高度重叠比如 rolled-in_scale 的边缘几乎看不清二是同一类别内部形态变化大裂纹可能只有几个像素宽。编码器通过对图像做多级下采样让高层特征图聚合足够大的感受野以此区分弱边界下的缺陷区域解码器再逐步恢复边缘细节。这种结构之所以在缺陷检测里经得起考验是因为网络可以自己决定在哪一层保留类别语义、在哪一层保留空间细节完全不依赖人为设计的纹理基元。3.2 baseline 选型、灰度输入与分辨率设定模型选型有一个经验性规则。数据规模不足 5000 张时U-Net 配合 ResNet34 编码器当 baseline参数量小不容易过拟合解码器通过通道拼接获得较多空间细节对裂纹这类小目标友好。DeepLabV3 的 ASPP 模块在多尺度缺陷场景下更有优势但训练开销和显存占用明显增加适合数据充足的情况。实际竞赛中两个模型都值得跑通先拿 U-Net 出分再用 DeepLabV3 在同样配置下对比取各折 mIoU 更高的方案进入后处理阶段。编码器加载 ImageNet 预训练权重是标准操作NEU-DET 这类灰度数据集需要先转换成三通道才能输入。直接复制三份会让初始卷积的响应与预训练预期不一致更好的做法是把预训练 conv1 的三通道权重取均值后复制到单通道import torch import torchvision.models as models def load_grayscale_resnet(arch: str resnet34) - models.ResNet: model getattr(models, arch)(weightsmodels.ResNet34_Weights.IMAGENET1K_V1) state model.state_dict() key conv1.weight if key in state and state[key].shape[1] 3: w state[key].mean(dim1, keepdimTrue) # 取通道均值 state[key] w model.load_state_dict(state) return model取均值比直接复制任一通道更接近预训练特征层的响应中心等于用平均灰度输入替代原三通道输入保留更多预训练统计特性。如果后续从零训练base_lr 要同步下调到 0.0003 附近。另一个容易被忽略的参数是训练分辨率。NEU-DET 原始尺寸 200×200输入到带四层下采样的编码器后特征图只有 13×13对裂纹来说每个像素都很关键。显存允许时把训练分辨率从 256 提高到 384 或 512细长缺陷的 Recall 通常明显上涨代价是训练时间按平方增长。习惯做法是先以 256 跑通全流程再用更高分辨率做第二轮增量训练。3.3 损失函数组合与超参数行为缺陷分割最常见且最稳定的损失组合是 CrossEntropy 加 DiceLoss按 1:1 加权相加。CE 负责保留类别区分度Dice 负责抑制背景像素主导的问题。下面这份实现可以不改直接用于六类缺陷任务import torch import torch.nn as nn import torch.nn.functional as F class DiceLoss(nn.Module): def __init__(self, smooth: float 1.0): super().__init__() self.smooth smooth def forward(self, logits: torch.Tensor, targets: torch.Tensor) - torch.Tensor: probs F.softmax(logits, dim1) targets_onehot F.one_hot(targets, num_classesprobs.shape[1]) targets_onehot targets_onehot.permute(0, 3, 1, 2).float() intersection (probs * targets_onehot).sum(dim(2, 3)) union probs.sum(dim(2, 3)) targets_onehot.sum(dim(2, 3)) dice (2 * intersection self.smooth) / (union self.smooth) return 1.0 - dice.mean() class MixedLoss(nn.Module): def __init__(self, ce_weight: float 1.0, dice_weight: float 1.0): super().__init__() self.ce nn.CrossEntropyLoss() self.dice DiceLoss() self.ce_weight ce_weight self.dice_weight dice_weight def forward(self, logits, targets): return self.ce_weight * self.ce(logits, targets) \ self.dice_weight * self.dice(logits, targets)smooth 默认 1.0 是常用起点作用是防止目标类别在某个 batch 里完全缺失时除零。如果训练后期细长缺陷在预测图上频繁断裂把 ce_weight 下调到 0.5给 Dice 更多权重让网络更聚焦区域重叠如果出现早停式全背景收敛说明 CE 权重偏小先调回 1.0 观察。Focal Loss 对难例重加权适合麻点、裂纹这类像素占比小但漏检率高的缺陷但 alpha 和 gamma 两个超参数调起来耗时我一般等 baseline 稳定后再用验证集小范围搜索 alpha 0.250.5、gamma 12 的组合决定是否替换 CE。损失函数特点风险适用场景CrossEntropy收敛稳定背景占优时偏向全背景作为基础项保留Dice Loss直接优化区域重叠梯度平缓、边界粗糙与 CE 搭配缓解不平衡Focal Loss侧重难例alpha、gamma 调参成本高裂纹、麻点等易漏检类别4. 训练配置、验证策略与调参路径4.1 用 YAML 组织分割实验竞赛实验动辄十几组对比超参数散落在 Python 脚本里很难回溯。常见做法是把实验配置抽成 YAML 文件训练脚本只负责加载配置并据此构建数据加载器、模型和优化器data: train_image_dir: ./data/train_images train_mask_dir: ./data/train_masks val_image_dir: ./data/val_images val_mask_dir: ./data/val_masks input_size: [256, 256] num_classes: 7 model: name: unet encoder: resnet34 pretrained: true train: batch_size: 16 epochs: 60 optimizer: adamw base_lr: 0.001 weight_decay: 0.01 scheduler: poly poly_power: 0.9 loss_ce_weight: 1.0 loss_dice_weight: 1.0 amp: true ema: true seed: 42这套配置的含义数据层面固定输入尺寸 256×256、类别数 7六类缺陷加背景模型采用 U-Net 加 ResNet34 预训练编码器训练层面用 AdamW、0.001 基础学习率、weight_decay 0.01、poly 调度、两项损失权重各 1.0同时开启混合精度与 EMA。seed 固定是为了让多次实验在同一数据划分下可比。注意 input_size 在 YAML 里写成 list读取后要显式转成 tuple 再传给 transform 与模型否则依赖 torchvision 的 transform 模块对 list 和 tuple 的处理不一致会引发隐蔽的类型错误。4.2 优化器参数、学习率调度与混合精度AdamW 与 Adam 的核心差别在权重衰减的注册方式前者把 decay 从动量中剥离在编解码结构里对 encoder 微调更稳定。base_lr0.001 是带预训练 backbone 模型的常规起点。poly 调度在每个 batch 更新学习率公式为 lr×1−iter/total_iters)^power其中 power 取 0.9相比 StepLR 能更平滑地过渡到后期精细调整阶段这对分割边界的收敛非常有帮助。混合精度训练的收益在分割任务上很明显显存减半、batch 翻倍训练速度在支持 TensorCore 的 GPU 上提升约 1.5 到 2 倍。常见的坑在 loss_scaler 的动态范围如果前几个 step 出现 inf把 GradScaler 的 init_scale 调低一档或者先检查模型输入是否存在 NaN不要急着改学习率。参数建议值说明base_lr1e-3decoder 用 2e-3encoder 保持 1e-3 或更低weight_decay0.01与 AdamW 搭配0.05 会拖慢收敛batch_size16 256px开启 AMP 后可尝试 32epochs60100超过 100 轮小模型开始过拟合ema_decay0.99验证切换 ema 权重通常涨 0.5 mIoU 以上4.3 五折交叉验证与 mIoU 评估细节单折验证在缺陷这类小规模数据集上波动常在 ±2 mIoU 以上因此竞赛里的稳定做法是五折交叉验证。切分时的两个细节先固定随机种子再用 StratifiedKFold 按图像主类别分层保证每折六类比例接近。如果一张图含多类缺陷可取面积占比最大的类别作为近似标签。from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for fold, (train_idx, val_idx) in enumerate(skf.split(image_paths, primary_labels)): print(fFold {fold}: train{len(train_idx)}, val{len(val_idx)}) # 在该折内训练保存 fold_{fold}.pth验证时使用原始尺寸推理不要沿用训练时的 256×256 resize否则会在评估中引入额外插值误差。如果单图过大按 patch 滑动推理后拼接而不是直接 resize。另一个容易踩的细节是竞赛指标如果只统计六类缺陷而不包括背景mIoU 计算必须显式排除索引 0写评估脚本时容易忽略。部分赛题还会对每类缺陷单独统计 IoU 再取平均如果方案在 rolled-in_scale 这类难例上特别弱五折均值会被它显著拖低这时优先处理难例类别比整体调参更有效。4.4 冻结编码器与 EMA 更新随机初始化的解码器在训练初期产生大梯度如果编码器同时以全学习率更新预训练权重很容易被冲掉。常见做法是训练起始冻结编码器只更新 decoder 约 5 个 epoch让解码器先建立基本输出再解冻编码器用小学习率微调。实现时通过设置 encoder 参数组的 requires_gradFalse 即可。EMA 是另一个简单且有效的稳定技巧维护一份参数的滑动平均副本验证时加载 EMA 权重而不是最新权重往往能比最后一轮权重高 0.5 个点以上。def update_ema(ema_model: nn.Module, model: nn.Module, decay: float 0.99): with torch.no_grad(): for ema_param, param in zip(ema_model.state_dict().values(), model.state_dict().values()): ema_param.copy_(ema_param * decay param * (1 - decay))decay 取 0.99 表示用最近约 100 个 step 做有效平均取 0.999 则更关注长期历史。建议第一个 epoch 结束后再启动 EMA避免初始训练波动污染平均值。5. 推理后处理TTA、面积过滤与模型集成5.1 与多尺度推理测试时增强TTA在竞赛里通常能带来每折 0.51.5 个 mIoU 的收益做法是推理时对输入做水平翻转和多种尺度缩放把多份 softmax 概率平均后再取 argmax。对于 patches、rolled-in_scale 这类尺度多变的缺陷多尺度效果好对细长裂纹插值会让预测在断裂处被进一步弱化因此 TTA 的尺度种类不是越多越好。我一般固定三个尺度 0.8、1.0、1.25加一次水平翻转再在验证集上确认每类增益。def tta_predict(model, image: torch.Tensor, scales(0.8, 1.0, 1.25)) - torch.Tensor: h, w image.shape[-2:] prob_sum None with torch.no_grad(): for scale in scales: x F.interpolate(image, scale_factorscale, modebilinear, align_cornersFalse) logits model(x) logits torch.flip(model(torch.flip(x, dims[3])), dims[3]) logits logits / 2 probs F.softmax(logits, dim1) probs F.interpolate(probs, size(h, w), modebilinear, align_cornersFalse) prob_sum probs if prob_sum is None else prob_sum probs return prob_sum / len(scales)5.2 连通域面积过滤与细线保护分割输出中常见一类噪声零星的孤立小区域通常是对背景纹理的误判。按类别做连通域分析并过滤面积小于阈值的区域是主流后处理import numpy as np from skimage import measure def filter_small_regions(mask: np.ndarray, min_area: int 16, class_ids: tuple (1, 2, 3, 4, 5, 6)) - np.ndarray: 按类别过滤面积小于min_area的连通域。 out np.zeros_like(mask) for cls in class_ids: binary (mask cls).astype(np.uint8) if binary.sum() min_area: continue labeled measure.label(binary, connectivity2) for region in measure.regionprops(labeled): if region.area min_area: out[labeled region.label] cls return outmin_area16 对应 4×4 像素的小噪点在 200×200 尺度下既能清除多数虚警又不会过度损伤细长裂纹。如果过滤后裂纹掩码断裂加重可以对裂纹类别单独把 min_area 下调到 8或者跳过该类的过滤。另一个容易漏掉的做法是结合灰度图的亮暗先验把过滤后保留的区域回贴到原图上做肉眼检查防止网络学到偏离标注的规律。5.3 集成预测与可视化输出U-Net 与 DeepLabV3 分别对细节和多尺度更敏感把它们输出的 softmax 概率逐像素平均通常比同构模型集成更能补足盲区。实现时保持每个模型的输入尺寸一致输出 logits 先软最大化再插值到相同分辨率最后平均并 argmax。可视化阶段用 cv2.addWeighted 把预测 mask 叠加到原图按类别着色然后保存成 PNG这样一张图就能定位问题噪声来自哪类、边界是否锯齿、细线是否断裂比盯着 mIoU 数值更直观。提交材料里最好同时附一份简短文档说明写清楚数据划分方式、模型结构、TTA 和后处理的执行顺序这份说明本身也是竞赛评审中判断方案完整度的重要依据。注意提交预测掩码一律保存为 PNG 或主办方要求的 RLE 编码不要用 JPG。JPG 的有损压缩会给裂纹轮廓带来锯齿和断点在 mIoU 评估中白丢分数。本文还有配套的精品资源点击获取