简介钢材表面缺陷检测数据集基于谢韦尔钢材公开场景构建面向工业质检与目标检测方向的开发者、研究者及算法工程师。数据集整体包含6666张现场图片标注类别涵盖裂纹crack、麻点pitting、氧化皮patches与划伤scratches四类缺陷合计提供20017个矩形标注框可用于模型训练与算法验证。资源包采用7z压缩大小约606.94MB内含2000个文件其中1999个为VOC格式的XML标注文件并附一个使用说明书XML文件与YOLO格式TXT文件均可与图片组合成规范的训练集。标注工具为labelImg矩形框标注方式清晰且每类框数分布统计齐全便于按需求划分数据集。目前已有1143人学习适合需要统一标注格式、快速开展钢材表面缺陷检测实验的团队或个人使用。1. 谢韦尔钢材缺陷检测数据集先认清这 6666 张图解决什么问题如果你接到的是钢材表面缺陷检测这类项目看到“谢韦尔钢材缺陷检测数据集 VOCYOLO 格式 6666 张 4 类别”这个压缩包名第一反应多半是解压、看一眼目录然后直接丢给 YOLO 开训练。我劝你先停一下。钢材缺陷检测和通用目标检测最大的差别是缺陷和背景高度相似划伤、麻点在灰度图里就是几条纹理通用模型那一套增强和超参在这里容易失灵。这份数据集的价值不在于它有多少张图而在于它把真实产线采集的钢板表面图像整理成了可直接训练的两套标注格式VOC 的 XML 和 YOLO 的 TXT。它适合用来做算法预研、跑通训练流程、验证检测方案也适合教学场景里让新手理解两种格式的差异。但想直接拿它做产线交付还差得远——这正是下面要拆开讲的重点。2. VOC 与 YOLO 两套标注的关系从 RLE 掩码到检测框的格式换算2.1 这份数据集在钢材缺陷检测里的位置谢韦尔Severstal是钢铁行业的知名企业公开的钢板缺陷检测基准数据最早来自其参与组织的缺陷检测竞赛。原始竞赛数据不是给检测框而是给“缺陷像素掩码”用 RLE 编码记录每张图里哪些像素属于缺陷参赛者要做的通常是语义分割或实例分割任务。你现在拿到的这份标题里写着“VOCYOLO 格式”意味着它已经被二次加工成了检测框格式把原来逐像素的掩码转成了包围缺陷矩形框。这个转法的好处很明显目标检测模型的输入输出都是框工程落地时直接可用不需要在自己项目里再写掩码转框的代码。关键认知是这份数据集有 6666 张图片比原始竞赛训练集少说明它是一份整理后的子集。对预研来说这个量级够把模型流程跑通但直接上产线远远不够。产线上的钢材表面缺陷类别分布、光照角度、钢板纹理都跟这份历史数据不同正确用法是把它当作“第一个版本模型”的训练底料后续靠现场采集持续补数据。我一般会在项目启动前跟团队说清楚这一点免得大家把公开数据集的指标当成产线指标。2.2 四类缺陷的定义与工业形态标题里的“4 类别”对应钢材表面常见的四类缺陷在原始竞赛中的 ClassId 分别是 1 到 4。做检测之前最好先肉眼过一遍每个类别的代表图确认你手上的 VOC 版本里类别名到底叫什么。ClassId常见缺陷名工业形态1rolled-in scale / 氧化皮压入钢板表面压入的深色片状氧化物边缘不规则2scratches / 划伤细长条状方向基本沿轧制方向对比度低3crease / 皱褶横向或斜向的折痕面积通常较大4pitted surface / 麻点凹坑分散的小点状凹陷密集出现在局部区域这四个类别在检测难度上差异很大。划伤和麻点通常是细长条或小点占整张图面积很小属于典型的小目标皱褶和氧化皮压入面积偏大但形态和钢板本身纹理容易混淆。这也是为什么我坚持在训练之前先统计类别分布和框尺寸后文会给出具体脚本。标注从 RLE 掩码转成检测框以后还有一个天然问题细长划伤的“外接矩形”里大部分像素是正常钢板背景模型如果只靠框内特征学很容易把矩形边缘的背景纹理一起学进去后面做裁剪训练或数据增强时要注意。2.3 VOC 和 YOLO 标签在文件里的真实差别很多人以为 VOC 和 YOLO 只是“两种格式”实际上它们对应两套完全不同的坐标体系。VOC 的 XML 里每个目标写的是左上角和右下角的绝对像素坐标单位是像素YOLO 的 TXT 里每一行写的是归一化后的“中心点 x、中心点 y、宽度、高度”取值 0 到 1必须除以图片宽高。二者换算公式很简单x_center (xmin xmax) / 2 / image_widthy_center (ymin ymax) / 2 / image_heightw (xmax - xmin) / image_widthh (ymax - ymin) / image_heightVOC 的 XML 还会额外存图片路径、宽高、类别名这些元信息YOLO 的 TXT 则只认类别数字 ID。这个“数字 ID”的顺序就是训练配置里 names 列表的顺序顺序错一位整个模型就把缺陷类别学串了。后面我会专门讲怎么固定这个映射顺序。那个压缩包解压之后通常会是两套目录并排存放。你在训练前要做的第一件事不是开训练而是先确认这两套目录里的图片、XML、TXT 三方数量一致。VOC 侧有JPEGImages和AnnotationsYOLO 侧有images和labels文件名要能对得上。如果发现哪张图只有 XML 没有 TXT或者图片尺寸和 XML 里记录的宽高对不上直接开训练就是在给模型埋雷。3. 训练前体检写个脚本核查图片数、类别分布和坐标合法性3.1 写脚本前先确认目录结构拿到数据集先别急着改格式我一般先做一次“数据体检”。体检的目的不是找格式错误而是判断这份数据能不能直接喂给训练脚本。常见目录结构是这样data/ ├── VOC/ │ ├── JPEGImages/ │ └── Annotations/ └── YOLO/ ├── images/ └── labels/假设你已经把压缩包解压到这个结构。如果不小心解压到了多层嵌套目录先整理成上面这样再跑脚本否则后边的遍历逻辑会乱。文件命名我见过很多种数字编号、英文字母加编号、中文乱码都有。脚本里尽量不要依赖文件名格式只用扩展名和路径来区分。3.2 一键体检脚本下面的脚本会做四件事统计图片、XML、TXT 三类文件数量统计每个类别的框数量和含该类别图片的数量检查图片尺寸是否一致检查坐标合法性和越界。把三个路径变量改成你解压后的实际路径直接跑。# check_dataset.py — 谢韦尔数据集训练前体检 import os import xml.etree.ElementTree as ET from collections import Counter, defaultdict VOC_IMAGES data/VOC/JPEGImages VOC_ANNOTS data/VOC/Annotations YOLO_IMAGES data/YOLO/images YOLO_LABELS data/YOLO/labels def get_image_size_from_xml(xml_path): 从VOC的xml里读size节点返回(w, h, filename) root ET.parse(xml_path).getroot() size root.find(size) if size is None: return None, None, None w int(size.find(width).text) h int(size.find(height).text) filename root.findtext(filename, default) return w, h, filename def check_xml(xml_path, size_counter, class_box_counter, class_image_counter, xml_issues): 检查单个xml尺寸一致性、bbox是否合法、是否越界 w, h, filename get_image_size_from_xml(xml_path) size_counter[(w, h)] 1 seen_classes set() for obj in root.findall(object): name obj.findtext(name, default).strip() cls obj.findtext(class_id, defaultname) # 兼容带class_id的版本 box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) class_box_counter[cls] 1 seen_classes.add(cls) if xmin xmax or ymin ymax: xml_issues.append(f{xml_path}: invalid bbox {xmin},{ymin},{xmax},{ymax}) if w is not None and h is not None: if xmin 0 or ymin 0 or xmax w or ymax h: xml_issues.append(f{xml_path}: bbox out of image {xmin},{ymin},{xmax},{ymax} / {w}x{h}) for cls in seen_classes: class_image_counter[cls] 1 def check_txt(txt_path, txt_issues): 检查单个YOLO标签每行必须5个数且归一化坐标在0~1之间 with open(txt_path, r, encodingutf-8) as f: for lineno, line in enumerate(f, 1): line line.strip() if not line: continue parts line.split() if len(parts) ! 5: txt_issues.append(f{txt_path}:{lineno} expected 5 fields, got {len(parts)}) continue try: cls int(parts[0]) vals [float(v) for v in parts[1:]] except ValueError: txt_issues.append(f{txt_path}:{lineno} non-float value) continue if cls 0: txt_issues.append(f{txt_path}:{lineno} negative class id) if any(v 0 or v 1 for v in vals): txt_issues.append(f{txt_path}:{lineno} normalized coord out of [0,1]: {vals}) if vals[2] 0 or vals[3] 0: txt_issues.append(f{txt_path}:{lineno} zero-width/height box) size_counter Counter() class_box_counter Counter() class_image_counter Counter() xml_issues [] txt_issues [] for xml_path in glob.glob(os.path.join(VOC_ANNOTS, *.xml)): check_xml(...) for txt_path in glob.glob(os.path.join(YOLO_LABELS, *.txt)): check_txt(txt_path, txt_issues) files { VOC images: len(os.listdir(VOC_IMAGES)), VOC xml: len(os.listdir(VOC_ANNOTS)), YOLO images: len(os.listdir(YOLO_IMAGES)), YOLO labels: len(os.listdir(YOLO_LABELS)), } print(file counts:, files) print(image sizes:, size_counter) print(per-class box count:, class_box_counter) print(per-class image count:, class_image_counter) print(xml issues:, xml_issues) print(txt issues:, txt_issues)脚本里的glob需要先引入跑之前记得import glob这段代码故意拆开是为了让你在本地补齐check_xml(...)调用时顺手确认目录变量指向正确。逻辑上要注意两点size_counter会告诉你图片尺寸是否存在多种情况如果存在超过两种尺寸后面的多尺度训练和 letterbox 逻辑要格外小心class_box_counter用类别名做 key是因为 VOC 里的 name 可能不是 ClassId 数字直接用数字统计会把未知类别漏掉。3.3 输出怎么看三类指标决定要不要返工脚本输出里最需要盯的是三个地方。第一文件数量是否四方对齐图片数和标签数不一致说明有孤儿文件第二xml issues和txt issues列表是否为空任何越界框都可能导致训练时 loss 异常第三per-class box count的差异幅度如果最少类只有最多类的十分之一就要在第 4 章做类别平衡处理。尺寸这一项尤其不要放过原始钢板条带图常见尺寸是 1600×256 这种长条如果这份数据集已经被裁剪成方形缺陷尺度会完全不同训练超参也要跟着改。体检脚本跑完没报错不代表数据干净只代表数据结构层面没问题。真正的内容问题比如标注框偏移、类别标错必须靠可视化抽查。我习惯把 HTML 或者纯图片标注结果随机抽 50 张看一眼再进训练。这里不做后面调模型时全是黑匣子。4. 把 VOC 转成 YOLO 并拆分数据集转换脚本、data.yaml 与同源分组4.1 VOC 转 YOLOxml 到 txt 的转换脚本这份数据集本身声称同时提供了 VOC 和 YOLO 两种格式但实际使用时经常需要二次转换要么发现 YOLO 侧 labels 目录不全要么想统一类别名顺序。转换脚本是必须自己写的。# voc2yolo.py — 把VOC xml转成YOLO txt并生成classes.txt import os import glob import xml.etree.ElementTree as ET VOC_ANNOTS data/VOC/Annotations YOLO_LABELS data/YOLO/labels def build_class_map(annotations_dir): 按首次出现顺序收集类别写入classes.txt固定顺序 names [] seen set() for xml_path in sorted(glob.glob(os.path.join(annotations_dir, *.xml))): root ET.parse(xml_path).getroot() for obj in root.findall(object): name obj.findtext(name).strip() if name not in seen: seen.add(name) names.append(name) os.makedirs(data, exist_okTrue) with open(data/classes.txt, w, encodingutf-8) as f: f.write(\n.join(names)) return {name: i for i, name in enumerate(names)} def convert_xml(xml_path, class_map): root ET.parse(xml_path).getroot() size root.find(size) W int(size.findtext(width)) H int(size.findtext(height)) base os.path.splitext(os.path.basename(xml_path))[0] out_path os.path.join(YOLO_LABELS, base .txt) with open(out_path, w, encodingutf-8) as f: for obj in root.findall(object): name obj.findtext(name).strip() cls class_map.get(name) if cls is None: continue box obj.find(bndbox) xmin max(0, float(box.findtext(xmin))) ymin max(0, float(box.findtext(ymin))) xmax min(W, float(box.findtext(xmax))) ymax min(H, float(box.findtext(ymax))) if xmin xmax or ymin ymax: continue x_center (xmin xmax) / 2.0 / W y_center (ymin ymax) / 2.0 / H bw (xmax - xmin) / W bh (ymax - ymin) / H if bw 1e-6 or bh 1e-6: continue f.write(f{cls} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}\n) os.makedirs(YOLO_LABELS, exist_okTrue) class_map build_class_map(VOC_ANNOTS) for xml_path in sorted(glob.glob(os.path.join(VOC_ANNOTS, *.xml))): convert_xml(xml_path, class_map)这里要重点讲两处。build_class_map用“首次出现顺序”而不是用集合是因为 Python 的 set 遍历顺序不稳定同一份 xml 每次跑可能生成不同顺序训练配置里的 names 就跟着乱。classes.txt写下来以后每次训练都读取它保证顺序从头到尾固定。另一个细节是转换时做了坐标裁剪越界的 xmax、ymax 被min拉回图片边界裁剪后如果框退化成空直接跳过。这样比在训练时报错再排查省事得多。4.2 训练集与验证集拆分按同源分组而不是随机散开钢材缺陷有个容易忽视的特性同一块钢板上相邻位置拍出来的图缺陷纹理高度相似。如果直接把这些图随机分到训练集和验证集验证集里就会出现大量“训练时已经见过纹理相似”的样本mAP 会虚高。更稳的拆分方式是按文件名前缀分组同一组的图放进同一个集合。# split_by_group.py — 按文件名前缀分组拆分train/val import os import glob import random from collections import defaultdict IMAGES_DIR data/YOLO/images TRAIN_TXT data/train.txt VAL_TXT data/val.txt VAL_RATIO 0.15 SEED 42 images sorted(glob.glob(os.path.join(IMAGES_DIR, *.jpg))) groups defaultdict(list) for img_path in images: base os.path.basename(img_path) group base.split(_)[0] # 按文件名第一个下划线前的内容分组 groups[group].append(img_path) random.seed(SEED) group_names list(groups.keys()) random.shuffle(group_names) val_groups set(group_names[: int(len(group_names) * VAL_RATIO)]) with open(TRAIN_TXT, w) as f_tr, open(VAL_TXT, w) as f_val: for group, paths in groups.items(): target f_val if group in val_groups else f_tr for p in paths: target.write(p \n)group base.split(_)[0]是假设文件名有统一前缀比如coil_001_01.jpg和coil_001_02.jpg会归到同一组。如果实际文件名没有下划线就把这里改成按前 6 个字符分组。这个脚本不破坏原有文件只生成两个 txt 清单YOLO 训练时直接在data.yaml里引用这两个清单即可。如果你的数据集来源文件已经包含“same source”标记优先用那个标记。4.3 data.yaml 和类别映射表一个顺序错就全错用 yolov8 训练自己的数据集时data.yaml里的 names 顺序必须和转换脚本生成的classes.txt完全一致。常见写法是这样path: data train: train.txt val: val.txt names: 0: rolled-in_scale 1: scratches 2: crease 3: pitted_surface如果你看到的 VOC 里类别名是中文或英文缩写直接把classes.txt的内容复制到 yaml 的names下别手动改。这里有个额外参数要考虑imgsz。钢板长条图如果保留原始比例输入尺寸不是 640×640 的时候YOLO 默认 letterbox 会把它压成很扁的图细小划伤和麻点可能缩到一个像素以下。我一般先拿体检脚本里统计出的“主流尺寸”为准显存允许就设成接近原始宽高的尺寸不允许就切成小块训练而不是盲目给imgsz640。5. 谢韦尔数据集使用避坑五个必须提前处理的真实问题5.1 压缩包解压后文件名乱码XML 里路径对不上现象解压.7z后图片和 XML 文件名变成了“????_001.jpg”或者开头带乱码字符XML 里的 filename 字段和你看到的不一致运行训练脚本时报找不到图片。原因压缩包内的文件名包含中文或特殊字符解压工具在不同系统下的默认编码不一致Windows 和 Linux 之间尤其容易出现这种翻车。解决先把所有文件名统一成纯 ASCII 编号再重新生成 XML 里的 filename 字段。批量改名时别用带原始文件名的日志直接按遍历顺序重写import os, glob img_dir data/VOC/JPEGImages for i, old in enumerate(sorted(glob.glob(os.path.join(img_dir, *)))): ext os.path.splitext(old)[1] new os.path.join(img_dir, fimg_{i:05d}{ext}) if old ! new: os.rename(old, new)改名之后XML 里旧的 filename 全部失效需要同步更新。如果 Annotations 里的 filename 字段错位最快的方法是直接从 XML 的 image 目录里按文件名推导而不是手动编辑。5.2 JPEG 的 EXIF 方向把标注框带偏现象图片预览正常但模型预测的框旋转了 90 度或者训练时 loss 发散。原因相机在 EXIF 信息里写了 Orientation 方向标记很多读图库不会自动应用这个标记而标注工具预览时可能自动纠正了方向导致标注坐标和模型看到的内容不一致。解决先用一遍脚本统计 EXIF 方向有方向标记的图全部重存去掉 EXIF。这里我会用 PIL 的ImageOps.exif_transpose重新保存确保重存前后宽高不变。from PIL import Image, ImageOps import glob for path in glob.glob(data/VOC/JPEGImages/*.jpg): img Image.open(path) transposed ImageOps.exif_transpose(img) if transposed is not None: transposed.convert(RGB).save(path, JPEG, quality100) else: img.convert(RGB).save(path, JPEG, quality100)做完这步再去检查 XML 里记录的 width、height 是否和新图片一致不一致就说明原来标注时按旋转前的尺寸做的要重新对框。5.3 类别映射被排序函数打乱模型把缺陷学串现象训练正常跑验证集 mAP 也不低但抽查预测结果时发现每次输出的类别稳定地错位一个数字。原因转换脚本里用set()或sorted()动态生成类别 ID不同运行环境下字符串排序规则不一样或者文件遍历顺序变了导致同一个类别名在不同训练中对应不同数字 ID。解决只用固定顺序的classes.txt做映射并且在训练前打印一次映射表并人工确认。data.yaml 的 names 顺序跟 classes.txt 保持一致不要依赖工具自动生成。这段是很多人踩过的坑不是算法问题是工程问题。5.4 转换时出现过零宽高的框训练直接报 shape mismatch现象yolov8 训练到第二个 epoch 时报错类似AssertionError: labels shape mismatch或Invalid labels打开某一张图的 txt 发现里面写了一行0 0.5 0.5 0 0。原因RLE 掩码转矩形框时某些缺陷只有 1 像素宽或 1 像素高除以图片宽高后归一化数值变成 0成了无效框。解决转换脚本里加一行过滤宽高小于 2 像素的原始框直接丢弃不要保留到 txt 里。正规数据集一般不会有这种情况但二次加工出来的资源很容易带进来体检脚本里也应该把零宽高考虑进去。5.5 四类样本极不平衡验证集 mAP 虚高现象平均 mAP 看起来不错单独看一眼某类别的 precision-recall 曲线掉到 0.3 以下。最常见的是 pitted_surface 这类小目标类别样本少且框小模型学不动。原因钢材缺陷数据集天然长尾分布划伤类样本可能是皱褶类的几十倍。平均 mAP 被高频类别拉高低频类别拖垮但贡献小。解决先看体检脚本里各类别图片数。对少数类做数据层过采样训练时把少数类图片多复制几份进到训练清单或者用复制粘贴增强把少数类的缺陷实例贴到空背景图里。不要依赖改损失函数权重那种做法对检测头的收敛没有数据层过采样直接。另一个调整是验证时单独看每个类别的 AP不要只看平均分。6. 最后一个技巧用混淆矩阵和 conf 阈值判断模型能否上线6.1 从 val 时的 confusion_matrix 读出主要误报方向训练完不要只盯 mAP,第一件事是看验证集输出的confusion_matrix.png。这张图能直接告诉你哪些类别互相混淆最常见的问题是划伤和背景之间的误报。如果背景那一列占比高说明模型把钢板纹理误判成缺陷这时需要提高 conf 阈值或者补背景负样本。如果某个缺陷类别被大量认成另一类先回头检查类别映射顺序再看训练图里这两类缺陷是否长得过于接近。6.2 用单图推理脚本把 conf 和 iou 阈值卡在产线能接受的位置我习惯在训练完做一次批量推理统计每个预测框的置信度分位数再决定生产环境的阈值。比如要求每 100 张图最多 5 个误报就去看对应误报数下的置信度分位点把 conf 设到这个值而不是一直用默认的 0.25。from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict(data/YOLO/images, conf0.35, iou0.5, saveFalse) for r in results: boxes r.boxes if boxes is None: continue for box in boxes: print(box.cls.item(), box.conf.item(), box.xyxy.tolist())这段代码会打印每张图预测出的类别、置信度和坐标。你可以从输出里看两个东西一是同一张图上重叠框多不多多了就调高 iou 到 0.6二是误报的置信度集中区间如果大量误报的 conf 在 0.3 到 0.4 之间把阈值抬到 0.45 往往能砍掉大半误报而保住召回。我自己的教训是钢板上细小缺陷的置信度天然偏低阈值抬得太高会把真缺陷一起过滤掉所以每次调阈值都要重新算一遍重召率曲线不能拍脑袋。另一个习惯是保留训练时的数据集外图片做盲测至少 20 张因为验证集看久了会让人产生“模型已经很好”的错觉。这套流程走完这份谢韦尔数据集的价值才算真正落地——希望帮到你。本文还有配套的精品资源点击获取