简介: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_name,annotations数组记录每个框的id、image_id、category_id、bbox,categories数组记录id和name。COCO 的bbox是[x, y, width, height]绝对像素值,而不是中心点加宽高,刚从 VOC 转过来的人最容易在这里写错。
从工程角度说,VOC 是一张图配一个 xml,COCO 是把全部标注塞进一个大 json。将来你想换 mmdetection 或 Detectron2,VOC 和 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_ok=True) 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", encoding="utf-8") as f: f.write(content)脚本逻辑分四段:从 xml 的<size>取宽高、遍历所有<object>、坐标截断和归一化、写出 txt。这里截断不是可选项,手工标注工具偶尔会把框拉到图像边缘之外,特别是油污在反光面上边界不清晰时,标注师经常把框画过头。不截断,归一化后的中心点可能大于 1,YOLO 训练算 loss 时会出现异常值。
提示:如果发现
root.find("size")取不到值,先用一段检查脚本把缺 size 的 xml 列出来。数量不多就补写缺失字段;数量多就改用 OpenCV 读同名图片的真实宽高,不要硬着头皮转换。
3.3 COCO JSON 转 YOLO txt:按 image_id 聚合、重排 category_id
import json import os def coco_to_yolo(json_path, out_dir): with open(json_path, encoding="utf-8") as f: data = json.load(f) # COCO 的 category_id 可以不连续,重排成从 0 开始的连续 id cat_sorted = sorted(data["categories"], key=lambda 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_ok=True) 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", encoding="utf-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_ok=True) os.makedirs(lbl_dir, exist_ok=True) 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 的权重文件由model=yolov8s.pt自动下载到当前目录,网络不畅时手动放进项目weights/目录,把model参数改成对应路径即可。
5.2 训练命令与超参:imgsz、batch、epochs、workers 的落地值
yolo detect train \ model=yolov8s.pt \ data=industrial_oil.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ workers=4 \ project=runs/oil_detect \ name=exp1参数取舍:imgsz是训练输入尺寸,油污检测里这个参数比想象中更重要,后面避坑部分单独说。batch受显存限制,16 在 RTX 3060 上是稳妥值;显存不够时优先把imgsz降到 480,而不是把 batch 降到 2,小 batch 会让 BN 层统计不稳定。epochs对这种类少但目标形态多的数据集,先跑 100 轮观察 loss 趋势,验证集 mAP 一般在 60 轮附近开始有参考意义。
环境配置上最容易出问题的是workers:Windows 下设 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 像素,imgsz=480压缩后可能只剩 18×18,模型很难学到稳定特征;同时小目标数量占比少,对大目标的学习倾向更强。 解决:把imgsz提到 640 或 800,同时检查原图尺寸是否远大于 1000 像素,如果是就考虑切成瓦片训练,而不是暴力缩放。
现象五:验证集 mAP 虚高,现场部署立刻打回原形。原因:划分时没按拍摄批次或夹具编号切,同一个工件的不同角度同时进了训练集和验证集,验证结果好看但实际分布不同;更隐蔽的是划分时没固定SEED,实验不可复现。 解决:划分固定SEED,验证时多看confusion_matrix而不是只看 mAP;数据里如果有批次、机台这类元信息,按批次划分才贴近现场分布。这也是把划分脚本单独讲一节的原因。
6. 部署前再检查一遍:混淆矩阵、漏检图和导出后的现场过图
6.1 验证命令、conf/iou 阈值和混淆矩阵的“总和”问题
训练结束后,先用验证集跑一轮标准验证:
yolo detect val \ model=runs/oil_detect/exp1/weights/best.pt \ data=industrial_oil.yaml \ imgsz=640 \ conf=0.25 \ iou=0.5conf和iou是推理时的两个阈值,直接影响验证集的 mAP。工业油污场景里,我习惯把conf压到 0.15 再看混淆矩阵,因为漏检比误报更让产线头疼——油污漏过去,后面整条工序都可能带着缺陷往下走。输出目录里的confusion_matrix.png重点看两类单元格:真实油污被预测成背景的占比,以及背景被预测成油污的占比。
这里说一个社区里反复出现的问题:“混淆矩阵总合不唯一”。Ultralytics 输出的混淆矩阵在某些版本里没有对行归一化,或者背景类按图像级统计而目标类按框级统计,两套口径混在一起,行和自然对不上。不用纠结总和是不是 100%,只看你关心的两类单元格比例就行。背景被预测成油污占比高,先调conf阈值;真实油污漏到背景占比高,回到数据层面看正样本是否过少。
6.2 导出 ONNX 固定 imgsz,用现场复拍图过五张再上产线
验证通过不代表能直接部署。我的习惯是先导出推理引擎通用格式:
yolo export \ model=runs/oil_detect/exp1/weights/best.pt \ format=onnx \ imgsz=640导出 ONNX 后先用 onnxruntime 在本机推理一批现场真实光线下的图,不要只用训练集里的验证图。工业油污的光照敏感性很强,同一个缺陷在直射光和漫反射光下特征完全不同;即使数据集是工厂实际采集的,现场换灯管、改机位都会让分布漂移。我一般会保留一个“现场复拍”文件夹,里面放两张油污图、两张正常工件图、一张干扰图,每次换环境先跑一遍这五张,才敢上产线。
如果漏检集中在现场复拍图里,多数不是模型权重的问题,而是预处理和现场不一致:推理时设的imgsz与训练时不统一、摄像机自动白平衡带来色彩偏移、或分辨率比训练数据大导致目标尺度过大。解决方向有三个:现场图等比例缩放留黑边而不是直接拉伸、训练数据里加入更多现场光线样本、把imgsz提到 800 再重新导出。最后一个小提醒:导出 ONNX 时imgsz固定,不要用动态尺寸,pipeline 部署少一个坑。
我现在逐步养成一个习惯:训练启动时就把划分脚本的SEED和完整 yaml 内容备份到实验目录,任何实验报告带上这两个文件,别人包括三个月后的我自己就能完整复现。数据、标签、划分、训练四个环节,最容易说不清的就是“当初那份数据是怎么分的”。希望这几条对正在整理工业油污数据集的你有帮助。
本文还有配套的精品资源,点击获取