谢韦尔钢铁缺陷检测:RLE mask转YOLO框训练实战与避坑指南
简介一套面向钢铁表面缺陷检测场景的目标检测数据集可直接用于YOLO等主流模型训练。资源包共2000个文件以1999个txt标注文件和1个yaml类别配置文件为主体压缩包整体约97.16MB其中txt标签遵循YOLO格式对应图片级标注信息解压后即可配合YOLO系列算法使用yaml文件则明确指定类别方便Faster Rcnn、SSD等框架快速读取配置。数据集包含6572张图片对应的标签数据已划分训练集、验证集与测试集省去手动整理与拆分时间也便于后续按需扩展。对于工业质检、生产线缺陷识别等场景这套数据能帮助开发者跳过数据预处理环节直接聚焦模型结构与调参同时清晰的标注格式也便于进行迁移学习或模型对比实验。目前已有601人学习使用是入门缺陷检测及相关算法验证的不错选择。1. 谢韦尔钢铁缺陷检测数据集看着是分割标注其实是给目标检测准备的最硬核练手场冷轧车间的质检工位屏幕上钢带以每秒几米的速度掠过工人要在一两秒内盯出氧化皮、斑块、裂纹和麻点。这种场景下漏检率每降一个点就是真金白银的良率提升。谢韦尔钢铁Severstal放出的这份钢带缺陷数据集就是冲着这个真实产线需求来的一万多张1600x2560的灰度钢带图标注了四类常见表面缺陷。但这里有一个反直觉的坑也是很多人拿到数据后第一天就翻车的地方这个数据集的原始标注是RLE编码的分割mask而不是目标检测要的矩形框。Kaggle上它挂在“目标检测”相关的竞赛里标注格式却是分割语义。所以真正的落地路径是先把mask转成bbox再喂给YOLO系模型训练。本文就把这条路径完整走一遍——数据集结构、RLE解码、转成YOLO格式、训练参数、验证方法以及我踩过的几个大坑。这套流程适合谁想用真实工业数据练目标检测基本功的人或者正在做表面缺陷检测、想找一份带瑕疵标注的灰度图数据来验证模型方案的人。它不像COCO那样“干净”类别极不平衡、裂纹样本少得可怜、小目标扎堆处理它本身就是在练工程能力。2. 数据集结构与标注格式为什么官方给的是mask而不是框2.1 下载与目录组织从Kaggle竞赛页把数据拿下来这份数据在Kaggle上以竞赛形式发布目录结构很标准下载后你会看到这几个东西severstal-steel-defect-detection/ ├── train.csv # 训练标注RLE编码 ├── sample_submission.csv # 提交样例格式与train.csv一致 ├── train_images/ # 训练灰度图约一万多张 ├── test_images/ # 测试图无标注需自行推理 └── train.csv 说明文档一个常见误解是直接把train.csv当作目标检测的标注来用然后发现YOLO不认。train.csv里每一行是一个对象的掩码字段包括ImageId图片文件名、ClassId缺陷类别1到4、EncodedPixelsRLE压缩后的像素位置。这里的ImageId不是全局唯一的一图一行而是同一张图可以出现多行——因为同一张图上可能有多个缺陷、多种类别。划分训练集和验证集时必须按ImageId去重划分不能直接按行随机切否则同一张图的多个mask会分别落进训练集和验证集造成数据泄漏。2.2 RLE解码把一长串数字还原成缺陷形状RLERun-Length Encoding的原理是把连续像素位置压缩成“起点长度”的序列。谢韦尔的EncodedPixels格式是每两个数一组前一个是起始像素索引1-based后一个是连续长度。重点来了这个像素索引是按列优先展开的也就是把整张图先按列从上到下、再从左到右拉成一维数组。解码后要reshape再转置才能还原成正常的图像坐标系。import numpy as np import pandas as pd from PIL import Image def rle_decode(rle, shape(2560, 1600)): 把RLE字符串解码成二值mask shape(height, width)注意谢韦尔图是 高2560 x 宽1600 if pd.isna(rle) or rle : return np.zeros(shape, dtypenp.uint8) s rle.split() starts np.asarray(s[0::2], dtypenp.int64) - 1 # RLE是1-based减1转0-based lengths np.asarray(s[1::2], dtypenp.int64) ends starts lengths img np.zeros(shape[0] * shape[1], dtypenp.uint8) for lo, hi in zip(starts, ends): img[lo:hi] 1 # 关键按列展开存储所以reshape后要转置 return img.reshape(shape[0], shape[1]).T # 验证一下 cs_path train.csv df pd.read_csv(cs_path) sample_row df[df[EncodedPixels].notna()].iloc[0] mask rle_decode(sample_row[EncodedPixels]) print(mask.shape) # 应该是 (2560, 1600) print(mask.sum()) # 缺陷像素总数应大于0RLE解码这段代码是后续所有操作的基石。参数上要注意shape必须严格是(2560, 1600)如果写反了mask会直接变成横竖颠倒转换出来的框全都错位。另外starts减1是必须的RLE计数从1开始而numpy索引从0开始。循环解RLE在Python里不算快但训练集总共就一两万个对象性能不是瓶颈没必要用cython优化。2.3 四类缺陷的分布与形态先认识敌人再训练类别编号对应的缺陷类型和典型形态如下ClassId缺陷名典型形态训练集占比直观感受1Rolled-in Scale轧入氧化皮细长条状、沿轧制方向延伸最多约占一半2Patches斑块较大面积的灰暗区域边界模糊次多3Cracks裂纹细线状断续分布极少约1%量级4Pitting麻点/点蚀密集的小圆坑成片出现中等这张表的意义在于如果你上来就按YOLO默认参数训练类别3大概率学了个寂寞。因为样本量差异达到几十倍模型会把所有注意力放在氧化皮上。处理办法有两个后面训练章节细说——一是按类别占比给loss加权二是对Crack类做过采样或复制粘贴增强。另外注意形态特征氧化皮是长条状长宽比极不均匀麻点是密集小目标斑块边界模糊框很难画准。这套数据做目标检测的最大阻力不是模型不够强而是这些真实工业缺陷的形态太离散框的回归边界天然模糊。3. mask转YOLO框转换脚本与四个必调参数3.1 为什么要先转格式YOLO系不吃RLEYOLO的训练标签是每个txt一行格式为class x_center y_center width height全部按图片宽高归一化。RLE是分段的、按列展开的且是像素级描述跟框的目标完全不同。YOLO系从v5到v8支持的分割格式是polygon或分割mask但检测任务只收框。所以你需要一个中间脚本把每个类别的mask提取出来用OpenCV找轮廓再把轮廓转成外接矩形最后写成YOLO txt。这里有一个隐藏的边界情况同一张图上同一类别的多个连通域如果在解码后直接对整张mask做一次findContours只会拿到最外层轮廓中间的独立缺陷会被吞掉。正确做法是对单个类别的mask找所有外轮廓每个轮廓独立成框。3.2 完整转换脚本从CSV到YOLO txt一站式import os import cv2 import numpy as np import pandas as pd from tqdm import tqdm IMG_HEIGHT, IMG_WIDTH 2560, 1600 CLASS_MAP {1: 0, 2: 1, 3: 2, 4: 3} # 将原始ClassId映射到0-3YOLO从0计数 MIN_AREA 100 # 面积阈值过滤孤立噪点 def rle_decode(rle, shape(IMG_HEIGHT, IMG_WIDTH)): # 与上一章相同略 ... def mask_to_yolo_boxes(mask, img_w, img_h, min_area): mask: 单类别的二值掩码0/255 返回归一化的 YOLO 格式框列表 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) boxes [] for cnt in contours: area cv2.contourArea(cnt) if area min_area: continue x, y, w, h cv2.boundingRect(cnt) # 归一化中心点坐标和宽高都除以对应的图片尺寸 cx (x w / 2) / img_w cy (y h / 2) / img_h bw w / img_w bh h / img_h # 防止归一化值超过1极端情况框贴边 cx min(max(cx, 0.0), 1.0) cy min(max(cy, 0.0), 1.0) bw min(bw, 1.0) bh min(bh, 1.0) boxes.append((cx, cy, bw, bh)) return boxes def convert_csv_to_yolo(csv_path, img_dir, label_dir): df pd.read_csv(csv_path) os.makedirs(label_dir, exist_okTrue) for image_id, group in tqdm(df.groupby(ImageId)): # 多类别缺陷合并到一张总mask上避免每个类单独循环图片IO combined_mask np.zeros((IMG_HEIGHT, IMG_WIDTH), dtypenp.uint8) for _, row in group.iterrows(): class_id CLASS_MAP[int(row[ClassId])] cls_mask rle_decode(row[EncodedPixels]) # 不同类别用不同的像素值标记方便按类提取 combined_mask[cls_mask 1] class_id 1 lines [] for cls_no in range(1, 5): # 1~4对应映射后的0~3 cls_mask ((combined_mask cls_no) * 255).astype(np.uint8) boxes mask_to_yolo_boxes(cls_mask, IMG_WIDTH, IMG_HEIGHT, MIN_AREA) for cx, cy, bw, bh in boxes: lines.append(f{cls_no - 1} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) if lines: with open(os.path.join(label_dir, image_id .txt), w) as f: f.write(\n.join(lines)) convert_csv_to_yolo(train.csv, train_images, yolo_labels)逻辑说明先按ImageId分组对每张图把四个类别的mask叠加到一张combined_mask上用不同像素值区分类别。这样只需要对每张图做一次findContours循环而不是每行标注做一次。mask_to_yolo_boxes里用RETR_EXTERNAL拿所有外轮廓每一个轮廓独立转框保证同一类别多个缺陷分开。3.3 四个必调参数的边界与翻车点**参数一shape顺序。**上面脚本里IMG_HEIGHT, IMG_WIDTH 2560, 1600宽高顺序一旦搞反RLE解码出来的mask是横向拉伸的框全部错位。如果把rle_decode里改成shape(1600, 2560)那reshape后转置的坐标就不对了。第一次跑完务必随机挑5张图把mask画在原图上人工确认。**参数二RETR_EXTERNAL与嵌套缺陷。**钢铁表面缺陷常有“斑块里套小麻点”的情况如果只取最外层轮廓内部的麻点框会全部丢失。解决方案是换用cv2.RETR_CCOMP做两层层级提取把内层轮廓也用上或者对每个mask先做形态学腐蚀把连通域稍微分离再提框。**参数三MIN_AREA面积阈值。**RLE解码后偶尔会有几个像素的孤立噪点直接产生2x2像素的极小球框训练时会让模型疯狂预测假目标。这个值按你的检测精度需求定我只想要能用的缺陷框设在100如果想抓微缺陷可以降到30并配合后续morphology操作。设太高也有风险——Pitting麻点面积大的几百像素小的可能小于100降阈值要看类别分布。**参数四归一化的分母。**YOLO标准化宽高必须除以原图尺寸不是除以训练时的输入尺寸。很多人前脚转好标签后脚在yaml里把图片resize成640然后发现框全偏了。YOLO自己会在训练时做letterbox自适应说白了标签里永远写原图归一化坐标模型内部自动变换。转换完的txt文件要和图片后缀对应谢韦尔的ImageId是带四位前缀的png文件名比如0002a0a19e1f.png生成的txt必须叫0002a0a19e1f.txt。大小写也要完全一致Linux下区分大小写这个坑我踩到过。4. 用YOLOv8训练自己的数据集目录、命令与超参数4.1 数据文件与目录组织yaml里的一个小陷阱YOLOv8的数据配置用一个yaml文件搞定目录结构建议这样安排severstal/ ├── images/ │ ├── train/ # 训练原图 │ └── val/ # 验证原图 ├── labels/ │ ├── train/ # 对应的txt │ └── val/ └── severstal.yaml在转换脚本输出之后你需要按ImageId把图片和txt同步划分。建议用sklearn.train_test_split对去重后的ImageId列表按8:2划分然后把对应的.png和.txt复制过去。不要手动随机从文件夹里挑一半序列化顺序会让同一个产线批次的图全堆在训练集里。# severstal.yaml path: /data/severstal train: images/train val: images/val names: 0: Rolling_in_Scale 1: Patches 2: Cracks 3: Pitting这里有一个误导很多人的点yaml里的names顺序必须和转换脚本里CLASS_MAP映射后的数字一致。如果你在转换脚本里把ClassId 3映射成了2而yaml里第2个name是Patches模型训练出来的“类别2”实际是Cracks。这种错位在训练时不会报错loss也正常降直到推理可视化才会发现猫腻。4.2 训练命令与必调超参不能照抄默认值的三个参数# 激活你的conda环境保证ultralytics版本 pip install ultralytics # 训练 yolo detect train \ data/data/severstal/severstal.yaml \ modelyolov8n.pt \ epochs150 \ imgsz1280 \ batch16 \ device0 \ workers8 \ mosaic0.0 \ close_mosaic0 \ cls1.0 \ box7.5 \ valTrueimgsz1280是我的核心调参经验。谢韦尔原图是1600x2560算下来宽高比1.6直接缩到640x640那些细裂纹和麻点早就没了。在这个数据集上1280比640的mAP能高5到10个点代价是训练时间约翻倍。显存不够就上batch8千万别折imgsz。mosaic0.0是另一个反直觉参数。YOLO默认做mosaic增强把四张图拼接成一张训练。但这个数据集的缺陷是表面纹理拼接缝会造成伪造的横向条纹模型会学到“拼接缝 缺陷”。v8有close_mosaic10参数是训练最后十轮自动关闭mosaic对大部分自然图像有效但这里我直接全程关闭。实测开启mosaic时验证集PR曲线的假阳性明显偏高。cls1.0是分类loss权重。默认0.5在自然图像上够用但这里的类别不平衡太严重。Crack样本只有氧化皮的五十分之一如果不提高分类权重模型几乎不会输出Crack框。调成1.0到1.5配合Crack类的过采样训练时把Crack样本重复复制到数据集里才能逼着模型去学裂纹的形态。训练结束后输出目录runs/detect/train/下会有weights/best.pt和weights/last.pt验证指标在results.csv里。别以为best.pt就是最终答案——在这个数据集上后期Crack的召回率还在缓慢爬升但Pitting的精度会下降best.pt是按综合mAP选的不一定适合你的实际业务需求按需选断点。4.3 效果预期与模型选型边界n还是s乃至mYOLOv8n在这个数据集上imgsz1280训练到位后mAP50大概能到0.6到0.7之间不同划分不同别拿去跟别人比绝对值。想上到0.75换成yolov8s或m叠加上focal loss和更强的增强收益明显。但别迷信模型放大——这个数据集真正的天花板是标注噪声斑块的边界在灰度图里本来就模糊不同标注员画出来的mask IoU可能只有0.8框的回归精度再高也高不过标注的一致性。一个值得做的对照组实验用同一个yaml分别跑yolov8n.pt和yolov8s.pt各一次比较两者的F1曲线。如果s相对n的提升不到3个点说明你当前的问题不在模型容量而在数据处理和类别不平衡上。先调数据再升级模型顺序不能反。5. 钢铁缺陷训练血泪避坑五个我踩过的深坑5.1 坑一RLE解码转置忘了做全部框丢失现象转换脚本跑完生成的txt每张图都是空的或者框数量远少于预期。用OpenCV把框画回原图发现缺陷区域全在镜像位置。原因谢韦尔RLE是按列展开的解码后直接reshape成(width, height)就是错的。当初我没转置直接reshape(1600, 2560)所有缺陷横向反向纵向错位框全落在钢板空白处。解决解码函数最后一定要.T转置。验证方法是在解码函数里加一个断言mask rle_decode(rle) assert mask.sum() 0, mask全空检查RLE解析 assert mask.shape (2560, 1600), fshape错误: {mask.shape}再用PIL把mask叠加到原图上肉眼确认一次别信数字。5.2 坑二同图同类别多个缺陷只有一个框现象某张图上有三处独立的氧化皮转换后txt里只有一个框而且这个框特别大把所有缺陷都包进去了。原因对整张类别的mask直接findContoursRETR_EXTERNAL只返回最外层轮廓。如果三个defect之间有微弱的连通比如灰度粘连、mask边缘相接会被算成一个轮廓。解决处理前先对mask做一次cv2.dilate和cv2.erode把细连接断开或者用cv2.connectedComponentsWithStats按连通域逐个提取每个连通域最小外接矩形独立成框。后者更稳但要注意stats的坐标索引是从左上角开始的别搞反。5.3 坑三类别极端不平衡Crack完全学不到现象训练完的混淆矩阵里Crack类别精度和召回全为0或者只在几个训练样本上“背下来”验证集全错。原因Crack在原始数据里占比约1%Epochs跑150轮模型其实没“见过”几次Crack。YOLO的loss是全局平均的少数类对梯度贡献可以忽略。解决一是对Crack类做过采样在训练列表里把含Crack的图片重复3到5轮在采样器层面处理不是物理复制文件。二是提高cls loss权重。三是用save_jsonTrue看每类的AP别只盯总mAP。我最后是过采样cls1.5组合Crack的AP从0.1拉到0.35左右。5.4 坑四小目标扎堆框mAP虚高但实际漏检现象mAP不错但跑测试图推理时发现散布的小麻点漏掉一半——尤其是几个像素级别的小缺陷模型直接无视。原因原图1600宽缩到640训练时一个10x10像素的麻点缩小到4x4跟噪声没区别。这是分辨率问题不是模型问题。解决imgsz提到1280配合SAHI方式做推理把大图切成小块分别检测再拼接。更粗暴的做法是训练时对Pitting类做随机裁剪放大把缺陷周围区域裁出来当独立样本。做好这个之后Pitting的召回率能提升十个点左右。5.5 坑五mask空洞导致bbox面积虚大误以为模型输出差现象用验证集图片推理发现预测框比实际缺陷大很多IoU普遍低于0.5以为是模型没学好。原因不是模型问题而是你自己转换标签时把bbox画在了mask的外接矩形上。钢铁缺陷mask常有内部孔洞缺陷内部有正常纹理外接矩形会把空洞也算进去导致标签框本身就比视觉缺陷大。解决要么接受这个事实用mask IoU来评估而不是框IoU要么在转换时对mask做cv2.convexHull拿凸包的最小外接矩形。后者会让框更贴紧缺陷但凸包也可能把凹陷处错误包含。换一种思路直接用yolov8n-seg.pt训练分割模型从根源上解决框边界模糊问题。这个数据集做检测的另一条路就是走分割后面细说。6. 验证与进阶用mAP和混淆矩阵说话用mask IoU治标又治本验证阶段的正确姿势不是只看训练日志跑一次标准评估拿可靠数字yolo detect val \ modelseverstal/runs/detect/train/weights/best.pt \ dataseverstal/severstal.yaml \ imgsz1280 \ conf0.25 \ iou0.5输出里的mAP50、mAP50-95和每类的AP列表是判断模型是否成熟的主要依据。我习惯额外看混淆矩阵.png如果Cracks大比例被识别成Rolling_in_Scale说明两类在灰度纹理上太接近需要按形态学特征长宽比、骨架长度加后处理过滤。进阶方向是把检测升级成分割验证。检测的矩形框在这个数据集上天然吃亏——长条状裂纹的框里大部分是背景斑块的边界又模糊得无从画框。我的建议是先用YOLO检测做到框架通、指标上线如果业务对缺陷面积有精确要求换yolov8n-seg.pt走mask分割标签就用原始的RLE mask不用转框。很多部署框架现在都支持分割模型输出落地成本比想象中低。最后给一个实操习惯每次训练完随机挑三张验证图输出推理可视化一边是标签框一边是预测框肉眼扫一遍再去看指标。指标会骗人图片不会。我在这个数据集上吃过一次大亏——mAP很好看一roll到现场图全是漏检从那以后我每个数据集版本跑完都强制做人工抽检且只看复杂样本多缺陷、小目标、光照不均的表现。这个习惯帮我避掉了至少三次换数据重训的返工。易错清单贴在自己工位屏幕上看看哪条你正在触发去掉它你的模型今晚就能涨点。希望帮到你。本文还有配套的精品资源点击获取