简介面向金属表面缺陷检测任务的目标检测数据集适合从事工业质检、计算机视觉目标检测的初学者与工程师。共包含约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 folderJPEGImages/folder filename0001.jpg/filename source databasemetal_defect/database /source size width640/width height480/height depth3/depth /size segmented0/segmented object namescratch/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin126/xmin ymin89/ymin xmax324/xmax ymax287/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字段是唯一识别依据这份数据集的缺陷类别都写在objectname里常见的类别名有scratch划痕、pitting麻点、oxide氧化、crack裂纹。训练模型时这些类别名会直接映射成类别编号映射顺序一般按字母排序或按出现频率排序。所以类别名的一致性特别重要scratch和Scratch在VOC格式里会被当成两个完全不同的类别训练出来的模型类别数就虚增了。另外difficult字段在工业检测数据里经常被置为1表示这个缺陷框很难判定比如缺陷纹理太浅、边界不清晰。大多数训练框架默认不加载difficult1的样本但如果你做的是细分场景也可以把这类样本单独挑出来作为人工复核集。拿到数据后建议先用脚本统计一下每个类别的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上一张 / 下一张图片CtrlS保存当前XMLC复制上一个框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}: xmaxxmin 或 ymaxymin) 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有人写.JPGWindows下不区分大小写能正常打开到了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文件名剔除difficult1的样本后按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_okTrue) 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里有difficult1的样本转换时要不要保留我的经验是默认保留因为它们在训练时可以充当难例但如果你想快速跑通流程可以先跳过它们减少噪声。转换完成后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_okTrue) os.makedirs(f{BASE_DIR}/images/val, exist_okTrue) os.makedirs(f{BASE_DIR}/labels/train, exist_okTrue) os.makedirs(f{BASE_DIR}/labels/val, exist_okTrue) 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 datametal_defect.yaml modelyolov8n.pt epochs100 imgsz640imgsz默认是640这个值要和你的图像尺寸匹配。这份数据的图像分辨率如果本身是640x480直接用640问题不大如果图像是1200宽的大图建议先缩放到640附近再训练否则显存占用高而且训练速度慢。6. 验证与进阶用PR曲线和混淆矩阵反推数据增强6.1 验证指标缺陷检测场景看什么训练完成后不要只盯着最终mAP数值。金属表面缺陷检测里mAP0.5只能说明框得大概准mAP0.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划分成训练、验证、盲测三份后面所有折腾都在训练和验证上进行。这套习惯沿用下来模型上线后的表现稳定了很多。希望帮到你。本文还有配套的精品资源点击获取
