ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

石油泄露目标检测实战:1000张遥感数据集与YOLO格式转换指南

石油泄露目标检测实战:1000张遥感数据集与YOLO格式转换指南 简介面向目标检测开发者和课程学习者聚焦石油泄露场景下的模型训练需求提供真实拍摄的高质量标注图片场景覆盖多样可直接用于YOLO系列算法训练与验证。压缩包共2000个文件约117.16MB核心为1000个xml标注与990个txt标签并同步整理VOC、COCO、YOLO三种格式标注文件免去手动转换格式的麻烦附带Python划分脚本、yaml配置及HTML说明页面便于按训练集、验证集、测试集批量拆分。资源同时提供YOLO环境搭建和训练教程区分Linux与Windows版本操作即使缺少基础也能照着完成数据准备到模型训练。整个包体结构清晰标注与脚本分层存放检索和使用都很方便。目前已有658人学习下载适合作为课程实训、算法对比和毕业设计的基础数据集。1. YOLO石油泄露目标检测这份 1000 张的遥感数据集为什么值得下做目标检测的人最不缺模型最缺的是「能直接喂进训练代码的干净数据」。石油泄露检测这个场景尤其典型海上平台巡检、输油管道无人机巡查、港口监控拍回来的图动辄几千乘几千油膜和阴影长得几乎一样人工标注一标注就头大标完还要折腾格式转换。你手里这份资源解决的就是这个链条1000 张石油泄露相关图片每张都给你配好 VOC、COCO、YOLO 三套标签再附划分脚本和训练教程。换句话说是把「标注→转格式→划数据集→开训」这条流水线给你焊死了。适合谁想用 YOLO 系列快速落地遥感小目标检测的工程师、做毕业设计需要带数据集撑腰的学生以及被格式转换折磨到想摔键盘的调参选手。2. 数据集解剖VOC、COCO、YOLO 三种标注格式到底差在哪2.1 一张图在三套格式里的三种「身份证」同一个泄露点VOC 记的是「这个框左上角右下角的像素坐标」COCO 记的是「这张图里有哪些目标、每个目标的框和类别编号」YOLO 记的是「归一化之后的目标中心点和宽高」。三套格式本质是同一份标注的三种编码方式但字段结构天差地别你下载下来之后如果直接拿 VOC 的 xml 喂给 YOLOv8训练入口直接报错。我把三种格式的关键字段拆开对比一下格式文件后缀坐标体系类别表示典型字段VOC.xml像素绝对坐标xmin/ymin/xmax/ymax 四个角点目标名字符串如 oil_leakannotation/object/bndboxCOCO.json像素绝对坐标x/y/width/height 左上角宽高数字类别 id靠 categories 表映射images/annotations/categoriesYOLO.txt归一化坐标x_center/y_center/width/height0~1 之间数字类别 id从 0 开始class_id x_center y_center width height三套格式里最容易翻车的就是 YOLO 的归一化坐标。VOC 的 (xmin, ymin, xmax, ymax) 是像素值直接除以图片宽高就能得到归一化坐标但这里有个隐藏坑——宽高必须用原图尺寸。如果数据里有 EXIF 旋转过的照片或者你预处理时把图 resize 了标签没跟着同步缩放训练时所有框全部偏移模型学到的全是错位特征烧掉一整天都查不出来。2.2 从 XML 到 JSON 再到 TXT转换脚本的核心逻辑这份资源直接给你三种格式省掉了转换步骤。但你要明白这套转换是怎么算出来的因为实际工作中你一定会遇到「只有 VOC 标签、要跑 YOLO」的项目。常见做法是把 XML 先读出 bndbox 四个值再除以图片的 width 和 height 得到归一化中心点import xml.etree.ElementTree as ET import cv2 def voc_to_yolo(xml_path, class_list): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) # 必须和实际图片宽一致 img_h int(root.find(size/height).text) boxes [] for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in class_list: continue # 跳过未定义类别的目标 cls_id class_list.index(cls_name) bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h boxes.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return boxes这段逻辑里最容易出问题的不是除法而是class_list的顺序。你的 class_list 是什么顺序YOLO 标签里的数字就是什么含义。如果训练时 dataset.yaml 里写的 categories 顺序和生成标签时的 class_list 不一致模型会把油膜学到管壁上去而且 loss 看起来还很正常属于最阴间的错法。COCO 转 YOLO 则多一步COCO 的 bbox 字段是[x, y, width, height]左上角坐标加宽高你要先算出右下角再求中心点。资源里提供的标签已经是转换好的但建议你抽查 10 张图用代码验证「YOLO txt 里的坐标反算回像素坐标后和目标在图片里的位置对不对得上」。2.3 标注质量体检先查漏标、错标、坐标越界再开工1000 张图的数据集拿过来第一件事不是开训是体检。我一般会写个小脚本一次性检查三类问题坐标越界归一化坐标小于 0 或大于 1、空标签文件图片存在但 txt 不存在或内容为空、类别 id 超出范围。这三类问题在别人打包的数据集里出现频率极高尤其是「空标签」——某几张图标注者漏标了txt 文件生成 0 字节YOLO 训练时那几张图直接跳过你的有效数据量悄悄缩水。import os from pathlib import Path label_dir Path(labels/train) img_dir Path(images/train) errors [] for txt_path in label_dir.glob(*.txt): img_path img_dir / (txt_path.stem .jpg) if not img_path.exists(): errors.append(fmissing image: {img_path}) with open(txt_path) as f: lines [line.strip() for line in f if line.strip()] if len(lines) 0: errors.append(fempty label: {txt_path}) for line in lines: parts line.split() if len(parts) ! 5: errors.append(fbad format: {txt_path}: {line}) try: vals [float(x) for x in parts[1:]] cls_id int(parts[0]) if not all(0 v 1 for v in vals): errors.append(fcoordinate out of range: {txt_path}: {line}) except ValueError: errors.append(fnon-numeric value: {txt_path}: {line}) print(\n.join(errors) if errors else all labels pass sanity check)这个脚本跑完能过滤掉百分之八十的「玄学报错」。尤其是坐标越界很多时候是人工标注时手滑把框拉出了图片边界打包工具没检查就写进了 txt。这种问题越早发现越好等训练跑到第 50 个 epoch 发现 loss 曲线抖动异常再回头查数据半天就没了。3. 划分脚本实操把数据集喂给 YOLOv8 训练入口的完整动作3.1 先定目录结构再谈划分train/val/test 怎么放很多人拿到数据集直接把 1000 张图全喂进去训练val 集和 test 集都不划这是新手最常犯的错。石油泄露检测这种场景泄露形态、光照条件、海面背景差异很大如果测试集里全是训练集见过的图mAP 虚高得能骗过所有人部署到真实巡检场景直接翻车。标准的目录结构是这样dataset/ ├── images/ │ ├── train/ # 约 700 张 │ ├── val/ # 约 200 张 │ └── test/ # 约 100 张 ├── labels/ │ ├── train/ # 与 images/train 同名同数量的 txt │ ├── val/ │ └── test/ └── dataset.yaml这里有个规矩图片和标签文件同名同前缀图片在 images/train 里标签在 labels/train 里YOLOv8 会自动根据路径找对应标签。文件名不能乱改改一个就要同步改另一个。划分时保证 train/val/test 三部分图片没有重叠这是底线。3.2 划分脚本逐行拆随机种子与按文件夹划分的两种玩法资源自带的划分脚本一般会做两件事把 1000 张图按比例分成三份再把对应标签复制过去。但有几个参数你必须自己确认划分比例、随机种子、是否做了图片去重。我先给你看一个带随机种子的标准划分脚本import os import random from pathlib import Path import shutil random.seed(42) # 固定随机种子保证每次划分结果一致 src_img_dir Path(all_images) src_label_dir Path(all_labels) train_ratio, val_ratio 0.7, 0.2 # test 占 0.1 all_images list(src_img_dir.glob(*.jpg)) random.shuffle(all_images) n_train int(len(all_images) * train_ratio) n_val int(len(all_images) * val_ratio) splits { train: all_images[:n_train], val: all_images[n_train:n_train n_val], test: all_images[n_train n_val:], } for split_name, img_list in splits.items(): img_out Path(fdataset/images/{split_name}) label_out Path(fdataset/labels/{split_name}) img_out.mkdir(parentsTrue, exist_okTrue) label_out.mkdir(parentsTrue, exist_okTrue) for img_path in img_list: label_path src_label_dir / (img_path.stem .txt) # 用 shutil.copy2 而不是 move原始数据保留作备份 shutil.copy2(img_path, img_out / img_path.name) if label_path.exists(): shutil.copy2(label_path, label_out / label_path.name) else: print(fWARNING: missing label for {img_path.name})random.seed(42)这一行是关键参数。不固定随机种子每次运行划分结果都不一样你跑了 100 个 epoch 后想复现实验怎么都对不上。shutil.copy2我刻意用了复制而不是移动因为原始数据是「后悔药」删错了找不回来就麻烦了。要注意「按文件复制」而不是「按目录移动」这个细节。有的划分脚本偷懒用os.rename原目录直接空了下次想调比例还得重新下载。有经验的人拿到脚本第一件事就是看它用的是 copy 还是 move。3.3 生成 YOLOv8 的 dataset.yaml路径和类别名一个都不能错数据划分完YOLOv8 训练还需要一个 yaml 文件告诉模型去哪找数据、有多少类别。标准写法如下# dataset.yaml path: /absolute/path/to/dataset # 改成你的实际路径 train: images/train val: images/val test: images/test names: 0: oil_leakpath字段最好写绝对路径因为 YOLOv8 的 train/val 字段是相对 path 的。names字典里数字必须从 0 开始连续石油泄露一般单类0: oil_leak就够了如果数据里还有 pump、pipe 等其他类别依次往下排。这里有个容易忽略的细节names 里的顺序必须和标签 txt 里的 class_id 一致如果转换标签时 class_list 是[oil_leak, pipe]yaml 里就不能写成0: pipe, 1: oil_leak否则类别全错位。检查 yaml 配没配对有个笨但有效的方法用 YOLOv8 自带的可视化脚本把标签画到图上人工看一眼框对不对。我这里给你一段代码能快速把 YOLO 标签绘制回图片import cv2 import numpy as np def draw_yolo_labels(img_path, label_path, class_names): img cv2.imread(img_path) h, w img.shape[:2] with open(label_path) as f: for line in f: parts line.strip().split() cls_id, xc, yc, bw, bh map(float, parts) 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, 0, 255), 2) cv2.putText(img, class_names[int(cls_id)], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imshow(check, img) cv2.waitKey(0) # 用法示例class_names [oil_leak] draw_yolo_labels(dataset/images/train/0001.jpg, dataset/labels/train/0001.txt, [oil_leak])画出来的效果框应该是紧贴目标边缘的矩形没有偏移、没有过大过小。如果出现框整体往右下偏基本可以断定是坐标归一化时的尺寸用错了。4. 训练教程复现从预训练权重到跑通第一个 epoch 的关键参数4.1 预训练模型怎么选yolov8n.pt 还是从零训练yolo 预训练模型下载是整个训练流程里第一个需要做选择的点。石油泄露数据集只有 1000 张不算大完全从零训练很容易过拟合因为背景中海洋、天空、船体的特征太丰富了纯随机初始化权重很难在 100 个 epoch 内学到稳定的语义。常见做法是加载 COCO 上预训练好的权重做迁移学习把特征提取层拿来复用只微调检测头。选择建议显存够就上yolov8s.pt一般单卡 8G 显存都能带得动显存紧张或想快速验证流程用yolov8n.pt。m 和 l 在这类小目标遥感场景里提升有上限但训练时间成倍增加性价比不高。我一般先用 n 跑通流程确认数据没问题再换 s 正式训练。石油泄露目标通常面积小大模型反而容易过拟合到背景纹理上。4.2 训练命令与超参数batch、epoch、imgsz 这样设数据集划分好、yaml 配好、预训练权重拿到手之后训练命令长这样yolo detect train \ datadataset.yaml \ modelyolov8s.pt \ epochs150 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ mosaic1.0 \ patience30 \ projectruns/oil_leak \ nameexp1 \ device0逐个说参数含义。imgsz640是训练分辨率YOLOv8 会把所有训练图缩放到 640×640。石油泄露目标本身偏小640 是起步如果你显卡有余量可以上 1280小目标召回率会明显提升。mosaic1.0是马赛克增强把四张图拼成一张训练对小目标检测有正负两面影响——增强多样性是好的但如果目标太小拼接时被切割掉就学不到了。有人会为了小目标场景把 mosaic 直接关掉mosaic0.0我建议你先用默认 1.0 跑一轮看曲线如果召回率上不去再关。patience30是早停参数连续 30 个 epoch val 指标不涨就自动停防止炼丹炼到地老天荒。这里要说一下——YOLOv8 的初始学习率策略和 YOLOv5 不太一样lr00.01是它的默认值配合 warmup 前 3 个 epoch 线性预热。如果你发现 loss 到第 10 个 epoch 还在剧烈振荡把lr0降到 0.001 重新试多半能稳下来。4.3 训练日志里看什么loss 曲线、P/R/mAP50 的读法训练开始后YOLOv8 会在runs/oil_leak/exp1/目录下输出results.csv和results.png。你要盯三个指标box_loss框回归损失、cls_loss分类损失、mAP50。石油泄露检测里最重要的不是 mAP50 的绝对值而是它的增长曲线是否还在爬升。训练过程有一个特别容易被忽视的细节——验证集的 mAP 是每 10 个 epoch 跑一次还是每个 epoch 都跑这直接影响早停判断。YOLOv8 默认每个 epoch 都跑 val所以 results.csv 里每行都有 val 指标。如果 val mAP 到了第 80 个 epoch 还在缓慢上升但 train loss 已经收敛说明模型还在学不要急着停如果 val mAP 长时间不动甚至下降而 train loss 还在降那就是过拟合了这时候该做的不是调参是去查 train 和 val 的分布差异——是不是划分脚本没去重导致验证集里混了训练集图片。我训练时习惯把训练过程截图留存尤其是第一个 epoch 结束后的 loss 值。如果第一轮 loss 数值巨大比如 5 以上大概率是标签出了问题先停下去跑第 2.3 节的体检脚本别浪费时间。5. 石油泄漏目标检测避坑五个血泪踩坑记录5.1 现象训练 Loss 收敛但 mAP 一直上不去训练完看曲线box_loss 和 cls_loss 都降到了 0.02 以下看起来完美但 val mAP50 只有 0.3 出头怎么调参都纹丝不动。原因排查后发现是类别标注不一致——部分图里的目标被标成 oil_leak另一部分标成了 oil_spill两个名字在 class_list 里各占一个 id同一个语义被拆成了两个类模型被逼着去区分两个完全相同的目标概率全被稀释了。解决方法是把所有标签的类别名统一或者在你自己的代码里做一个别名映射把 oil_spill 归一化到 oil_leak。5.2 现象验证集里检测框偏大油膜目标整体漏检这属于典型的小目标漏检。现象是模型把整片海面框出来框中心落在油膜边缘IoU 极低。原因大概率是imgsz640下小目标被缩得太小油膜在 640×640 的图里可能只有 20×15 像素特征几乎全部丢失。解决思路有两个一是把imgsz提高到 1280小目标像素面积翻四倍二是做Tiling切图策略把大图切成 640×640 的 patch 分别推理。我在这类场景下的实践是imgsz 提升对 mAP 的提升最直接但显存占用和训练时间同步上涨两者要权衡。5.3 现象LabelImg 打开 XML 正常YOLO 训练却报 no labels现象很直白数据检查时 XML 在 LabelImg 里显示一切正常但一训练YOLOv8 每轮都打印 found no labels in dataset/images/train/*.jpg。原因是 XML 里标注的类别名在 class_list 里不存在转换脚本遇到未定义类别直接 continue 跳过生成的 txt 是空文件。另一个常见原因是文件名编码问题——xml 里引用的图片名和实际文件名大小写不一致Linux 环境下大小写敏感直接找不到图。解决转换脚本里加一个统计逻辑跳过多少个目标就打印几个 warning不要静默跳过。5.4 现象COCO 格式 json 用 pycocotools 死活不通过用 COCO 格式做验证评估时pycocotools.coco.COCO加载 json 报错提示 categories 格式不正确。这是 COCO 格式最常见的一个坑categories 里的id必须从 1 开始而 YOLO 的类别 id 从 0 开始转换时很多人直接复制 YOLO 的 id 过去导致第一个类别 id0COCO 官方 API 直接不认。解决转换时给 COCO categories 的 id 全部加 1同时在 annotations 里的category_id也要同步加 1然后单独记住这个映射关系评估输出时再减回来。5.5 现象测试视频里检测框抖动严重同一目标时而检出时而丢失这个坑和训练参数无关纯粹是推理侧的问题。石油泄露在视频里目标小模型输出置信度在阈值附近反复横跳——conf_thres设成 0.25这一帧 0.26 检出了下一帧 0.24 又丢了。解决方法是调低置信度阈值到 0.15 左右再用跟踪算法对短时消失的目标做插值。另外检查iou_thres的 NMS 阈值0.5 是个合理的起始值拉到 0.7 容易让密集目标互相抑制拉到 0.3 会出现大量重复框。6. 进阶验证用滑窗推理 混淆矩阵给模型做最后一道体检6.1 滑窗推理大图上的小目标怎么检测才不漏石油泄露检测的实拍图动辄几千乘几千像素直接缩放到 640 推理小目标会直接消失。最实用的方案是滑窗推理把原图切成有重叠的 patch每个 patch 独立推理再把结果映射回原图坐标。我建议窗口 640×640、重叠率 20%目标越大重叠率可以越低目标越小重叠率要越高否则会有大量目标被窗口边界切碎。import cv2 from ultralytics import YOLO model YOLO(runs/oil_leak/exp1/weights/best.pt) def sliding_window_inference(img_path, window_size640, stride512): img cv2.imread(img_path) h, w img.shape[:2] detections [] for y in range(0, h, stride): for x in range(0, w, stride): x2 min(x window_size, w) y2 min(y window_size, h) patch img[y:y2, x:x2] results model(patch, conf0.15, imgsz640, verboseFalse) for r in results[0].boxes: x1, y1, x2_p, y2_p map(int, r.xyxy[0].tolist()) conf float(r.conf[0]) cls_id int(r.cls[0]) detections.append((x1 x, y1 y, x2_p x, y2_p y, conf, cls_id)) return detections注意边界 patchx2和y2用min()截断但 stride 不能和 window_size 相等否则相邻 patch 没有重叠被窗口边线切到的目标会丢。另外滑窗推理的耗时是成倍增长的推理前先把 conf 阈值调低0.15~0.2最后再统一做一次 NMS 合并重叠框。6.2 混淆矩阵和 mAP 的验证习惯模型训练完我习惯做三件事第一是看 confuse_matrix.png——石油泄露单类场景下矩阵只有两行两列重点关注漏检率真实目标的右下角格子的数值这一项如果超过 20%部署就要小心了第二是拿 20~30 张训练集之外的实拍图做人工验证不追求精确计算 mAP但要肉眼确认每个框的位置和置信度是否靠谱第三是测试不同 conf 阈值下的输出找一个「漏检尽量少、误检能接受」的临界值。这套验证流程走下来基本能把模型的底摸清楚。想起之前那次石油泄漏项目我直接把训练集里的图拿去测试mAP 拉到 0.85 沾沾自喜换到真实无人机巡检视频直接掉到 0.4后来才意识到测试图里有大量训练集见过的样本验证集划分时 2.3 节的去重没做干净。从那以后我每次拿到数据集第一件事就是强制走一遍标签体检和划分去重绝不因为「这图看起来不一样」就跳过。希望这份数据集的避坑经验能帮你少烧几个本来就不够用的夜晚。本文还有配套的精品资源点击获取
返回列表