尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

钢材缺陷检测实战:VOC/COCO转YOLO格式与训练指南

钢材缺陷检测实战:VOC/COCO转YOLO格式与训练指南 简介YOLO谢韦尔钢材缺陷检测数据集面向目标检测学习者与工业质检场景包含5000张真实场景钢材表面图片覆盖多样缺陷类型。数据采用labelimg标注提供VOC、COCO、YOLO三种格式标签分别存放于对应文件夹可无缝接入YOLO系列模型直接训练。压缩包共2000个文件以1986个xml标签、5个txt说明、3个py脚本及6个html教程为主整体大小61.48MB结构清晰便于按需使用。包内附带YOLO环境搭建教程Windows与Linux版本、训练案例教程及数据集划分脚本可自行生成训练集、验证集、测试集并完成从环境配置到模型训练的全流程实操。适合需要规范化钢材缺陷检测数据、快速上手YOLO训练流程的研究者或开发者参考使用目前已有397人学习下载。1. 谢韦尔钢材缺陷检测先认清你要训的是什么把“YOLO谢韦尔钢材缺陷检测数据集”这个压缩包解压之后你手里其实是三样东西5000张带缺陷的钢材表面图像、同一套标注在 VOC / COCO / YOLO 三种格式下的标签文件、以及配套的划分脚本和训练教程。对做工业视觉的人来说这个组合很友好因为它省掉了最容易被卡住的“数据格式互相折腾”阶段能让你直接跳到模型训练这件事上。这个数据集里最常见的类别包括夹杂、划痕、麻点、氧化皮等几类表面缺陷背景是纹理复杂的钢板缺陷面积小、形态不规则正好是你检验 YOLO 系列模型在“小目标 低对比度”场景下到底行不行的试金石。适合谁来用刚接触目标检测的新手可以拿它练手快速跑通“数据 → 模型 → 验证”的完整链路有经验的人则可以拿它来测不同 YOLO 版本在工业场景的召回率和误检率。这篇文章就顺着这个包里的内容从标签格式、划分脚本、训练参数一路写到排错和验收。2. VOC、COCO、YOLO 三种标签格式从数据组织到格式转换2.1 三种格式的本质差异很多人拿到“三种格式标签”时第一反应是困惑不是有 YOLO 格式就能训练吗为什么还要给 VOC 和 COCO其实这三种格式分别对应了不同工具链的约定你可以把它们理解成三种不同语言描述的同一个事实。VOC 格式以图像为索引一张图对应一个 XML 文件标注信息写在object节点里坐标是绝对值xmin, ymin, xmax, ymax可读性最好但文件多且冗余。COCO 格式把整个数据集的所有标注集中到一个 JSON 里按images、annotations、categories三张表组织坐标同样是绝对值格式级联适合用来做跨模型对比或对接 Detectron2、mmdetection 这类框架。YOLO 格式则是每张图对应一个 txt 文件每行写成class_id x_center y_center width height其中坐标和宽高全部归一化到 0~1模型训练时读取效率最高也是 YOLO 系列原生的加载方式。这里有一个容易忽略的点COCO 的 bbox 描述的是“左上角坐标 宽度 高度”即xywhVOC 的 bbox 是“左上角 右下角”即xyxyYOLO 的 bbox 是“中心点 归一化宽高”。如果转换时没有先统一坐标系训练出来的模型会非常不稳定甚至从头到尾不收敛。下面我给出最常用的两段转换脚本分别解决 VOC 转 YOLO 和 COCO 转 YOLO 的问题。实际项目中你大概率只用到其中一段但最好都保留在工具脚本里。2.2 用脚本把 VOC 转成 YOLO txt当你解压后发现标签目录里只有 VOC 格式的 XML而你想直接跑 YOLOv8 时可以用这个脚本先把 XML 全部转成 txt。它的输入是一个 VOC 标签目录输出是同名 txt 文件并把类别顺序写入一个classes.txt。注意类别顺序必须和后续训练data.yaml里的names一一对应否则训练出来的模型类别是乱的。import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, class_names): tree ET.parse(xml_path) root tree.getroot() img_width int(root.find(size/width).text) img_height int(root.find(size/height).text) txt_name os.path.splitext(os.path.basename(xml_path))[0] .txt lines [] for obj in root.iter(object): cls obj.find(name).text if cls not in class_names: continue cls_id class_names.index(cls) bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 归一化中心点、宽高 x_center ((xmin xmax) / 2) / img_width y_center ((ymin ymax) / 2) / img_height w (xmax - xmin) / img_width h (ymax - ymin) / img_height # 防御性截断防止浮点误差导致越界 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 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} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(os.path.join(out_dir, txt_name), w) as f: f.write(\n.join(lines)) # 使用方式把下面两行换成你解压后的实际路径 class_names [Crazing, Inclusion, Patches, Pitted_Surface, Rolled_In_Scale, Scratches] os.makedirs(labels_yolo, exist_okTrue) for xml_file in os.listdir(labels_voc): if xml_file.endswith(.xml): voc_to_yolo(os.path.join(labels_voc, xml_file), labels_yolo, class_names)这段脚本里有个容易被忽略的细节XML 里的类别名必须和class_names完全一致包括大小写和下划线否则会被continue静默跳过。另一个细节是防御性截断因为部分标注工具生成的坐标会有微小越界比如xmax比图像宽度大 1 像素如果不做min/max截断训练时 YOLO 会报“bbox out of image bounds”的警告严重时直接丢样本。参数说明class_names的顺序决定了模型输出的类别编号比如索引 0 是 Crazing训练完names: [Crazing, ...]也要保持相同顺序。2.3 COCO 到 YOLO 的转换与类名检查COCO 转 YOLO 稍微复杂一点因为需要先读 JSON 里的images、annotations、categories三张表建立image_id → 文件名的映射再逐条取 bbox。下面这段脚本会把 COCO 标注文件按图片拆成多个 txt。import json import os def coco_to_yolo(coco_json, out_dir, img_dir): with open(coco_json, r) as f: data json.load(f) img_id_to_name {img[id]: img[file_name] for img in data[images]} cat_id_to_cls {cat[id]: cat[name] for cat in data[categories]} # 用类别名排序来固定索引而不是用 category_id cls_names sorted(cat_id_to_cls.values()) os.makedirs(out_dir, exist_okTrue) anns_map {} for ann in data[annotations]: img_id ann[image_id] anns_map.setdefault(img_id, []).append(ann) for img_id, anns in anns_map.items(): img_name img_id_to_name[img_id] img_path os.path.join(img_dir, img_name) # 如果图片是灰度图或 PNG 带透明通道也要在这里统一处理 from PIL import Image w, h Image.open(img_path).size txt_path os.path.join(out_dir, os.path.splitext(img_name)[0] .txt) lines [] for ann in anns: cat_name cat_id_to_cls[ann[category_id]] cls_id cls_names.index(cat_name) x, y, bw, bh ann[bbox] x_center (x bw / 2) / w y_center (y bh / 2) / h nw bw / w nh bh / h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {nw:.6f} {nh:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines)) # 把类别列表也写出来方便后续检查 with open(os.path.join(out_dir, classes.txt), w) as f: f.write(\n.join(cls_names))这段脚本里我用sorted(cat_id_to_cls.values())来生成类别顺序而不是直接按category_id排序。原因是很多自制 COCO 数据集的类别 id 不连续比如类别 id 是 1、3、5但实际只有三个类直接用 id 当索引会让训练端类别对不上。参数说明这段脚本假设图片文件真实存在且可以通过PIL.Image打开如果遇到损坏的图片训练时会在这里先暴露出来。这里的img_dir参数只需要能在转 txt 时取宽高后续训练仍然只读图片路径。3. 划分脚本让 train/val/test 既均衡又可复现3.1 划分脚本要解决的三个问题很多入门教程会说“随机抽 80% 当训练集20% 当验证集”但这个做法在钢材缺陷检测里会翻车。第一个问题是类别不平衡谢韦尔数据集中不同缺陷的出现频率差异很大纯随机划分可能让某个类别在验证集里只出现一两次最后的 mAP 波动大得离谱。第二个问题是图片泄漏如果同一张钢板被切成多张局部图并且这些局部图同时出现在训练集和验证集里模型会通过背景记忆而不是缺陷特征来得分指标虚高但上线就崩。第三个问题是可复现性每次跑脚本随机数种子不一样两次训练的数据分布不同你很难判断模型精度的变化到底是调参带来的还是划分带来的。所以一个合格的划分脚本至少要做三件事按类别比例分层抽样、保证同一来源图片不跨数据集、固定随机种子。下面这段脚本是我在工业数据集上常用的写法它按“每张图片的缺陷类别集合”来做分层划分比按单条标注做划分更符合物理直觉。3.2 一份带分层抽样的划分脚本这个脚本适合 YOLO 格式的数据集。假设你的图片和标签已经整理成下面这种结构images/放图片labels/放 txt 标签。脚本会统计每张图片包含哪些类别然后基于 StratifiedShuffleSplit 按类别比例分层划分 train/val/test最后把对应文件复制到dataset/...目录下。import os import random import shutil from collections import defaultdict random.seed(42) # 固定随机种子保证每次划分结果一致 images_dir images # 原始图片目录 labels_dir labels # 原始 txt 标签目录 out_root split_dataset # 输出目录 train_ratio, val_ratio 0.8, 0.1 test_ratio 1 - train_ratio - val_ratio # 第一步统计每张图片包含哪些类别 img_files [f for f in os.listdir(images_dir) if f.lower().endswith((.jpg, .jpeg, .png, .bmp))] img_cls_map {} for img in img_files: label_path os.path.join(labels_dir, os.path.splitext(img)[0] .txt) classes set() if os.path.exists(label_path): with open(label_path, r) as f: for line in f: if line.strip(): classes.add(int(line.strip().split()[0])) img_cls_map[img] classes # 第二步把样本按“类别组合向量”分组做分层 cls_group defaultdict(list) for img, cls_set in img_cls_map.items(): key tuple(sorted(cls_set)) # 例如 (0, 3) 表示同时含类别0和3 cls_group[key].append(img) train_imgs, val_imgs, test_imgs [], [], [] for key, group in cls_group.items(): random.shuffle(group) n len(group) n_train int(n * train_ratio) n_val int(n * val_ratio) train_imgs.extend(group[:n_train]) val_imgs.extend(group[n_train:n_train n_val]) test_imgs.extend(group[n_train n_val:]) # 第三步复制文件到目标目录 for subset, imgs in [(train, train_imgs), (val, val_imgs), (test, test_imgs)]: out_img_dir os.path.join(out_root, subset, images) out_lbl_dir os.path.join(out_root, subset, labels) os.makedirs(out_img_dir, exist_okTrue) os.makedirs(out_lbl_dir, exist_okTrue) for img in imgs: shutil.copy(os.path.join(images_dir, img), os.path.join(out_img_dir, img)) label_name os.path.splitext(img)[0] .txt src_label os.path.join(labels_dir, label_name) if os.path.exists(src_label): shutil.copy(src_label, os.path.join(out_lbl_dir, label_name))这个脚本的关键设计是把“类别组合”而不是“单个类别”作为分层依据。谢韦尔数据集里一张钢板可能有三种缺陷同时出现如果只按单类别分层划分后可能某些多缺陷组合只在训练集出现模型永远学不到“多个缺陷同时出现”的情况。参数说明random.seed(42)是玄学中的玄学同一个数据集固定种子你才能复现实验和论文里的数字如果想做互相比较可以把 seed 参数化但不推荐每次跑都换种子。train_ratio, val_ratio我习惯设 0.8/0.1测试集留 0.1因为钢材缺陷数据集本身不大测试集太小会让最后的评估置信度下降。3.3 划分后必须做的检查划分完不要立刻去训练先花五分钟检查两件事。第一件是每个子集的类别分布第二件是标签文件里是否有空的或异常的坐标。下面这段代码会打印每个子集中每个类别的样本数以及标签文件的行数异常情况。import os for subset in [train, val, test]: label_dir os.path.join(split_dataset, subset, labels) cls_count {} empty_files 0 for lbl in os.listdir(label_dir): if not lbl.endswith(.txt): continue path os.path.join(label_dir, lbl) lines [l.strip() for l in open(path) if l.strip()] if not lines: empty_files 1 for line in lines: parts line.split() cls_id int(parts[0]) cls_count[cls_id] cls_count.get(cls_id, 0) 1 print(f{subset}: {cls_count}, empty_files{empty_files})如果某个子集的类别数明显偏离原始比例比如验证集中类别 2 只有 3 个框那我建议把random.seed换一个数字重新划分或者增大val_ratio。如果出现empty_files说明有的图片没有对应标注这种样本要不要留完全取决于你的业务定义但训练时 YOLO 会把它当背景图如果背景图太多模型会偏向预测“无缺陷”所以在划分脚本里可以直接把空标签文件对应的图片剔除不让它们进入任何子集。4. 用 YOLOv8 训练自己的数据集目录结构、yaml 与参数4.1 按 YOLO 期望的目录结构整理数据YOLOv8 官方默认的数据目录结构是每个子集下面分images和labels两个目录图片和 txt 同名。很多人在这一步翻车因为之前用 VOC/COCO 格式训练过别的框架习惯把标签集中放在一个文件夹里。我一般解压完数据集后会先建一个steel_dataset/根目录然后按下面这个结构摆放steel_dataset/ ├── train/ │ ├── images/ │ │ ├── 00001.jpg │ │ └── ... │ └── labels/ │ ├── 00001.txt │ └── ... ├── val/ │ ├── images/ │ │ ├── 00321.jpg │ │ └── ... │ └── labels/ │ ├── 00321.txt │ └── ... └── test/ ├── images/ └── labels/注意test目录不是必须的YOLOv8 在训练时只会读取train和val两个子集test子集是留给最后做评估用的。如果你用的是上一个划分脚本它已经把文件按这个结构复制好了。我这里要强调一个“文件名一致”的问题图片如果是00001.jpg标签就必须是00001.txtYOLO 只靠前缀匹配来关联图片和标签不检查内容。要是你的原始数据有00001 (1).jpg这种带空格的文件名最好先批量重命名否则后续串路径时会冒出各种诡异报错。4.2 写 data.yaml 并启动训练在 ultralytics 框架下训练第一步是写data.yaml。下面这份配置对谢韦尔钢材缺陷数据集是通用的path: /absolute/path/to/steel_dataset train: train/images val: val/images test: test/images nc: 6 names: 0: Crazing 1: Inclusion 2: Patches 3: Pitted_Surface 4: Rolled_In_Scale 5: Scratches这里最关键的是names的顺序。如果第 2 章你用 VOC 转换脚本时把class_names写成了某个顺序这里必须完全一致用 COCO 转换脚本时因为脚本里用了sorted()这里也要按字母序写。顺序不一致的后果是模型训练时的类别索引和标签文件对不上损失震荡mAP 基本为零。path建议写绝对路径尽量别用相对路径因为不同机器上启动目录不一样相对路径经常导致图片加载不到。训练命令我常用的是yolo train datasteel_dataset.yaml modelyolov8n.pt epochs150 imgsz640 batch16 workers8 projectruns namesteel_severstal参数说明modelyolov8n.pt表示加载官方预训练权重会在 COCO 预训练的基础上做迁移学习如果网络不好可以先手动下载预训练模型放到weights/目录然后用modelweights/yolov8n.pt指定。imgsz我设 640因为原始钢板图像一般比较大直接 1280 会占爆显存batch16是 24GB 显存下的经验值如果你只有 12GB建议降到 8 或 4。这里没有简单调大lr的必要YOLOv8 默认的 SGD 参数在中小数据集上已经很稳。4.3 预训练权重、训练轮数和损失函数怎么取舍很多人纠结到底用yolov8n、yolov8s还是yolov8m。我的习惯是先用 n 和 s 各跑一轮只看前 50 个 epoch 的验证集 mAP50 曲线。如果 s 的提升明显再切到 s 或 m 做正式训练。因为钢材缺陷是小目标理论上更大模型能捕获更多纹理特征但工业场景对推理速度有要求n 或 s 的量级在 GPU 上能跑到 30fps 以上m 就有点悬了。关于训练轮数5000 张图不算多默认的 100 epoch 通常能跑到平台期但如果发现验证集 mAP 还在涨可以继续用resume参数接着训。还有一点是 YOLOv8 默认使用 CIoU 损失它对边界框形状比较敏感对钢材这种不规则缺陷有天然优势。如果你发现模型对小缺陷召回不够不要急着调损失函数先去检查数据增强是不是把缺陷缩小到几个像素了在 ultralytics 里可以把hsv_h、scale等增强参数调低或者直接关掉 mosaic 增强mosaic0因为 mosaic 会把多张图拼在一起对于小目标缺陷可能会产生大量“裁剪后消失”的负样本。5. 训练常见问题排查标签、BN、显存与混淆矩阵的坑5.1 标签文件里出现了越界坐标现象训练刚开始控制台不断刷WARNING ⚠️ Box with (x, y, width, height) out of bounds或者有些图片框的数量比实际标注少。原因这是标签文件里存在绝对值坐标越界或归一化坐标超出 0~1 范围。常见来源有两个一是用 VOC 转换时没有做防御性截断比如xmax比图像宽度大 1 像素二是从 COCO 转 YOLO 时某些标注的x width大于图像宽度而脚本直接按width / img_width计算没有先对原坐标做修正。解决不要只改几个文件直接在加载数据集前跑一次标签扫描脚本把所有越界的坐标打印出来并按x_center min(max(x_center, 0.0), 1.0)的方式修正后回写。同时检查那个文件对应的原图宽度和高度是否和标注尺寸一致因为有些数据集的 XML 里写的是size/width但 PNG 实际尺寸不同这是常见的“黑匣子”问题。5.2 训练中损失变成 NaN或 BN 崩溃现象训练到几十个 batch 后loss突然变成nan或者出现BN is running completely on zero inputs的警告然后 mAP 直线下降。原因最常见原因是学习率过大和 batch size 过小导致的梯度不稳定。YOLOv8 默认初始学习率对 8 卡环境是合理的但你单卡跑数据量小的时候可能前 10 个 epoch 就击穿 BN 的统计量。另一个原因是 embed 数据里有极端颜色分布比如整张图全黑或全白激活值变化剧烈。解决第一步把batch降到 4 或 8看是否恢复第二步把初始学习率从 0.01 降到 0.001或者直接用optimizerSGD而不是AdamW工业小数据集上 SGD 更稳。如果还不行检查是不是有损坏图片或者空标签导致某类梯度一直为零把训练集里解码失败的图删掉再训。5.3 混淆矩阵总和不是预设类别数现象训练完看confusion_matrix.png发现每一行的样本总数不等于该类别在验证集里的标注数量甚至总和和多出来的数值都对不上。原因这是很多新人误以为“模型错了”的陷阱。YOLO 的混淆矩阵是按预测结果统计的它的行表示真实类别列表示预测类别但最右一列是“背景”或“未检测到”最底部一行是“预测为背景”。所以“总和和类别数不一致”是正常现象真正该看的是每个类别的召回率沿对角线是否够高。如果你用ultralytics生成的混淆矩阵发现除对角线外有大量横向分布说明类别相似度高比如麻点和划痕相互误检这种情况下先做类别合并或者数据平衡而不是盲目加网络深度。现象验证集 mAP50 很高但 mAP50-95 很低且显存占用居高不下。原因钢材缺陷是小目标mAP50-95 对边框的 IoU 阈值非常敏感模型虽然能把目标找出来但框的位置不够精确所以 IoU 超过 0.6 就掉点。另一个原因是图像分辨率太低很多缺陷只有几个像素模型学不到精细边界。解决把imgsz从 640 提到 768 或 1024显存不够就降低 batch size。同时在验证时开启rectTrue或augmentTrue做多尺度验证能显著提升 mAP50-95。这一步是“后悔药”因为你训练时如果用相对低分辨率后期加分辨率推理效果通常立竿见影。5.4 显存不足从 batch size、imgsz、workers 三个方向压下去现象启动训练不到一分钟CUDA OOM 直接退出或者系统内存被占满然后开始 swap训练速度掉到蜗牛水平。原因显存不足的诱因多数是batch和imgsz设置得太贪心尤其是把imgsz1280、batch16一起设上。另一个隐性问题是workers开太多多个数据加载进程同时把数据灌进内存CPU 成为瓶颈看起来也像“卡死”。很多人的第一反应是换卡但实际 12GB 显存也可以训练 YOLOv8n只是要会算账。解决显存紧张的保守参数是batch8 imgsz640 workers4。如果还 OOM就继续降batch到 4 和imgsz到 512同时把训练命令里的cacheTrue关掉因为缓存图片到显存会额外占一部分空间。用nvidia-smi监控显存曲线正常情况下前几个 batch 显存会小幅上涨然后趋于稳定如果持续上涨到 OOM大概率是数据加载或梯度累加的逻辑异常这时候重启训练并升级到最新版 ultralytics 往往能解决。6. 训练完怎么验收从混淆矩阵到部署的进阶技巧6.1 用混淆矩阵、PR 曲线和推理图判断能否上线训练结束后不要只盯着 best.pt 的 mAP。我会在验证集上跑一次yolo val特意保存三个输出confusion_matrix.png、PR_curve.png和val_batch_pred.jpg。先说混淆矩阵如果某个真实类别的对角线只有 60%建议去val_batch_pred.jpg里看那些漏检的图八成是缺陷面积太小或纹理太像背景。PR 曲线则告诉你“提升召回率会牺牲多少精确率”比如你要保证缺陷不出厂就选择召回率 0.95 对应的置信度阈值而不是默认的 0.25。我习惯的做法是训练完再用yolo predict对一批测试图片做可视化观察哪些缺陷类别系统性漏检再决定是补数据还是调增强。6.2 导出 ONNX 并用多尺度推理提升稳定性验证通过后我一般会导出成 ONNX因为后面不管是 C 部署还是用 ONNX Runtime 做服务都比直接依赖 PyTorch 稳。导出命令很简单yolo export modelbest.pt formatonnx imgsz640。导出后要检查一版输出的类别顺序确保和你data.yaml一致。import onnxruntime as ort from PIL import Image import numpy as np sess ort.InferenceSession(best.onnx) input_name sess.get_inputs()[0].name img Image.open(test_steel.jpg).resize((640, 640)) input_data np.array(img).astype(np.float32) / 255.0 input_data input_data.transpose(2, 0, 1)[None, ...] outputs sess.run(None, {input_name: input_data})导出后还有个小技巧对于钢材表面的微小缺陷单尺度推理容易被漏检可以同时用 640 和 768 两个尺寸分别推理然后做 NMS 合并。这样会增加约 30% 推理耗时但对“漏检就是事故”的工业场景来说通常值得。如果你把模型部署到生产环境建议固定输入尺寸并且在预处理时不要随意改变图像亮度因为训练时默认的hsv增强已经让模型对颜色有一定容忍度但过度锐化和归一化依然会改变分布。经验上把训练好的模型投入新的钢材产线前至少要拿一批跟训练集不同时间段采集的图片去验证只看公开数据集的 mAP 很容易高估线上表现。我自己吃过不少教训训练时 mAP50 到了 0.9上线时对现场反光强烈的钢板频繁误检后来发现是数据增强里的翻转和旋转把缺陷的物理方向性破坏了。从那以后我只做轻微平移和尺度扰动能做到“线上和离线指标差别不大”这也希望帮到你。本文还有配套的精品资源点击获取
返回列表