简介:本资源为钢材缺陷图像分割数据集,面向从事工业缺陷检测、语义分割方向的深度学习开发者与研究者,尤其适合需要多类别分割实战数据的学生和工程师。数据集已预处理完毕,可直接投入训练,标签共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,像素值即类别 id:0 背景 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} " f"image_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(p=0.5), A.VerticalFlip(p=0.5), A.RandomRotate90(p=0.5), # 亮度扰动控制在 ±10%,避免破坏氧化铁皮/结疤的色调差异 A.RandomBrightnessContrast(brightness_limit=0.1, contrast_limit=0.1, p=0.5), A.GaussNoise(var_limit=(5.0, 20.0), p=0.3), A.Resize(512, 512), ]) val_tf = A.Compose([A.Resize(512, 512)])参数说明:brightness_limit=0.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], dtype=np.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_pytorch(smp)把编码器和解码器都封装好了,换 backbone 只改一个字符串。
import segmentation_models_pytorch as smp import torch NUM_CLASSES = 6 # 含背景 model = smp.Unet( encoder_name="resnet34", # 换 efficientnet-b3 只改这里 encoder_weights="imagenet", # 工业数据少,预训练权重必开 in_channels=3, classes=NUM_CLASSES, decoder_attention_type="scse", # 通道+空间注意力,细缺陷有增益 ) # 类别不平衡:背景权重压低,稀有类抬高 class_weights = torch.tensor([0.2, 2.0, 2.5, 1.5, 2.0, 3.0]) criterion = smp.losses.DiceLoss(mode="multiclass") \ + smp.losses.SoftCrossEntropyLoss( smooth_factor=0.1, weight=class_weights)参数说明:encoder_weights="imagenet"在工业小数据上是刚需,从头训基本没戏。decoder_attention_type="scse"对细长缺陷有可复现的增益,我实测划痕类 mIoU 涨 2~3 个点。损失用 Dice + 加权交叉熵组合:Dice 管类别不平衡,交叉熵管像素级分类稳定,smooth_factor=0.1是标签平滑,缓解标注边界噪声。class_weights不是拍脑袋,按 2.1 统计的像素占比反比来设,背景压到 0.2 是因为它占 95% 以上,不压模型会躺平全预测背景。
3.3 输入尺寸、batch 与显存的取舍
512×512 输入、batch 8,ResNet34 编码器,单张 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-4(backbone 用预训练权重时),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(), lr=3e-4, weight_decay=1e-4) warmup = LinearLR(optimizer, start_factor=0.01, total_iters=500) cosine = CosineAnnealingLR(optimizer, T_max=80, eta_min=1e-6) scheduler = SequentialLR(optimizer, schedulers=[warmup, cosine], milestones=[500])参数说明:start_factor=0.01表示 warmup 从 1% 学习率起步,线性升到设定值。T_max=80是总 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, minlength=num_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_px=30, kernel_size=3): 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_size=3的闭运算能补上细划痕的断口,但太大(比如 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 换到 ResNet50,mIoU 不升反降。原因:数据量不够,大 backbone 过拟合;或者学习率没跟着调,大模型需要更小学习率。解决:换大模型时学习率降一半,加更强正则(dropout、weight decay);4100 张这个量级,ResNet34 往往就是性价比拐点,别盲目堆大。
6. 把模型推到产线:滑窗推理与阈值标定的实操技巧
训练指标好看和产线可用之间,还差推理工程这一步。钢材表面图往往很大(比如 2048×4096),直接缩到 512 会丢细缺陷,必须滑窗推理再拼接。滑窗有两个参数:窗口大小和重叠率。窗口用训练时的 512,重叠率 0.25 起步——重叠太小,拼接缝处缺陷会被切断;重叠太大,推理耗时翻倍。拼接时重叠区取各类别概率的最大值,别简单覆盖,否则缝上会出现类别跳变。
def sliding_inference(model, img, window=512, stride=384, num_classes=6): # stride = window * (1 - overlap),overlap=0.25 时 stride=384 h, w = img.shape[:2] prob_map = np.zeros((num_classes, h, w), dtype=np.float32) count_map = np.zeros((h, w), dtype=np.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, dim=1)[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)逻辑说明:stride=384对应 25% 重叠。概率累加再平均,比直接取最后一个窗口稳。y1 = max(0, y2-window)处理边缘不足一个窗口的情况,保证每个 patch 都是 512。推理完得到概率图,argmax 前可以按类别设不同阈值——稀有类阈值调低提高召回,背景类阈值调高减少误检。
阈值标定别在验证集上拍脑袋。拿一批产线真实图(含已知缺陷位置),画每类的 precision-recall 曲线,按业务定:漏检代价高就把召回拉到 0.95 以上,接受一定误检;误检代价高就反过来。这个阈值是业务参数,不是技术参数,必须和质检标准对齐。
我自己的习惯是:每次换数据批次,先跑一遍 2.1 的类别统计,再抽 50 张做滑窗推理可视化,确认细缺陷没丢、边界没糊,才进阈值标定。这套流程跑顺了,4100 张数据撑起一个可用的钢材缺陷分割系统是现实的,但别指望一次调参就到位——工业缺陷检测的功夫,大半花在数据和评估口径上,模型只是最后那一下。希望帮到你。
本文还有配套的精品资源,点击获取