简介:面向金属表面缺陷检测任务的目标检测数据集,适合从事工业质检、计算机视觉目标检测的初学者与工程师。共包含约3600张图像及对应XML标注文件,按VOC格式整理,类别涵盖crazing、patches、inclusion、pitted_surface、rolled-in_scale、scratches六类典型缺陷。资源共2000个文件,其中约1800个XML标注、198个JPG图像,另有1个Python脚本和1个JSON文件,便于直接读取与格式转换;压缩包约24.54MB。已有47人浏览学习。数据按文件夹保存并经测试可直接用作目标检测数据集,无需额外预处理。借助描述中关联的YOLO实战及改进教程,可快速完成数据加载、模型训练与评估,适合入门和进阶的工业视觉实践。
1. 金属表面缺陷检测数据集:一份能直接开跑的VOC标注目标检测数据
做金属表面缺陷检测的目标检测项目,最耗时间的往往不是模型调参,而是手里没有一份带规范标注的图像数据。这份资源给的是金属表面的目标检测数据集,采用VOC标注格式,图片配好XML标签,解压后就能直接喂给YOLO、Faster R-CNN这类检测模型。覆盖的缺陷类型按常见工业场景来划分,包含划痕、麻点、氧化、裂纹等,每张图都有对应的标注框和类别标签,适合刚入门目标检测、想拿一份干净数据跑通全流程的人,也适合做工业质检算法的工程师拿来做预训练或迁移学习。有了这套数据,标注这一关能直接跳过,省下来的时间去调模型和分析结果。
2. 拆解VOC标注格式:目录结构、XML标签与类别体系
2.1 目录结构:先看懂文件怎么摆
VOC格式的全称是PASCAL VOC,它原本是给目标检测和语义分割竞赛用的标准数据组织方式,后来成了目标检测领域事实上的交换格式。拿到这份数据后,第一步不是急着训练,而是把目录结构盘清楚,否则后边写路径、做划分时容易乱。标准的VOC格式通常长这样:
metal_defect_dataset/ ├── Annotations/ # 存放所有XML标签文件 │ ├── 0001.xml │ ├── 0002.xml │ └── ... ├── JPEGImages/ # 存放所有原始缺陷图像 │ ├── 0001.jpg │ ├── 0002.jpg │ └── ... └── ImageSets/ └── Main/ # 存放训练/验证/测试的图片名清单 ├── train.txt ├── val.txt └── test.txt这个目录结构里,Annotations和JPEGImages是一一对应的,0001.xml描述0001.jpg里有几个缺陷、每个缺陷的类别和位置。ImageSets/Main下的txt文件不是图片本身,而是图片文件名清单,每行一个不带后缀的文件名,训练脚本靠它来区分哪些图参与训练、哪些图参与验证。
有一个细节值得注意:这份数据的txt清单里只有文件名,没有路径。所以后边做训练时,代码里要自己拼好JPEGImages的完整路径。我一般习惯把数据集根目录定义成变量,再用os.path.join拼接,这样换机器跑时只改一处路径就行。
2.2 XML标签结构:定位信息都在 里
VOC格式的核心是XML标签文件,它把一张图里的所有缺陷框信息都结构化地描述出来。打开一个标注文件,内容大概是这样:
<annotation> <folder>JPEGImages</folder> <filename>0001.jpg</filename> <source> <database>metal_defect</database> </source> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <name>scratch</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>126</xmin> <ymin>89</ymin> <xmax>324</xmax> <ymax>287</ymax> </bndbox> </object> </annotation>字段含义如下表:
| 字段 | 含义 | 影响 |
|---|---|---|
| filename | 对应图像文件名 | 转换脚本按它匹配图片 |
| size/width、height | 图像原始宽高 | 归一化坐标时必须用到 |
| object/name | 缺陷类别名 | 类别清单由它产生 |
| object/difficult | 是否为难例 | 训练时常被忽略 |
| object/bndbox | 缺陷框的绝对像素坐标 | 转YOLO时需要归一化 |
这里最关键的是bndbox里的四个值:xmin、ymin、xmax、ymax,它们存的是绝对像素坐标,也就是缺陷左上角和右下角在图像上的真实位置。VOC格式坚持用绝对坐标是有原因的:它作为中间交换格式,要同时方便人类查看和脚本处理,而且不同模型的输入尺寸不一致,等转换到YOLO或其它格式时再做归一化,这样信息不会丢失。
2.3 类别体系:name字段是唯一识别依据
这份数据集的缺陷类别都写在<object><name>里,常见的类别名有scratch(划痕)、pitting(麻点)、oxide(氧化)、crack(裂纹)。训练模型时,这些类别名会直接映射成类别编号,映射顺序一般按字母排序或按出现频率排序。所以类别名的一致性特别重要,scratch和Scratch在VOC格式里会被当成两个完全不同的类别,训练出来的模型类别数就虚增了。
另外,difficult字段在工业检测数据里经常被置为1,表示这个缺陷框很难判定,比如缺陷纹理太浅、边界不清晰。大多数训练框架默认不加载difficult=1的样本,但如果你做的是细分场景,也可以把这类样本单独挑出来作为人工复核集。拿到数据后,建议先用脚本统计一下每个类别的XML数量,确认类别分布是否符合你的检测目标,再进入下一步。
3. 标注工具与实操:LabelImg标注流程和标签自检脚本
3.1 选型理由:为什么是LabelImg
如果你要在这份数据基础上补充自己的缺陷样本,那标注工具绕不开LabelImg。它是目前处理VOC格式最顺手的开源工具,界面简单,支持直接导出VOC XML和YOLO txt两种格式,不依赖浏览器环境,单机就能跑。相比CVAT、Roboflow这类在线平台,LabelImg的优势是轻量,不需要部署服务端,适合本地维护几百到几千张的数据集;缺点是没有多人协同和自动标注,但扩展标注量不大时完全够用。
安装和启动很直接:
pip install labelimg labelimg推荐用Python 3.8以上的环境安装。启动后界面里主要操作是:左边选图片目录,右边是标注画布,中间是类别列表。需要先创建一个classes.txt类别文件,在命令行通过labelimg classes.txt指定,比如:
labelimg classes.txt ./JPEGImages ./Annotationsclasses.txt里每行写一个类别名,顺序决定了类别编号,后续转YOLO格式时scratch排第几行,训练时它的class_id就是几。所以一旦确定类别顺序,就不要轻易调整,否则已生成的标签全部要重映射。
3.2 标注实操:画框的细节决定训练上限
标注的直观目标是“把缺陷框出来”,但框怎么画直接决定模型上限。我一般用这套流程:
- 先把类别文件建好,启动LabelImg,设置自动保存。
- 逐张浏览图片,发现有缺陷就用
W键画框。 - 画框时贴着缺陷最外缘,不要留太多背景,也不要切掉缺陷的边缘。
- 一张图里多个缺陷互相靠近时,分别标注,不要把两个缺陷并成一个框。
- 保存后按
D键进入下一张。
常用快捷键:
| 快捷键 | 功能 |
|---|---|
| W | 创建标注框 |
| A / D | 上一张 / 下一张图片 |
| Ctrl+S | 保存当前XML |
| C | 复制上一个框 |
| Del | 删除选中框 |
标注的时候有几个原则要明确。相似的纹理容易误标,比如拉丝纹理和细划痕,如果标的时候把背景纹理也框进去,模型就会学到错误特征。另外,金属表面的反光区域很容易被误判为缺陷,我会把这类样本归到difficult,让它在训练时被忽略,而不是硬标成缺陷。
标注完成后,LabelImg默认把XML存到图片目录,我习惯单独放到Annotations目录统一管理,避免和图片混在一起导致路径扫描出错。
3.3 标签自检脚本:训练前必须跑一遍
标注这件事,人眼出错的概率比想象中高。坐标拖出边界、类别没选、文件名打错,这些问题在训练阶段会被放大成莫名其妙的loss异常。我写了一个简单的校验脚本,每次拿到数据或标完一批图都先跑一遍:
import os import xml.etree.ElementTree as ET ANNOTATIONS_DIR = "./Annotations" IMAGE_DIR = "./JPEGImages" ALLOWED_CLASSES = {"scratch", "pitting", "oxide", "crack"} def check_xml(xml_path): tree = ET.parse(xml_path) root = tree.getroot() filename = root.findtext("filename") img_path = os.path.join(IMAGE_DIR, filename) if not os.path.exists(img_path): print(f"[文件不匹配] {filename} 找不到对应图片") for obj in root.iter("object"): name = obj.findtext("name") if name not in ALLOWED_CLASSES: print(f"[未知类别] {xml_path}: {name}") bndbox = obj.find("bndbox") xmin = int(bndbox.findtext("xmin")) xmax = int(bndbox.findtext("xmax")) ymin = int(bndbox.findtext("ymin")) ymax = int(bndbox.findtext("ymax")) if xmax <= xmin or ymax <= ymin: print(f"[坐标异常] {filename}: xmax<=xmin 或 ymax<=ymin") img_et = ET.parse(img_path) if False else None for xml_file in os.listdir(ANNOTATIONS_DIR): if xml_file.endswith(".xml"): check_xml(os.path.join(ANNOTATIONS_DIR, xml_file)) print("校验完成")ALLOWED_CLASSES定义当前数据集的合法类别集合,脚本会找出不在集合里的name,这个检查很重要,因为类别名的空格或大小写错误在这里就能暴露。文件匹配检查用filename字段拼图片路径,如果图片不存在,说明文件名可能写错或图片放错目录。坐标合理性检查针对xmax <= xmin或ymax <= ymin,这类错误在标注时手抖很容易产生。
这个脚本不检查坐标是否超过图片宽高,因为XML里已经存了size字段,把width和height读出来再和xmax、ymax比对即可,需要的话可以自行加上。跑完脚本没有输出异常,数据才算干净。
4. 常见问题排查:标注与训练适配的五个高发坑
4.1 标注环节的坑:文件不匹配和坐标越界
现象:训练脚本启动时,报错提示找不到某张图片的标注文件,或者标签解析到一半崩溃。
原因:最常见的是文件名大小写不一致。VOC格式里XML的filename字段和磁盘上的实际文件名必须完全一致,但标注时有人写.jpg有人写.JPG,Windows下不区分大小写能正常打开,到了Linux训练环境瞬间就翻车。另外一个高发原因是图片和XML多了一个空格,比如0001 .jpg,肉眼看不出来,程序匹配不上。
解决:拿到数据后先做一次文件清单对比,用脚本提取XML的filename字段,和JPEGImages目录下的实际文件名做差集。我一般在Linux服务器上跑训练,所以下载数据后会先强制把所有扩展名统一成小写:
find ./JPEGImages -type f -name "*.JPG" -exec mv {} {}.tmp \; find ./JPEGImages -type f -name "*.JPG.tmp" -exec mv {} {} .jpg \;现象:训练时loss出现NaN,或者输出的检测框坐标出现负数。
原因:标注时鼠标拖出了图像边界,xmin或ymin为负数,xmax或ymax超过了图片宽度。VOC格式对这类越界坐标不拦截,转换到YOLO时归一化后会出现小于0或大于1的值,模型训练的损失函数直接爆掉。
解决:在第3章的校验脚本里加上边界判断,读取XML里的width和height,检查xmin >= 0、ymax <= width、ymax <= height。发现越界坐标时,把框裁回图像边界内,而不是删掉这个样本,因为缺陷本身是完整的,只是框画大了。
4.2 训练适配的坑:类别名不一致和划分清单失效
现象:训练时类别数量比预期多,或者某个类别的样本数出奇地少。
原因:类别名大小写或空格不一致,scratch和Scratch被当成两个类,原本属于scratch的样本被拆到两个类别里,每个类别的样本量都变得不充分。这是我实际踩过的一个坑,标注时有人输入了“scratch ”,尾部多了一个空格,shape检查时一切正常,训练时类别数从4变成5,模型收敛速度明显变慢。
解决:用脚本统计所有XML里name字段的唯一值集合,打印出来人工核对。我一般会写一行Python把唯一值输出到控制台,看到集合里出现形似的类别名直接改XML文本。如果只用这份数据,直接在Annotations目录里批量sed替换即可,但要先备份。
现象:训练时发现验证集的mAP波动很大,或者验证集上效果极好、测试集上一塌糊涂。
原因:ImageSets/Main/train.txt和val.txt划分没有与XML实际内容同步。比如你补标了100张新图,XML文件加了100个,但txt清单没更新,新样本全被忽略;或者划分时随机种子没固定,每次重跑训练验证集都在变。
解决:做一次全量清单重写,扫描Annotations目录下的所有XML文件名,剔除difficult=1的样本后按8:2重新划分,固定随机种子:
import os import random xml_files = [f.replace(".xml", "") for f in os.listdir("./Annotations")] random.seed(42) random.shuffle(xml_files) split = int(len(xml_files) * 0.8) with open("./ImageSets/Main/train.txt", "w") as f: f.write("\n".join(xml_files[:split])) with open("./ImageSets/Main/val.txt", "w") as f: f.write("\n".join(xml_files[split:]))这样每次划分结果一致,不会出现两次训练验证集对不上的情况。
现象:某些缺陷类别在工业现场检测得很准,但模型对另外一类几乎完全漏检。
原因:数据分布不均匀。金属表面麻点可能比裂纹样本多很多,模型偏向学样本量大的类,小样本类学不到稳定的特征。VOC标注格式本身不解决这个问题,需要从数据层面调整。
解决:先统计类别分布,对样本少的缺陷类别做针对性扩增,比如水平翻转、旋转、亮度扰动,而不是整个数据集统一增强。这类操作需要把变换应用到图片和XML坐标上,建议直接转成YOLO格式后再做,因为YOLO的归一化坐标在图像变换时更容易同步修改。
5. VOC转YOLO:转换脚本、目录重组与训练配置
5.1 转换脚本:把XML的绝对坐标归一化
YOLO系列训练用的标签格式不是XML,而是每个图片对应一个txt文件,每行描述一个目标。格式是class_id x_center y_center width height,全部是归一化浮点数。把VOC转成YOLO,本质上就是读取XML里的bndbox,根据图片宽高做归一化换算,再写出txt。
import os import xml.etree.ElementTree as ET CLASS_MAP = {"scratch": 0, "pitting": 1, "oxide": 2, "crack": 3} def convert_voc_to_yolo(xml_path, output_dir): tree = ET.parse(xml_path) root = tree.getroot() filename = root.findtext("filename") width = int(root.findtext("./size/width")) height = int(root.findtext("./size/height")) txt_name = os.path.splitext(filename)[0] + ".txt" with open(os.path.join(output_dir, txt_name), "w") as f: for obj in root.iter("object"): name = obj.findtext("name") class_id = CLASS_MAP[name] bndbox = obj.find("bndbox") xmin = int(bndbox.findtext("xmin")) ymin = int(bndbox.findtext("ymin")) xmax = int(bndbox.findtext("xmax")) ymax = int(bndbox.findtext("ymax")) x_center = (xmin + xmax) / 2 / width y_center = (ymin + ymax) / 2 / height w = (xmax - xmin) / width h = (ymax - ymin) / height f.write(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n") os.makedirs("./labels", exist_ok=True) for xml_file in os.listdir("./Annotations"): if xml_file.endswith(".xml"): convert_voc_to_yolo(os.path.join("./Annotations", xml_file), "./labels") print("转换完成")CLASS_MAP是类别名到整数编号的映射表,顺序必须固定。x_center的计算方式是(xmin + xmax) / 2再除以图片宽度,得到0到1之间的浮点数。这里有一个关键点:如果XML里有difficult=1的样本,转换时要不要保留?我的经验是默认保留,因为它们在训练时可以充当难例,但如果你想快速跑通流程,可以先跳过它们,减少噪声。
转换完成后,labels目录下出现和图片同名的txt文件,每行一个缺陷框。建议抽查几个txt文件,手动验证一下归一化坐标是否在0到1区间内,如果出现负数或大于1的值,说明坐标越界的坑在第4章就没排干净。
5.2 目录重组:拆成images和labels两个分支
YOLO训练框架读数据时,一般要求图片和标签分开放,而且训练集和验证集各占一个目录。常见的目录结构是:
metal_defect_yolo/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/按这个结构来做划分,代码逻辑就是读第四章生成的train.txt和val.txt,把对应图片和标签文件分别复制到四个目录:
import os import shutil BASE_DIR = "./metal_defect_yolo" os.makedirs(f"{BASE_DIR}/images/train", exist_ok=True) os.makedirs(f"{BASE_DIR}/images/val", exist_ok=True) os.makedirs(f"{BASE_DIR}/labels/train", exist_ok=True) os.makedirs(f"{BASE_DIR}/labels/val", exist_ok=True) def copy_split(txt_path, image_src, label_src, image_dst, label_dst): with open(txt_path) as f: names = [line.strip() for line in f if line.strip()] for name in names: shutil.copy(f"{image_src}/{name}.jpg", f"{image_dst}/{name}.jpg") shutil.copy(f"{label_src}/{name}.txt", f"{label_dst}/{name}.txt") copy_split( "./ImageSets/Main/train.txt", "./JPEGImages", "./labels", f"{BASE_DIR}/images/train", f"{BASE_DIR}/labels/train" ) copy_split( "./ImageSets/Main/val.txt", "./JPEGImages", "./labels", f"{BASE_DIR}/images/val", f"{BASE_DIR}/labels/val" ) print("目录重组完成")这一步看起来只是复制文件,但不要省略。很多训练框架会直接扫描目录下的所有文件,如果图片目录里混着XML或txt,解析就会报错;如果验证集目录里混进训练集图片,验证指标就失真了。目录重组其实就是把数据边界切干净。
5.3 训练配置:yaml参数怎么写
用YOLOv8等框架训练自己的数据集时,核心是写一个数据集配置文件,描述图片路径、训练集验证集路径和类别名列表:
path: ./metal_defect_yolo train: images/train val: images/val names: 0: scratch 1: pitting 2: oxide 3: crackpath是数据集的根目录,train和val是相对path的子目录路径。names列表里的顺序必须和CLASS_MAP一致,否则类别编号就会错位,训练出的模型预测结果全是乱的。我习惯在启动训练前先打印一行配置里的类别列表,确认和CLASS_MAP一致再开始训练。
训练命令本身很简单,比如:
yolo detect train data=metal_defect.yaml model=yolov8n.pt epochs=100 imgsz=640imgsz默认是640,这个值要和你的图像尺寸匹配。这份数据的图像分辨率如果本身是640x480,直接用640问题不大;如果图像是1200宽的大图,建议先缩放到640附近再训练,否则显存占用高而且训练速度慢。
6. 验证与进阶:用PR曲线和混淆矩阵反推数据增强
6.1 验证指标:缺陷检测场景看什么
训练完成后,不要只盯着最终mAP数值。金属表面缺陷检测里,mAP@0.5只能说明框得大概准,mAP@0.5:0.95对框质量要求更高,更贴近真实质检标准。我会先看验证集上每个类别的PR曲线,如果某个类的PR曲线面积明显小于其它类,说明这个类没学好。此时数据分布不均的那个坑就暴露出来了,优先查一下这个类别的样本数量和标注质量,再决定是补数据还是调anchor。
6.2 从混淆矩阵反推短板
混淆矩阵是进阶调优的最好入口。金属表面缺陷里,划痕和裂纹在视觉上都是细长结构,模型经常把它们互相误判。如果混淆矩阵显示scratch和crack之间误判率较高,说明这两个类的纹理特征太接近,单靠增加样本数量不一定有效。我一般会做两个操作:一是把这两类样本裁剪出来做一次特征对比,确认标注边界是否区分清楚;二是针对性增强,只对crack类做小角度的旋转和宽度扰动,让模型注意到两者的长宽比差异。数据增强参数可以在训练yaml里配,比如fliplr: 0.5、hsv_h: 0.015,但对金属缺陷要克制,过度色彩增强会让纹理失真。
6.3 留存一份完全没见过的测试集
最后说一个我自己的习惯,也是吃过亏才有的规矩:永远把数据集的10%单独抽出来不参与训练和验证,放在一边。所有调参完成后,用这份没见过的数据做最终测试。工业场景里最怕的是模型在实验室验证集上跑得好,上了产线就开始漏检,原因往往是你为了调参反反复复地看了验证集,模型客观上发生了轻微过拟合。留一份盲测数据,等于给模型加了一道质检关。从那以后我每次拿到新数据集,都强制先把校验脚本跑一遍,再按8:1:1划分成训练、验证、盲测三份,后面所有折腾都在训练和验证上进行。这套习惯沿用下来,模型上线后的表现稳定了很多。希望帮到你。
本文还有配套的精品资源,点击获取