简介YOLO工业油污缺陷检测数据集面向制造质检与目标检测开发者包含约10000张真实产线场景的高质量图片标注框质量高覆盖油污缺陷提供VOC、COCO、YOLO三种格式标签可直接用于YOLO系列模型训练。压缩包共2000个文件以VOC格式xml标注为主另含YOLO格式txt标签/列表、数据集划分py脚本和YOLO环境搭建与训练教程html文档整体大小约797.79MB。配套内容覆盖Windows/Linux双系统的YOLO环境搭建与训练案例从环境安装到根据自身数据集修改配置均有说明三种数据集划分脚本可灵活生成训练集、验证集与测试集还能生成ImageSets下的索引txt文件显著降低数据准备门槛。目前已有286人学习下载适合需要快速落地工业油污检测方案的研究者与工程师也是目标检测入门与制造业视觉应用的实用参考资料。1. 10000 张工业油污缺陷图最磨人的却不是算法这条线该按什么顺序走做工业油污缺陷检测的工程师时间多半耗在数据上而不是 YOLO 算法上。解压这个 rar 包之后你会看到 10000 张油污图样本以及 VOC 的 xml、COCO 的 json、YOLO 的 txt 三套标注。多数人真正的卡点不在训练命令而在把三套格式统一成 YOLO 能直接读的 txt、再按图片粒度划分 train/val/test 的那一步——类别编号错一位后面所有 mAP 都是白看。下面按拿到数据的顺序走一遍三种标签格式怎么选、转换脚本参数怎么设、划分脚本怎么用、训练时的预训练权重和超参怎么调最后讲几个我踩过的坑。2. 解压后的三套标签VOC、coco、yolo 格式差异与选型理由2.1 油污缺陷数据集里的类别表怎么列一个统计脚本打底工业油污缺陷检测和一般目标检测不同油污是弱对比度、边缘模糊、还有大面积的渐变反光区域类别通常不会太多。常见数据集里一般会出现oil_stain、oil_leak、oil_drop这类类别名也可能干脆只有一个类别oil。你拿到数据后第一件事绝对不是跑训练而是把所有 xml 和 json 里出现的类别名统计出来列成一张表。import glob import xml.etree.ElementTree as ET names set() for xml_path in glob.glob(Annotations/*.xml): root ET.parse(xml_path).getroot() for obj in root.findall(object): names.add(obj.find(name).text) print(names)这段脚本把 VOC xml 里出现的所有类别名去重收集输出类似{oil_stain, oil_leak, oil_drop}的集合。COCO 格式更简单直接读 json 里categories数组的name字段就能拿到同样信息。类别名单是后续所有映射工作的唯一依据转换前先看清能避免后面被“类别名多一个空格”“两边大小写不一致”这种细节卡住一晚上。2.2 VOC 的 XML 与 COCO 的 JSON同一个框的两种写法VOC 格式用 XML 记录标注最外层是annotation每个object块对应一个目标框关键字段是name、bndbox下的xmin/ymin/xmax/ymax另外size里的width/height也要留意——从 VOC 转 YOLO 格式必须用到这组值而部分第三方标注工具导出的 xml 里size经常缺失后面转换脚本里要单独处理。COCO 格式则把所有标注集中到一个 JSON 文件里顶层images数组记录每张图片的id、width、height、file_nameannotations数组记录每个框的id、image_id、category_id、bboxcategories数组记录id和name。COCO 的bbox是[x, y, width, height]绝对像素值而不是中心点加宽高刚从 VOC 转过来的人最容易在这里写错。从工程角度说VOC 是一张图配一个 xmlCOCO 是把全部标注塞进一个大 json。将来你想换 mmdetection 或 Detectron2VOC 和 COCO 是通用接口而 YOLO 的 txt 更接近训练器内部的直接输入。三套格式信息等价但维护成本完全不同这份数据集把三套都给齐省的是将来换框架重新标注的时间不是训练时都用得上。2.3 YOLO txt 的归一化坐标与“为什么只有它直接进训练”YOLO 标签文件是每张图对应一个同名 txt每一行表示一个目标class_id x_center y_center width height四个坐标全部归一化到 0~1使用中心点表示法。类别 id 从 0 开始计数比如oil_stain是 0、oil_leak是 1。习惯写 VOC 格式的人容易从 1 开始编号这一步出错在训练初期几乎发现不了只能等 mAP 出来才发现全部错位。YOLO 选这套格式是因为模型输出天生就是“中心点宽高”的回归txt 直接当监督信号吃进去少做一次坐标系变换。Ultralytics YOLO 读取数据时会自动把配置里的images路径替换为同级的labels去找 txt所以目录组织必须严丝合缝训练才能少出问题。3. 把 VOC/COCO 标签统一成 yolo txt转换脚本与两个边界坑3.1 先定类别映射表顺序一旦写死就不能改转换的本质是两件事类别名到数字 id 的映射绝对像素坐标到归一化坐标的换算。前者错一个整个数据集标签漂移一位后者错一个框的位置直接偏移或反了。先把你统计出来的完整类别名单按固定顺序写死CLASS_NAMES [oil_stain, oil_leak, oil_drop] # 以你实际统计结果为准顺序定下来就不要变后续训练 yaml 里的names必须和这里完全一致。这个问题看似笨但在工业油污这类类间差异小的任务里训练时很难靠视觉发现错误只能在评估阶段暴露所以开工前先打印一句确认“类别 0 oil_stain”。3.2 VOC XML 转 YOLO txt脚本、坐标截断与缺 size 的处理import os import glob import xml.etree.ElementTree as ET CLASS_NAMES [oil_stain, oil_leak, oil_drop] def voc_to_yolo(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w float(size.find(width).text) img_h float(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASS_NAMES: continue cls_id CLASS_NAMES.index(name) bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) # 标注边界可能超出图像范围先截断再归一化 xmin max(0.0, min(xmin, img_w)) xmax max(0.0, min(xmax, img_w)) ymin max(0.0, min(ymin, img_h)) ymax max(0.0, min(ymax, img_h)) if xmax - xmin 1 or ymax - ymin 1: continue # 过滤掉退化成点的框 x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return \n.join(lines) xml_dir Annotations out_dir yolo_labels os.makedirs(out_dir, exist_okTrue) for xml_path in glob.glob(os.path.join(xml_dir, *.xml)): stem os.path.splitext(os.path.basename(xml_path))[0] content voc_to_yolo(xml_path) with open(os.path.join(out_dir, stem .txt), w, encodingutf-8) as f: f.write(content)脚本逻辑分四段从 xml 的size取宽高、遍历所有object、坐标截断和归一化、写出 txt。这里截断不是可选项手工标注工具偶尔会把框拉到图像边缘之外特别是油污在反光面上边界不清晰时标注师经常把框画过头。不截断归一化后的中心点可能大于 1YOLO 训练算 loss 时会出现异常值。提示如果发现root.find(size)取不到值先用一段检查脚本把缺 size 的 xml 列出来。数量不多就补写缺失字段数量多就改用 OpenCV 读同名图片的真实宽高不要硬着头皮转换。3.3 COCO JSON 转 YOLO txt按 image_id 聚合、重排 category_idimport json import os def coco_to_yolo(json_path, out_dir): with open(json_path, encodingutf-8) as f: data json.load(f) # COCO 的 category_id 可以不连续重排成从 0 开始的连续 id cat_sorted sorted(data[categories], keylambda c: c[id]) cat_map {c[id]: idx for idx, c in enumerate(cat_sorted)} imgs {img[id]: img for img in data[images]} anns_by_img {} for ann in data[annotations]: anns_by_img.setdefault(ann[image_id], []).append(ann) os.makedirs(out_dir, exist_okTrue) for img_id, img in imgs.items(): anns anns_by_img.get(img_id, []) if not anns: continue img_w, img_h img[width], img[height] lines [] for ann in anns: cls_idx cat_map[ann[category_id]] x, y, w, h ann[bbox] # COCO bbox 是 x,y,width,height像素值 # 边界约束 x max(0.0, min(x, img_w)) y max(0.0, min(y, img_h)) w max(1.0, min(w, img_w - x)) h max(1.0, min(h, img_h - y)) x_center (x w / 2) / img_w y_center (y h / 2) / img_h lines.append(f{cls_idx} {x_center:.6f} {y_center:.6f} {w / img_w:.6f} {h / img_h:.6f}) txt_name os.path.splitext(img[file_name])[0] .txt with open(os.path.join(out_dir, txt_name), w, encodingutf-8) as f: f.write(\n.join(lines))两个细节值得单独说。第一COCO 的category_id不一定从 0 开始连续排列直接把原始 id 当 YOLO 类别号会留下空洞YOLO 会认为还存在不存在的类别上面的cat_map把 categories 按 id 排序后重新映射成 0、1、2……。第二COCO 的 bbox 是x, y, w, h有人转的时候当成 VOC 的xmin, ymin, xmax, ymax结果宽高被当成右下角坐标一组出来框全飘。转换完成别急着往下走抽查 5~10 张图把 txt 里的归一化坐标乘回原图宽高画出来看框是否贴合油污区域。这一步是转换过程里最便宜的校验能拦下大部分格式错误。4. 用划分脚本拆 10000 张图目录结构、随机种子与 yaml 写法4.1 按图片粒度切固定随机种子划分脚本三个参数数据划分是“划分脚本”的核心价值。工业油污检测的划分有两个原则按图片切而不是按文件夹切固定随机种子保证可复现。按图片切的意思是同一张图的各种缺陷样本不会因为目录归属而泄漏到另一个集合如果同一拍摄批次都在一个文件夹里只按文件夹切会让验证集分布和训练集高度相似线上表现直接打折。固定随机种子则保证你调参时对比的是同一份数据分布。import os import random import shutil SRC_IMAGES images # 原图所在目录 SRC_LABELS yolo_labels # 转换出来的 yolo txt 目录 OUTPUT_DIR dataset_split RATIOS (0.8, 0.1, 0.1) SEED 42 random.seed(SEED) image_files [f for f in os.listdir(SRC_IMAGES) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(image_files) n_total len(image_files) n_train int(n_total * RATIOS[0]) n_val int(n_total * RATIOS[1]) split_map { train: image_files[:n_train], val: image_files[n_train:n_train n_val], test: image_files[n_train n_val:], } for split_name, files in split_map.items(): img_dir os.path.join(OUTPUT_DIR, split_name, images) lbl_dir os.path.join(OUTPUT_DIR, split_name, labels) os.makedirs(img_dir, exist_okTrue) os.makedirs(lbl_dir, exist_okTrue) for img_file in files: stem os.path.splitext(img_file)[0] src_img os.path.join(SRC_IMAGES, img_file) src_lbl os.path.join(SRC_LABELS, stem .txt) if not os.path.exists(src_lbl): print(f[skip] {img_file} 缺少标签文件) continue shutil.copy2(src_img, os.path.join(img_dir, img_file)) shutil.copy2(src_lbl, os.path.join(lbl_dir, stem .txt))分割的比值参数按需调要做交叉验证改成 (0.7, 0.15, 0.15) 也可以。shutil.copy2保留文件元数据以后排查“这个文件什么时候进来的”会方便很多。提示没有标签的图片宁可跳过也不要塞进训练集当负样本。对 YOLO 来说空 txt 代表“这张图没有任何目标”但油污数据里缺失标注往往只是漏标。把这些图当负样本模型会学到“这类图上不该有框”评估时漏检全算在你头上。4.2 train/val/test 目录怎么摆images 与 labels 的同级约定划分完之后的目录应该长这样dataset_split/ train/ images/ img_0001.jpg labels/ img_0001.txt val/ images/... labels/... test/ images/... labels/...为什么labels必须和images同级而不是放在images内部Ultralytics YOLO 训练时会拿配置里的图片路径自动把路径中的images替换成labels去找标注文件。你把 labels 换个位置就得传额外的label路径参数多一层传参就多一个出错点。保持这个约定训练命令干净很多。同级还有个好处将来要把数据从 YOLO 再转回 VOC 或 COCO 交给别的框架路径可以直接复用不用维护两套目录映射。4.3 yaml 配置path/train/val/names 与类别顺序的一致性训练前写一个数据描述文件path: /home/engineer/industrial_oil/dataset_split train: train/images val: val/images test: test/images nc: 3 names: [oil_stain, oil_leak, oil_drop]path是划分后数据集的绝对根目录train、val、test是相对path的图片目录路径YOLO 会自动找到同级的 labels。不用 test 集的话把test行删掉也不影响训练。nc必须和names的长度相等names的顺序必须和转换脚本里的CLASS_NAMES完全一致。老版 YOLOv5 教程里常见写法是相对当前目录的路径训练命令换一个工作目录就全部失效。我一般直接用绝对路径写path再配合训练命令里的project参数把输出固定到专门目录排查问题更快。5. YOLO 训练教程与避坑预训练权重、关键超参和五个翻车点5.1 预训练权重选择n/s/m 怎么根据硬件和数据量挑油污缺陷检测在工厂场景里类别很少通常 1~3 类但目标形态却不少小油滴、长条油痕、大面积油渍。模型尺寸参考硬件条件RTX 3060 级别显卡从yolov8n.pt或yolov8s.pt起步V100 或 A100 这类算力富余的卡可以上yolov8m.pt。10000 张图不算小数据集预训练权重仍然比从零初始化更有价值COCO 预训练模型已经学到丰富的纹理和边缘特征油污虽然难但对边缘、渐变这些基础模式是通用的。预训练模型下载注意一点用和训练代码同版本系列的权重。YOLOv8 的权重文件由modelyolov8s.pt自动下载到当前目录网络不畅时手动放进项目weights/目录把model参数改成对应路径即可。5.2 训练命令与超参imgsz、batch、epochs、workers 的落地值yolo detect train \ modelyolov8s.pt \ dataindustrial_oil.yaml \ epochs100 \ imgsz640 \ batch16 \ workers4 \ projectruns/oil_detect \ nameexp1参数取舍imgsz是训练输入尺寸油污检测里这个参数比想象中更重要后面避坑部分单独说。batch受显存限制16 在 RTX 3060 上是稳妥值显存不够时优先把imgsz降到 480而不是把 batch 降到 2小 batch 会让 BN 层统计不稳定。epochs对这种类少但目标形态多的数据集先跑 100 轮观察 loss 趋势验证集 mAP 一般在 60 轮附近开始有参考意义。环境配置上最容易出问题的是workersWindows 下设 4 或 8 经常撞上内存溢出因为每个 worker 都会复制一份数据队列Linux 下要留意共享内存/dev/shm被占满出现时把 workers 降到 2 基本能缓解。训练日志会分别显示cls_loss、box_loss和dfl_loss你不必完整推导损失函数但要看懂三条 loss 各自的下降趋势box_loss掉不下去大多是标签坐标转换出问题cls_loss掉不下去先查类别不平衡再怀疑类别名映射。5.3 训练日志看什么loss、mAP 和早期漏检信号训练不是把命令丢进终端就完事。看训练日志并不是玄学前 20 轮看 loss 是否在下降下降速度别奢望线性波动很正常40 轮以后看val/box_loss和val/cls_loss是否同步下降如果单调上涨说明过拟合已经发生把 epochs 减半重跑。第三阶段看metrics/mAP50和mAP50-95的差距工业油污类间重叠多两者差太大会怀疑标注框本身不够精确回头检查 xml 里的 bndbox 是不是整图框或点框。早期漏检信号不容易从数值上看出来两种常见做法每 10 轮保存的权重都拿去跑一次单独验证直接看图或者用model.val()在验证集上看混淆矩阵能明确告诉你油污类是不是被大面积误判为背景。5.4 五个高频翻车点现象 → 原因 → 解决以下几条都是血泪经验换来的直接按“现象 → 原因 → 解决”写。现象一训练强制中断报错里带 BN 字样。原因batch 太小或学习率太大BN 层的均值和方差在迭代中震荡发散也有工业数据集图分辨率差距悬殊同 batch 里混入 1920×1080 的原图和已被压得很小的局部截图归一化特征尺度对不上。 解决保证 batch 至少 8或者把imgsz统一到同尺度用预训练权重自带的默认学习率起步不要一上来就翻倍。YOLO 训练中 BN 崩溃是社区高频问题本质就是超参和数据分布双重作用。现象二训练日志一切正常但 mAP 为 0。原因标签 txt 与图片文件名不配对。比如图片是oil_001.JPG标签却写成小写.jpg或者划分脚本把没有标签的图也复制过去模型在空标签图上空转。 解决划分脚本里已经做了os.path.exists(src_lbl)检查。已经跑过头的话写一段对照脚本把images和labels的文件名做一次差集把缺标签的图全部找出来。现象三框的位置全对类别全错。原因从 VOC 转 YOLO 时类别编号从 1 开始导致oil_stain被当成 id 1而模型眼里的 id 1 是oil_leak。 解决对照CLASS_NAMES.index(name)检查编号逻辑确认训练 yaml 的names顺序完全一致。这条太常见我每个新项目都先把“类别 0 哪个类”打出来确认再开工。现象四小油滴漏检率极高大油渍却检得很好。原因imgsz太低。假设原图里最小油滴只占 30×30 像素imgsz480压缩后可能只剩 18×18模型很难学到稳定特征同时小目标数量占比少对大目标的学习倾向更强。 解决把imgsz提到 640 或 800同时检查原图尺寸是否远大于 1000 像素如果是就考虑切成瓦片训练而不是暴力缩放。现象五验证集 mAP 虚高现场部署立刻打回原形。原因划分时没按拍摄批次或夹具编号切同一个工件的不同角度同时进了训练集和验证集验证结果好看但实际分布不同更隐蔽的是划分时没固定SEED实验不可复现。 解决划分固定SEED验证时多看confusion_matrix而不是只看 mAP数据里如果有批次、机台这类元信息按批次划分才贴近现场分布。这也是把划分脚本单独讲一节的原因。6. 部署前再检查一遍混淆矩阵、漏检图和导出后的现场过图6.1 验证命令、conf/iou 阈值和混淆矩阵的“总和”问题训练结束后先用验证集跑一轮标准验证yolo detect val \ modelruns/oil_detect/exp1/weights/best.pt \ dataindustrial_oil.yaml \ imgsz640 \ conf0.25 \ iou0.5conf和iou是推理时的两个阈值直接影响验证集的 mAP。工业油污场景里我习惯把conf压到 0.15 再看混淆矩阵因为漏检比误报更让产线头疼——油污漏过去后面整条工序都可能带着缺陷往下走。输出目录里的confusion_matrix.png重点看两类单元格真实油污被预测成背景的占比以及背景被预测成油污的占比。这里说一个社区里反复出现的问题“混淆矩阵总合不唯一”。Ultralytics 输出的混淆矩阵在某些版本里没有对行归一化或者背景类按图像级统计而目标类按框级统计两套口径混在一起行和自然对不上。不用纠结总和是不是 100%只看你关心的两类单元格比例就行。背景被预测成油污占比高先调conf阈值真实油污漏到背景占比高回到数据层面看正样本是否过少。6.2 导出 ONNX 固定 imgsz用现场复拍图过五张再上产线验证通过不代表能直接部署。我的习惯是先导出推理引擎通用格式yolo export \ modelruns/oil_detect/exp1/weights/best.pt \ formatonnx \ imgsz640导出 ONNX 后先用 onnxruntime 在本机推理一批现场真实光线下的图不要只用训练集里的验证图。工业油污的光照敏感性很强同一个缺陷在直射光和漫反射光下特征完全不同即使数据集是工厂实际采集的现场换灯管、改机位都会让分布漂移。我一般会保留一个“现场复拍”文件夹里面放两张油污图、两张正常工件图、一张干扰图每次换环境先跑一遍这五张才敢上产线。如果漏检集中在现场复拍图里多数不是模型权重的问题而是预处理和现场不一致推理时设的imgsz与训练时不统一、摄像机自动白平衡带来色彩偏移、或分辨率比训练数据大导致目标尺度过大。解决方向有三个现场图等比例缩放留黑边而不是直接拉伸、训练数据里加入更多现场光线样本、把imgsz提到 800 再重新导出。最后一个小提醒导出 ONNX 时imgsz固定不要用动态尺寸pipeline 部署少一个坑。我现在逐步养成一个习惯训练启动时就把划分脚本的SEED和完整 yaml 内容备份到实验目录任何实验报告带上这两个文件别人包括三个月后的我自己就能完整复现。数据、标签、划分、训练四个环节最容易说不清的就是“当初那份数据是怎么分的”。希望这几条对正在整理工业油污数据集的你有帮助。本文还有配套的精品资源点击获取