简介:YOLO石油泄露目标检测数据集面向计算机视觉学习者与工业安全巡检场景,包含1000张真实场景高质量图片,数据场景丰富,且由LabelImg标注,标注框质量高。文件按VOC(xml)、COCO(json)、YOLO(txt)三种标签格式分类存放于独立文件夹,可直接投喂YOLO系列深度学习模型训练,免去自行转换格式的繁琐步骤。压缩包内共2000个文件,以xml与txt标签文件为主,另含6个HTML环境搭建与训练教程、3个Python数据集划分脚本及1个yaml配置文件,整体约117.16MB,目录结构清晰便于按需取用。附赠Linux/Windows双系统YOLO环境搭建与训练教程,并提供训练集、验证集、测试集划分脚本,支持自定义比例一键拆分图片与标签。已有658人学习,适合需要在课程实践、毕业设计或油田巡检项目中快速落地目标检测方案的研究者与开发者。
1. 石油泄露目标检测数据集:1000张图、三种标签一次到位,先看清它能干什么
“石油泄露目标检测数据集”听起来只是1000张图片加标签,但实际拿到手你会发现,它同时给了VOC、COCO和YOLO三种格式的标注文件,还附带划分脚本和训练教程,这在目标检测数据集里算得上“全家桶”配置。目标检测项目里真正消耗时间的不是模型调参,而是数据整理:标签格式不统一、划分不合理、类别索引错位,每一样都能让你在训练刚开始就反复翻车。本文围绕这套石油泄露检测数据集,把三种格式的差异、划分脚本的用法、YOLO训练流程和常见坑位一次讲透。适合手里有自定义检测需求、需要在海洋溢油监测或石油管道巡检场景做污染目标定位的工程师,也适合想通过一套完整数据集把YOLO训练流程跑通的初学者。
2. VOC、COCO、YOLO三套标签:同一个泄漏框的三种写法与转换实操
2.1 XML、JSON、TXT:三套格式的字段差异与选型逻辑
做目标检测离不开标注格式,而VOC、COCO、YOLO是三类最常遇到的标签“方言”。VOC格式以XML文件存储,每个文件对应一张图片,文件里每个object节点描述一个目标,包含name、pose、truncated、difficult以及bndbox子节点,bndbox里放的是xmin、ymin、xmax、ymax四个绝对像素坐标,人眼直接可读。COCO格式把整批标注合并成一个JSON文件,外层是images、annotations、categories三个数组,annotations里每条记录一个实例,核心字段是image_id、category_id、bbox和segmentation,bbox采用绝对像素的[x, y, width, height]写法,注意它和VOC的xmax/ymax是两套坐标体系,互相转换时务必先换算宽高。YOLO格式则是每张图片对应一个同名TXT,每行描述一个目标,五个数字依次是类别ID、归一化中心x、归一化中心y、归一化宽、归一化高,所有坐标值都在0到1之间。
把三套格式放在一起对比,选型逻辑才清晰。VOC的XML适合在LabelImg里做增量标注,COCO的JSON适合交给pycocotools做评估和多任务训练,YOLO的TXT文件最小、零依赖,是当前YOLOv5、YOLOv8、YOLOv11训练框架的输入主流。实际项目里常见做法是统一用CVAT或LabelImg完成标注,再按框架需求导出对应格式。这套石油泄露数据集直接把三份标签都配齐,省掉的就是这层转换,也意味着你不管用哪个检测框架都不用重新改标签。下面是三套格式的核心差异,后面做转换时随时回看。
| 格式 | 文件形态 | 坐标体系 | 类别记录 | 主要消费方 |
|---|---|---|---|---|
| VOC | 每图一个XML | 绝对像素xmin/ymin/xmax/ymax | 类别名字符串 | LabelImg人工标注、数据查验 |
| COCO | 整个集合一个JSON | 绝对像素[x, y, w, h]或分割点 | 类别ID数字 | pycocotools评估、实例分割 |
| YOLO | 每图一个TXT | 归一化中心点及宽高 | 类别ID从0开始连续 | YOLO系列训练、TensorRT部署 |
石油泄露场景里有个现实问题,标签里的框往往不是规则矩形,因为泄露区域在海面上会随水流扩散,边缘呈弥散状。如果标注时把大片海水背景一起框进去,训练出来的模型在真实巡检画面里很容易把暗色水纹误判成泄露。所以拿到标签后建议先做的事情之一,就是抽几张图检查框的贴合度,而不是直接开训。
2.2 从VOC到YOLO:坐标换算与Python脚本实现
数据集里配好的VOC标签没必要改动,但如果你想用YOLO框架训练,就得有一份能跑的转换脚本。转换逻辑不复杂:把bndbox四个角点转成中心点与宽高,再除以原图宽高完成归一化。下面是核心逻辑,适配石油泄露这类单张图多个目标的情况。
import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, class_names, out_txt_path): tree = ET.parse(xml_path) root = tree.getroot() img_w = float(root.findtext('size/width')) img_h = float(root.findtext('size/height')) lines = [] for obj in root.findall('object'): name = obj.findtext('name') if name not in class_names: continue cls_id = class_names.index(name) xmin = float(obj.findtext('bndbox/xmin')) ymin = float(obj.findtext('bndbox/ymin')) xmax = float(obj.findtext('bndbox/xmax')) ymax = float(obj.findtext('bndbox/ymax')) cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_txt_path, 'w') as f: f.write("\n".join(lines))这段代码有两个关键点必须说明。第一个是class_names列表的顺序,必须和后续训练用的data.yaml里的names字段保持一致,否则类别ID对不上关系。石油泄露这类数据如果只有oil_spill一个类别,那顺序无所谓,但若标注里同时有油罐、管道、船只等目标,顺序一旦改动,所有TXT的类别索引全部偏移。第二个是img_w和img_h一定要取原图尺寸,而不是预处理缩放后的尺寸,代错了坐标就全错了。
石油泄露图片的特点是背景是海面或暗色土壤,目标是大片灰度差异不明显的色块,很多标注框宽高比例悬殊。YOLO格式里宽高都是归一化后的0到1小数,训练时不会因为框大小悬殊产生加载问题,但会影响anchor匹配。所以这类场景我一般建议后续训练时使用自动anchor重算,不要直接沿用默认值。
2.3 从COCO到YOLO:类别重映射是一个高频暗坑
COCO格式里categories的category_id是原始编号,可能跨度很大,而YOLO要求类别ID必须连续且从0开始。直接把category_id写进TXT,模型训练时读到的标签会错位到完全不同的语义。转换时必须先建立一张重映射表。
import json import os category_map = {1: 0, 2: 1, 3: 2} # 原始COCO id -> 连续YOLO id def coco_to_yolo(coco_json, out_dir): with open(coco_json, 'r') as f: data = json.load(f) image_info = {img['id']: img for img in data['images']} anns = {} for ann in data['annotations']: anns.setdefault(ann['image_id'], []).append(ann) for image_id, ann_list in anns.items(): img = image_info[image_id] w, h = img['width'], img['height'] txt_name = os.path.splitext(img['file_name'])[0] + '.txt' lines = [] for ann in ann_list: cat_id = ann['category_id'] if cat_id not in category_map: continue cls_id = category_map[cat_id] x, y, bw, bh = ann['bbox'] # COCO的bbox是x,y,w,h cx = (x + bw / 2) / w cy = (y + bh / 2) / h bw_norm = bw / w bh_norm = bh / h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw_norm:.6f} {bh_norm:.6f}") with open(os.path.join(out_dir, txt_name), 'w') as f: f.write('\n'.join(lines))这里的bbox是绝对像素的x、y、w、h,不是VOC那套xmin、ymin、xmax、ymax,所以中心点计算是x加上半宽,不是两端相加再除二。不少从VOC转过来的老手在这里翻过车,两个公式长得很像,语义完全不同。代码里category_map需要按实际分类逐一核对,先打印COCO JSON里的categories数组,人工确认后再固化成映射,不要再随手改。
转换完成后建议做一次抽检:随机挑出三到五张图,把TXT里的坐标反画回原图上,肉眼确认框与泄露区域吻合。用OpenCV几行就能实现,这一步是整个转换流程里成本最低、价值最高的质检环节,能拦住绝大部分低级错误。
注意:坐标反画时如果框出现在图外或大面积偏移,先回到原图和TXT逐行比对,不要急着怀疑训练框架。
3. 图片与标签的同步切分:1000张图的训练/验证/测试划分脚本拆解
3.1 不做样本泄漏:为什么划分要在转换和训练之前想清楚
石油泄露检测项目里我见过最多的翻车现场不是模型结构选错,而是train/val/test切分时样本泄漏。所谓样本泄漏,就是同一来源的图片同时出现在训练集和验证集,评估指标虚高,一到真实巡检现场立刻现形。石油泄露数据往往来自同一段无人机视频的连续抽帧或同一片海域的多角度拍摄,相邻两张图画面高度相似,如果划分时不做去重,val分数会失去参考意义。
划分脚本在一套完整数据集里之所以重要,就是因为它把“随机但不泄漏”这件事程序化。常见做法是先用文件名做shuffle,再按比例切分。但光shuffle不够,如果图片来自视频抽帧且文件名带帧号,一定要按视频源或时间段分组,而不是对单帧做完全随机抽样。把随机种子固定、把划分比例参数化,是防止每次重跑导致实验不可复现的最朴素手段。
3.2 划分脚本的输入输出与随机种子控制
这套数据集附带的划分脚本,核心逻辑通常是这样:扫描images目录下的所有图片,按比例切分到train、val、test三个集合,同时把同名的三种格式标签一起迁移到对应子目录。下面这段代码演示主流程,重点在于三个集合与同名标签的同步。
import os import random import shutil random.seed(42) # 固定随机种子,保证每次划分结果一致 train_ratio = 0.7 val_ratio = 0.2 test_ratio = 0.1 image_dir = "images" out_root = "dataset_split" files = [f for f in os.listdir(image_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png'))] random.shuffle(files) n = len(files) n_train = int(n * train_ratio) n_val = int(n * val_ratio) split_map = {} for i, f in enumerate(files): if i < n_train: split = 'train' elif i < n_train + n_val: split = 'val' else: split = 'test' split_map[f] = split参数说明:seed固定后每次划分结果一致,这比反复随机划分、实验不可复现要稳妥得多。train_ratio、val_ratio、test_ratio三个比例之和必须为1,具体数值看样本量调整,1000张图我一般用8:1:1或者7:2:1,如果样本来源单一,test可以只留50到100张,更多图喂给训练。
接下来是同步复制图片和标签文件:
for img_name, split in split_map.items(): base, ext = os.path.splitext(img_name) src_img = os.path.join(image_dir, img_name) dst_img = os.path.join(out_root, split, 'images', img_name) os.makedirs(os.path.dirname(dst_img), exist_ok=True) shutil.copy2(src_img, dst_img) for sub_dir in ['labels_voc', 'labels_coco', 'labels_yolo']: if sub_dir == 'labels_coco': continue # COCO是单JSON,单独复制,见下方说明 ext_map = {'labels_voc': '.xml', 'labels_yolo': '.txt'} src_label = os.path.join(sub_dir, base + ext_map[sub_dir]) if os.path.exists(src_label): dst_label = os.path.join(out_root, split, sub_dir, base + ext_map[sub_dir]) os.makedirs(os.path.dirname(dst_label), exist_ok=True) shutil.copy2(src_label, dst_label)这里有一个容易忽略的细节:VOC是xml后缀,YOLO是txt,COCO是单个JSON,三种格式不能放同一个目录里按同名扩散,否则txt互相覆盖。常见做法是维护labels_voc、labels_yolo两个顶层目录,COCO的JSON单独切出对应的image_id子集后重写一个val.json和train.json。不要把整个COCO JSON复制到每个划分目录里,那会造成验证集里混有训练图片的标注。
3.3 划分后的核对清单:五秒钟发现切分翻车
完成划分后别急着训练,先执行三条核对逻辑:对比三个集合图片数量之和与总数是否一致,随机抽五张训练图确认标签存在且非空,再确认val和train没有同名文件。用几行Python就能完成:
train_files = set(os.listdir('dataset_split/train/images')) val_files = set(os.listdir('dataset_split/val/images')) test_files = set(os.listdir('dataset_split/test/images')) assert len(train_files) + len(val_files) + len(test_files) == len(files) assert len(train_files & val_files) == 0 assert len(train_files & test_files) == 0这个断言写起来不到五条,但能把划分脚本常见的重复分配、漏分配一次拦完。石油泄露数据的文件名如果带帧序号,这一步还能看出来你抽的是帧还是视频源。如果发现某张图片没有对应标签,建议回到转换脚本检查,看VOC的XML里是不是空object节点。空目标图片在部分框架里会被自动当作背景样本,少量没问题,数量太多会把正样本比例冲淡,模型倾向输出大量背景预测。
4. 用YOLOv8训练石油泄露检测模型:目录结构、训练命令与必调超参
4.1 建立YOLO标准目录与data.yaml配置
拿到划分好的数据集后,下一步就是把数据和训练框架接在一起。我以YOLOv8为例,因为yolov8训练自己的数据集这个流程在工程里覆盖最广,代码仓库自带数据校验和评估逻辑。先看目录结构,这是最容易出问题也最容易被忽略的环节:
petrol_spill_yolo/ ├── train/ │ ├── images/ │ └── labels/ ├── val/ │ ├── images/ │ └── labels/ ├── test/ │ ├── images/ │ └── labels/ └── data.yamldata.yaml是训练入口,字段不多但一个都不能错:
path: /home/user/petrol_spill_yolo train: train/images val: val/images test: test/images nc: 1 names: ['oil_spill']path字段建议写绝对路径,换用户或换服务器时相对路径很容易踩坑。nc是类别数,这里按单一类别oil_spill举例,如果数据集里还有船只、管道等目标,要按实际类别数填写。names的顺序就是类别索引顺序,和之前转换脚本里class_names的顺序必须一一对应,顺序颠倒时模型不会报错,但预测结果的语义全部错位,这一点很多人在评估阶段才反应过来。
4.2 训练命令与超参选择
训练命令在ultralytics工具包里已经封装得很干净,真正费心的是参数。下面给一份适配石油泄露场景的启动命令:
yolo detect train \ model=yolov8n.pt \ data=petrol_spill_yolo/data.yaml \ epochs=100 \ batch=16 \ imgsz=1280 \ patience=15 \ lr0=0.001 \ seed=42参数说明:model=yolov8n.pt是官方预训练权重,网络会自动下载到本地,追求更高精度可换成yolov8s.pt或yolov8m.pt。batch=16在单卡3090或V100上比较稳妥,显存紧张时降到8,但尽量不要低于4,否则BatchNorm统计不稳,训练中期容易出现loss突然变为nan。imgsz=1280针对石油泄露目标小、灰度差异低的特点,是比较实用的输入尺寸,如果遥感原图分辨率极高,先确认显存能撑住,撑不住就回退到640配合关键帧裁剪。
训练命令里有一个容易被忽略但值得专门调的点是anchor。石油泄露在画面里往往只占很小一块,默认anchor对小目标召回偏低。可以直接让框架自动重算:
yolo detect train \ model=yolov8n.pt \ data=petrol_spill_yolo/data.yaml \ epochs=100 \ batch=16 \ imgsz=1280 \ auto_anchor=Trueauto_anchor会依据标签框的真实宽高分布重新计算先验框。如果验证集recall训练多轮后还上不去,优先怀疑anchor与目标尺度不匹配。这个参数调整成本很低,但效果往往比盲目增加epoch明显。
4.3 中断恢复与训练日志判断
训练到一半被任务抢占,或者想降低学习率续跑,是常有的事。YOLOv8支持从断点继续:
yolo detect train \ model=runs/detect/train/weights/last.pt \ data=petrol_spill_yolo/data.yaml \ epochs=200 \ resume=Trueresume=True会读取last.pt里的epoch计数、优化器状态和学习率调度状态。注意,如果此时改动过data.yaml里的nc或names,续跑会报维度不匹配,这是框架在保护你,避免新旧数据语义不一致时继续训练。
训练过程中看loss曲线不要只盯第一个epoch。如果train_loss持续下降而val_loss横盘甚至上升,说明模型过拟合了。1000张图的石油泄露样本不算多,100个epoch足够看到分化,过拟合时优先考虑增强数据增强、增大weight_decay或使用更大的模型配合合成数据,三个方向同时试比闷头加epoch有效得多。训练完成后,runs/detect/train目录下会有weights/best.pt和last.pt两个权重文件:best.pt对应验证集指标最好的轮次,last.pt对应最后一轮,实际使用和继续训练都以best.pt为准。
5. 石油泄露数据集训练排障:五个反复出现的高频问题记录
5.1 验证集mAP长期为0,训练loss却正常下降
现象:训练loss稳步下降,看起来一切正常,但每轮结束输出的验证集mAP始终是0,precision和recall也都接近0。抽检验证集图片时发现模型输出了大量框,但和标签完全不重叠。
原因:最常见的是转换脚本里类别索引错位,训练时读到的标签语义和验证集不一致。比如VOC里的类别名顺序和data.yaml的names不一致,或者COCO转YOLO时category_map映射错误,导致验证标签的类别ID在训练语义里指向别的类别,模型学到的是另一种分布。
解决:回到转换脚本,先打印任一标签文件的完整内容,再用反画框脚本把标签画到原图上人工核对。确认类别名和顺序完全匹配后再重新转换,替换标签文件后重新训练。不要直接在训练中临时打开数据增强看效果,那样只会把问题复杂化。
5.2 训练中期BN崩溃,loss突然变成nan
现象:前二十个epoch训练正常,loss从2.1下降到1.2附近,某个epoch开始loss突然出现nan,之后所有指标全部失效。
原因:这是典型的yolo训练中bn崩溃。当batch_size过小或某个批次内绝大多数标签框面积接近0时,BatchNorm层的方差统计趋于不稳定,训练过程发散。石油泄露数据如果做了比较激进的随机裁剪,容易产生空标签或极小的框,进一步诱发这个问题。
解决:先把batch_size调回16或更大,不要小于8。如果显存实在有限,采用梯度累积,保证参与归一化的样本量足够。同时检查数据增强配置,关掉针对小目标的过度裁剪,把空标签图片从训练集中过滤掉。必要时可以分阶段训练:前50个epoch冻结骨干网络,只训练检测头,等loss稳定后再解冻。
注意:bn崩溃后不必从头重训,用崩溃前的last.pt配合resume=True,同时调整batch_size和优化器参数,多数情况下可以接回来。
5.3 海上小目标漏检率特别高,recall上不去
现象:训练收敛后,对包含大面积海水背景的验证图片进行预测,模型只能检测出面积较大的泄露区域,小面积油膜或油带几乎全部漏检。
原因:输入分辨率不足和anchor尺度不适配是两大主因。石油泄露的形态往往是长条状油带或离散油膜,在imgsz=640的输入下只占几十个像素,小目标特征在下采样过程中被磨掉了。
解决:优先把imgsz调到1280或更高,配合图片切块训练。切块时保留overlap,避免泄漏区域恰好被切到两块的交界处。推理时适当降低置信度阈值,从默认0.25下调到0.1,再用NMS合并重叠框。对于真正的高分辨率遥感影像,建议采用切块推理后再拼接,而不是把整张大图直接缩放送进网络。
5.4 VOC转YOLO后推理框偏移到图像边缘
现象:训练过程中没有明显报错,但推理结果里所有检测框都偏向右下角,且宽高比例和真实目标不一致。
原因:坐标换算时把归一化除数弄错了。最常见的是在批量处理脚本里用了处理后的宽高替代原图宽高,或者把x、y坐标和VOC的xmin、ymin混用。另一个容易忽视的问题是标签文件路径和图片文件不同名,导致某个标签被重复读取或漏读。
解决:写一个反画验证脚本,把TXT的五个数字换算成绝对坐标,用OpenCV的rectangle画到原图上,逐张检查。这个脚本建议在整个数据集转换完成后固定保留,后续每次新增标注数据都跑一遍,比靠肉眼抽查稳妥得多。如果批量转换时用过的图片尺寸记录方式不统一,优先从json元数据或xml原图节点里重新读取宽高,不要沿用上一次的全局变量。
import cv2 def draw_yolo_label(image_path, txt_path, class_names): img = cv2.imread(image_path) h, w = img.shape[:2] with open(txt_path, 'r') as f: for line in f: parts = line.strip().split() if len(parts) < 5: continue cls_id, cx, cy, bw, bh = map(float, parts) xmin = int((cx - bw / 2) * w) ymin = int((cy - bh / 2) * h) xmax = int((cx + bw / 2) * w) ymax = int((cy + bh / 2) * h) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 0, 255), 2) cv2.putText(img, class_names[int(cls_id)], (xmin, max(ymin - 5, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow('check', img) cv2.waitKey(0)5.5 划分脚本重跑后,同一张图出现在训练集和验证集
现象:更新了一批标注数据后重新执行划分脚本,发现某些图片文件名同时出现在train和val目录里,训练指标虚高,真实场景表现差。
原因:划分时只做了基础shuffle,没有按视频源或拍摄场景去重。石油泄露数据如果来自无人机巡检视频,同一段飞行轨迹抽出的相邻帧画面高度相似,随机划分天然会把相似的帧分到两个集合。另一个原因是文件重名,不同子目录下的图片用简单文件名拼接后产生覆盖。
解决:划分前先给每张图片建立唯一标识,包含来源视频或拍摄批次信息,按标识分组后再划分。具体做法是把图片列表先按source_key聚合,每个组整体划入同一个集合。文件名规范化也值得做一次,全部改成类似spill_2025_shot001_0001.jpg这种带来源的命名,后续任何脚本都不会因为同名覆盖而翻车。
6. 训练完成后别急着跑:混淆矩阵读取、推理验证与部署导出技巧
训练结束后,runs目录里会生成confusion_matrix.png、results.csv等一系列评估文件。很多人只看一眼mAP就收工,其实混淆矩阵更值得细看。yolo混淆矩阵总合不唯一是正常现象,因为矩阵默认做了行归一化,每一行代表这个类别真实样本被预测到各个类别的比例,行和列加起来没有固定意义,别把行归一化的矩阵当多分类准确率来读。读矩阵时重点看主对角线数值和背景列的误检率,石油泄露模型常见问题是把暗色水纹预测成oil_spill,这时背景列的数值会很高,需要回去调整负样本或增加背景类图片。
推理验证环节,我会用一段小脚本对验证集里置信度低但真实存在泄漏的图片做统计,评估当前阈值是否合适。石油泄露漏检的代价比误检高,实际部署时把置信度阈值从0.25降到0.15,会明显提升recall,代价是少量误检,对于这类场景是划算的取舍。如果希望保持高precision,则上调阈值并配合后续的形态学过滤,把面积过小的检测框剔除。
部署导出也是容易被忽略的一步。模型训练完直接挂到服务器推理可以,但到了边缘端还是建议导出ONNX再转TensorRT或OpenVINO:
yolo export model=best.pt format=onnx imgsz=1280导出时imgsz要和训练时保持一致,否则检测框坐标会因letterbox换算不一致产生偏差。导出后先用onnxruntime跑一次单图推理,和PyTorch原始结果对比,确保数值一致性在可接受范围,再进TensorRT的精度校准流程。
我自己的习惯是每次训练结束后,把data.yaml、训练命令和混淆矩阵截图一起存档,标注清楚当时的类别顺序和imgsz。这套东西看似琐碎,但三个月后回看,能让你快速判断当时的模型为什么是那个效果,也方便给后续项目做起点。石油泄露检测这类环境监测场景,数据往往来自不同光照、不同水域,训练集分布和真实部署总有差距,保留好每一次实验的上下文,比临时翻代码要可靠得多。希望这些经验对你有帮助。
本文还有配套的精品资源,点击获取