ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

YOLO遥感油罐检测:从VOC/COCO格式转换到训练部署全流程实战

YOLO遥感油罐检测:从VOC/COCO格式转换到训练部署全流程实战 简介YOLO遥感油罐检测数据集面向遥感目标检测学习者和项目开发者提供1000张真实场景高质量油罐图片及配套标注解决油罐目标样本难获取、标签格式需反复转换的痛点。资源共2000个文件压缩包约224.22MB其中包含1000个xml、990个txt、6个HTML教程、3个Python脚本及1个YAML配置等标注由LabelImg完成质量较高并提供VOC、COCO、YOLO三种格式标签可直接用于YOLO系列模型训练。除数据外附赠YOLO环境搭建含Linux/Windows、训练教程文档和数据集划分脚本便于按需求切分训练集、验证集与测试集也适合教学演示与课程设计。目前已有247人学习下载适合希望快速构建遥感检测项目的初中级开发者。1. 拿到遥感油罐检测数据集第一件事不是训练而是校验标签做目标检测的同行应该都有过这种经历从网上下载一个带标签的压缩包解压出来发现标注格式五花八门有的给XML有的给JSON有的只给TXT光整理数据就耗掉大半天。标题里这个“YOLO遥感油罐检测数据集”打包了1000张图片同时给出VOC、COCO和YOLO三种格式标签外加划分脚本和训练教程本质上就是为了省掉你统一格式的那一步。遥感油罐目标在俯拍视角下特征明显、背景相对干净但难点在于小目标多、排列密集、单体尺度差异大所以这个数据集非常适合用来入门遥感目标检测的完整流程——从数据校验、格式转换、划分训练集到跑通YOLO训练并在验证集上确认模型真的学到了东西。这个资源包对三类人最有用第一次做检测但不想在数据清洗上耗时间的新手需要用公开数据快速验证某个YOLO改进思路的研究者以及要给团队做内部培训、需要一个可直接复现的pipeline的工程师。但有个前提必须先说清楚1000张图片在遥感检测里属于能跑通流程的量级不是能直接把模型推到极致的量级训练出来的效果够你做实验、做验证、做教学演示但别指望它直接达到工业级落地精度。先把预期摆正后面每一步才不会走歪。2. 三种标注格式到底差在哪VOC是目录、COCO是字典、YOLO是归一化坐标在动手训练之前得先把数据的存储逻辑理清楚。很多人上来就写转换脚本结果VOC转COCO时漏了segmentation字段或者VOC转YOLO时把类别id从1开始编号导致训练时背景被当成目标这些坑都源于对三种格式底层结构理解不透。2.1 VOC格式最直观的XML标签结构VOC格式的核心是每个XML文件对应一张图片文件名与图片名保持一致。打开一个标注文件你会看到object节点里嵌套着name和bndbox其中bndbox记录的是绝对像素坐标xmin、ymin、xmax、ymax。这种格式对人来说最友好直接拉进XML编辑器或文本编辑器就能人工核对。但它的问题也很明显一个目标的坐标被拆成多个子节点解析必须靠ElementTree或lxml而且它不包含图片的宽高信息——虽然size节点通常会记录但很多标注工具导出的VOC格式里size缺失导致后续转换时必须单独从图片文件读取尺寸。我看过的遥感数据集里VOC格式的标注最常见的问题是坐标超出图片边界尤其是贴着图像边缘的油罐标注框可能比图片还大一圈。校验时遇到这种情况先看bndbox里的数值是否在0到图片宽高范围内超了就说明标注工具处理边缘目标时出了偏差这类样本要么修正要么丢掉直接带病训练会让边界框损失忽高忽低。另外VOC格式里类名是字符串比如oil_tank到了YOLO格式里要映射成整数id顺序一旦搞错训练出来的模型预测类别和实际物体就完全对不上。2.2 COCO格式JSON嵌套结构里的坑COCO是MS COCO数据集定义的格式核心是一个JSON文件整体描述整个数据集的图片列表、标注列表和类别列表。它的数据组织靠images、annotations、categories三个数组互相关联每张图片在images里有一个id每个标注在annotations里用image_id指向图片同时记录bbox这[x, y, width, height]形式的绝对像素坐标类别则用category_id索引到categories数组。遥感油罐这种单类别的检测任务用COCO格式其实有点大材小用但如果你后续要跑MMDetection或者Detectron2COCO就是默认输入格式。转换时需要特别注意的是annotations里每个标注对象的id必须全局唯一不能重复还要确认每个标注的area字段是否计算正确——有些转换脚本直接写成width * height但如果是多边形标注正确做法是算多边形面积否则训练时如果读取area做采样权重结果就会偏离。还有一点容易被忽略COCO里bbox的x、y是左上角坐标从VOC的xmin、ymin直接映射没问题但width和height是xmax - xmin不是xmax - xmin 1差一个像素在遥感大图上几乎无感在切patch训练时却会累计放大误差。2.3 YOLO格式四个数字表达一个目标YOLO格式每个目标只有一行文本类别id、归一化中心点x、归一化中心点y、归一化宽、归一化高。这五个数字用空格分隔对应一个TXT文件每行一个目标。归一化就是拿绝对像素坐标除以图片的宽和高所以所有数值都在0到1之间。这个格式最大的特点是省空间、解析快训练时标注读取几乎不占额外CPU时间。但它对人极不友好——直接打开TXT看到一堆小数根本没法直观判断目标框在哪。VOC转YOLO的具体做法先从XML里读出xmin、ymin、xmax、ymax从图片文件读取width和height不要依赖XML里的size因为经常填错或缺省然后计算中心点坐标(xmin xmax) / 2 / width、(ymin ymax) / 2 / height以及归一化宽高(xmax - xmin) / width、(ymax - ymin) / height。核心换算其实就是一步除法但问题往往出在除数上如果XML里的size节点缺失或者标注用的坐标系和图片实际尺寸不一致算出来的归一化坐标就会整体偏移。所以转换脚本里用PIL或OpenCV读取图片实际尺寸这是一个必须养成的习惯。提示YOLO的归一化坐标不是百分比而是比例值。0.5代表图片宽度的一半0.25表示四分之一处取值永远在0到1之间。检查标注是否合理先看是不是有坐标小于0或大于1出现这种情况一定是转换脚本或标注过程出了Bug。3. 把VOC标签转成COCO和YOLO一个脚本看清全部转换逻辑标题里说这个数据集自带三种格式标签但实际工作中最常遇到的情况是别人给你的是VOC你要的是YOLO。与其依赖不明来源的转换工具不如自己写一个转换脚本顺便把数据校验做掉。这个脚本能把VOC XML转成YOLO TXT和COCO JSON同时校验坐标越界和文件缺失问题。import xml.etree.ElementTree as ET import os import json from PIL import Image def voc_to_yolo(xml_path, img_path, output_txt_path): tree ET.parse(xml_path) root tree.getroot() img Image.open(img_path) img_w, img_h img.size lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_dict: class_dict[name] len(class_dict) class_id class_dict[name] bbox obj.find(bndbox) xmin int(float(bbox.find(xmin).text)) ymin int(float(bbox.find(ymin).text)) xmax int(float(bbox.find(xmax).text)) ymax int(float(bbox.find(ymax).text)) xmin max(0, min(xmin, img_w - 1)) xmax max(0, min(xmax, img_w - 1)) ymin max(0, min(ymin, img_h - 1)) ymax max(0, min(ymax, img_h - 1)) if xmax xmin or ymax ymin: continue x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h width (xmax - xmin) / img_w height (ymax - ymin) / img_h lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) with open(output_txt_path, w) as f: f.write(\n.join(lines))这段代码里有两个细节值得说明一是坐标边界裁剪把超出图片范围的坐标用max和min钳制回合法区间避免训练时YOLO层抛出“box坐标无效”的内部错误二是xmax xmin时的跳过逻辑这种退化框通常来自标注工具操作失误保留它们只会污染训练数据。为什么用PIL而不是XML里的size因为转换脚本会遍历上百张图片PIL读尺寸的速度足够快而且能保证拿到的尺寸一定和实际像素严格一致。接着把同一个XML转成COCO JSONdef voc_to_coco(xml_path, img_path, img_id, ann_id, coco_images, coco_annotations): tree ET.parse(xml_path) root tree.getroot() img Image.open(img_path) img_w, img_h img.size coco_images.append({ id: img_id, file_name: os.path.basename(img_path), width: img_w, height: img_h }) for obj in root.findall(object): name obj.find(name).text if name not in class_dict: class_dict[name] len(class_dict) category_id class_dict[name] 1 # COCO的category_id从1开始 bbox obj.find(bndbox) xmin int(float(bbox.find(xmin).text)) ymin int(float(bbox.find(ymin).text)) xmax int(float(bbox.find(xmax).text)) ymax int(float(bbox.find(ymax).text)) width xmax - xmin height ymax - ymin coco_annotations.append({ id: ann_id, image_id: img_id, category_id: category_id, bbox: [xmin, ymin, width, height], area: width * height, iscrowd: 0 }) ann_id 1 return ann_idCOCO转换要注意几个细节category_id和YOLO的class_id差1COCO的类别id从1开始0默认给背景area字段在单类别检测里用不到但MMDetection这类框架会读取它做在线难例挖掘填错会导致训练时的采样逻辑异常iscrowd默认填0表示这是普通目标框不是密集人群标注。ann_id是全局递增的不能每张图都从0开始否则COCO的索引结构就乱了。3.1 类别映射是转换里最容易出错的地方刚才的脚本里class_dict是全局字典第一次遇到某个类别名就给它分配一个新的id。这个逻辑在类别顺序固定的情况下是安全的但如果你重新排序了字典所有已转换的TXT文件就全部作废。所以转换之前先检查类别的数量遥感油罐数据集通常只有oil_tank一个类别它的class_id是0用于YOLO格式的TXT文件里每一行开头都写0转到COCO后category_id变成1。一个容易翻车的场景如果同一张图里有两个不同类别但你的字典顺序是第一次遇到时动态分配的那第二次重新跑转换脚本时字典顺序可能不同导致同一个目标类别映射到不同的id。解决办法是把类别列表硬编码在脚本里不要动态分配。比如这个数据集里的oil_tank直接在代码顶部写class_dict {oil_tank: 0}任何情况下都只有这一个映射。3.2 转换后必须做的三件事转换脚本跑完不是结束必须做三轮验证。第一轮检查数量统计每个TXT文件的行数和XML里object的数量不一致就说明某个目标在转换时被跳过了。第二轮检查内容随机抽五张图把YOLO格式的归一化坐标乘以图片宽高还原成像素框用PIL画在图上人工比对看框是否贴合油罐边缘。第三轮检查结构COCO JSON的images、annotations、categories三个数组的长度都要大于0且所有image_id都能在images里找到对应条目。这三步做完才算真正获得了一份干净的训练数据。很多人跳过这一步训练到中途发现loss异常回头排查半天才意识到是数据转换问题这个时间成本比直接做校验高十倍。4. 划分脚本为什么不能直接随机切遥感油罐数据的分布陷阱标题里提到附带的划分脚本这是整个数据集包里最容易低估价值的部分。很多人以为划分就是随机挑一部分图片做训练、一部分做验证分完就算完事。但在遥感油罐检测这个任务上这种做法会让你的验证集形同虚设训练出来的模型指标虚高部署到新区域马上翻车。4.1 同源影像泄漏遥感数据划分的第一大忌遥感油罐影像通常来自若干次卫星拍摄或无人机航拍同一个区域的多张切片图在光照条件、地物背景、油罐分布模式上高度相似。如果随机划分这些同源的图片会同时出现在训练集和验证集里模型在验证集上的表现是虚高的因为它已经“见过”这片区域的纹理特征。这里的“见过”不是说模型记住了同一张图片模型没有记忆能力它是学习了纹理分布规律导致同源区域的验证样特征与训练高度重合性能评估失去意义。正确做法是按拍摄批次或地理区域划分所有来自同一场景的切片必须整组归入同一个集合。如果数据集包里没有提供拍摄批次信息可以用文件名前缀判断很多遥感数据集的命名格式里前缀就是场景标识符。划分脚本里的split_by_group而不是split_by_random就是这个用途。单类别检测任务里同一区域的油罐形态高度相似这种泄漏对指标的影响会更大。4.2 strtify划分脚本按场景分组写一个可控的划分器import os import random import shutil from collections import defaultdict def group_by_prefix(image_files, prefix_len8): groups defaultdict(list) for f in image_files: prefix os.path.basename(f)[:prefix_len] groups[prefix].append(f) return list(groups.values()) def split_dataset(groups, train_ratio0.7, val_ratio0.2, test_ratio0.1): random.seed(42) random.shuffle(groups) train_cnt int(len(groups) * train_ratio) val_cnt int(len(groups) * val_ratio) train_groups groups[:train_cnt] val_groups groups[train_cnt:train_cnt val_cnt] test_groups groups[train_cnt val_cnt:] train_files [f for group in train_groups for f in group] val_files [f for group in val_groups for f in group] test_files [f for group in test_groups for f in group] return train_files, val_files, test_files def write_split_txt(files, path): with open(path, w) as f: for file in files: f.write(file \n)这段脚本的核心是group_by_prefix按文件名的前8个字符分组把同场景的数据锁死在一个组里然后对组而不是对单张图片切片。random.seed(42)保证每次运行划分结果一致这对复现实验很关键——别人用你的代码跑不出相同的数据分布实验就失去可比性。三个集合的默认比例是7比2比1遥感数据如果总量只有1000张验证集200张、测试集100张是合理的因为目标检测任务中验证集需要足够多的目标框才能稳定评估mAP。4.3 划分后必做的平衡性检查划分完成后不要直接开训练先统计三个集合里油罐框的数量分布。如果训练集有3000个框验证集只有200个框说明验证集的评估结果方差会非常大——连续训练几次可能得到差异很大的mAP还会让你误以为模型训练不稳定。如果某个集合里的框太少可以调整比例或者对框多的集合做下采样增强复制一些样本进训练集。另一个检查是尺度分布遥感油罐的像素尺寸从十几像素到几百像素不等如果训练集中大目标占比过高模型会倾向预测大框验证集里集中了小目标分数立刻掉下来。这个检查在单类别任务里格外重要因为没有类别多样性来分散模型的注意力尺度就成了唯一的变化维度。用一个小脚本统计每个TXT文件里所有框的宽高平均值再按集合分组对比就能快速发现分布不平衡。提示划分脚本里三个集合是互斥的训练时不要既用train.txt又用val.txtYOLO训练脚本里data.yaml同时指定train和val路径但test路径可以留空。测试集只有在最终评估时才使用不要在训练和调参过程中触碰它否则测试集就失去评估意义了。5. 用YOLOv5跑通油罐检测训练训练教程里的参数和避坑记录有了三种格式的标签和划分好的数据集接下来就是训练环节。这里以YOLOv5为例因为这个资源包里提到的VOC和COCO标签最容易映射到YOLOv5的data.yaml配置。YOLOv8也适用但YOLOv5的社区资料多报错排查时更容易搜到答案。在整个训练过程中有几个坑是遥感油罐检测任务特有的提前说清楚能帮你避免不少数据预处理上的无谓返工。5.1 写data.yaml之前先确认路径和类别数train: ./datasets/oil_tank/images/train val: ./datasets/oil_tank/images/val test: ./datasets/oil_tank/images/test nc: 1 names: [oil_tank]这个配置看起来简单但有两个细节必须确认路径指向的目录存在且包含图片文件YOLOv5会根据标签文件的命名规则自动找到与图片同名的txt标签文件不需要在yaml里单独指定标签路径。第二个细节是nc类别数这个写错会导致模型输出维度错误轻则loss异常重则训练直接崩溃。这里的names列表必须和前面转换脚本里的class_dict顺序一致oil_tank对应id 0names列表里第一个元素就是它。注意标签文件和图片文件必须在同名目录下。YOLOv5的默认约定是图片放images/train标签放labels/train目录名和路径结构不能乱改。如果资源包解压后标签文件散落在一个目录里用一段脚本整理目录结构import os import shutil def organize_labels(img_dir, label_dir, output_base): split_names [train, val, test] for split in split_names: img_split os.path.join(img_dir, split) lbl_split os.path.join(label_dir, split) if not os.path.exists(img_split): continue os.makedirs(os.path.join(output_base, images, split), exist_okTrue) os.makedirs(os.path.join(output_base, labels, split), exist_okTrue) for img_file in os.listdir(img_split): if not img_file.endswith((.jpg, .png)): continue stem os.path.splitext(img_file)[0] lbl_file os.path.join(lbl_split, stem .txt) if not os.path.exists(lbl_file): print(fmissing label: {lbl_file}) continue shutil.copy(os.path.join(img_split, img_file), os.path.join(output_base, images, split, img_file)) shutil.copy(lbl_file, os.path.join(output_base, labels, split, lbl_file))这个整理脚本同时做了一件事检查每个图片是否都有对应的txt标签缺失的打印出来。这个检查在训练前做掉能省下训练跑到一半才发现FileNotFoundError的时间。5.2 训练命令与关键参数python train.py --data oil_tank.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100 --cacheyolov5s.pt是官方预训练权重在COCO上预训练过用迁移学习的方式微调遥感油罐数据集比从头训练的收敛速度快得多。--img 640是输入分辨率遥感油罐属于小目标如果觉得目标太小可以调大到960或1280但显存消耗随分辨率平方增长。--batch 16在单卡12G显存下刚好能跑显存不够就降到8。--cache是把图片预加载到内存里省去每轮epoch反复读磁盘的时间第一次运行时需要额外内存8G内存不够的话就加上--cache-images只缓存部分。训练过程中需要盯的参数Box Loss、Obj Loss、Cls Loss以及最后的mAP_0.5和mAP_0.5_0.95。遥感油罐是单类别mAP_0.5_0.95会比多类别任务高一些因为这个指标会算多个IoU阈值下的均值单类别没有类别混淆问题稳定在0.6以上算正常。5.3 遥感油罐训练中三个必踩的坑从现象到原因到解决坑一loss在几十个epoch后突然跳高。现象训练曲线前30个epoch稳定下降第35个epoch左右Obj Loss突然跳升之后再也回不到之前的最低点。原因图片里有极暗或极亮的油罐YOLOv5的Mosaic增强在切图时把这些高反差区域混合在一起模型被异常像素扰动。解决先检查训练集中是否存在低质量的HDR影像遥感数据常受光照角度和阴影影响油罐罐壁的高光部分会形成大量强边缘影响特征提取。可以降低--hsv增强强度YOLOv5默认开启或者关掉Mosaic增强--mosaic 0跑几个epoch观察loss是否恢复。另一个更朴素的原因是学习率在某个节点突然跳变如果是用默认的--cos-lr或--lrf参数LR schedule在训练后期会急剧下降叠加数据里的异常样本loss曲线就会出现尖峰这个和模型本身无关观察后续epoch是否回稳即可。坑二mAP_0.5很高mAP_0.5_0.95很低。现象训练100个epoch后mAP_0.5达到0.92mAP_0.5_0.95只有0.35。原因IoU阈值越严格对预测框的位置精度要求越高油罐这种圆形目标在高IoU下特别吃亏——框稍微偏移几个像素IoU就从0.7掉到0.5。遥感影像里的油罐存在大量相互遮挡或间距极小的排列情况预测框在罐体边缘来回摇摆导致高IoU阈值下指标崩掉。解决数据增强里加一些小角度的旋转YOLOv5默认没有旋转增强油罐是圆形目标接地点和罐体阴影特征对旋转不敏感模型对罐体的圆形边缘理解不充分导致框的位置不够紧贴。在超参搜索里启用--hyp的自定义yaml把degrees设为15度左右。坑三训练正常但推理时大量漏检小油罐。现象训练时mAP_0.5超过0.85用训练好的权重跑视频或大图推理时10像素大小的油罐全被漏掉。原因YOLO的--img 640意味着推理时也是把完整图像缩放到640像素分辨率遥感大图动辄4000乘4000像素油罐缩小到原始尺寸的1/6可能只剩下不到10个像素特征彻底丢失。解决遥感大图推理必须切片。把大图切成长宽重叠的patch每个patch按840或960推理再把框映射回原图坐标重叠区域用NMS合并。或者在数据层面解决——训练时用--img 1280跑让模型看到更大尺度下的细节但显存需求会显著增加。提示训练时不要不停上调--img到超过1280超过这个数值后显存占用曲线陡增且mAP的提升会越来越小。遥感油罐这个任务640到960之间的提升最明显性价比最高。5.4 训练完了怎么判断模型真的可以用核心不看训练集的最终loss而是看验证集上的PR曲线和混淆矩阵。YOLOv5训练完成会在runs/train/exp目录下生成PR_curve.png和confusion_matrix.png。打开PR曲线如果曲线整体偏右上角mAP_0.5在0.85以上说明模型可用。打开混淆矩阵看背景类background那一行如果模型把大量背景误判为油罐说明泛化不行需要增加难负样本如果把油罐误判为背景说明目标太模糊需要提高输入分辨率。然后做一次真正的测试集推理用val.py或detect.py跑测试集图片把预测结果可视化保存人工扫一遍看有没有明显错漏。这一步不要只盯着mAP数值因为你最终部署时看的是模型的视觉行为是否符合直觉。如果测试集里有阴影覆盖的油罐模型漏掉了这是正常现象遥感影像里阴影覆盖的目标本来就难标注者自己都可能看不清。6. 想提升油罐检测精度从预处理到部署前的三个进阶技巧当基础训练跑通、模型能在验证集上稳定输出时下一步就是思考怎么提升。这个资源包本身只有1000张图片上限摆在那里但你可以在流程层面做三件事让模型逼近这个上限自定义数据增强、难例挖掘和切片推理。先看数据增强。YOLOv5默认的增强组合对自然图像效果很好但遥感油罐是俯拍视角没有“上下左右”的语义方向之分旋转增强可以放开。一个有效做法是在hyp.scratch.yaml里把degrees: 30改到degrees: 45同时把hsv_h、hsv_s、hsv_v的值调低因为油罐识别更多依赖几何特征而非颜色纹理过强的颜色扰动会让模型学到不稳定的颜色关联。另外一个对遥感场景特别有用的增强是复制粘贴增强把一张图里的油罐框裁下来随机粘贴到另一张图的空地上相当于免费扩充样本同时也提升了模型对密集排列目标的鲁棒性。再看难例挖掘。训练结束后用训练好的权重对训练集再推理一遍把所有预测置信度低于0.3的样本挑出来。这些就是模型学不好的难例——可能是油罐被阴影遮挡、间距过近、或者背景纹理和罐体颜色接近。把这些难例收集起来配合原图里已有的标签做一次小学习率微调。这个做法在数据集只有1000张的情况下尤其有用相当于在有限数据里做了一次自我提升成本极低。最后是切片推理。这是遥感检测落地最常用的技巧做法是把大图切成分辨率适中的patch分别推理再合并结果。我的习惯是用sa hi这个Python库来实现设定patch大小为960像素、重叠率0.2推理完成后自动合并重叠检测框。这个技巧能把10像素级别的小油罐检测率提升30%以上代价只是推理时间变长。如果你的目标场景是监控视频流用RTSP流拉帧后逐帧切片推理需要考虑实时性——这时可以降低重叠率到0.1或直接跳过切片用小分辨率全图推理牺牲少量召回率换实时性。说到这里我自己的经验是数据集的1000张图片决定了模型的天花板但流程里的每个细节决定了你离这个天花板有多近。转换脚本多校几轮、划分时多想一下同源泄漏、训练完多看一眼混淆矩阵再做部署判断这些都不是什么高深的技巧却是真正影响最终检测效果的地方。希望这份记录能帮你在油罐遥感检测这条路上少走几步弯路。本文还有配套的精品资源点击获取
返回列表