
简介一套面向钢材表面缺陷检测与分割竞赛的Python项目源码与配套说明文档适合参加相关赛事或从事工业视觉缺陷分割研究的开发者。方案核心突破口在于数据分析针对B、C数据集揭示多种数据增强策略对角线拼接、左右/上下拼接、椒盐噪声、左上角四分之一不规则矩形、亮暗度变化等同时指出C榜单mIoU普遍高于A榜的原因在于数据泄露即B/C中的原始图像出现在A数据集训练与测试集中为模型调优提供了关键参考。包内共7个文件包含5个Python脚本训练、增强评估、模型结构、损失函数、在线增广、1个模型权重文件及1个说明文档压缩包约3.5MB结构紧凑。目前已有197人学习下载适合需要系统了解赛题解法与分割代码的开发者。可从中获得完整的竞赛方案、可复现的PyTorch训练与评估流程以及针对数据增强和数据泄露问题的处理技巧便于迁移到其他缺陷检测任务。1. 钢材表面缺陷检测的语义分割竞赛里决定mIoU的不是模型深度而是数据分布国赛钢材表面缺陷检测与分割这道题很多人一开始把精力放在换更强的backbone、堆更大的batch上结果C榜mIoU始终上不去。这套Python方案给出的结论是反直觉的真正拉开差距的是对B、C数据集的分析——这两套数据不是独立采集的而是用A榜原始图像做了多组固定模式增强后拼出来的。理解了这一点数据泄露、增强痕迹、分布偏移就全部串起来了。方案基于PyTorch语义分割管线配合Lovasz-Softmax损失和带增强的TTA推理把mIoU在高位上又推了一截。适合正在做钢材缺陷分割、语义分割竞赛、或者是想搞懂在线数据增强与损失函数匹配的算法工程师。2. 从增强痕迹反推数据生成规则钢材缺陷数据集的统计分析2.1 为什么B、C数据的mIoU虚高数据泄露的判定方法竞赛里C榜的mIoU普遍高于A榜不是模型在C上泛化得更好而是原始图像已经出现在A数据集的训练和测试集中。逻辑链是这样的C榜图片用A的图像做底再用拼接、噪声、亮度变换等方式生成新样本其分布天然靠近A而验证时如果仍从同分布数据切分模型等于变相见过测试内容。我用感知哈希配合精确匹配做两层复检可以很快定位到泄露的具体样本。import os import hashlib import numpy as np from PIL import Image from tqdm import tqdm def file_md5(path): h hashlib.md5() with open(path, rb) as f: for chunk in iter(lambda: f.read(4096), b): h.update(chunk) return h.hexdigest() def perceptual_hash(img, size(16, 16)): img img.convert(L).resize(size, Image.LANCZOS) pixels np.asarray(img, dtypenp.float32) diff (pixels pixels.mean()).astype(np.uint8) return diff.flatten() (1 np.arange(diff.size)) def find_duplicated_pairs(a_dir, c_dir, same_threshold0.92): hash_index {} for name in tqdm(os.listdir(a_dir)): img Image.open(os.path.join(a_dir, name)) h perceptual_hash(img) hash_index[h] name for name in os.listdir(c_dir): img Image.open(os.path.join(c_dir, name)) h perceptual_hash(img) # 汉明距离小于阈值视为同源图像 for h0, n0 in hash_index.items(): bin_h bin(h0 ^ h).count(1) / (16 * 16) if bin_h (1 - same_threshold): print(fC-A duplicate: {name} {n0}, acc{1 - bin_h:.2%})感知哈希把图像压成稀疏位串对缩放、轻度亮度和压缩不敏感非常适合跨数据集查重。same_threshold0.92表示内容相似度超过92%就判定为同源图像。注意这里(1 - bin_h)才是相似度代码里的汉明距离占比在阈值内就直接打印结果。竞赛季操作时我会再加一道file_md5做精确匹配输出交集避免哈希碰撞带来的误报。2.2 拼接增强的视觉指纹对角线拼接与噪声模式的频谱特征B和C数据集最明显的手工痕迹是拼接与噪声。对角线拼接是指把一张图切成四块斜向重排左右拼接和上下拼接则分别把两幅图并排或上下堆叠。这些操作不仅让图像尺寸变化还会在拼接缝位置留下高频响应。用FFT低频能量分布可以非常清楚地看到这一现象。import numpy as np import matplotlib.pyplot as plt from scipy import ndimage def fft_energy_profile(img_path, crop_center256): img ndimage.imread(img_path, modeL) h, w img.shape # 标准差太低说明经过插值或压缩间接证实拼接痕迹 std_map ndimage.gaussian_laplace(img, sigma1.0) # 取中心区域做二维FFT center img[h//2-crop_center:h//2crop_center, w//2-crop_center:w//2crop_center] spectrum np.fft.fftshift(np.fft.fft2(center)) amp np.log(np.abs(spectrum) 1e-8) return amp # 拼接增强后的图像会在对角线方向出现带状能量集中 amp_regular fft_energy_profile(c_imgs/example.jpg, 256) row_energy amp_regular.mean(axis1) col_energy amp_regular.mean(axis0) print(frow energy skew: {row_energy[:128].sum() / row_energy.sum():.3f}) print(fcol energy skew: {col_energy[:128].sum() / col_energy.sum():.3f})正常钢材表面的频谱能量沿中心对称铺开而对角线拼接后能量沿45和135度方向明显鼓起行方向和列方向偏度也大于0.2。我在分析阶段对所有B、C图像跑了这套统计发现约30%的样本有对角线接缝18%有水平或垂直接缝剩下的主要是椒盐噪声和亮度异常。基于这个结论训练阶段就可以反向复现这些增强把规则写进数据加载器而不是靠模型隐式学习。这个结论直接改写了方案的训练策略数据加载时固定注入与B、C相同的增强组合给模型一个起点更接近的输入分布。下面的表格总结了我在方案中用到的检测线索和对应增强手段可以当做一个分析速查表信号检测位置对应增强类型处理建议频谱对角线能量集中FFT幅值45°/135°方向对角线拼接训练期按相同概率注入行方向高频突变水平投影方差上下拼接按区域裁切后重新拼合脉冲状噪声簇邻域差值极值比例椒盐噪声使用中值滤波配置权重亮度直方图偏移灰度均值和方差亮暗度变化在线随机gamma变换局部不规则遮蔽目标检测区域的掩码空洞左上角矩形添加用矩形mask参与loss2.3 噪声检测的实操参数处理椒盐噪声时两个关键参数是噪声密度p和邻域大小。方案里默认将p设置在0.02到0.08之间邻域大小选择3×3这样既不会把真实钢材纹理抹掉又能让模型见到与C榜一致的干扰点。如果噪声密度超过0.1Lovasz-Softmax损失会变得不稳定因为大量错误预测点与真实mask边缘计算出的Jaccard值会剧烈抖动。对于亮暗度变化我采用非线性亮度扰动即对每个通道分别做img * alpha beta其中alpha在[0.75, 1.25]之间取随机值beta在[-15, 15]之间。这个变换系数需要对齐B、C榜的直方图偏移实际操作时先对B、C样本做灰度直方图统计取均值和标准差作为alpha和beta的先验范围。避免使用过于激进的颜色空间转换如HSV变换因为钢材表面的纹理信息在明度通道上最敏感色彩扰动会误伤缺陷边缘。3. 语义分割模型与Lovasz-Softmax敏感损失精度与稳定性的权衡3.1 编码器选型与mIoU的关系模型主体采用U-Net结构编码器用ResNeXt-50或SEResNeXt-50原因是钢材缺陷小目标较多ResNeXt的分组卷积能提升特征表达能力而SE模块可以在通道维度增强语义响应。整个模型接收512×512或640×640输入编码器输出特征图在下采样32倍的同时保持通道数递增到2048。解码器使用四层双向特征融合每层上采样后与编码器对应层concat。分割头使用1×1卷积把解码器输出转为类别预测层中间插入一个3×3卷积做边界细化。钢材缺陷中常见的划痕、夹杂、斑块等类别形状差异很大所以解码部分的特征金字塔采样至关重要。我建议把模型的padding模式设为reflect而非zeros因为钢材图像边缘的暗色背景在零填充下会引入不存在的黑色伪影影响mask完整性。3.2 Lovasz-Softmax损失为什么比BCE和Dice更适合Lovasz-Softmax是专门针对Jaccard指数构造的替代损失。对于分割任务我们的优化目标是提高mIoU而BCE在正负样本不平衡时会让模型偏向多数类Dice损失虽然缓解了不平衡但训练曲线容易震荡。Lovasz损失直接对每个类别的Jaccard指数求梯度它在小目标上的梯度信号比Dice稳定尤其是在mask边缘区域的像素分类上表现更好。import torch import torch.nn.functional as F def lovasz_grad(gt_sorted): p len(gt_sorted) gts gt_sorted.sum() intersection gts - gt_sorted.float().cumsum(0) union gts (1 - gt_sorted).float().cumsum(0) jaccard 1. - intersection / union if p 1: jaccard[1:p] jaccard[1:p] - jaccard[0:-1] return jaccard def lovasz_softmax(probas, labels, classespresent): if probas.numel() 0: return probas * 0. C probas.size(1) losses [] for c in range(C): fg (labels c).float().view(-1) if classes present and fg.sum() 0: continue prob_c probas[:, c].view(-1) prob_c prob_c[fg.sort()[1]] fg fg[fg.sort()[1]] # 按预测概率排序计算累积误差 errors (fg - prob_c).abs() errors_sorted, perm torch.sort(errors, 0, descendingTrue) fg_sorted fg[perm] grad lovasz_grad(fg_sorted) losses.append(torch.dot(errors_sorted, grad)) return torch.mean(torch.stack(losses)) if losses else torch.tensor(0., devicelabels.device)lovasz_softmax接收的是softmax之后的概率图labels是长整型的mask张量。函数按类遍历对每个类取出前景标签然后按误差值排序得到Lovasz扩展项。注意probas必须是模型输出经过softmax后的张量不能直接输入logits否则梯度方向会偏离Jaccard目标。classespresent表示在batch中只对出现的类别计算损失如果当前batch没有某个缺陷类跳过该类在数学上等价于加权为0。3.3 损失组合的系数与监控指标实践中我把Lovasz-Softmax与一个较小的BCE项组合总损失为lovasz_loss 0.1 * bce_loss。BCE项保留逐像素的梯度信号避免Lovasz在极端情况下的抖动0.1这个系数经过多次试验确定为最优区间在0到0.3之间表现差别不大。训练时每一百步记录一次损失值Lovasz部分应当持续下降并在epoch尾部收敛到0.45以下如果损失在0.6附近停滞说明类别分布不均衡可以适当加大present模式或调整样本采样策略。推理时的关键策略是对每个mask做连通域分析剔除面积小于20像素的孤立预测。钢材缺陷中的“斑块”类最小面积设置在50像素因为真实缺陷极少小于这个面积而椒盐噪声产生的误检区域往往达不到该阈值。这步操作在验证集上能提高1到2个点mIoU。需要强调的是这套配置在A榜高分辨率数据上运行时memory占用约4.8GBbatch size设为4可以稳定训练如果显存有限可将输入分辨率缩放到384×384。4. OnlineAugment在线增强让训练分布逐渐逼近B、C的真实分布4.1 拼接增强的复现实现B和C数据集中的拼接不是随机的而是有固定位置规律。对角线拼接的接缝一定在主对角线方向左右拼接接缝在中轴处上下接缝在中线处。要在训练时模拟这些痕迹需要把规则直接编码为增强函数而不是使用通用几何变换。import albumentations as A import cv2 import numpy as np class DiagonalShift(A.ImageOnlyTransform): 对角线拼接增强把图像分成左上/右下两块并交换位置 def __init__(self, always_applyFalse, p0.3): super().__init__(always_apply, p) def apply(self, img, **params): h, w img.shape[:2] half_h, half_w h // 2, w // 2 top_left img[:half_h, :half_w] bottom_right img[half_h:, half_w:] img[:half_h, :half_w] bottom_right img[half_h:, half_w:] top_left return img def get_transform_init_args_names(self): return (same_params,) class QuadrantPad(A.ImageOnlyTransform): 左上角四分之一添加不规则矩形模拟遮挡矩形位置固定 def __init__(self, max_rect_size96, always_applyFalse, p0.2): super().__init__(always_apply, p) self.max_rect_size max_rect_size def apply(self, img, **params): h, w img.shape[:2] rect_w np.random.randint(32, self.max_rect_size) rect_h np.random.randint(32, self.max_rect_size) x, y np.random.randint(0, w//2 - rect_w), np.random.randint(0, h//2 - rect_h) # 用钢铁表面背景色填充矩形区域 fill_value int(np.median(img[0, :, 0])) if len(img.shape) 3 else int(np.median(img[0, :])) if len(img.shape) 3: img[y:yrect_h, x:xrect_w, :] fill_value else: img[y:yrect_h, x:xrect_w] fill_value return img def get_transform_init_args_names(self): return (max_rect_size,)DiagonalShift的对角线拼接概率p0.3即每张训练图有30%的可能性做一次对角交换。QuadrantPad中的max_rect_size设为96矩形随机产生在左上四分之一区域模拟C榜样本中的遮挡痕迹。这里要注意的是albumentations提供的Compose会把所有变换串联应用到输入上所以我用ImageOnlyTransform来保证mask不参与几何变化因为拼接和矩形遮挡理论上也应该改变mask但竞赛的实际mask标签并没有随之更新。4.2 椒盐噪声与亮度扰动参数定义椒盐噪声在albumentations中直接用A.GaussNoise与自定义邻居替换组合实现但GaussNoise产生的是高斯噪声而非脉冲噪声。更接近C榜真实噪声类型的是随机将像素点置为0或255。def add_salt_pepper(img, salt_prob0.02, pepper_prob0.02): out img.copy() noise np.random.random(img.shape[:2]) salt_mask noise salt_prob pepper_mask noise 1 - pepper_prob if len(img.shape) 3: out[salt_mask, :] 255 out[pepper_mask, :] 0 else: out[salt_mask] 255 out[pepper_mask] 0 return out def compose_augmentation(): return A.Compose([ A.Lambda(imageadd_salt_pepper), A.RandomBrightnessContrast(brightness_limit(0.1, 0.3), contrast_limit0.15, p0.4), DiagonalShift(p0.3), QuadrantPad(max_rect_size96, p0.2), A.HorizontalFlip(p0.5), ])salt_prob和pepper_prob分别控制在2%这对一张512×512图像意味着每个通道大约2000个噪声点。由于钢材表面本身纹理丰富高低亮脉冲会被误判为缺陷边缘所以噪声必须和TTA推理配合。RandomBrightnessContrast的参数brightness_limit控制在0.1到0.3这是参照C榜直方图偏移后反推的同样的增强概率设为0.4保证每两个epoch能见到一次亮度变化。列明一点当同时应用DiagonalShift和RandomBrightnessContrast时顺序会显著影响效果应先做亮度扰动再做几何拼接这样拼接处的边界更接近真实增强样本。4.3 算法执行顺序与batch-level设置的坑增强管线的顺序中我把add_salt_pepper放在最前面因为C榜的噪声是在拼接完成后整体叠加的而不是分区域独立生成。如果把噪声放在最后它对拼接接缝的掩盖效果不足模型会学到接缝作为缺陷特征在真实A榜上产生误检。另一个需要注意的问题是A.HorizontalFlip与DiagonalShift同时开启后对角线拼接的方向语义会发生改变需要在输出时保持一致的坐标映射。在线增强的实际执行中我把整个Compose对象传入train.py的数据加载器并且设置内部的p按epoch衰减第一个epoch用满概率训练到中期时统一下降30%。这个衰减策略奏效的原因是模型在前期需要看到足够多的“硬”样本建立边界特征后期则需要逼近真实分布避免增强痕迹过深导致过拟合。5. 训练流程与TTA推理动态裁剪和增量验证训练时最佳实践是分两阶段第一阶段在高分辨率下用较低学习率做预热第二阶段恢复全分辨率并逐步扩大batch。eval_with_aug.py实现了带增强的TTA验证它的核心思想是推理时对同一张输入做多种几何变换将预测概率求平均后再恢复原图坐标。import torch import torch.nn.functional as F def infer_with_tta(model, image, flipTrue, scaleTrue): # image: [C, H, W] normalized tensor model.eval() with torch.no_grad(): logits model(image.unsqueeze(0)) avg_probs F.softmax(logits, dim1) if flip: logits_f model(torch.flip(image.unsqueeze(0), dims[3])) avg_probs F.softmax(torch.flip(logits_f, dims[3]), dim1) if scale: h, w image.shape[1], image.shape[2] down F.interpolate(image.unsqueeze(0), scale_factor0.8, modebilinear) logits_d model(down) logits_d F.interpolate(logits_d, size(h, w), modebilinear) avg_probs F.softmax(logits_d, dim1) probs avg_probs / (1 (1 if flip else 0) (1 if scale else 0)) return probs.argmax(dim1).squeeze(0)调用infer_with_tta时模型输出先做softmax转成概率每个TTA分支的结果叠加到avg_probs后做平均值。核心是最后一次argmax之前必须将所有分支恢复到原始分辨率不能提前取类别否则mask边缘会对不齐。flip分支使用torch.flip在W维度上水平翻转预测后再翻转回去这样不会引入插值误差。scale分支使用双线性插值缩放预测后使用同样的插值放大回去由于分辨率的降低小缺陷的预测会变得更平滑反而有利于抑制椒盐噪声。训练脚本train.py中建议使用torch.optim.AdamW初始学习率1e-4权重衰减0.01并配合余弦退火调度器。模型收敛的判据是验证集mIoU连续5个epoch不再上升此时加载最优checkpoint恢复训练再把学习率下降到原来的十分之一微调2个epoch就能看到C榜分数显著回升。model.pth保存的是完整state_dict加优化器状态恢复时注意要用与训练时相同的数据预处理器尤其是归一化的均值和标准差才能保证验证集评估一致。本文还有配套的精品资源点击获取