多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

YOLO工业油污缺陷检测数据集:从VOC/COCO转YOLO到YOLOv8训练全流程

YOLO工业油污缺陷检测数据集:从VOC/COCO转YOLO到YOLOv8训练全流程 简介面向工业质检与机器视觉方向的开发者、算法工程师及高校学生这份资源提供了一套真实场景下的油污缺陷检测数据集可直接用于YOLO系列目标检测模型的训练与验证。压缩包共收录2000个文件约797.79MB其中1985个xml标注文件构成VOC格式主体另含少量txt、html与py文件分别承载YOLO标签、教程说明与划分脚本voc、coco、yolo三种格式标签分目录存放便于按需取用。资源同时附赠YOLO环境搭建与训练案例教程覆盖Windows与Linux双平台并给出训练集、验证集、测试集划分脚本可依据自身需求重新组织数据。目前已有286人学习适合希望快速跑通工业缺陷检测流程、验证模型效果或开展课程实践的读者从数据准备到训练配置均可获得较完整的支撑。1. 工业油污检测为什么值得单独做一个数据集产线上的油污缺陷和通用目标检测里的猫狗行人完全不是一回事。油渍边缘模糊、和金属底色对比度低、形状随机、面积跨度大反光一打有时候肉眼都难分辨。拿 COCO 预训练权重直接推理漏检率能高到让你怀疑模型没加载成功。这也是为什么YOLO工业油污缺陷检测数据集这类垂直数据集有存在价值——它把工业场景里最脏最难的样本集中起来让模型见过足够多的油污形态。这个数据集的核心配置是 10000 张图片配 VOC、COCO、YOLO 三种格式标签外加划分脚本和训练教程。它解决的是从有原始标注到能直接喂给 YOLOv8 训练之间的工程断层。适合两类人一是做工业质检落地、需要快速验证油污检测可行性的算法工程师二是刚入门 YOLO、想拿一个真实工业数据集练手的学生和转行者。下面按数据组织、格式转换、训练调参、踩坑排查的顺序讲透。2. 拿到压缩包先做什么目录结构与三种标签格式的对应关系2.1 先看清 VOC、COCO、YOLO 三种格式到底差在哪很多人解压完看到三个标签文件夹就懵了不知道训练时该用哪个。先把三者的结构差异理清楚后面选格式才不会翻车。VOC 格式是每张图对应一个 XML 文件标注信息用object节点描述坐标是绝对像素值原点在左上角。它的好处是可读性强用文本编辑器就能改坏处是解析慢文件数量翻倍。COCO 格式把所有标注塞进一个 JSON 文件用images、annotations、categories三个数组组织坐标同样是绝对像素值还带bbox的宽高。它适合多类别、需要统计分析的场景但单文件体积大改一个标注要动整个 JSON。YOLO 格式是每张图对应一个 txt每行是类别 中心x 中心y 宽 高全部归一化到 0~1。它读取最快是 YOLOv5/v8 训练的原生格式但可读性差坐标错了不好肉眼定位。格式单图标签文件坐标类型是否归一化训练直接可用VOC.xml绝对像素否需转换COCO单一 .json绝对像素否需转换YOLO.txt归一化是是提示如果你的训练框架是 Ultralytics 系的 YOLOv8直接用 YOLO 格式省掉转换环节减少出错概率。2.2 目录结构核对与文件完整性检查解压后第一件事不是急着训练而是核对目录。一个规范的工业油污数据集通常长这样dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── annotations_voc/ ├── annotations_coco/ │ ├── instances_train.json │ ├── instances_val.json │ └── instances_test.json ├── split_dataset.py └── train_tutorial.md用下面这段脚本快速核对图片和标签是否一一对应避免训练到一半报找不到标签import os img_dir dataset/images/train lbl_dir dataset/labels/train imgs {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .png, .jpeg))} lbls {os.path.splitext(f)[0] for f in os.listdir(lbl_dir) if f.lower().endswith(.txt)} missing_lbl imgs - lbls # 有图没标签 missing_img lbls - imgs # 有标签没图 print(f图片总数: {len(imgs)}, 标签总数: {len(lbls)}) print(f缺标签的图片: {len(missing_lbl)}) print(f缺图片的标签: {len(missing_img)})逻辑说明用集合差集找出不匹配项。imgs - lbls是有图无标签这类样本训练时会被跳过或报错lbls - imgs是有标签无图属于脏数据。参数上img_dir和lbl_dir要按你实际的目录层级改注意扩展名大小写工业相机导出的图有时是.JPG大写。如果缺标签数量超过总数的 1%不要硬训先回去查是不是解压不完整或者划分脚本跑错了。这一步花五分钟能省掉后面几小时的排查。3. 用划分脚本切分数据集比例、随机种子与类别均衡3.1 划分比例怎么定7:2:1 不是万能公式数据集自带的划分脚本一般默认按 7:2:1 切 train/val/test。但工业油污场景有个特点缺陷样本本身就不均衡有些批次油污多、有些批次几乎没有。如果纯随机切可能出现验证集里某个油污子类一张都没有的情况。我的习惯是先按 8:1:1 切保证训练集足够大如果验证集指标波动超过 5 个点再调回 7:2:1 增加验证样本。测试集不用太大500~1000 张足够评估泛化重点是它要覆盖不同光照、不同产线批次的样本。划分脚本的核心逻辑通常是先收集所有图片路径用random.shuffle打乱再按比例切片。关键参数是随机种子seed一定要固定否则每次跑结果不一样实验没法复现。import os import random import shutil random.seed(42) # 固定种子保证可复现 src_img dataset/images/all src_lbl dataset/labels/all dst_root dataset all_files [f for f in os.listdir(src_img) if f.lower().endswith((.jpg, .png))] random.shuffle(all_files) n len(all_files) n_train int(n * 0.8) n_val int(n * 0.1) splits { train: all_files[:n_train], val: all_files[n_train:n_train n_val], test: all_files[n_train n_val:] } for split, files in splits.items(): img_out os.path.join(dst_root, images, split) lbl_out os.path.join(dst_root, labels, split) os.makedirs(img_out, exist_okTrue) os.makedirs(lbl_out, exist_okTrue) for f in files: stem os.path.splitext(f)[0] shutil.copy(os.path.join(src_img, f), os.path.join(img_out, f)) lbl_file stem .txt if os.path.exists(os.path.join(src_lbl, lbl_file)): shutil.copy(os.path.join(src_lbl, lbl_file), os.path.join(lbl_out, lbl_file)) print(f{split}: {len(files)} 张)逻辑说明先打乱再切片保证随机性seed42是惯例换成任意固定值都行但一旦定了就别改。参数上0.8/0.1/0.1是比例改比例就改这三个数注意n_train n_val不能超过总数。复制而非移动是为了保留原始数据切错了还能重来。3.2 类别均衡检查别让某一类油污消失切完之后必须统计每个 split 里各类别的实例数。油污数据集常见 2~5 个类别比如油渍污点划痕油污混合。如果验证集里某类为 0训练出来的模型对那类就是瞎猜。from collections import Counter def count_classes(lbl_dir): counter Counter() for f in os.listdir(lbl_dir): if not f.endswith(.txt): continue with open(os.path.join(lbl_dir, f)) as fp: for line in fp: cls int(line.split()[0]) counter[cls] 1 return counter for split in [train, val, test]: c count_classes(fdataset/labels/{split}) print(split, dict(sorted(c.items())))逻辑说明逐行读 YOLO 标签取第一个字段作为类别 id 计数。参数上没有需要调的直接跑。如果发现某类在 val 里是 0两个处理办法一是重新划分并调大 val 比例二是手动从 train 里挪几张含该类的图到 val。后者更快但要保证挪过去的图标签完整。注意类别 id 从 0 开始还是从 1 开始取决于你的data.yaml配置。YOLO 官方要求从 0 开始如果数据集标签是从 1 开始训练时不会报错但类别全错位这是最隐蔽的坑之一。4. 从 VOC/COCO 转 YOLO转换脚本与四个边界坑4.1 VOC XML 转 YOLO txt 的完整脚本如果你拿到的标签是 VOC 格式需要转成 YOLO 才能直接训练。转换的核心是把绝对像素坐标转成归一化中心坐标。import os import xml.etree.ElementTree as ET classes [oil_stain, spot, scratch_oil] # 按你的类别顺序改 cls2id {c: i for i, c in enumerate(classes)} def voc_to_yolo(xml_path, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name not in cls2id: continue 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) # 边界裁剪防止越界 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls2id[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) return lines逻辑说明解析 XML 拿到每个 object 的类别和 bbox先做边界裁剪再归一化。参数上classes列表的顺序必须和data.yaml里的names完全一致顺序错了类别就全乱。img_w和img_h从对应图片读取不能写死因为工业数据集里图片分辨率可能不统一。4.2 四个最容易翻车的边界情况第一个坑坐标越界。有些标注工具导出的 xmax 会等于图片宽度甚至超一点归一化后大于 1YOLO 训练时直接报错。上面脚本里的max(0, min(...))就是防这个。第二个坑空标签文件。一张图里没有目标时VOC 里没有 object 节点转出来是空 txt。YOLO 允许空标签表示背景但如果你不希望背景图参与训练要在转换后过滤掉。第三个坑类别名大小写不一致。有的 XML 里写Oil_Stain有的写oil_staincls2id匹配不上就被跳过导致大量样本丢失。转换前先统计所有出现的 name 值。第四个坑图片和 XML 文件名不匹配。VOC 的 XML 里filename字段有时和实际文件名不一致转换时要以实际文件名为准别信 XML 里的。# 统计所有类别名排查大小写问题 names set() for f in os.listdir(annotations_voc): if f.endswith(.xml): tree ET.parse(os.path.join(annotations_voc, f)) for obj in tree.getroot().findall(object): names.add(obj.find(name).text) print(names)跑一遍这个统计如果输出的名字数量比你预期的类别多说明有拼写或大小写变体先统一再转换。4.3 COCO JSON 转 YOLO 的注意点COCO 转 YOLO 相对简单因为 JSON 里已经结构化了。关键是用image_id把 annotation 关联到对应图片拿到width和height做归一化。注意 COCO 的bbox格式是[x, y, width, height]不是[xmin, ymin, xmax, ymax]转换时别搞混。import json with open(annotations_coco/instances_train.json) as f: coco json.load(f) img_info {img[id]: img for img in coco[images]} cat_info {cat[id]: cat[name] for cat in coco[categories]} # 按 image_id 分组 from collections import defaultdict anns defaultdict(list) for ann in coco[annotations]: anns[ann[image_id]].append(ann) for img_id, img in img_info.items(): w, h img[width], img[height] lines [] for ann in anns[img_id]: x, y, bw, bh ann[bbox] cx (x bw / 2) / w cy (y bh / 2) / h nw bw / w nh bh / h cid cls2id[cat_info[ann[category_id]]] lines.append(f{cid} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) # 写入对应 txt逻辑说明先建 image_id 到图片信息的映射再按 image_id 聚合标注。参数上cls2id依然要和data.yaml对齐。COCO 的category_id往往不是从 0 连续开始的必须通过cat_info映射到名字再转 id不能直接用category_id。5. 用 YOLOv8 训练油污数据集data.yaml 配置与关键参数5.1 data.yaml 怎么写才不会报错Ultralytics 的 YOLOv8 靠一个 yaml 文件定位数据和类别。工业油污数据集的 yaml 长这样path: /home/user/dataset train: images/train val: images/val test: images/test names: 0: oil_stain 1: spot 2: scratch_oilpath是数据集根目录train/val/test是相对路径。names的键必须从 0 开始连续值要和标签里的类别 id 对应。最常见的报错是path写成绝对路径但train也写了绝对路径导致路径拼接错误。记住path用绝对train/val/test用相对。5.2 训练命令与必调参数yolo detect train \ datadataset/data.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ device0 \ projectruns/oil \ nameexp1参数说明model选yolov8s还是yolov8n看你的算力油污检测建议至少用 sn 对小目标油渍召回不够。imgsz640是默认值如果油污面积很小可以提到 1024但显存占用翻倍。batch16按显存调8G 显存用 816G 用 16。lr00.01是初始学习率油污数据集样本量 10000 张属于中等0.01 比较稳太大容易震荡。patience20是早停20 轮验证指标不升就停省时间。提示第一次训练先用epochs10跑通流程确认没有路径和标签错误再上 100 轮。直接上 100 轮结果报错浪费的是你自己的时间。5.3 训练过程看什么指标训练日志里重点看三个box_loss是否稳定下降、mAP50是否上升、cls_loss有没有异常波动。油污检测里box_loss下降慢是正常的因为边缘模糊导致定位难。如果mAP50卡在 0.3 以下不动八成是标签有问题回去查类别 id 和坐标。验证集指标用yolo detect val单独跑yolo detect val \ modelruns/oil/exp1/weights/best.pt \ datadataset/data.yaml \ imgsz640输出里看mAP50和mAP50-95前者宽松后者严格。工业落地一般要求mAP50到 0.85 以上才算可用低于这个值先别急着部署。6. 避坑与排查油污检测训练中最容易翻车的五件事6.1 现象训练 loss 正常但 mAP 一直是 0原因类别 id 错位。标签里的类别 id 和data.yaml的names对不上模型学的是错的映射。常见于 VOC 转 YOLO 时类别顺序写反或者 COCO 的category_id直接当类别 id 用了。解决写个脚本统计标签里出现的所有类别 id和names的键对比。不一致就重新生成标签别在训练时硬改。6.2 现象验证集 mAP 高但实际推理漏检严重原因验证集和训练集同分布但实际产线的光照、角度、油污形态和数据集差异大。数据集里的 10000 张如果都来自同一批次泛化性会很差。解决从实际产线再采一批图做测试集别用数据集自带的 test。如果差异大做数据增强时重点加亮度、对比度、旋转扰动。6.3 现象训练报 No labels found原因YOLO 找标签的路径规则是images替换成labels扩展名换成.txt。如果你的目录结构不是images/train对labels/train它就找不到。解决要么按标准结构重排目录要么在 yaml 里用train: ../labels/train这种显式路径。最稳的办法是保持标准结构。6.4 现象小面积油渍全部漏检原因imgsz640下小油渍缩放后只剩几个像素特征提取不到。或者 anchor 尺寸和油渍尺寸不匹配。解决把imgsz提到 1024 或 1280同时检查标签里小目标的宽高归一化后是不是小于 0.01太小的话考虑用切片推理SAHI方案。6.5 现象训练到一半显存溢出原因batch设太大或者imgsz提高后没降 batch。油污数据集图片分辨率如果不统一YOLO 会按最大尺寸对齐显存占用比预期高。解决先把所有图片 resize 到统一尺寸再训练或者用batch-1让 YOLO 自动选 batchUltralytics 支持自动 batch。显存不够就降imgsz别硬撑。7. 让油污检测真正可用的两个进阶技巧第一个技巧是切片推理。工业油污里小目标占比高整图缩放到 640 会丢细节。做法是把原图切成有重叠的小块每块单独推理再合并结果。Ultralytics 本身不直接支持但可以用 SAHI 库包一层。核心参数是切片大小和重叠率切片 640、重叠 0.2 是常用起点。代价是推理时间翻几倍产线节拍紧的话要权衡。第二个技巧是难例挖掘。第一轮训练完把验证集里漏检和误检的图挑出来人工重新标注或补充加入训练集再训一轮。油污检测里这一招提升最明显通常两轮难例挖掘能把 mAP50 拉高 5~10 个点。具体做法是用yolo detect predict跑验证集把置信度低于 0.3 但实际有目标的图筛出来。yolo detect predict \ modelruns/oil/exp1/weights/best.pt \ sourcedataset/images/val \ conf0.3 \ save_txtTrue \ projectruns/predict跑完对比save_txt的输出和原始标签差异大的图就是难例。这个流程我一般跑两轮第三轮收益就很小了。验证模型是否真的可用别只看 mAP。拿 50 张实际产线图人工数漏检和误检算一个业务口径的准确率。如果业务准确率低于 90%mAP 再高也别上线。我自己踩过的坑就是盯着 mAP 调到 0.9结果上线第一天漏检一堆反光油渍血泪经验。后来养成习惯任何模型上线前必须过一遍真实产线样本mAP 只是参考业务指标才是准绳。希望帮到你。本文还有配套的精品资源点击获取
返回列表