简介:面向目标检测学习者与石油泄漏监测应用开发者,这份YOLO石油泄露目标检测数据集提供了真实场景下的高质量图像与人工标注,可支撑从YOLO环境搭建、数据格式转换到模型训练与验证的完整流程。压缩包共2000个文件,容量约117MB;其中1000张图片对应的标签已按voc xml、coco json、yolo txt三种格式分别存放,另有6个环境搭建与训练教程页面、3个Python划分脚本和1个模型训练yaml配置,目录结构清晰,便于直接选择所需格式开展训练。当前已有658人学习。配套内容不止于数据:Linux与Windows双平台的YOLO安装配置说明、按案例修改训练自己数据集的教程,以及训练集/验证集/测试集划分脚本,能帮助初学者减少配环境与造数据的弯路,快速完成石油泄露场景的目标检测模型训练与评估。
1. YOLO石油泄露目标检测数据集:为什么1000张图加三种格式标签能省一半时间
石油泄露检测的难处,不是“看到油膜”,而是从颜色接近海水、红外图里深浅渐变的小目标中把它找出来,误报率和漏检率都很难压。标题里的这个“YOLO石油泄露目标检测数据集”打包了1000张已标注图片,同一份标注导出成了VOC、COCO、YOLO三种格式,还配了划分脚本和训练教程,组合起来就是一条“从标注到训练一次跑通”的流水线。对刚接触目标检测、想用现成数据把YOLO流程走通的学生,以及准备用YOLO评估海上巡检可行性的一线工程师,这套资源的用法都是同一个:解压、检查、转格式、划分、训练,跳过最耗时的人工标注和格式转换。这篇文章就按这个顺序,把每一步的坐标计算、参数设置和最容易翻车的地方拆开讲。
2. 三种标签格式互转:VOC、COCO、YOLO 的结构差异与转换脚本
拿到数据集后第一眼看到的往往是三个目录:Annotations、labels、annotations.json,分别对应VOC、YOLO、COCO三种格式。很多初学者会问:明明是同一批框,为什么要存三份?因为不同工具链只认其中一种标签。标注软件常用 VOC 的 XML 或 COCO 的 JSON 导出,而主流训练框架读取的是 YOLO 的 txt;反过来,你要是想用 CVAT 复查标注,它导入 COCO 最顺手。同一份标注保留三种格式,本质是让你不用在工具之间来回转换,但这不意味着零风险——三种格式的坐标含义完全不同,混用一处就会让模型训练结果变成黑匣子。
2.1 三种格式:同一批框,三种坐标写法
先建一个最基础的对照表,后面所有脚本都围绕这几行差异写:
| 格式 | 存储形态 | 坐标内容 | 类别编号 |
|---|---|---|---|
| VOC | 每张图一个.xml | xmin, ymin, xmax, ymax绝对像素 | 标签名(字符串) |
| COCO | 整个数据集一个.json | x, y, width, height绝对像素 | category_id,从 1 开始 |
| YOLO | 每张图一个.txt | x_center, y_center, width, height归一化 | class_id,从 0 开始 |
VOC 的 XML 里,<object>标签下有一个<bndbox>,四个值分别代表左上角和右下角的绝对像素坐标,类别名写在<name>里。COCO 的 JSON 主体是三个数组:images记录每张图的宽度、高度和文件名,annotations记录每个框的bbox和category_id,categories负责把 id 映射回类名。YOLO 的 txt 最直接,每行五个数:类别id、中心点x、中心点y、宽、高,全部除以原图宽高做归一化。
转换的核心就是坐标换算。VOC 到 YOLO 的公式是cx = (xmin + xmax) / 2 / img_w,w = (xmax - xmin) / img_w,高度同理用img_h;COCO 到 YOLO 则先要把左上角加宽高换算成中心点。这里最容易出错的分寸是:x 方向的偏移只能用图片宽去归一化,y 方向的只能用图片高,两个分母不能混用。另一个隐蔽的坑是类别编号:COCO 的category_id从 1 开始,YOLO 的class_id从 0 开始,转换时忘了减 1,训练出来的模型预测的类别会整体错一位。
2.2 VOC 转 YOLO:从 XML 到 txt 的最小脚本
常见做法是用xml.etree.ElementTree解析 XML,不引入额外依赖。下面这段脚本可以直接丢进数据集目录跑:
import xml.etree.ElementTree as ET import os CLASSES = ["oil_leak"] # 必须与训练配置 data.yaml 中的 names 顺序一致 def voc2yolo(xml_file, out_txt, img_w, img_h): tree = ET.parse(xml_file) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in CLASSES: continue # 跳过不关心的类别 cid = CLASSES.index(name) box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) # 中心点坐标和宽高,x 方向除以图宽,y 方向除以图高 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"{cid} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_txt, "w", encoding="utf-8") as f: f.write("\n".join(lines)) # 批量转换示例:遍历 Annotations 目录,读取同名图片尺寸 xml_dir = "Annotations" img_dir = "images" out_dir = "labels" os.makedirs(out_dir, exist_ok=True) for xml_name in os.listdir(xml_dir): if not xml_name.endswith(".xml"): continue stem = os.path.splitext(xml_name)[0] # 读原图尺寸时用 cv2 或 PIL,下面以 PIL 为例 from PIL import Image img = Image.open(os.path.join(img_dir, stem + ".jpg")) w, h = img.size voc2yolo(os.path.join(xml_dir, xml_name), os.path.join(out_dir, stem + ".txt"), w, h)这段代码里,CLASSES列表的顺序就是 YOLO 的类别编号顺序,这个顺序一旦定下来就不能改。训练时data.yaml的names如果和它不一致,模型训练不会报错,但推理结果的类别标签全是错的,属于最典型的“看起来训练成功、实际完全没法用”。另外注意读尺寸时用 PIL 的img.size返回(宽, 高),不是(高, 宽),手滑写反会让所有框的归一化坐标全体变形。
提示:如果 XML 里没有
<size>节点,一定从原图读尺寸,不要用 XML 里的宽高,因为部分标注工具的 XML 尺寸字段不可靠,尤其是图片被压缩过的情况下。
2.3 COCO 转 YOLO:注意 category_id 从 1 开始
COCO 转 YOLO 的代码常见做法是按image_id聚合annotations,再逐条写 txt:
import json import os def coco2yolo(json_path, label_dir): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) images = {img["id"]: img for img in data["images"]} anns = {} for ann in data["annotations"]: anns.setdefault(ann["image_id"], []).append(ann) os.makedirs(label_dir, exist_ok=True) for img_id, ann_list in anns.items(): img = images[img_id] stem = os.path.splitext(img["file_name"])[0] img_w, img_h = img["width"], img["height"] lines = [] for ann in ann_list: # COCO 的 category_id 从 1 开始,YOLO 的 class_id 从 0 开始 cid = ann["category_id"] - 1 x, y, w, h = ann["bbox"] # 左上角 x, y + 框宽高 cx = (x + w / 2) / img_w cy = (y + h / 2) / img_h lines.append(f"{cid} {cx:.6f} {cy:.6f} {w / img_w:.6f} {h / img_h:.6f}") with open(os.path.join(label_dir, stem + ".txt"), "w") as f: f.write("\n".join(lines)) coco2yolo("annotations.json", "labels_coco2yolo")这里有个容易忽略的细节:COCO 的bbox可能是浮点数,转换时千万不要为了整齐做int()取整。1000 张图的石油泄露场景里,很多框是贴着油膜边缘画的,取整会丢掉小数部分的框偏移,训练时 IoU 计算跟着受影响。另外,攻击点在于ann["bbox"]的长度一定是 4,如果用的是带分割标注的 COCO 文件,segmentations是多边形坐标,和bbox无关,别混着读。
2.4 转换完先验证,再进训练
所有标签转完后不要直接开训。先随机抽三张图,把 txt 里的归一化坐标乘回原图宽高,画框保存,肉眼核对框的位置和形状是否和油膜区域吻合。这一步花五分钟,能挡掉大部分格式转换的翻车:框整体偏移、宽高被压扁、类别编号错位,这些问题看输出图一眼就能发现。没有 OpenCV 的话,用matplotlib的Rectanglepatch 也能画,重点不是画得好看,而是确认坐标换算没有系统性错误。
3. 数据集划分脚本:为什么按场景分组,以及一套可复现的划分方式
数据集里有划分脚本,很多人直接双击运行完就往训练里送,等到验证集指标虚高到 0.95 才反应过来不对劲。石油泄露数据的采集方式和自然场景不同,无人机或者固定机位拍摄时,同一个泄露事件的连续帧高度相似——背景几乎不动,只有油膜边缘轻微变化。如果划分脚本只是把所有图片随机打散,同一个事件的多张连续帧会同时出现在训练集和验证集里,验证集的数据分布和训练集几乎重叠,指标自然好看,但模型一遇到从未见过的新画面就露馅。这种“数据泄露”在目标检测数据集里造成的假象,比模型本身的效果问题更难排查。
3.1 三种划分策略与数据泄露问题
常见划分方式有三种。第一种是纯随机划分,代码最简单,但只适合独立拍摄、彼此无关的图片;第二种是按文件名前缀或采集批次分组,把同一事件的连拍帧全部归到同一个集合里,这是石油泄露这类巡检数据最稳妥的做法;第三种是按目标数量分层划分,适合正负样本不均衡的数据集——比如 1000 张图里有大量正常海面负样本,纯随机划分可能把负样本都分进训练集,验证集里几乎没有负样本,误报率完全没被检验出来。
对标题里这套资源,我的习惯是先用第二种方式划分,再做一次负样本比例检查。石油泄露检测里负样本不是噪音,它们负责压制模型对海水纹理、船舶尾迹的误报,如果训练集和验证集的负样本比例差太多,训练曲线会很奇怪:loss 在降,但验证集的 precision 上不去。处理办法是划分前先统计每个分组里含目标框的图片数量,确保每个集合里正负样本比例接近原始数据集。
3.2 按场景分组 + 固定随机种子的划分脚本
下面这段划分脚本可以直接抄,思路是先按文件名前缀分组,再在组级别做随机分配,保证同一事件的连拍帧不会跨集合:
import os import random random.seed(20240601) # 固定种子,保证每次划分结果一致 img_dir = "images" label_dir = "labels" train_txt = "train.txt" val_txt = "val.txt" test_txt = "test.txt" train_ratio, val_ratio = 0.7, 0.2 # 剩余 10% 做测试集 # 1. 按前缀分组。假设文件命名是 platform_001.jpg / ship_002.jpg groups = {} for name in os.listdir(img_dir): if not name.lower().endswith((".jpg", ".jpeg", ".png")): continue stem = os.path.splitext(name)[0] # 跳过没有对应标签的图片 if not os.path.exists(os.path.join(label_dir, stem + ".txt")): continue prefix = stem.split("_")[0] # 按场景前缀分组 groups.setdefault(prefix, []).append(name) # 2. 组级别打乱并切分 group_names = list(groups.keys()) random.shuffle(group_names) n_train = int(len(group_names) * train_ratio) n_val = int(len(group_names) * val_ratio) def write_list(path, selected_groups): with open(path, "w") as f: for g in selected_groups: for img_name in groups[g]: img_path = os.path.abspath(os.path.join(img_dir, img_name)) f.write(img_path + "\n") write_list(train_txt, group_names[:n_train]) write_list(val_txt, group_names[n_train:n_train + n_val]) write_list(test_txt, group_names[n_train + n_val:]) print(len(group_names), "个场景组,已写入三个 txt")这个脚本有两个参数需要按实际数据调整:一是stem.split("_")[0]的分隔规则,如果文件名是纯数字编号,那就改成取前 4 位字符;二是比例,1000 张图用 7:2:1 是常见比例,但如果某个场景组特别大,比如一个视频序列占了 400 张,按组划分会出现某组占比过高,这时候可以先缩小组内采样数,或者把该组的帧做抽稀再划分。
提示:划分后一定要验证三个 txt 里的图片路径都真实存在,且对应的 label 文件都存在。这个检查脚本里已经写了一半,实际使用时可以把 label 存在性检查也加到写入循环里,避免某个 txt 混进缺失标签的样本。
3.3 三份清单在训练中怎么用
生成的三个 txt 配合 YOLO 的data.yaml使用,train和val字段直接指向这两个 txt,不需要把图片复制到子目录。用 txt 而不是目录的好处是灵活——想换比例、想去掉某些样本,改文本文件就行,不用动图片存储。测试集的 txt 日常训练用不到,但建议保留,等最终模型训完用它做一次独立评估,这部分我放在最后一章讲。
另外给一个后悔药:划分完先别删原始图片和原始标签。后续如果发现某个场景组在验证集里表现特别差,或者训练集里混进了损坏图片,你可以随时重新划分,而不需要重新解压整个 rar。
4. YOLO 训练教程落地:data.yaml、训练命令与关键参数怎么设
格式转完、划分完毕,接下来就是把数据送进 YOLO 训练。现在主流做法是直接用 Ultralytics 的 YOLO 训练脚本,一条命令就能跑起来,但很多人死在前置配置上:data.yaml写错路径、names顺序和标签编号对不上、imgsz设得不符合小目标检测需求。这一章把训练教程里最关键的三个文件和一个命令讲透,剩下的交给训练日志。
4.1 data.yaml 的写法:路径、清单与类别名
data.yaml是训练流程里最容易被忽视的配置文件,百度一搜“YOLO 训练自己的数据集”,第一课都是写它。石油泄露检测是单类任务,但names依然要用列表形式,并且顺序必须和转换脚本里的CLASSES完全一致:
# 石油泄露单类检测的 data.yaml path: /data/oil_leak_dataset # 解压后的数据集根目录,绝对路径最省事 train: train.txt # 训练集图片清单 val: val.txt # 验证集图片清单 test: test.txt # 测试集清单,可选 # 类别编号从 0 开始 names: 0: oil_leakpath写绝对路径是最稳的做法,因为训练命令可能在任意目录执行,相对路径容易出现“File not found”但报错信息不明确的情况。train和val这两个字段的值会与path拼接,如果直接写绝对路径的 txt,记得path也要能对上。names是字典还是列表都行,但顺序永远按编号来,一个字节都不能错。
4.2 完整训练命令与关键参数
基础训练命令如下,以小模型起步、显存友好为优先:
yolo detect train \ model=yolov8n.pt \ data=data.yaml \ epochs=150 \ batch=16 \ imgsz=960 \ patience=30 \ optimizer=auto \ close_mosaic=10 \ device=0逐个说参数,方便你按自己机器条件改。model=yolov8n.pt是官方预训练权重,n 是 nano 版本,只有几 MB,对石油泄露这种目标小但背景杂的场景,n 起步能快速验证 pipeline 是否通。epochs=150看起来多,但配合patience=30实际会在验证集指标连续 30 轮不提升时自动早停,不会傻跑完。batch=16在 960 分辨率下大约需要 16GB 左右显存,如果你的卡是 8GB,就降到 8,或者把imgsz降到 640。imgsz=960是关键设置,石油泄露油膜往往只占图片很小一块,640 分辨率下目标像素可能只有十几个,960 能让小目标的特征保留明显更多;显存不够时优先降batch,别降imgsz。close_mosaic=10表示最后 10 轮关闭马赛克增强,因为马赛克会让小目标被切割得更碎,关掉后模型能在真实分布上做最后收敛。optimizer=auto让框架自己选,省心。
第一次跑建议用默认超参,只改imgsz、batch和epochs。石油泄露数据集只有 1000 张,属于中小规模,大批量和高学习率容易直接过拟合,lr0可以保守点设成 0.005。如果用的是 2 卡或 4 卡机器,加参数device=0,1,数据并行训练 YOLO 内置支持,不需要改代码。
4.3 训练过程的监控:看到什么算正常
训练开始后重点看两个曲线:train/box_loss和val/box_loss。石油泄露场景里目标边界比较模糊,油膜的边缘本身就是渐变过渡,框的精确度天然受限,所以 box loss 不会像人像检测那样降到很低。正常现象是训练集 loss 稳步下探,验证集 loss 在某个点后开始震荡甚至回升,那就是过拟合信号,patience会自动停住。如果验证集 loss 从头到尾纹丝不动,别急着加数据,先回去检查标签转换环节——大概率是归一化坐标算错了,模型学了个寂寞。
训练结束时输出目录下会有一堆结果文件,包括confusion_matrix.png、results.png、val_batch_pred.jpg。先看val_batch_pred.jpg,这是验证集预测结果的可视化,如果框全部落在油膜上,说明格式转换和训练都通了;如果框满天飞,再去看混淆矩阵和 loss 曲线。
5. 石油泄露检测训练常见的五个坑:现象、原因、解决
这一章是我最想让你认真看的部分。石油泄露检测数据集的整理难度不高,但训练过程中的翻车现场高度一致。下面五条都是常见且隐蔽的坑,每条按照“现象 → 原因 → 解决”写,方便你对照排查。
5.1 类别编号错位:训练不报错,推理结果全错
现象:训练过程一切正常,loss 正常下降,验证集 mAP 也很高,但拿训练好的权重跑到新图片上,预测结果里出现了一些明显不对的类别标签,或者所有框的类别都和实际不符。
原因:VOC 转 YOLO 时CLASSES列表的索引顺序,和data.yaml里names的顺序不一致。比如转换脚本里CLASSES = ["oil_leak"]对应编号 0,但data.yaml里写成了names: {0: "leak_oil"},如果类别名拼写不一致,框架不会报错,模型只在编号层面工作,最终输出的类别映射就乱了。
解决:做一次全流程校验,训练前用脚本打印一份类别编号对照表,转换脚本、data.yaml、txt 文件三方的编号和类名必须逐字对上。多类别时这个问题尤其致命,单类数据稍微好排查,但也别心存侥幸。
5.2 归一化坐标分母用错:框全部压扁或拉长
现象:训练时 box loss 偏高且降不下去,验证集预测框的宽高比例明显不对劲——框是扁的,或者高度只有正常的一半。
原因:转换公式里把 x 方向坐标除以了图片的高,或者写成了统一的max(w, h)。xml 里width和height是分开的字段,归一化必须 x 方向用图宽、y 方向用图高。这类错误在单张看时不容易发现,因为框的位置大致还在目标附近,只有画出来对比原图边界才能看出宽高被系统性地扭曲。
解决:转换脚本里单独打印几组原始坐标和归一化坐标,手工验算一遍公式。再抽三张图把 txt 坐标乘回原图尺寸画框,和原图标注叠在一起看。这一步省不得。
5.3 图片与标签文件名对不上:大量样本被静默丢弃
现象:训练时发现每个 epoch 的样本数量比预期少,或者验证集里有些图片永远没有预测框。翻日志才注意到训练框架把找不到标签的图片直接跳过了。
原因:下载的数据集里,jpg 和 txt 的命名不完全一致。常见的有三种情况:图片叫leak_001.jpg而标签叫leak_001.txt这种前缀一致但后缀不同导致匹配失败;或者解压时系统给文件加了(1)后缀;或者部分图片本身就是负样本,没有对应标签文件,但划分脚本没做过滤就写进了清单。
解决:跑一个文件名一致性检查脚本,统计 images 和 labels 两个目录下同名文件的数量差异。负样本图可以存在,但要在清单里明确标注,或者直接单独分一个负样本目录,不要混在正样本清单里让框架猜。
5.4 BatchNorm 崩溃:loss 变成 NaN,训练直接中断
现象:训练跑到中间某个 epoch,loss 曲线突然掉到 NaN,之后全是 NaN,results.png上留下一段断崖。重启训练后问题复现,只是崩溃的 epoch 位置不一样。
原因:batch 太小加学习率偏大,是常见的组合。石油泄露图像有不少是夜间红外、大范围暗色,像素值方差大,BatchNorm 在小 batch 下统计量不稳定,一个极端样本就能让梯度爆炸。这个现象在 YOLO 训练里有个专门的讨论叫“bn 崩溃”,小数据集上特别容易出现。
解决:先把batch提升到 16 或 32,如果显存不够就降低imgsz;再把lr0降到 0.003 以下。另外把warmup_epochs保持默认的 3 轮,不要关掉 warmup,它是防 NaN 的第一道防线。改完之后如果还崩,加载预训练权重时换成官方原版yolov8n.pt,不要用别人二次转换过的权重。
5.5 数据泄露导致验证集指标虚高:新的监控画面上效果崩盘
现象:训练时验证集 mAP50 高达 0.95,模型看起来已经收敛得很好,但部署到新的监控画面后,漏检率明显比训练时高一个档次。
原因:划分脚本不够严谨,同一个场景的连续帧被随机分到了训练集和验证集。验证集里藏着和训练集几乎一样的画面,模型其实是在“背题”,而不是在“解题”。石油泄露数据中无人机巡航路径上的连拍帧相似度极高,这种问题几乎必现。
解决:用 3.2 节的分组划分脚本重新划分,保证同一个场景的所有帧只进入一个集合。判断是否发生数据泄露的一个土办法:把验证集里检测效果最好的几张图找出来,和训练集里相似度最高的图做对比,如果背景几乎一样,那就中招了。另外验证集指标高不要开心太早,测试集的独立评估才是最终通行证。
6. 训练完怎么验收:混淆矩阵挑着看,抽帧比着看
训练结束后的验收阶段,很多人只盯一个数字:mAP。石油泄露检测这种小目标、低对比度场景,mAP 要分开看。mAP50 反映的是框大致到位就行,对油膜这种边缘模糊的目标更友好;mAP50-95 对框的精确度要求高,分数低一点是正常的,不用焦虑。关键在于:如果 mAP50 高但 mAP50-95 很低,说明模型能找到目标但框偏了,对后续的泄漏面积估算这类应用会有影响;如果两者都低,先怀疑标签级数据问题,别急着调模型。
混淆矩阵的总和不是 100%,这是正常现象。矩阵里每个格子的比例是相对“真实目标总数”来的,漏检的框不会出现在任何格里,背景区域被误检也会单列一类,所以列求和可能小于 100%。看到矩阵里background列有值不是坏事,反而说明模型对海面纹理有过学习;真正需要警惕的是oil_leak那一行的对角线占比太低,意味着大量油膜区域被模型漏掉,这才是巡检场景最不可接受的失败模式。
我最推荐的验收手段是抽帧对比。训练完输出目录下的val_batch_pred.jpg只展示了模型挑的一部分样本,不够全面。建议单独跑一次批量推理:
yolo predict model=runs/detect/train/weights/best.pt \ source=val_samples/ \ imgsz=960 \ conf=0.25 \ save=True把验证集里随机抽出的 20 张图(注意从不同场景组抽,别从同一段连拍里抽)放进一个目录,推理保存后逐一和原图对比,重点看两张图:一张是大面积油膜、高置信度的容易样本,一张是小油膜、背景干扰强的困难样本。如果容易样本都漏检,检查置信度阈值是不是设太高;如果困难样本全检不出来,说明数据里小目标占比不够,后续该做的是补充近距离采集或者把 imgsz 再提一档。我自己的习惯是训练完隔一天再回来看这批抽帧图,刚训完时的兴奋感容易让人忽略漏检。
这套“数据检查 → 格式转换 → 分组划分 → 训练 → 抽帧验收”的流程,适用于任何一个小样本工业检测场景。石油泄露数据集帮我们把最费时的标注工作省掉了,剩下的稳定性,靠的是每一步验证而不是盲目相信脚本。希望帮到你。
本文还有配套的精品资源,点击获取