简介:YOLO钢表面缺陷检测数据集,收录真实工业场景下10000张钢表面图片,覆盖划痕、麻点、氧化皮等常见缺陷形态,适合目标检测算法学习者、工业视觉开发者及课题研究使用。压缩包内含VOC(xml)、COCO(json)、YOLO(txt)三种标签格式,分别存放于独立文件夹,可直接对接YOLO系列训练流程,省去自行转换标注的时间。同时附赠划分训练集、验证集、测试集的Python脚本,以及Windows、Linux两种环境下的YOLO搭建与训练教程,帮助使用者按需分配数据、配置环境并快速复现模型训练。资源共2000个文件,以xml标签为主,配合py脚本、txt清单和html图文教程,总大小约212MB,目录结构清晰,便于逐项检索学习。目前已有549人学习下载,对于希望快速构建钢表面缺陷检测模型或入门YOLO实战的读者,是一份即取即用的完整资料。
1. 钢表面缺陷检测的10000张赌注:三套标签和一套能直接跑的流程
做钢表面缺陷检测的人都知道,标注比模型本身更磨人。一张图里可能同时挂着麻点、划痕和氧化皮,标注框差几个像素,训练出来的就是一堆假阳性。我拆过不少这类数据集,真正愿意反复复现的,是那种“图片、标签、脚本、教程”四件套齐全的——YOLO钢表面缺陷检测数据集就是这种。它包含10000张真实场景的高质量图片,使用labelimg标注,标注框质量高,同时给出VOC(xml)、COCO(json)和YOLO(txt)三种格式标签,分别存放在不同文件夹,拿到手省去自己写转换脚本的麻烦;文末附赠三套数据集划分脚本和Linux/Windows双版本的YOLO环境搭建、案例训练教程。适合两类人:刚进工业质检视觉项目、想快速跑通一个完整闭环的新手;手里有自己数据、想找个干净基线的工程师。
2. VOC/COCO/YOLO三种标签格式:结构差异、读取逻辑与训练前的选型
2.1 VOC的xml:绝对像素坐标与object结构
VOC格式的核心是一个xml文件,每个文件对应一张图片,所有目标框都挂在<object>节点下。先看一段典型的标注文件:
<annotation> <folder>JPEGImages</folder> <filename>steel_20241011_023.jpg</filename> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <object> <name>pitted_surface</name> <difficult>0</difficult> <bndbox> <xmin>120</xmin> <ymin>85</ymin> <xmax>230</xmax> <ymax>190</ymax> </bndbox> </object> </annotation>读取时务必注意三点:<bndbox>里存的是绝对像素坐标,xmax - xmin才是真实框宽;<name>是类别名,要和你的类别列表严格一致,大小写写错会在训练时报“class not in names”;<size>里的宽高是标注时的原图尺寸,如果训练前做了resize,要么重新算坐标,要么让数据加载器自己处理。很多VOC转YOLO的脚本在最后一步出错,根因就是拿resize后的图去对原始xml的绝对像素框。
2.2 COCO的json:image_id、category_id与bbox的映射关系
COCO把整份数据集的标注汇总到一个json文件里,结构分三段:images记录图片id和文件名,annotations记录每个目标框归属哪张图、属于哪个类别,categories给出类别id和类别名的映射。看下面的示意结构:
{ "images": [ {"id": 1, "file_name": "steel_20241011_023.jpg", "width": 640, "height": 480} ], "annotations": [ {"id": 1, "image_id": 1, "category_id": 2, "bbox": [120, 85, 110, 105], "area": 11550} ], "categories": [ {"id": 1, "name": "crazing"}, {"id": 2, "name": "pitted_surface"} ] }读COCO最容易踩的坑是bbox的格式:它存的是[x, y, width, height],不是VOC那种左上右下两个点。我见过有人直接把COCO的bbox塞给一个按[x1, y1, x2, y2]设计的可视化函数,画出来的框全是歪的,还以为是标注质量问题。另外category_id是从1开始编号(有的转换工具会从0开始),而YOLO的类别id是从0开始,跨格式转换时这个偏移量是经典的“差一错误”来源。
2.3 YOLO的txt:归一化坐标与类别id的对应
YOLO格式最简洁,每行一个目标,五个字段:类别id、中心点x、中心点y、框宽、框高,全部除以图片宽高做了归一化。一行数据长这样:
2 0.3438 0.2917 0.1719 0.2188前一个2是类别id,对应names列表里的第3个名字(索引从0开始);后面四个浮点数都是0到1之间的小数,读取的时候必须乘以图片的width和height才能还原成像素坐标。检验一个YOLO txt是否干净,我一般会写个三行脚本,把所有框的数值范围打出来:如果出现负数或大于1的数,说明要么是手工标注时填错,要么是转换脚本没有归一化。这类脏标签不会让训练直接报错,但会在推理阶段产生大量落在图片外的预测框,表现就是mAP莫名其妙地低。
2.4 训练前到底选哪套标签:按框架读取习惯决定
很多初学者会在一个数据集里同时保留三套标签,然后纠结“是不是都要用”。实际上训练时只用一套。我的选择逻辑很简单:用Ultralytics YOLO系列仓库,直接读取YOLO txt;用mmdetection,读VOC或COCO更顺;自己写加载器,则选自己最熟的那套格式。项目里三套标签分文件夹存放,真正的价值在于省去格式转换的时间,并且可以交叉验证标注一致性——比如随机抽50张图,把VOC的xml还原成框、YOLO的txt还原成框,叠到同一张图上对比,偏差超过3个像素就说明某套标签生成时出了错。
| 标签格式 | 文件组织 | 坐标形式 | 适合的框架 |
|---|---|---|---|
| VOC xml | 每图一个xml | 绝对像素[x1,y1,x2,y2] | mmdetection、老版Faster R-CNN生态 |
| COCO json | 全量数据一个json | [x,y,width,height] | Detectron2、mmdetection、COCO API |
| YOLO txt | 每图一个txt | 归一化[cx,cy,w,h] | YOLOv5/v8系列官方仓库 |
# 交叉验证三套标签一致性时,我常用这条命令把YOLO txt画到图上 python draw_yolo_boxes.py --img-dir JPEGImages --label-dir labels --out-dir check_visual命令背后的逻辑是:实际画出来的框和原图上缺陷边缘对得上,这组标签才算可用。如果只信训练loss不看可视化,等模型部署到产线才发现框偏了,返工成本反而更高。
3. 三个划分脚本怎么用:从train_list.txt到ImageSets目录的完整分工
3.1 图片标签一起搬:按比例生成train/val/test三个物理目录
项目里最直观的一个脚本是把图片和标签按比例复制到新目录,生成的目录结构能被YOLO官方仓库直接读取。核心逻辑是:先取全部图片文件名,随机打乱,按比例切成三段,再把每段的图片连同同名txt一并复制过去。我重写了一个等价的最小版本,方便你理解它的行为:
import os import random import shutil # 路径配置:根据自己的实际目录改 img_src = "JPEGImages" # 原始图片目录 label_src = "labels" # YOLO格式txt标签目录 out_root = "dataset" # 划分结果输出根目录 # 划分比例:先留测试集,再在剩余里拆train/val train_ratio, val_ratio = 0.8, 0.1 # 余下0.1自动作为test random.seed(42) # 固定随机种子,保证可复现 imgs = [f for f in os.listdir(img_src) if f.lower().endswith((".jpg", ".png", ".jpeg"))] random.shuffle(imgs) n = len(imgs) n_train = int(n * train_ratio) n_val = int(n * val_ratio) bucket = { "train": imgs[:n_train], "val": imgs[n_train:n_train + n_val], "test": imgs[n_train + n_val:], } for split_name, files in bucket.items(): # 每个split下都建 images/ 和 labels/ 两个子目录 img_out = os.path.join(out_root, split_name, "images") lb_out = os.path.join(out_root, split_name, "labels") os.makedirs(img_out, exist_ok=True) os.makedirs(lb_out, exist_ok=True) for f in files: stem = os.path.splitext(f)[0] shutil.copy(os.path.join(img_src, f), os.path.join(img_out, f)) shutil.copy(os.path.join(label_src, stem + ".txt"), os.path.join(lb_out, stem + ".txt")) print(f"{split_name}: {len(files)} 张图片")这个脚本有三个参数需要你按实际情况改:img_src和label_src是数据集的原始路径,注意labels里放的是YOLO格式的txt,不是VOC的xml;train_ratio和val_ratio是划分比例,常见配置是0.8/0.1,如果你想用五五开做对比实验,改成0.5/0.2即可,剩余部分会自动归到test。random.seed(42)这行的价值在于复现:同一份数据、同一个种子,两次划分结果完全一致,方便你排查“为什么我用这个脚本跑出来的效果跟教程不一样”。
3.2 split_train_val生成ImageSets下的txt:VOC工具链的索引文件
第二个脚本做的事和第一个完全不同:它不复制任何文件,只在ImageSets/Main/目录下生成三个txt——train.txt、val.txt、test.txt,每行是图片文件名(不带扩展名和路径)。这类索引文件是给VOC风格的工具链用的,模型训练时按这个列表去对应目录里找图,而不是遍历整个文件夹。脚本本身的逻辑很简单,就是读取全部图片名、按比例打散、分别写入三个txt:
import os import random img_dir = "JPEGImages" out_dir = "ImageSets/Main" os.makedirs(out_dir, exist_ok=True) imgs = [os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.lower().endswith((".jpg", ".png", ".jpeg"))] random.seed(42) random.shuffle(imgs) total = len(imgs) line_train = "\n".join(imgs[:int(total * 0.8)]) line_val = "\n".join(imgs[int(total * 0.8):int(total * 0.9)]) line_test = "\n".join(imgs[int(total * 0.9):]) for name, text in [("train", line_train), ("val", line_val), ("test", line_test)]: with open(os.path.join(out_dir, f"{name}.txt"), "w") as fp: fp.write(text)使用这个脚本时有一个容易被忽略的点:生成的txt里不能有多余的空行或标题信息,每行干干净净只有文件名。有些编辑器会在文件末尾自动加空行,YOLO的datasets.py读到最后一行时解析出一个空字符串,经常报“AssertionError: train.txt从未找到图片”。如果你遇到类似错误,先检查txt尾部是不是有看不见的换行符。
3.3 train_list.txt与两类划分脚本的边界:什么时候用哪个
很多下载这份资源的人会被三个脚本名绕晕:train_list.txt是生成列表的入口配置,不是脚本本体;两个“划分脚本”一个是物理复制文件,一个是生成索引txt。我的判断标准很简单:用YOLO官方仓库,物理目录结构最快,因为datasets.py本身就支持按images/train和labels/train这种结构递归寻找;用老式VOC训练链路,就生成ImageSets索引txt,再配合voc_label.py把xml转成训练用的txt。train_list.txt这种文件给的是纯文件名清单,一般配合自定义数据加载器使用,它不是你想直接跑就能跑的脚本。
这里还牵涉一个选型问题:物理复制和索引文件哪个更好?物理复制会占用双倍磁盘空间,但调试起来直观,删掉某个split的目录也不会影响其他部分;索引文件省空间,但前提是你的图片和标签目录必须保持整洁,不能存在“图片在但txt被误删”的孤儿文件。我自己在本地实验更喜欢物理复制,因为可以随时打开目录数一下文件总数,心里有底;在服务器上跑大规模实验则用索引文件,避免重复复制上千张图浪费IO。
3.4 给划分脚本加一层校验:图片与标签一一对应的兜底检查
拆过几个数据集之后,我养成了一个习惯:不论用哪个划分脚本,跑完后必须先过一遍校验,再启动训练。校验的核心是检查每个split里图片和标签的数量、文件名是否一一对应。最常见的翻车是label文件名是*.txt但图片是.jpg,而脚本只做了stem + ".txt"拼装,若某张图漏标注,复制时就会报FileNotFoundError。我在原脚本基础上加了一段最朴素的检查:
missing_label = 0 for split_name, files in bucket.items(): for f in files: stem = os.path.splitext(f)[0] lb_path = os.path.join(label_src, stem + ".txt") if not os.path.exists(lb_path): missing_label += 1 print(f"[{split_name}] 缺少标签: {f}") print(f"检查完成,共 {n} 张图片,缺失标签 {missing_label} 张")这段脚本本身不复杂,但价值在于把“文件不存在”的异常从训练阶段提前到准备阶段。训练跑到一半才发现2%的图片没标签,损失的不只是时间,还有试错的心情。另一个值得检查的维度是空标签文件:一个txt文件大小为0,训练时读取不到任何目标,YOLO会把这张图当背景训练,如果空文件比例超过5%,模型的召回率会被明显拉低。
4. Linux与Windows双环境跑通YOLO:环境搭建、案例改造与断点续训
4.1 Ubuntu侧:驱动、CUDA、conda的安装顺序不能乱
项目附带的Linux教程里把Ubuntu环境安装放在最前面,这个顺序很关键。NVIDIA驱动、CUDA Toolkit、PyTorch三者的关系是:驱动负责让显卡能被系统识别,CUDA提供运行时的底层库,PyTorch通过CUDA调用显卡计算。装错顺序最常见的现象是nvidia-smi能显示显卡信息,但PyTorch里torch.cuda.is_available()返回False。我的建议是先装驱动,重启后确认nvidia-smi正常,再装CUDA,最后建conda虚拟环境装PyTorch:
# 1. 安装驱动后确认显卡状态 nvidia-smi # 2. 创建独立虚拟环境,避免污染系统Python conda create -n yolo python=3.9 -y conda activate yolo # 3. 安装与CUDA版本匹配的PyTorch pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118安装完PyTorch后,强烈建议先跑一条验证命令再做别的:
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"输出True和你的显卡型号才说明环境通了。如果输出False,不要急着重装全套,先检查nvcc -V看CUDA版本是否和PyTorch的编译版本对应,很多时候只是PATH环境变量没刷新。
4.2 Windows侧:先看驱动再装包,避免CUDA版本白折腾
Windows环境搭建和Linux没有本质区别,但有个血泪经验值得单独说:不要一上来就装最新版CUDA,先看显卡驱动支持的CUDA版本。nvidia-smi右上角会显示“CUDA Version: 12.x”,这个数字是驱动能支持的上限,不是说你必须装这个版本,而是你装的CUDA不能比它新。教程里Windows版本的路径依赖做得比较细,我按它的顺序在本地跑过一遍,关键步骤是:装好显卡驱动后,在conda里新建环境,然后直接用pip安装PyTorch,而不是先去NVIDIA官网下完整版CUDA。PyTorch的pip包会自带CUDA运行时,对绝大多数训练任务够用了。
Windows上比Linux多一档麻烦:路径。数据集千万不要放在带中文或空格的路径下,D:\缺陷检测\数据集这种目录会在数据加载时报一堆诡异错误,最典型的报错是FileNotFoundError但文件明明就在那里。我一般会在项目根目录建一个纯英文的datasets/steel_defect/,所有训练操作都在这个目录里做。
4.3 把案例训练改成自己的数据集:数据yaml、类别数与权重路径三处必改
教程里说“根据案例修改训练自己的数据集”,实际需要动的就三个地方:数据配置文件、类别数量、预训练权重。先写一个数据yaml,这是YOLO读取数据集的唯一入口:
# steel_defect.yaml path: ./datasets/steel_defect # 数据集根目录 train: images/train # 训练图片目录 val: images/val # 验证图片目录 nc: 4 # 类别数,改成你labels里的实际缺陷类别数 names: ["crazing", "inclusion", "patch", "pitted_surface"] # 顺序必须和txt里的类别id一致nc和names是训练脚本里最容易出错的两个字段。nc填错会导致最后一层卷积输出维度不匹配,训练直接报错;names的顺序错了更隐蔽,训练能跑,但预测出来的类别标签全是错的——比如txt里id为0的缺陷是crazing,你的names列表第一项却是inclusion,模型就会把crazing识别成inclusion。我在改完yaml后的第一件事,永远是打开一张标注好的图,确认txt里的类别id和yaml里的names能对上。
接下来是训练命令。以YOLO官方仓库常见的训练入口为例,一份可以直接套用的命令长这样:
python train.py --data steel_defect.yaml --weights yolov5s.pt \ --epochs 100 --batch-size 16 --img 640 --project runs/steel几个参数的实际意义:--weights指定预训练权重,建议优先用官方提供的COCO权重,它能显著加快收敛速度;--epochs对钢表面缺陷这类小目标任务,100轮是及格线,200轮能更稳;--batch-size按你的显存调整,16G显存跑yolov5s用16没问题,显存不够就降到8;--project是输出路径,所有训练日志和权重都会存到这里面。
4.4 训练中断与断点续训:不用每次从零开始
工业场景里训练100轮要跑好几个小时,中间断电、显存溢出、手动停掉都是常事。YOLO的断点续训很成熟,不需要额外装任何工具,训练中断后用同一条命令加上--resume参数即可:
python train.py --data steel_defect.yaml --weights yolov5s.pt \ --epochs 100 --batch-size 16 --img 640 --project runs/steel \ --resume runs/steel/exp/weights/last.pt--resume支持两种写法:给last.pt的完整路径,或者直接写--resume让它自动找runs/steel/exp/下的last.pt。我习惯显式写出路径,因为一次实验可能在多个exp目录里留有历史记录,自动寻找有找错的风险。续训后要注意的是学习率:脚本会按上次保存的状态恢复学习率调度器,不会从初始值重新开始。
5. 避坑排查:标签错位、划分失真与loss不收敛的5条记录
5.1 标注框与图片错位:训练loss正常但预测框全偏
现象:训练过程loss曲线一路下降,看起来非常顺利;验证集上mAP也不低,但把预测结果画到原图上,发现框和真正的缺陷边缘错开一大截。原因:数据集里某张图片被人为resize过,但它的txt标签仍按原尺寸归一化。比如原图是640×480,实际缺陷中心点在像素(320, 240),归一化是(0.5, 0.5);图片被压缩成320×240后缺陷中心像素挪到了(160,120),若归一化结果还是按640算,画出来就偏了。解决:训练前批量读取所有txt的归一化坐标范围,确认都在0到1之间且分布均匀;再用可视化脚本随机抽100张图叠加画框,人眼扫一遍,错位问题一眼就能暴露。
5.2 测试集里某类缺陷一张都没有:随机划分的分层陷阱
现象:划分比例设的0.8/0.1/0.1,训练集和验证集都正常,测试集上某一类缺陷的AP直接为0。原因:数据划分时只做了整体随机shuffle,没有按类别分布做分层抽样。钢表面缺陷里麻点和划痕出现频率可能相差数倍,小样本类全部落在训练集里,测试集一张都没有,算出的AP自然失真。解决:在划分前统计每张图片包含的类别,按“图片级类别标签”做分层抽样,保证每个split里各类别占比接近原始数据分布。如果某类样本太少,直接把它全部留在训练集,测试集只评估样本足够的类别,并在报告里注明。
5.3 中文路径导致DatasetNotFound:一类典型的启动报错
现象:训练命令一执行就报Dataset not found,或者图片读取数量为0。原因:数据集放在带中文、空格的路径下,Windows下最常见的触发点是桌面文件夹叫“新建文件夹”或“我的数据集”。Python的路径库在不同系统间处理非ASCII字符的表现不一致,轻则警告,重则直接读不到文件。解决:把整个数据集挪到纯英文、无空格的路径,比如F:\steel_defect\。这个操作看似简单,但我见过有人因为嫌麻烦不挪,连续两天卡在环境报错上,最后挪完三分钟就能跑了。
5.4 loss变成nan:学习率、batch与BN的连锁反应
现象:训练到第20到30轮时,box_loss或cls_loss突然变成nan,之后所有指标全部失效。原因:学习率设置过大导致梯度爆炸,或batch太小、BN层的滑动统计不稳定。钢表面缺陷数据里如果存在大量空白背景图,BN的均值和方差会被带偏,进一步加剧数值不稳定性。解决:先把学习率降到默认值的十分之一试跑50轮;batch小于8时开启梯度累积;另外检查label里是否混入了归一化后边界越界的框——一张图只有0.5×0.5的框反而比没有框更危险。
5.5 预训练权重与类别数不匹配:改nc就报错
现象:用官方预训练权重微调自己的数据集,加载权重时报维度不一致,或者训练能启动但每个epoch开头打印一串“Error loading state_dict”。原因:COCO预训练模型输出层有80个类别,你的数据集只有4类,最后一层卷积的参数维度完全不同。解决:训练命令里带上--freeze参数冻结backbone层,或者使用官方提供的迁移学习策略——加载预训练权重时跳过输出层,从零初始化head部分。两种做法都能跑,区别在主分支上:冻结backbone收敛更快但最终精度可能被锁死;只跳过head收敛稍慢,但模型对钢表面缺陷的适配能力更强。
6. 不看单次loss,用混淆矩阵和mAP给训练结果做一次“复检”
训练完成不代表交付完成。我第一次独立做钢表面缺陷模型时,看训练曲线挺漂亮就直接部署,结果现场报告一晚上17条误检,全是把麻点认成划痕。从那以后,我每次训练完都强制走一遍下面的复检流程。
先重跑一次验证,让模型在测试集上完整推理,得到官方的结果热力图:
python val.py --data steel_defect.yaml --weights runs/steel/exp/weights/best.pt --img 640跑完后在runs/steel/exp/目录下会生成confusion_matrix.png和results.png。我看这两张图的顺序是固定的:先看混淆矩阵,找到对角线之外最亮的格子——那一格就是最容易互相误检的缺陷对;再看PR曲线,看每个类别的召回率拐点出现在哪里。如果混淆矩阵里pitted_surface被大量识别成patch,我不会急着加大epoch,而是先抽20张误检图看具体形态,确认是两类缺陷本身太像,还是标注框边界切得太接近。确认是标注问题,就回去修数据;确认是纹理相似,就针对这两个类别单独做数据增强或增加样本,而不是盲目调阈值。
对精度要求更高的场景,我还会用脚本按类别分别统计AP,只看平均mAP会掩盖某个小类完全没学会的事实。逐类AP表上低于0.5的类别,才是下一轮迭代真正要补的短板。这一套流程走下来,模型的真实水平才算被测透,部署时心里才有点底。希望帮到你。
本文还有配套的精品资源,点击获取