ARTICLE DETAIL

资讯详情

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

钢材缺陷分割实战:从4100张标签到产线部署的语义分割全流程

钢材缺陷分割实战:从4100张标签到产线部署的语义分割全流程 简介本资源为钢材缺陷图像分割数据集面向从事工业缺陷检测、语义分割方向的深度学习开发者与研究者尤其适合需要多类别分割实战数据的学生和工程师。数据集已预处理完毕可直接投入训练标签共5类0为背景4种缺陷对应png像素值1、2、3、4类别定义见classes文件。数据已划分训练集约2900张、验证集约1200张均含images图片目录与masks掩膜目录总计约4100张图像及标签。压缩包为7z格式共2000个文件以1273个png掩膜、725个jpg原图为主另含1个txt类别说明与1个py可视化脚本整体约102.62MB。该脚本可随机抽取一张图片展示原图、GT图像及GT在原图上的蒙板效果并保存至当前目录便于快速核验标注质量。目前已有63人学习适合作为UNet、SwinUnet、TransUnet等分割网络的训练与改进基线。1. 钢材缺陷分割4100 张标签背后的工业语义分割实战产线上刚轧出来的钢板表面一道 0.3 mm 的划痕人眼在强反光下盯三秒就花漏检一次下游冲压就是批量开裂。钢材缺陷分割要解决的就是这件事把工业相机拍到的钢板表面图逐像素判成划痕、夹杂、氧化铁皮、结疤、孔洞等类别输出一张和原图等大的掩膜。它属于语义分割不是实例分割——同一类缺陷连成一片还是分成三块对判定「要不要降级」没影响我们只关心每个像素属于哪一类。4100 张带标签数据在工业缺陷检测里已经算能起步的量级但离「随便训个模型就上线」还差得远。这篇写给两类人一类手里刚拿到一批钢材表面图和标注想跑通第一个语义分割基线另一类已经训过模型但 mIoU 卡在 0.6 上不去想搞清楚数据、类别、后处理到底哪一环在拖后腿。下面按「数据怎么整 → 模型怎么选 → 训练怎么调 → 坑在哪 → 怎么验证」的顺序讲透。2. 从 4100 张标签到可训练数据集钢材缺陷语义分割的数据工程拿到标注先别急着写 DataLoader。工业缺陷数据的脏和公开数据集完全不是一个量级。我见过最典型的一批4100 张里约 600 张的掩膜是标注员用矩形框「凑」出来的缺陷边缘全是直角还有一批把氧化铁皮和结疤标反了因为两者在灰度图上确实接近。数据工程没做干净后面调参全是玄学。2.1 先做标签体检三类必须清掉的脏标注第一步不是统计类别分布而是把掩膜叠加回原图逐类抽查。具体做法随机抽 200 张把 mask 以 0.5 透明度叠到原图上导出成对比图人工过一遍。重点看三件事。第一边缘是否贴合。语义分割对边界敏感矩形框凑出来的掩膜会让模型学到「缺陷是方的」这种错误先验。第二类别是否串标。钢材表面几类缺陷在低对比度下极易混氧化铁皮偏暗红、结疤偏亮灰、夹杂常带条状纹理标注规范里必须给标注员配这几类的典型图例。第三是否有漏标。一张图上主缺陷标了旁边一条细划痕没标模型会把它当背景学直接拉低该类召回。体检完做一次类别像素统计用下面这段脚本输出每类的像素占比和图像数占比import os import numpy as np from PIL import Image from collections import defaultdict # 假设掩膜是单通道 PNG像素值即类别 id0 背景 1 划痕 2 夹杂 3 氧化铁皮 4 结疤 5 孔洞 MASK_DIR masks CLASS_NAMES {0: background, 1: scratch, 2: inclusion, 3: scale, 4: scar, 5: hole} pixel_cnt defaultdict(int) image_cnt defaultdict(int) total_pixels 0 files [f for f in os.listdir(MASK_DIR) if f.endswith(.png)] for f in files: m np.array(Image.open(os.path.join(MASK_DIR, f))) total_pixels m.size present np.unique(m) for c in present: pixel_cnt[int(c)] int((m c).sum()) image_cnt[int(c)] 1 for cid, name in CLASS_NAMES.items(): ratio pixel_cnt[cid] / total_pixels print(f{name:12s} pixel_ratio{ratio:.5f} fimage_ratio{image_cnt[cid]/len(files):.3f})这段脚本的关键在最后两列。pixel_ratio告诉你类别在像素级有多不平衡——钢材缺陷里背景通常占 95% 以上划痕可能只占 0.3%。image_ratio告诉你这个类别出现在多少张图里如果某类image_ratio低于 0.05说明样本太少要么补标要么在损失函数里给它加权。两个指标要一起看像素少但出图多细长划痕和像素少且出图也少罕见孔洞处理策略完全不同。2.2 划分与增强别让同一块钢板的图跨进训练和验证集工业数据有个隐蔽的泄漏源同一块钢板会被拍多张。如果按图随机划分同一块板的图可能一半在训练、一半在验证验证指标虚高上线就翻车。正确做法是按「板号 / 卷号」分组划分同一块板的所有图只进一个集合。常见比例 7:1.5:1.5缺陷类别少的类要保证验证集里至少出现 20 张。增强策略上钢材表面图有几条硬约束。水平/垂直翻转、90 度旋转可以随便用因为缺陷方向没有语义。但颜色抖动要克制氧化铁皮和结疤的区分很大程度靠色调你把饱和度拉狠了等于把两类搅在一起。我一般只做 ±10% 的亮度扰动和轻微高斯噪声。随机裁剪要小心钢材缺陷常是细长条裁太小会把一条划痕裁成两段反而制造噪声。import albumentations as A train_tf A.Compose([ A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomRotate90(p0.5), # 亮度扰动控制在 ±10%避免破坏氧化铁皮/结疤的色调差异 A.RandomBrightnessContrast(brightness_limit0.1, contrast_limit0.1, p0.5), A.GaussNoise(var_limit(5.0, 20.0), p0.3), A.Resize(512, 512), ]) val_tf A.Compose([A.Resize(512, 512)])参数说明brightness_limit0.1是血泪经验早期我用默认 0.2氧化铁皮那类 mIoU 直接掉了 8 个点。Resize(512,512)是权衡——钢材缺陷有的很细下采样太狠会丢512 是多数工业相机 ROI 裁剪后的常用尺寸显存也扛得住。如果你的缺陷普遍小于 5 像素宽考虑 768 或滑窗推理。2.3 标签格式统一单通道 id 图是语义分割的通用货币不同标注工具导出的格式五花八门有的给 COCO json有的给彩色 PNG有的给多张二值图。语义分割训练统一转成单通道 id 图像素值就是类别编号。彩色转 id 时最容易错的是颜色映射对不上务必用标注规范里定义的调色板别自己猜。import numpy as np from PIL import Image # 标注规范定义的调色板颜色 - 类别 id PALETTE { (0, 0, 0): 0, # 背景 (255, 0, 0): 1, # 划痕 (0, 255, 0): 2, # 夹杂 (0, 0, 255): 3, # 氧化铁皮 (255, 255, 0): 4, # 结疤 (255, 0, 255): 5, # 孔洞 } def color_to_id(color_mask_path, out_path): rgb np.array(Image.open(color_mask_path).convert(RGB)) id_map np.zeros(rgb.shape[:2], dtypenp.uint8) for color, cid in PALETTE.items(): match np.all(rgb np.array(color), axis-1) id_map[match] cid Image.fromarray(id_map).save(out_path) color_to_id(raw/steel_0001_color.png, masks/steel_0001.png)逻辑说明逐颜色做全图匹配再赋值简单可靠。注意np.all(..., axis-1)是沿通道维比较得到 H×W 的布尔图。如果标注工具有抗锯齿边缘会出现混合色匹配不上会留成背景这时要么在标注端关掉抗锯齿要么加一步最近邻颜色归类。转完抽查几张确认没有整类丢失。3. 语义分割模型选型钢材缺陷场景下 U-Net、DeepLab 与 SegFormer 怎么挑模型选型不看排行榜看你的缺陷形态和算力。钢材缺陷分割有三个特点缺陷细长、类别边界模糊、背景占比极高。这三点决定了选型偏好。3.1 编码器-解码器结构为什么适合工业缺陷U-Net 这类结构靠跳跃连接把浅层高分辨率特征直接送到解码器对细长缺陷的边缘恢复很友好。钢材划痕往往只有几个像素宽纯靠深层语义特征上采样边缘会糊成一团。DeepLab 系列用空洞卷积扩大感受野对大面积氧化铁皮这种区域型缺陷判别强但空洞卷积在极细结构上容易丢细节。SegFormer 用 Transformer 编码器全局建模能力强对「这块区域整体像结疤」这种判断准但小目标、细目标需要足够的分辨率和数据量撑着。我的经验排序数据量 4000 张上下、缺陷偏细优先 U-Net 强编码器ResNet34/50 或 EfficientNet缺陷以大区域为主、边界要求不高DeepLabV3 省心数据量上万、算力充足再上 SegFormer。4100 张这个量级直接上大 Transformer 容易过拟合除非你做很强的预训练微调。3.2 用 segmentation_models_pytorch 搭一个 U-Net 基线不重复造轮子segmentation_models_pytorchsmp把编码器和解码器都封装好了换 backbone 只改一个字符串。import segmentation_models_pytorch as smp import torch NUM_CLASSES 6 # 含背景 model smp.Unet( encoder_nameresnet34, # 换 efficientnet-b3 只改这里 encoder_weightsimagenet, # 工业数据少预训练权重必开 in_channels3, classesNUM_CLASSES, decoder_attention_typescse, # 通道空间注意力细缺陷有增益 ) # 类别不平衡背景权重压低稀有类抬高 class_weights torch.tensor([0.2, 2.0, 2.5, 1.5, 2.0, 3.0]) criterion smp.losses.DiceLoss(modemulticlass) \ smp.losses.SoftCrossEntropyLoss( smooth_factor0.1, weightclass_weights)参数说明encoder_weightsimagenet在工业小数据上是刚需从头训基本没戏。decoder_attention_typescse对细长缺陷有可复现的增益我实测划痕类 mIoU 涨 2~3 个点。损失用 Dice 加权交叉熵组合Dice 管类别不平衡交叉熵管像素级分类稳定smooth_factor0.1是标签平滑缓解标注边界噪声。class_weights不是拍脑袋按 2.1 统计的像素占比反比来设背景压到 0.2 是因为它占 95% 以上不压模型会躺平全预测背景。3.3 输入尺寸、batch 与显存的取舍512×512 输入、batch 8ResNet34 编码器单张 12G 显存够用。如果显存紧用梯度累积模拟大 batch别轻易降分辨率——钢材细缺陷对分辨率敏感。混合精度训练AMP能省约 40% 显存几乎不掉点工业场景建议默认开。from torch.cuda.amp import autocast, GradScaler scaler GradScaler() for imgs, masks in loader: imgs, masks imgs.cuda(), masks.cuda() optimizer.zero_grad() with autocast(): logits model(imgs) loss criterion(logits, masks) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()逻辑说明autocast让前向用 fp16 算GradScaler防止 fp16 梯度下溢。注意损失计算放在autocast内反向和更新走 scaler。如果出现 loss 变 NaN先查是不是某些类别权重设得过大导致梯度爆炸把class_weights上限压到 5 以内通常能解。4. 训练调参与评估让钢材缺陷分割的 mIoU 真正涨上去模型搭好只是开始钢材缺陷分割的指标提升七成功夫在训练策略和评估口径上。4.1 学习率、优化器与调度别用默认值硬跑AdamW 余弦退火是语义分割的稳妥组合。初始学习率 3e-4backbone 用预训练权重时weight decay 1e-4。如果 backbone 是随机初始化学习率要降到 1e-4 量级。warmup 500 步能明显减少早期震荡尤其 batch 小的时候。from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR, LinearLR, SequentialLR optimizer AdamW(model.parameters(), lr3e-4, weight_decay1e-4) warmup LinearLR(optimizer, start_factor0.01, total_iters500) cosine CosineAnnealingLR(optimizer, T_max80, eta_min1e-6) scheduler SequentialLR(optimizer, schedulers[warmup, cosine], milestones[500])参数说明start_factor0.01表示 warmup 从 1% 学习率起步线性升到设定值。T_max80是总 epoch 数余弦从 3e-4 退到 1e-6。如果验证集 mIoU 在 40 epoch 后还在涨把T_max加到 120 再跑。别用固定学习率硬跑钢材缺陷这种小数据后期不降学习率会在最优点附近反复横跳。4.2 mIoU 之外必须看的三个指标mIoU 是平均值会被背景和易分类别拉高掩盖稀有类的崩溃。必须同时看每类 IoU、每类召回、混淆矩阵。我遇到过整体 mIoU 0.78 看着不错但孔洞类 IoU 只有 0.21因为孔洞样本太少被背景吞了。上线后孔洞漏检客户直接退货。import numpy as np def confusion_matrix(pred, target, num_classes): # pred/target: 展平后的 int 数组 mask (target 0) (target num_classes) idx target[mask] * num_classes pred[mask] cm np.bincount(idx, minlengthnum_classes**2) return cm.reshape(num_classes, num_classes) def per_class_iou(cm): ious [] for i in range(cm.shape[0]): tp cm[i, i] fp cm[:, i].sum() - tp fn cm[i, :].sum() - tp iou tp / (tp fp fn 1e-6) ious.append(iou) return np.array(ious)逻辑说明混淆矩阵第 i 行是真实类 i第 i 列是预测类 i。per_class_iou逐类算交并比。拿到每类 IoU 后对低于 0.4 的类单独分析是样本太少还是和某个类系统性混淆。如果是混淆看混淆矩阵里它主要被预测成谁再回去查这两类的标注边界是否一致。4.3 后处理小连通域过滤与形态学修补模型输出 argmax 后常有零星误检的小斑点。钢材缺陷有最小尺寸要求比如划痕长度小于 2 mm 不算缺陷可以用连通域面积过滤。但要注意过滤阈值设大了会误杀真缺陷设小了没效果必须结合像素当量每像素对应多少毫米来定。import cv2 import numpy as np def postprocess(pred_mask, min_area_px30, kernel_size3): out pred_mask.copy() kernel np.ones((kernel_size, kernel_size), np.uint8) for cid in range(1, pred_mask.max() 1): binary (pred_mask cid).astype(np.uint8) # 先闭运算补断口再过滤小连通域 binary cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) n, labels, stats, _ cv2.connectedComponentsWithStats(binary) for i in range(1, n): if stats[i, cv2.CC_STAT_AREA] min_area_px: out[labels i] 0 return out参数说明min_area_px要按像素当量换算别直接抄 30。kernel_size3的闭运算能补上细划痕的断口但太大比如 7会把两条邻近划痕粘成一条反而制造假缺陷。后处理只在推理阶段用训练时不要加否则模型学不到真实边界。5. 钢材缺陷分割的避坑清单五条踩过的坑5.1 坑一验证集 mIoU 很高上线全崩现象本地验证 mIoU 0.82产线跑一周漏检率超标。原因按图随机划分导致同板泄漏验证集和训练集高度相似指标虚高。解决按板号分组划分重新评估如果分组后 mIoU 掉到 0.65那才是真实水平按这个基线重新调。5.2 坑二氧化铁皮和结疤互相误判现象这两类混淆严重各自 IoU 都上不去。原因标注规范里两类边界定义模糊标注员各标各的加上颜色增强过度色调特征被破坏。解决先统一标注规范给标注员配典型图例和边界判定规则增强里把颜色扰动压到 ±10% 以内损失函数对这两类加权重。5.3 坑三细划痕整条丢失现象宽划痕能分出来细划痕2~3 像素整条不见。原因下采样太狠或损失函数被背景主导细目标梯度被淹没。解决输入分辨率提到 768损失里 Dice 权重加大解码器加注意力后处理闭运算补断口。别指望一个改动全解决通常是分辨率 损失 后处理三管齐下。5.4 坑四训练 loss 正常但 mask 全黑现象loss 在降但推理输出几乎全是背景。原因类别权重设得极端背景权重压到 0.05 以下模型为了降 loss 干脆全预测背景或者标签 id 映射错了所有缺陷被映射成 0。解决先可视化几张训练标签确认 id 正确类别权重下限控制在 0.1 以上用 Dice loss 兜底它对全背景预测惩罚重。5.5 坑五换了个 backbone 指标反而降了现象从 ResNet34 换到 ResNet50mIoU 不升反降。原因数据量不够大 backbone 过拟合或者学习率没跟着调大模型需要更小学习率。解决换大模型时学习率降一半加更强正则dropout、weight decay4100 张这个量级ResNet34 往往就是性价比拐点别盲目堆大。6. 把模型推到产线滑窗推理与阈值标定的实操技巧训练指标好看和产线可用之间还差推理工程这一步。钢材表面图往往很大比如 2048×4096直接缩到 512 会丢细缺陷必须滑窗推理再拼接。滑窗有两个参数窗口大小和重叠率。窗口用训练时的 512重叠率 0.25 起步——重叠太小拼接缝处缺陷会被切断重叠太大推理耗时翻倍。拼接时重叠区取各类别概率的最大值别简单覆盖否则缝上会出现类别跳变。def sliding_inference(model, img, window512, stride384, num_classes6): # stride window * (1 - overlap)overlap0.25 时 stride384 h, w img.shape[:2] prob_map np.zeros((num_classes, h, w), dtypenp.float32) count_map np.zeros((h, w), dtypenp.float32) for y in range(0, h, stride): for x in range(0, w, stride): y2, x2 min(y window, h), min(x window, w) y1, x1 max(0, y2 - window), max(0, x2 - window) patch img[y1:y2, x1:x2] with torch.no_grad(): logits model(patch[None].cuda()) prob torch.softmax(logits, dim1)[0].cpu().numpy() prob_map[:, y1:y2, x1:x2] prob count_map[y1:y2, x1:x2] 1 prob_map / np.maximum(count_map, 1) return prob_map.argmax(0)逻辑说明stride384对应 25% 重叠。概率累加再平均比直接取最后一个窗口稳。y1 max(0, y2-window)处理边缘不足一个窗口的情况保证每个 patch 都是 512。推理完得到概率图argmax 前可以按类别设不同阈值——稀有类阈值调低提高召回背景类阈值调高减少误检。阈值标定别在验证集上拍脑袋。拿一批产线真实图含已知缺陷位置画每类的 precision-recall 曲线按业务定漏检代价高就把召回拉到 0.95 以上接受一定误检误检代价高就反过来。这个阈值是业务参数不是技术参数必须和质检标准对齐。我自己的习惯是每次换数据批次先跑一遍 2.1 的类别统计再抽 50 张做滑窗推理可视化确认细缺陷没丢、边界没糊才进阈值标定。这套流程跑顺了4100 张数据撑起一个可用的钢材缺陷分割系统是现实的但别指望一次调参就到位——工业缺陷检测的功夫大半花在数据和评估口径上模型只是最后那一下。希望帮到你。本文还有配套的精品资源点击获取
返回列表