
简介这是一份用于钢材表面缺陷检测的YOLO数据集面向计算机视觉学习者、工业质检项目开发者及课程实践者可支撑目标检测入门练习、模型训练与课程设计。压缩包内共2000个文件以1986个xml标签为主同时提供json、txt等多种格式标注覆盖VOC、COCO、YOLO三种主流目标检测规范三类标签分别存放在不同文件夹中配合真实场景的高质量图片可直接用于YOLO系列模型训练与效果评估。资源附赠Linux与Windows双平台YOLO环境搭建教程、训练案例说明以及训练集/验证集/测试集划分脚本便于读者根据实际需求灵活重组数据并快速跑通训练流程。数据集整体约61.48MB轻量易用目前已有397人学习下载尤其适合需要规范标签、完整训练指引与可复用脚本的中初级学习者。1. 谢韦尔钢材缺陷检测5000 张真实钢板图能把目标检测整条链路跑通吗在一家钢厂的表面质检工位上钢板以每秒几米的速度从辊道经过表面的划痕、氧化皮、麻点和斑块稍纵即逝人工盯屏漏检率居高不下产线需要的是一套能在几百毫秒内把缺陷框出来的目标检测模型。谢韦尔钢材缺陷检测数据集就是为这类需求准备的真实工业数据5000 张钢板表面图像同时给出 VOC、COCO、YOLO 三种格式标签外加划分脚本和训练教程。解压之后不用再折腾格式转换直接把数据丢给 YOLO 就能开训。对想用 YOLO 入门目标检测、又不想拿人造图片练手的人来说这是很好的起点对要搭工业质检 demo 的工程师它也省掉了标注格式混杂的坑。2. 三种标签格式到底装了什么VOC、COCO、YOLO 的结构与选型钢材表面缺陷检测和常规的目标检测有一个很大的不同原始数据集的标签往往不是框而是像素级掩码。谢韦尔数据集源自 Kaggle 上的 Severstal Steel Defect Detection 比赛比赛任务要求参赛者输出缺陷区域的像素级掩码用 RLE 编码存储在 CSV 文件里。而这个数据包帮你把掩码转成了检测模型真正需要的边界框再包装成 VOC、COCO 和 YOLO 三种主流格式。理解这三者各自的组织方式比直接开训更重要因为后面所有脚本和训练配置都建立在这套结构之上。2.1 原始标签是像素级掩码检测任务要的是框谢韦尔钢材缺陷共分四类rolled-in scale含氧化皮、patches斑块、pitting麻点/蚀斑、scratch划痕。热轧钢板表面缺陷的特点是背景噪声大、缺陷对比度低、划痕细长而氧化皮却可能占据大块面积同一张图上往往同时存在多个小缺陷。这种数据直接做分割是可行的但如果只是判断“有没有缺陷、缺陷在哪儿”目标检测的性价比更高——标注体积小、训练速度快、部署更容易落地。我解压过这类数据集后一般会先做一件事统计每张掩码图里的连通域数量和最小外接框尺寸。这一步能直接暴露出后面训练时的大部分问题比如某些缺陷的框可能只有几个像素宽低于 YOLO 的默认下采样倍数时基本学不到。常见做法是把掩码 PNG 读取为单通道灰度图然后用cv2.connectedComponentsWithStats找出每个独立缺陷区域的最小外接矩形再把矩形写成检测格式。这个思路是理解后面三个转换脚本的基础因为三种格式的标签最终都来自同一个掩码转框的结果。2.2 VOC 与 COCO两种标注规约的“普通话”Pascal VOC 是目标检测领域历史最悠久的标注格式核心思想是“一张图对应一个 XML 文件”。XML 里记录文件名、图像尺寸、目标类别以及每个目标的xmin, ymin, xmax, ymax像素坐标。它的优点是直观任何人打开 XML 都能看懂缺点也明显——类别列表分散在各个 XML 里统计类别分布要遍历整个目录。COCO 则把所有信息收拢到一个 JSON 文件里用四个顶层数组组织数据images存图像 ID 和尺寸categories存类别 ID 和名称annotations存每个框的image_id、category_id、bbox和area另外还带info和licenses两个元信息字段。我在实际项目里更常用 COCO因为一个 JSON 就能完成类别统计、数据筛选和跨库传递但新手第一次打开 COCO JSON 时往往被那串嵌套结构绕晕。下面这张表可以直接看清差异维度VOCCOCOYOLO txt标注组织方式一张图一个 XML全量数据一个 JSON一张图一个 txt边界框表示xmin, ymin, xmax, ymax像素x, y, w, h像素xc, yc, w, h归一化类别定义写在每个 XML 里categories 数组统一管理data.yaml 的 names 列表常用配套工具labelImglabelme / CVATCVAT / Roboflow训练时读取速度需解析 XML偏慢需解析 JSON中等纯文本直接读最快选型上没有绝对的“最优”只有“适不适合”。如果你的训练代码是基于 Detectron2 或 MMDetection 的建议直接用 COCO如果只是跑 YOLO 系列用 YOLO 格式最省事。2.3 YOLO 格式txt 只存归一化坐标训练时直接读YOLO 格式是三者中最“简洁粗暴”的每张图对应一个同名 txt 文件每一行代表一个目标格式是class x_center y_center width height其中位置和宽高都除以了图像宽高归一化到 0 到 1 之间。比如一张 1280×1024 的图像里某个框左上角是 (320, 256)右下角是 (960, 768)那么归一化中心就是 (0.5, 0.5)宽高是 (0.5, 0.5)txt 里存的就是0 0.5 0.5 0.5 0.5。这种格式把坐标从像素空间映射到相对空间意味着训练时不管输入图片被缩放到 640 还是 1280标签都不用跟着改。Ultralytics YOLO 在训练时会直接读 txt 文件跳过 XML 或 JSON 解析所以读取速度最快。代价是 txt 文件的可读性很差手工改一个数字都可能导致标注错位所以数据包的转换脚本就显得尤其重要。提示不要迷信“YOLO 格式只能给 YOLO 用”。只要坐标归一化的规则不变Faster R-CNN 等模型也可以在训练前把 txt 转回绝对坐标使用。3. 从掩码到三种标注转换脚本逐段拆解理解了三种格式之后你一定想知道转换脚本是怎么运作的。谢韦尔数据集的转换链路一般是读取掩码 PNG → 连通域分析得到 bbox → 分别写出 VOC XML、COCO JSON、YOLO txt。下面四段脚本基本可以应对这个数据包的标准场景每一步都建议在解压后的副本上操作不要在原目录上直接覆盖标签。3.1 掩码转 bbox连通域统计与最小外接矩形掩码图里每一个像素值不为 0 的区域都对应一个缺陷但同一张图里可能有多个互不相连的缺陷区域所以要用连通域分析把它们拆开。下面这段代码可以遍历一个掩码目录输出每个连通域的外接框import cv2 import numpy as np from pathlib import Path def mask_to_boxes(mask_path: Path, min_area: int 16) - list: 把掩码 PNG 转成若干边界框。 mask_path: 单张掩码图路径 min_area: 面积小于该值的连通域直接丢弃 返回 [(x1, y1, x2, y2), ...] mask cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) if mask is None: return [] num_labels, labels, stats, _ cv2.connectedComponentsWithStats( mask, connectivity8 ) boxes [] for i in range(1, num_labels): # 0 是背景 x, y, w, h, area stats[i] if area min_area: continue x1 int(x) y1 int(y) x2 int(x w - 1) y2 int(y h - 1) boxes.append((x1, y1, x2, y2)) return boxes逻辑说清楚connectedComponentsWithStats返回四个值第一个num_labels是连通域总数第二个labels是每个像素所属的编号第三个stats是每个连通域的统计信息列顺序固定为x, y, width, height, area。代码从 1 开始遍历因为 0 永远是背景。min_area这个参数很关键钢材缺陷里有的划痕细到只有一个像素宽转换成框以后宽高可能小于 2 个像素这种框在 YOLO 里基本是负作用建议丢弃或者用后续的形态学膨胀先合并。3.2 生成 VOC一张图一个 XML用 ElementTree 写拿到 bbox 列表后生成 VOC 格式最简单的方式是使用 Python 标准库的xml.etree.ElementTree不需要额外依赖。注意 XML 里的filename要写成纯文件名path字段是绝对路径二者不要混用否则后续跑 MMDetection 等框架时容易出校验错误from xml.etree import ElementTree as ET from pathlib import Path import cv2 def write_voc_xml(img_path: Path, boxes: list, xml_path: Path) - None: img cv2.imread(str(img_path)) h, w img.shape[:2] root ET.Element(annotation) ET.SubElement(root, filename).text img_path.name ET.SubElement(root, path).text str(img_path.resolve()) size ET.SubElement(root, size) ET.SubElement(size, width).text str(w) ET.SubElement(size, height).text str(h) ET.SubElement(size, depth).text str(img.shape[2]) for x1, y1, x2, y2 in boxes: obj ET.SubElement(root, object) ET.SubElement(obj, name).text defect ET.SubElement(obj, pose).text Unspecified ET.SubElement(obj, truncated).text 0 ET.SubElement(obj, difficult).text 0 bndbox ET.SubElement(obj, bndbox) ET.SubElement(bndbox, xmin).text str(x1) ET.SubElement(bndbox, ymin).text str(y1) ET.SubElement(bndbox, xmax).text str(x2) ET.SubElement(bndbox, ymax).text str(y2) ET.ElementTree(root).write(str(xml_path), encodingutf-8, xml_declarationTrue)几个关键点width和height如果直接读图片而不是用掩码尺寸可以避免掩码被 resize 后坐标错位的隐患depth字段对灰度图写 1对 RGB 图写 3YOLO 训练一般用不上但 VOC 校验会检查。ElementTree.write默认不写 XML 声明加xml_declarationTrue可以保证文件在第三方解析器下兼容。如果要把 VOC 再转为 COCO一般推荐用voc2coco脚本但前提是 XML 里的difficult字段必须存在否则转换会中断所以这里即使所有目标都填 0 也要写出来。3.3 生成 COCO一次 dict 组装一把 json.dumpCOCO JSON 的组织方式和 VOC 截然不同它是一个嵌套 dict。最容易踩坑的是image_id的分配必须先给所有图片建立id → 文件名的映射再填写annotations否则很容易出现“标注的 image_id 指向了错误的图片”的隐性错误。先看一下核心结构import json from pathlib import Path def build_coco_json(images: list, all_boxes: dict, out_path: Path) - None: images: 图片路径列表 all_boxes: {图片名: [(x1, y1, x2, y2), ...]} 这里约定所有缺陷统一归为类别 1如果想按四类缺陷分开 在生成 boxes 时就应保留类别编号。 ann_id 1 coco_dict { info: {description: severstal steel defect}, licenses: [{id: 1, name: MIT}], categories: [{id: 1, name: defect}], images: [], annotations: [], } for img_id, img_path in enumerate(images, start1): h, w cv2.imread(str(img_path)).shape[:2] coco_dict[images].append({ id: img_id, file_name: Path(img_path).name, width: w, height: h, }) for x1, y1, x2, y2 in all_boxes[Path(img_path).name]: box_w x2 - x1 1 box_h y2 - y1 1 coco_dict[annotations].append({ id: ann_id, image_id: img_id, category_id: 1, bbox: [x1, y1, box_w, box_h], area: box_w * box_h, iscrowd: 0, }) ann_id 1 with open(out_path, w, encodingutf-8) as f: json.dump(coco_dict, f, indent2)这段代码里bbox用的是[x, y, width, height]而不是 VOC 的(xmin, ymin, xmax, ymax)两个方向写反是新手最常犯的错。area在 COCO 中建议写真实像素面积虽然训练时很多框架会重新计算但用评估工具时会拿它和模型输出做对比。iscrowd0表示每个框是独立目标如果后续想过滤重叠标注可以改成 1但目前你不需要。如果这个数据集包含四类缺陷你需要把互联域分析和类别 ID 绑定在掩码图上按像素值区分缺陷类型再传进category_id。3.4 生成 YOLOtxt 小文件与 classes.txt 的配合最后一步是生成 YOLO 训练直接读取的 txt 标签。这里要格外小心归一化的精度YOLO 坐标系里x_center和width都必须大于 0 且小于等于 1浮点数越界一个像素都会在训练时报AssertionError。我习惯在写出前做一次 clamp 兜底def write_yolo_txt(boxes: list, img_w: int, img_h: int, txt_path: Path) - None: boxes 里每一项建议是 (class_id, x1, y1, x2, y2) 坐标在归一化前先 clamp 到图像范围内。 lines [] for cls_id, x1, y1, x2, y2 in boxes: x1 max(0, min(x1, img_w - 1)) x2 max(0, min(x2, img_w - 1)) y1 max(0, min(y1, img_h - 1)) y2 max(0, min(y2, img_h - 1)) if x2 x1 or y2 y1: continue xc (x1 x2) / 2 / img_w yc (y1 y2) / 2 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h # 归一化后超过 [0,1] 时直接截断 xc min(max(xc, 0.0), 1.0) yc min(max(yc, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) lines.append(f{cls_id} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}) txt_path.write_text(\n.join(lines), encodingutf-8)这里保留了 6 位小数是因为 YOLO 训练读取标签时普遍用float()解析位数太少会导致框偏移几个像素对小目标影响尤其明显。另外 txt 文件命名要和图片名保持严格一致比如0001.jpg对应0001.txt大小写和扩展名都要统一。常见的翻车是图片是.jpegtxt 是.jpg导致训练时一半标签读不到。数据包解压后建议先跑一遍脚本检查子目录里图片和标签的文件名是否一一对应不要相信肉眼。4. 划分脚本按图划分才能避免数据泄漏随机种子必须固定很多人在数据划分上很随意直接shuffle一下就把 5000 张图按 8:1:1 分开了。但对钢材缺陷这类数据划分方式直接决定训练结果的真实性。如果同一张钢板的左右两半被分别分进训练集和验证集模型实际上记住了钢板的纹理特征而不是缺陷特征验证指标会虚高部署到新产线时立刻现原形。4.1 按图片划分不要把同一个钢板的多个框拆开谢韦尔数据集的原始掩码里常常一张图有多个缺陷框有的框之间距离非常近甚至来自同一个物理缺陷被切断后又合并。划分脚本最核心的一条规则以“图片”为最小单位划分而不是以“标注框”为最小单位。下面这段脚本是常规的按比例随机划分思路from pathlib import Path import random import shutil random.seed(2024) # 固定随机种子保证每次划分结果一致 data_root Path(dataset) image_dir data_root / images label_dir data_root / labels images sorted(image_dir.glob(*.jpg)) random.shuffle(images) train_ratio, val_ratio 0.8, 0.1 train_cut int(len(images) * train_ratio) val_cut int(len(images) * (train_ratio val_ratio)) splits { train: images[:train_cut], val: images[train_cut:val_cut], test: images[val_cut:], } for split_name, split_images in splits.items(): (data_root / split_name / images).mkdir(parentsTrue, exist_okTrue) (data_root / split_name / labels).mkdir(parentsTrue, exist_okTrue) for img_path in split_images: shutil.copy2(img_path, data_root / split_name / images / img_path.name) label_file label_dir / (img_path.stem .txt) if label_file.exists(): shutil.copy2(label_file, data_root / split_name / labels / label_file.name)random.seed(2024)这一步不是可有可无。如果你不固定种子每次跑脚本都会得到不同的划分训练结果无法复现后面做超参数对比时你会发现所有实验都像是在黑匣子里调参。还有一个重要细节这里用copy2而不是move是为保留原始目录作为备份。工业数据集往往要反复调整划分比例如果直接把文件移动了想改回原比例就得重新解压。4.2 划分比例、分层策略和类别统计8:1:1 是常规默认值但钢材缺陷检测数据集中负样本无缺陷图片和四类缺陷的分布并不均匀。如果验证集里某种缺陷特别少mAP 会被整体拉低这不能说明模型不行而是数据划分不均匀。更可靠的做法是先统计每类缺陷在图片级别的出现次数再按类别做分层划分。下面这个参数表可以作为参考参数默认值调整建议train_ratio0.8数据量小可降到 0.7但要保证验证集不少于 200 张val_ratio0.1尽量不要低于 0.05否则验证结果抖动大test_ratio0.1如果没有测试需求可合并进 valseed2024每次实验换 seed 相当于换数据分布不要频繁换min_area16低于该面积的缺陷建议丢否则训练 loss 会被小框干扰我一般会在划分脚本后面加一段统计输出打印训练集和验证集中每个类别的图片数。如果出现“验证集完全没有划痕类”的情况就需要用sklearn.model_selection.train_test_split的stratify参数按类别比例分层。注意这里的“分层”是针对图片级类别标签一张包含多类缺陷的图在分层时要归到多个类别里容易重复计数所以我更推荐自己写一个按类别分配的循环而不是完全依赖现成 API。4.3 生成 data.yaml为 YOLO 训练准备目录与类别映射划分完成后YOLOv8 训练前需要一个data.yaml文件告诉框架图片路径、验证集路径和类别名称相当于数据集和模型之间的“适配层”。最常见的写法是path: dataset train: train/images val: val/images test: test/images names: 0: rolled-in_scale 1: patches 2: pitting 3: scratchpath可以写绝对路径也可以写相对路径但要注意 YOLO 在解析时会把它和train拼成完整目录如果数据集根目录不在执行命令的当前目录下最好写绝对路径。names的顺序必须和生成 YOLO txt 时写入的索引一一对应0对应rolled-in_scale1对应patches如果顺序写反训练过程不报错但预测结果全部错位而且这种错位很难通过 loss 曲线察觉。判断自己的data.yaml是否正确的办法是训练前跑一次yolo detect train datadata.yaml modelyolov8n.pt epochs0框架会打印出每个类别的样本数量你要逐一核对。5. 常见问题与避坑从解压到训练会遇到的 5 个坑这节内容是从真实训练经验里沉淀出来的每个坑都曾经让人苦等几个小时的训练后才发现。钢材缺陷检测数据集本身不大但正因为小任何标签问题都会被放大成明显的 mAP 异常。下面是按出现频率排序的 5 个典型问题。5.1 空标签被跳过导致训练时图片和标签对不上现象训练启动时输出的训练集图片数为 5000但实际读完标签后只训练不到 4000 张还有 1000 张图“消失”了。原因谢韦尔数据集包含大量无缺陷背景图这类图没有对应的缺陷掩码转换脚本生成 YOLO txt 时直接写了个空文件或者干脆没生成。Ultralytics YOLO 在读取标签时会把空 txt 认为是“无目标”但有些版本会把它过滤掉。解决在转换脚本里对所有图片统一生成 txt哪怕是空文件也要写出来。对于确实没有缺陷的背景图可以单独归到一个negative类或者把空标签保留YOLO 训练时会把它们当作背景样本参与训练。检查方法很简单统计图片目录和标签目录的文件数两者之差就是出问题的图。5.2 过小的缺陷在归一化后变成 0训练直接报错现象训练到第一个 epoch 时抛出类似AssertionError: bbox width must be positive的异常检查后发现某个 txt 里存在0.000000的宽度值。原因部分划痕缺陷只有 1 到 2 个像素宽经过 YOLO 的 mosaic 增强放大后归一化宽度计算出来小于最低精度阈值被四舍五入成 0。这类目标对检测模型来说本来就是噪声。解决在mask_to_boxes阶段提高min_area阈值我建议钢材数据最小面积设为 25 到 36 像素并按宽高分别过滤比如w 3 or h 3直接丢弃。不要只过滤面积因为面积大但宽度为 1 的细长条仍然会产生非法框。5.3 三种格式都转了一遍标签错位了现象VOC 的 XML 里某个框位于图像顶部但转成 COCO 后在底部或者 YOLO 训练时重复检测出多个异常框。原因转换脚本里遍历图片的排序不一致。VOC 是遍历 XML 目录COCO 是先遍历图片目录YOLO 是读取图片列表三个循环的排序方式不同在文件名未按字典序排列时就会整体错位。解决所有循环统一用sorted()排序并确保转换时按图片名索引 bbox而不是按列表顺序索引。更稳妥的做法是把图片路径作为字典的唯一键先构建{图片名: 原始掩码路径}的映射再基于这份映射生成所有格式。5.4 解压路径带中文或空格训练时读不到图片和标签现象在 Windows 上解压后一切正常把整个文件夹传到 Linux 服务器上训练启动时报大量FileNotFoundError路径里的中文显示为乱码。原因.rar压缩包里的文件在 Windows 下以 GBK 编码解压而 Linux 默认 UTF-8如果文件名或目录名含中文会出现编码错乱。就算文件名全英文目录嵌套层级过深或包含空格也会有隐患。解决一拿到压缩包先解压到纯英文路径下比如~/data/severstal/不要放在“桌面/钢材检测”这类目录里。解压后用file命令检查文件编码或直接在 Linux 上重新解压一次。训练前用find . -name *.txt | head抽查几个文件路径是否和data.yaml里的配置完全一致这一步能省下 90% 的“训练时找不到文件”类报错。5.5 类别不平衡mAP 虚高但实际漏检率居高不下现象训练完 mAP 达到 0.85看起来不错但在新样本上划过一批钢板后发现划痕类几乎全部漏检。原因四类缺陷里划痕出现次数可能只占 5% 到 10%模型学到的“平均分布”偏向高频类。mAP 是各类别 AP 的平均高频类 AP 高就拉高了整体值掩盖了低频类的问题。解决训练前统计类别分布并打印出来。如果某一类占比过低有两种常见做法一是对低频类做过采样让训练脚本每轮多读几次含该类别的图片二是降低 NMS 的置信度阈值因为钢材表面缺陷不像自然场景那样有大量难以区分的背景误检的代价远低于漏检。从工程角度看你更应该关注的是“每条钢板的漏检率”而不是 mAP这指标写进验收报告更有说服力。6. 训练教程与验证技巧把 YOLO 跑通并调到能用的三个技巧6.1 最小可行训练目录结构数据准备好后目录结构尽量保持简单。我会把数据集放在dataset/下子目录是train/images、train/labels、val/images、val/labels不再额外嵌套。data.yaml放在dataset/根目录下训练命令和它保持同级。这样做的原因是 YOLO 训练时大量依赖相对路径结构越扁平越不容易出错。6.2 复现一个能用的训练YOLOv8 命令行参数怎么定用 YOLOv8 训练这个数据集的最小命令是yolo detect train datadataset/data.yaml modelyolov8n.pt \ epochs100 imgsz640 batch16 lr00.01三个必调参数是imgsz、batch和lr0。imgsz我倾向设为 800 或 1024因为钢材图原始分辨率高划痕类小目标在 640 下可能只剩几个像素但显存有限时 640 也能跑前提是min_area过滤得足够狠。batch和显存直接挂钩10GB 显存跑 640 分辨率时 batch16 差不多是极限如果爆显存就减半。lr0别从默认 0.01 乱加钢材数据背景单一学习率偏高时 loss 曲线会来回震荡。做实验前建议用train子集跑 10 个 epoch 画出损失曲线确认 YOLO 的损失函数里分类和框回归两项都在下降再放手跑长训练。6.3 验证看什么从 metrics 到 ONNX 导出训练结束后不要只看results.png。先用验证集跑一遍打开生成的混淆矩阵重点看划痕类是否大面积错分成氧化皮。确认模型可用后导出 ONNX 是让模型脱离训练框架走向部署的第一步yolo export modelbest.pt formatonnx imgsz640。导出的 ONNX 可以直接在 TensorRT 里转成 engine跑在 AGX Orin 这类边缘设备上这也是很多实际产线的部署路径。我第一次跑这个数据集时在空标签统计上翻过车白白浪费了三个小时一看前几轮 loss 全在抖动后来才发现是转换脚本把背景图漏写为空 txt。从那以后我养成了习惯任何数据集到手先写一段脚本把每张图的标签条数统计出来再用可视化工具把框画出来随机抽查 50 张确认框和缺陷位置对得上才启动训练。这个习惯帮我避开了不少标注错位的坑也希望帮到你。本文还有配套的精品资源点击获取