简介这份资源是面向计算机视觉与目标检测学习者的海面石油原油泄漏检测数据集适用于环境监测、海上应急响应等场景下的模型训练与算法验证。数据集共1817张jpg图片每张均配有对应的VOC格式xml标注与YOLO格式txt标注标注类别为oil_spillage共3871个标注框说明部分图片中存在多个泄漏点可支撑多目标检测任务。压缩包内文件总数2000个以1817个xml标注文件和183个txt文件为主整体约79.05MB采用labelImg完成标注格式规范、类别单一便于直接接入YOLO或Faster R-CNN等主流检测框架。目前已有283人学习下载适合需要快速构建泄漏检测基线模型、验证数据增强策略或开展迁移学习实验的读者参考使用。1. 海面溢油检测数据集1800 张 VOCYOLO 到底能训出什么海面石油原油泄漏的遥感与近岸监控里目标检测要解决的核心问题很具体把水面上那层反光的油膜从太阳耀斑、云影、船舶尾迹和生物藻华里分出来。这个标题给的是一个 1800 张规模、同时提供 VOC 与 YOLO 两种标注格式的数据集直接对准了目标检测训练链路里最耗时的环节——数据准备。它适合三类人想快速验证溢油检测模型可行性的算法工程师、需要做海面异常目标检测但缺标注数据的在校研究者、以及要把检测能力嵌进岸基或无人机巡检系统的落地开发者。1800 张不算大但双格式意味着你不用自己写转换脚本就能分别喂给 YOLO 系列和基于 VOC 的框架省下的时间足够你多跑几轮消融实验。2. 溢油检测为什么难从油膜光学特性到标注边界2.1 油膜在图像里到底长什么样原油泄漏到海面后会经历扩散、乳化、风化几个阶段不同阶段在可见光和红外图像上的表现完全不同。新鲜油膜在可见光下呈现高反射的银灰色或彩虹色干涉条纹和太阳耀斑的镜面反射高度相似乳化后的油水混合物偏棕褐色对比度低容易和悬浮泥沙混淆薄油膜则几乎透明只在特定太阳高度角下才显现。这意味着标注时不能只靠颜色还要结合纹理和上下文——油膜通常有连续的边界和内部纹理渐变而云影边界更锐利、内部更均匀。从检测模型的角度看溢油目标有几个麻烦特性尺度变化极大从几米宽的船用燃油泄漏到几公里长的井喷油带都可能出现形状不规则没有固定长宽比正负样本极度不平衡一张图里可能只有一小块油膜其余全是海水。这些特性决定了数据增强策略和损失函数的选择不能照搬通用目标检测的默认配置。2.2 VOC 与 YOLO 格式的差异不只是文件结构VOC 格式用 XML 描述每张图的标注包含 filename、size、object 列表每个 object 有 name、bndboxxmin/ymin/xmax/ymax。YOLO 格式用每张图对应的 txt 文件每行是 class_id x_center y_center width height全部归一化到 0-1。表面看只是坐标表示不同实际影响的是训练管线的几个环节。VOC 的绝对坐标在数据增强时更直观比如你要做随机裁剪后重新计算框位置绝对坐标加减就行YOLO 的归一化坐标在缩放和填充时更省事因为不用管原图尺寸。另一个差异是类别管理VOC 在 XML 里写类别名YOLO 用数字 id需要额外维护一个 classes.txt 或 data.yaml。如果你打算用 YOLOv8 训练官方推荐 YOLO 格式如果要用 Detectron2 或 MMDetectionVOC 转 COCO 更顺。这个数据集同时给两种格式等于把选择权留给了你。2.3 1800 张的规模怎么用才不浪费1800 张在目标检测里属于小数据集直接从头训一个 backbone 几乎必然过拟合。合理的做法是分两步先用 COCO 或 Objects365 预训练权重初始化再在溢油数据上微调。微调时冻结 backbone 的前几层只训 neck 和 head学习率设小一点比如 1e-4 到 1e-3 之间。如果溢油目标和预训练类别差异大可以解冻更多层但要注意监控验证集损失一旦连续几个 epoch 不降就停。另一个策略是把它当验证集用。如果你有更大的海面图像库但没有标注可以用这 1800 张训一个基线模型然后对无标注图像做伪标签人工筛选后再加入训练。这个过程迭代两三轮有效数据量能翻倍。但伪标签的阈值要设高比如置信度 0.7 以上才保留否则错误会累积。3. 把 VOC 转成 YOLO脚本、参数与四个边界坑3.1 转换脚本的核心逻辑虽然数据集号称双格式但实际拿到的 VOC 标注可能需要你自己转成 YOLO 来适配特定训练框架。下面这个脚本处理单目录下的所有 XML输出对应的 txt。关键点是坐标归一化和类别映射。import xml.etree.ElementTree as ET import os from pathlib import Path # 类别映射根据你的数据集实际类别修改 CLASS_MAP {oil_spill: 0, ship: 1, look_alike: 2} def voc_to_yolo(xml_dir, out_dir, img_dir): xml_dir Path(xml_dir) out_dir Path(out_dir) out_dir.mkdir(parentsTrue, exist_okTrue) for xml_file in xml_dir.glob(*.xml): tree ET.parse(xml_file) root tree.getroot() # 读取图像尺寸优先从 XML 的 size 节点取 size root.find(size) if size is not None: w int(size.find(width).text) h int(size.find(height).text) else: # 兜底用 PIL 读原图尺寸 from PIL import Image img_path Path(img_dir) / (xml_file.stem .jpg) with Image.open(img_path) as im: w, h im.size lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASS_MAP: continue # 跳过未定义类别 cls_id CLASS_MAP[name] 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, w)) ymin max(0, min(ymin, h)) xmax max(0, min(xmax, w)) ymax max(0, min(ymax, h)) # 跳过无效框 if xmax xmin or ymax ymin: continue # 归一化并计算中心点 x_center (xmin xmax) / 2.0 / w y_center (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}) out_file out_dir / (xml_file.stem .txt) out_file.write_text(\n.join(lines), encodingutf-8) # 调用示例 voc_to_yolo(annotations_xml, labels_yolo, images)逻辑说明先解析 XML 拿到图像宽高优先用 XML 里的 size 节点因为有些数据集的原图可能被裁剪过而 XML 没更新这时用 PIL 读实际文件更可靠。然后遍历每个 object做类别映射把绝对坐标转成归一化的中心点加宽高。裁剪到边界内是必须的我见过不少 VOC 标注的 xmax 超出图像宽度不处理的话 YOLO 训练时归一化坐标会大于 1导致损失计算异常。参数说明CLASS_MAP 要根据你的数据集实际类别改如果只有油膜一个类就写 {oil_spill: 0}。输出精度用 6 位小数足够YOLO 官方也是这个精度。如果 XML 里有些 object 的 name 不在映射表里脚本会跳过建议先统计一下所有出现的类别名再决定是跳过还是合并。3.2 转换后必须做的三项校验转换完不能直接开训至少做三项检查。第一统计每张图的框数量如果某张图 txt 为空但 XML 里有 object说明类别映射漏了。第二随机抽 10 张图用可视化脚本画框确认框位置和原图对齐。第三检查归一化坐标是否都在 0-1 之间有超出就说明边界裁剪没生效。import cv2 import numpy as np from pathlib import Path def visualize_yolo(img_path, label_path, class_names): img cv2.imread(str(img_path)) h, w img.shape[:2] lines Path(label_path).read_text().strip().split(\n) for line in lines: if not line: continue parts line.split() cls_id int(parts[0]) xc, yc, bw, bh map(float, parts[1:5]) # 反归一化 x1 int((xc - bw/2) * w) y1 int((yc - bh/2) * h) x2 int((xc bw/2) * w) y2 int((yc bh/2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[cls_id], (x1, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(check_vis.jpg, img) visualize_yolo(images/001.jpg, labels_yolo/001.txt, [oil_spill, ship, look_alike])这个可视化脚本把归一化坐标还原成像素坐标画框如果框明显偏移或尺寸不对回去检查转换时的宽高是否用错——常见错误是把 XML 里的 width 和 height 搞反导致框旋转 90 度。3.3 四个容易翻车的边界情况第一个坑XML 里 size 节点的 width 和 height 与实际图像不一致。有些标注工具在图像被重新保存后没更新 XML导致归一化基准错误。解决办法是始终用 PIL 读实际图像尺寸或者转换前批量校验一遍。第二个坑类别名有空格或大小写不一致。比如 oil spill 和 oil_spill 被当成两个类映射表里只写了一个另一个全被跳过。转换前先跑一遍grep -h name *.xml | sort | uniq -c统计所有类别名。第三个坑图像文件和 XML 文件名不匹配。VOC 标准里 XML 的 filename 节点应该和实际文件名一致但实际数据里经常有 .jpg 和 .JPG 混用或者 XML 叫 001.xml 而图像叫 001_aug.jpg。脚本里用 stem 匹配只能处理同名情况更稳妥的做法是解析 XML 的 filename 节点来定位图像。第四个坑空标注文件。有些图确实没有目标XML 里 object 列表为空转出来的 txt 也是空文件。YOLO 训练时空 txt 是合法的负样本但如果你用某些数据加载库空文件可能被当成损坏数据跳过。确认你的训练框架支持空标签不支持的话要么删掉这些图要么在 txt 里写一个占位行再在数据集配置里忽略。4. 用 YOLOv8 跑通第一个溢油检测基线4.1 数据集目录结构与 data.yamlYOLOv8 对目录结构有固定要求images 和 labels 分开各自下面再分 train、val、test。data.yaml 里写路径和类别名。假设你把 1800 张按 7:2:1 划分目录长这样oil_spill_dataset/ ├── images/ │ ├── train/ (1260 张) │ ├── val/ (360 张) │ └── test/ (180 张) ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamldata.yaml 内容path: /absolute/path/to/oil_spill_dataset train: images/train val: images/val test: images/test names: 0: oil_spill 1: ship 2: look_alike注意 path 要写绝对路径YOLOv8 对相对路径的解析有时会出问题。names 的 id 必须和 txt 里的 class_id 对应顺序错了模型学到的类别就全乱了。4.2 训练命令与关键参数yolo detect train \ data/absolute/path/to/oil_spill_dataset/data.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.001 \ lrf0.01 \ patience20 \ augmentTrue \ mosaic1.0 \ mixup0.1 \ degrees10.0 \ translate0.1 \ scale0.5 \ fliplr0.5 \ projectruns/oil_spill \ namebaseline_v8s参数说明model 选 yolov8s 而不是 n 或 m是因为 1800 张数据量下 s 的容量和速度比较平衡n 容易欠拟合m 容易过拟合。epochs 设 100 配合 patience 20验证损失 20 轮不降就早停。lr0 用 0.001 是微调的常见起点如果你解冻了 backbone 可以降到 1e-4。imgsz 640 是默认值如果溢油目标普遍很小可以提到 1280但显存占用会翻倍。mosaic 和 mixup 是强增强对小数据集有帮助但 mixup 设 0.1 就够了太高会让油膜边界变模糊。degrees 10 和 translate 0.1 针对海面图像做适度旋转平移fliplr 0.5 水平翻转对海面场景安全因为油膜没有固定方向。4.3 训练过程看什么指标启动训练后第一轮看 loss 是否正常下降。YOLOv8 会输出 box_loss、cls_loss、dfl_loss前几轮波动大是正常的10 轮后应该看到明显下降趋势。如果 box_loss 一直不降检查标签格式如果 cls_loss 不降检查类别映射。验证集指标重点看 mAP50 和 mAP50-95。溢油检测里 mAP50 到 0.7 以上算可用0.8 以上算不错。如果 mAP50 高但 mAP50-95 低说明框的位置不够准可以调 dfl_loss 的权重或增加定位相关的增强。混淆矩阵在 runs 目录下会生成重点看 oil_spill 和 look_alike 之间有没有大量误判有的话说明这两类在视觉上太接近需要重新定义标注规则或增加区分性特征。4.4 推理与结果导出yolo detect predict \ modelruns/oil_spill/baseline_v8s/weights/best.pt \ sourcetest_images/ \ conf0.25 \ iou0.45 \ saveTrue \ save_txtTrue \ projectruns/oil_spill \ nameinferenceconf 0.25 是召回优先的阈值溢油检测里漏检比误检代价高所以阈值设低一点。iou 0.45 用于 NMS如果油膜区域重叠多可以调到 0.5。save_txt 会输出每张图的检测结果格式和 YOLO 标签一样方便后续做定量分析。5. 避坑与排查训练不收敛、mAP 虚高、显存爆炸5.1 损失不降反升现象训练前 10 轮 box_loss 从 2.0 涨到 3.0 以上mAP 一直是 0。原因通常是学习率太大或标签格式错误。先检查 txt 里的坐标是否在 0-1 之间有超出就是转换时没裁剪。如果坐标正常把 lr0 降到 1e-4 再试。还有一种可能是 data.yaml 里的 path 写错模型实际加载的是空数据集这时 loss 会随机波动。5.2 mAP 很高但实际检测全是误报现象验证集 mAP50 到 0.9但拿真实海面图推理时满屏框。原因是验证集和训练集分布太接近模型过拟合了。检查划分时是否按图像来源分层如果训练集和验证集来自同一批连续拍摄的图像它们几乎一样。解决办法是按时间或区域划分比如用前 70% 时间的图训练后 30% 验证。另外看混淆矩阵如果 look_alike 类被大量预测为 oil_spill说明标注边界模糊需要重新定义这两类的区分标准。5.3 显存爆炸现象batch 16 时 CUDA out of memory。YOLOv8s 在 640 分辨率下 batch 16 大约需要 8-10GB 显存如果卡只有 8GB降到 batch 8 并开 gradient accumulation 补回来。或者用 yolov8n显存减半但精度会降几个点。另一个隐藏原因是 imgsz 设太大1280 分辨率下显存占用是 640 的四倍左右先确认你的 imgsz 是不是被无意中改大了。5.4 空标签导致的训练中断现象训练到某个 epoch 突然报错提示某张图的标签文件为空或格式错误。YOLOv8 对空 txt 是支持的但如果 txt 里只有换行符或空格解析会失败。批量检查所有 txt把只有空白字符的文件清空或删除。另外如果某张图的所有框都被边界裁剪掉了也会产生空 txt这种图作为负样本是合理的但确认你的数据加载器版本支持。5.5 类别不平衡导致的漏检现象油膜类 mAP 正常但 ship 类几乎检不出来。原因是 ship 样本太少1800 张里可能只有几十个。解决办法是在 data.yaml 里给 ship 类加权或者用 copy-paste 增强把 ship 实例复制到更多图里。YOLOv8 本身没有直接的类别权重参数但可以通过重复采样含 ship 的图像来间接平衡。6. 把 1800 张用出 5000 张的效果离线增强与伪标签迭代小数据集的瓶颈不在模型而在数据多样性。我一般会做两轮离线增强加一轮伪标签把有效训练量撑到 5000 张以上。第一轮离线增强用 albumentations 做几何和色彩变换重点模拟不同海况雾天降低对比度、逆光增加眩光、波浪纹理叠加。代码不复杂关键是增强后的标签要同步变换。import albumentations as A import cv2 from pathlib import Path transform A.Compose([ A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(brightness_limit0.3, contrast_limit0.3, p0.7), A.HueSaturationValue(hue_shift_limit10, sat_shift_limit20, val_shift_limit20, p0.5), A.GaussNoise(var_limit(10, 50), p0.3), A.MotionBlur(blur_limit5, p0.2), A.RandomFog(fog_coef_lower0.1, fog_coef_upper0.3, p0.2), A.ShiftScaleRotate(shift_limit0.1, scale_limit0.2, rotate_limit15, p0.5), ], bbox_paramsA.BboxParams(formatyolo, label_fields[class_labels])) def augment_image(img_path, label_path, out_img_dir, out_lbl_dir, n3): img cv2.imread(str(img_path)) h, w img.shape[:2] lines Path(label_path).read_text().strip().split(\n) bboxes, labels [], [] for line in lines: if not line: continue parts line.split() labels.append(int(parts[0])) bboxes.append([float(x) for x in parts[1:5]]) for i in range(n): augmented transform(imageimg, bboxesbboxes, class_labelslabels) out_name f{Path(img_path).stem}_aug{i} cv2.imwrite(str(Path(out_img_dir) / f{out_name}.jpg), augmented[image]) with open(Path(out_lbl_dir) / f{out_name}.txt, w) as f: for bbox, lbl in zip(augmented[bboxes], augmented[class_labels]): f.write(f{lbl} { .join(f{v:.6f} for v in bbox)}\n)这个脚本对每张原图生成 3 张增强图1800 张变 7200 张。注意 RandomFog 的系数别设太高0.3 以上油膜就完全看不见了反而引入噪声。MotionBlur 模拟无人机或岸基摄像头的抖动对海面场景很实用。第二轮用训好的基线模型对无标注海面图像做推理置信度 0.7 以上的框作为伪标签人工过一遍筛掉明显错误的再加入训练集。这个过程做两轮模型对真实场景的泛化会明显提升。我自己的习惯是每轮伪标签后重新训一个模型而不是在旧模型上继续训这样能避免错误累积。最后一轮用全部数据训一个最终模型在独立测试集上验证如果 mAP50 比基线提升不到 3 个点说明数据多样性已经饱和该去补真实场景的图而不是继续增强。希望帮到你。本文还有配套的精品资源点击获取