☰
YOLO遥感油罐检测实战:VOC/COCO/TXT格式转换与训练避坑全指南
2026/10/8 19:07:38 网站建设 项目流程

简介:面向遥感目标检测与YOLO系列模型训练需求,这份资料提供真实场景下的油罐检测数据集,共含1000张高质量图片,使用LabelImg标注,标注框质量高,可满足日常教学或科研中的检测任务。压缩包内共2000个文件,对应VOC、COCO、YOLO三种标注格式,标签文件以xml和txt为主,另含yaml配置与Python划分脚本;同时内置html版环境搭建教程和训练案例教程,覆盖Linux与Windows双系统,便于在不同平台快速复现。教程从环境配置、数据集划分到模型训练均有说明,三个划分脚本可自动生成训练集、验证集、测试集,帮助用户省去手动整理图片与标签的烦琐步骤。当前已有247人学习下载,适合遥感目标检测入门者及需要现成标注数据进行YOLO系列实验的研究人员。

1. 拿到YOLO遥感油罐检测数据集,先别急着解压

我拿到标注好的数据集,固定动作从来不是立刻解压扔进训练脚本,而是先想清楚:油罐这种密集排列的小目标,和日常马路上的车、人是两类完全不同的图像分布。遥感影像里的油罐是俯视视角,圆形、金属反光强、受阳光照射还拖一条长影,通用目标检测数据集里很难见到这种形态学特征。你手头这个压缩包里有1000张油罐影像,配了VOC、COCO、YOLO三套标签,还附带了划分脚本和训练教程,这其实已经替新手回答了最常见的两个问题:标注怎么转成训练要的格式、数据怎么划分。但真正值钱的不是文件数量,而是这1000张图里包含的边界情况——阴影遮挡、油罐并排紧贴、不同分辨率下的尺度差异。这篇笔记按「读懂三格式 → 跑通划分脚本 → 训练出模型 → 小目标改进与部署」的顺序展开,新手可以照着一步一步做,熟手直接跳到第4章和第6章看边界条件。

2. 三种格式标签的内在逻辑:VOC、COCO、YOLO各自怎么描述一个油罐

2.1 从一张遥感图出发:VOC的XML文件字段逐个拆解

VOC格式对新手最友好,因为它的组织方式非常直观:一个XML文件对应一张图片,里面把目标类别、外接矩形坐标、图片尺寸全部写清楚。但缺点也在这里——1000张图就有1000个XML文件,复制、备份、传输时小文件特别多,容易出现漏拷贝。

打开一个典型的VOC标注文件,你会看到这样的结构:

<annotation> <folder>JPEGImages</folder> <filename>oil_tank_001.jpg</filename> <size> <width>1024</width> <height>768</height> <depth>3</depth> </size> <object> <name>oil_tank</name> <bndbox> <xmin>412</xmin> <ymin>289</ymin> <xmax>468</xmax> <ymax>345</ymax> </bndbox> </object> </annotation>

这段XML说的是:图片oil_tank_001.jpg中有一个目标,类别名为oil_tank,外接矩形的左上角是(412, 289),右下角是(468, 345)。注意这个框是轴对齐的矩形,不表达旋转角度。遥感影像里的油罐虽然是圆形,但标注时统一用外接矩形包住,YOLO类检测器默认也只输出矩形框,所以这种表达足够训练使用。

逻辑说明:写XML时最容易出错的是filename字段与真实文件名不一致,或文件夹层级写错。脚本改写到VOC时,如果发现标注加载不出来,十有八九是filename里带了绝对路径或者Windows风格的反斜杠。另一个问题是多目标时object块会有多个,每个object必须单独写完整,VOC的解析器按object块数量读取,少一个就少一个目标。

2.2 COCO的JSON结构:什么时候用得上它

COCO格式把整份标注聚合到一个JSON文件里,images、annotations、categories三个数组各司其职。这种设计的好处是官方配套工具链多,模型评估、可视化、Mask标注都能直接用pycocotools处理。你在YOLO训练时基本用不到它,但想换成DETR、Mask2Former或者其他支持COCO输入格式的框架时,这份JSON能省下大半天的时间。

一个精简过的COCO JSON长这样:

{ "images": [ {"id": 1, "file_name": "oil_tank_001.jpg", "width": 1024, "height": 768} ], "annotations": [ {"id": 1, "image_id": 1, "category_id": 1, "bbox": [412, 289, 56, 56], "area": 3136, "iscrowd": 0, "segmentation": [[412,289,468,289,468,345,412,345]]} ], "categories": [ {"id": 1, "name": "oil_tank"} ] }

参数说明:COCO的bbox是「左上角x、左上角y、宽度、高度」,单位是像素,不归一化;segmentation是多边形顶点坐标,我自己给油罐做分割标注时一般只给外接矩形的四个角点,不画精细轮廓,因为油罐边缘在遥感影像里受光照和阴影影响波动大,多边形勾得太细反而把噪声教给了模型。area字段就是宽乘高,pycocotools评估时会用它做面积分层的AP统计,例如小目标AP、中等目标AP。

COCO的坑是JSON里所有整数不能写成浮点数,尤其是images宽度和高度,一旦写成1024.0,某些严格校验的框架会直接报类型错误。还有iscrowd字段默认0,如果出现1,评估时该目标会被当作人群处理,特别影响mAP计算结果。

2.3 YOLO的TXT格式:训练脚本真正读的是什么

这是压缩包里最重要的一份标签,因为YOLOv5、YOLOv8在训练时读的是同名TXT,不是XML也不是JSON。YOLO标签格式的核心是归一化的中心点坐标,一行代表一个目标:

0 0.4297 0.4128 0.0547 0.0729

按顺序解读这五个值:类别id为0(对应oil_tank)、目标中心点的x在图片宽度42.97%的位置、中心点的y在图片高度41.28%的位置、框宽度占图片宽度5.47%、框高度占图片高度7.29%。所有值都在0到1之间,宽度和高度严格大于0。

这种相对坐标的表达精妙之处在于:图片从1024x768换成2048x1536,TXT里数值不需要改动,模型在数据加载时按当前分辨率重新映射。但代价是理解门槛高了,新人在转换时容易忘记除以宽高,导致坐标值超过1,训练过程大概率报错或框全在图片角落。

我在第2.1节例子中的VOC框,换算成YOLO格式是这样的:

x_center = (412 + 468) / 2 / 1024 # 0.4297 y_center = (289 + 345) / 2 / 768 # 0.4128 w = (468 - 412) / 1024 # 0.0547 h = (345 - 289) / 768 # 0.0729

逻辑说明:中心点坐标取(xmin+xmax)/2再除以图片宽度,框宽是(xmax-xmin)除以图片宽度,全部用浮点数运算,除法不能取整。这些换算关系就是作者做格式转换脚本时最核心的逻辑,你在复现或二次转换时同样绕不开这一步。

2.4 三种格式互转的关键坐标换算:一份对照表

把三种格式放在同一张表里看,边界框的表达方式一目了然:

格式坐标体系是否归一化文件组织典型用途
VOC XMLxmin, ymin, xmax, ymax否每图一个XML标注工具导出、教学示例
COCO JSONx, y, width, height否全量一个JSON框架迁移、pycocotools评估
YOLO TXTx_center, y_center, width, height是每图一个TXTYOLOv5/v8训练直接读取

平时做项目,我一般以YOLO格式为主仓库,训练时直接喂;需要做评估或者对比实验时,再从YOLO转回COCO,这个转换是无损的。反过来的路径——从COCO转YOLO——同样是纯算术操作,但要注意COCO的坐标x、y是左上角,而YOLO需要的是中心点。油罐检测场景尤其要看宽度和高度的换算,因为圆形目标的外接矩形近似正方形,如果某个标注框的宽高比明显偏离1:1,很可能是标注工具或转换脚本出了问题,排查时先从这种异常值入手。

提示:判断一份标签是否转换正确,最快的方法是把标注框画回原图直接肉眼检查。不要相信脚本不出错就等于结果正确,坐标换算的错误经常是静默发生的。

3. 划分脚本的正确用法:保证图片和三种标签同步移动

3.1 先盘点解压后的目录,弄清三类标签的摆放层级

解压rar之后,第一步不是急着改代码,而是把目录结构完整地看一遍。常见的一种布局是这样:

oil_tank_dataset/ ├── images/ │ ├── oil_tank_001.jpg │ └── ... ├── annotations_voc/ │ ├── oil_tank_001.xml │ └── ... ├── annotations_coco/ │ └── annotations.json └── labels_yolo/ ├── oil_tank_001.txt └── ...

先把三类标签和图片的数量对起来,再抽查文件名的前缀是否一致。我通常会写一个极短的Bash命令做校验:

for f in images/*.jpg; do base=$(basename "$f" .jpg) [ -f "annotations_voc/$base.xml" ] || echo "缺少VOC: $base" [ -f "labels_yolo/$base.txt" ] || echo "缺少YOLO: $base" done

逻辑说明:遍历所有图片,用去掉后缀的文件名去检查对应XML和TXT是否存在,缺失时打印提示。压缩包标题写的是「对应」三种格式标签,正常情况下三类数量应该完全一致;如果发现缺失,先别跑划分,缺标签的图片在训练时会被静默跳过或报出警告,不报错的黑匣子问题最麻烦。

我建议也在这一步统计一下空标签的数量。YOLO的TXT文件允许是0字节的,代表这张图没有任何油罐目标,这类图是背景负样本,训练时可以保留,但数量需要控制在合理范围。

3.2 按比例划分的Python脚本:同步移动三类文件

划分脚本是压缩包里最实用的小工具,它做的事情本质上是:把图片按比例分成train、val、test三份,同时把对应的VOC XML、YOLO TXT移动到相同结构的目录下。下面是实际可用的版本:

import os import random import shutil src_img = "images" src_voc = "annotations_voc" src_yolo = "labels_yolo" dst_root = "dataset" split_ratio = {"train": 0.8, "val": 0.1, "test": 0.1} random.seed(42) imgs = [f for f in os.listdir(src_img) if f.endswith(".jpg")] random.shuffle(imgs) for split, ratio in split_ratio.items(): n = int(len(imgs) * ratio) part = imgs[:n] imgs = imgs[n:] for img in part: base = os.path.splitext(img)[0] for sub_src, sub_dst in [ (src_img, "images"), (src_voc, "annotations_voc"), (src_yolo, "labels_yolo")]: ext = ".jpg" if sub_src == src_voc: ext = ".xml" elif sub_src == src_yolo: ext = ".txt" src = os.path.join(sub_src, base + ext) dst_dir = os.path.join(dst_root, split, sub_dst) os.makedirs(dst_dir, exist_ok=True) shutil.copy(src, dst_dir)

参数说明:split_ratio字典控制划分比例,默认8:1:1,想改成7:2:1直接把字典里的数值换掉;random.seed(42)保证每次运行得到完全一样的划分结果,这个细节在对比实验时特别重要,没有固定随机种子,两次划分到train里的图片不同,实验结论就不客观。

用shutil.copy而不是move,是因为原始文件只有一份,move跑错一次目录就乱了,留一份原始文件算是给自己留了后悔药。注意这里的取整逻辑:每次从imgs列表头部取走n张,剩下的继续给下一个集合,所以最后三个集合的图片总数一定等于全部图片数,不会出现同一张图出现在两个集合里的情况。

提示:划分完成后,COCO的JSON也要跟着拆。因为一个JSON包含全量标注,简单做法是读JSON后按image_id归属分别写train.json、val.json、test.json,这个步骤常被忽略,等到换框架训练时才发现验证集里混进了训练图片。

3.3 划分后数据质量如何验证:不可跳过的一步

划分脚本跑完,输出一个数字总让人心里不踏实,因为复制操作是否完整、有没有丢文件,脚本本身不会告诉你。我的习惯是立刻再跑一遍校验:

import glob for split in ["train", "val", "test"]: imgs = glob.glob(f"dataset/{split}/images/*.jpg") xmls = glob.glob(f"dataset/{split}/annotations_voc/*.xml") txts = glob.glob(f"dataset/{split}/labels_yolo/*.txt") print(split, "imgs:", len(imgs), "xml:", len(xmls), "txt:", len(txts), "OK" if len(imgs) == len(xmls) == len(txts) else "MISMATCH")

逻辑说明:glob按目录模式匹配出所有文件,统计每个集合内图片、XML、TXT的数量。三个数必须相等,只要有一个不等,说明划分脚本里有路径或扩展名的疏漏,不要带着这个隐患开始训练。

数量校验通过之后,还要抽查TXT内容里的数值范围。YOLO格式的x_center、y_center必须在0到1之间,width、height必须大于0。如果有数值越界,说明标签本身有损坏,这类图片在训练时会导致loss异常升高。写一个快速的检查脚本扫一遍所有TXT,看到异常值就打印文件名,这个习惯能帮你避开很多莫名其妙的训练翻车。

4. 遥感油罐数据集的5个避坑点:格式转换与训练前检查

4.1 坑一:VOC转YOLO时把归一化坐标写成了整数

现象:用转换工具把XML转成TXT,训练能正常启动,但loss始终降不下去,用可视化脚本画标注框时发现所有框都堆在图片左上角。

原因:VOC的xmin、ymin是绝对像素整数,转YOLO的归一化坐标时,有人直接用(xmin + xmax) // 2再除以width,或者转换脚本里把除法写成了整除,结果中心坐标全部变成了0,目标框自然全跑到左上角。

解决:转换时强制使用浮点数运算,width和height加上非零保护:x_center = (xmin + xmax) / 2.0 / img_width。转换完成后随机抽5张图,把TXT中的归一化坐标乘以图片宽高反算回像素值,和XML里的原始坐标核对,误差不超过1个像素才算通过。

4.2 坑二:划分脚本没有固定随机种子

现象:两次训练代码一模一样,但mAP忽高忽低,你以为模型随机初始化造成的波动,换了三个种子都在0.5到0.8之间乱跳。

原因:划分脚本没写random.seed,每次运行把不同的图片分到了train和val,验证集不稳定,评估结果互相之间没有可比性。

解决:在脚本开头固定random.seed(42),同时保证Python的random、NumPy、PyTorch三者的种子全部设好。这样别人拿到你的脚本能复现同一个划分结果,你的对比实验也才有意义。

4.3 坑三:空标注文件被当作负样本,还是该删掉

现象:训练日志里出现大量「Image has no labels」警告,训练本身不报错,但val集mAP在0.3左右徘徊,误检率特别高。

原因:数据集中存在一些没有油罐的纯背景图,TXT为空。这类负样本如果占比超过10%,模型会偏向于输出「没有目标」的预测,漏检率上升;如果完全删掉,模型又容易把类似油罐的圆形物误检为正样本。

解决:先统计空TXT数量,如果占比在3%以下,直接连同对应图片一起移除;占比在3%到10%之间,保留但单独建一个负样本目录,别和正样本混在同一个train集合里;占比超过10%,检查标注过程是否漏标了图片。

4.4 坑四:小目标漏检,不是改个分辨率就能解决

现象:val集mAP50达到了0.8,但把模型拿到大场景遥感图上推理,远处的油罐全漏掉。

原因:输入分辨率640时,一个小油罐在特征图上可能只占一个点,YOLOv8的默认下采样倍数下,小目标信息在深层特征图里已经丢失。调高imgsz到1024能改善,但显存占用翻倍,训练时间也成倍增加。

解决:先分析数据集中目标的像素尺寸分布,统计宽度和面积的中位数。如果多数油罐小于32x32像素,优先考虑在YOLOv8里添加P2检测层,或者用NWD替换IoU损失函数,这些属于结构性的改进;单纯调imgsz是从数据端缓解,不是从模型端根治。

4.5 坑五:密集排列的油罐导致NMS误抑制

现象:两个油罐紧挨着摆放,模型检测时只输出一个框,另一个被抑制掉了,可视化时看着像漏检。

原因:遥感油罐经常成排密集布局,真实目标的IoU本身就很高,NMS算法认为它们是同一个目标,把得分低的那个框抑制了。

解决:推理时把NMS的IoU阈值从默认的0.45调低到0.2或0.25,允许两个高重叠框同时保留。这需要在验证集上重新测量mAP,找到阈值调低后漏检率下降而误检率不激增的平衡点。另一种做法是训练时数据增强里加入Mosaic和Cutout的强度控制,让模型对遮挡更鲁棒。

5. 用YOLO训练油罐检测模型:最小可跑通的训练流程

5.1 数据配置文件:把划分好的目录告诉YOLO

训练前需要写一个描述数据集的YAML文件,YOLOv5和YOLOv8的格式基本一致。以YOLOv8为例,在数据集根目录创建oil_tank.yaml:

path: /home/user/oil_tank_dataset train: train/images val: val/images test: test/images nc: 1 names: ["oil_tank"]

参数说明:path是绝对路径,train和val填写的是相对path的目录,指向划分脚本生成的train/images与val/images。nc是类别数量,这里只有一个油罐类别所以是1。names列表里的顺序必须和TXT标签第一列的id对应,id从0开始,所以第一个名字就是油罐。

YOLOv8在首次训练时会扫描所有图片和标签,生成缓存文件以加速后续读取,这个过程会打印扫描进度,如果发现某个TXT文件名与图片对不上,会在这一步直接报错。这个机制本身就是对划分结果的一次自动校验。

5.2 训练命令与关键参数:跑通一个最小可用的模型

写好了YAML,直接执行训练命令:

yolo detect train \ --data oil_tank.yaml \ --model yolov8s.pt \ --epochs 100 \ --imgsz 1024 \ --batch 16 \ --device 0

参数说明:model用yolov8s.pt作为预训练权重,COCO数据集上预训练过的模型已经学会了通用的边缘、纹理特征,迁移到油罐数据集上收敛速度远快于随机初始化。imgsz=1024是为遥感小目标设置的,第一次跑也可以先用640验证流程通不通,通之后再调成1024正式训练。batch=16在1024分辨率下需要大约12GB显存,如果CUDA out of memory就降成8。device指定GPU编号,多卡机器写成0,1,2,3即可。

YOLOv5对应命令:

python train.py \ --data oil_tank.yaml \ --weights yolov5s.pt \ --img 1024 \ --batch 16 \ --epochs 100

两者的本质是一样的,选择时主要看团队里已有的代码基础设施和部署链路。我自己的经验是:如果只想快速出结果,YOLOv8的命令行更干净;如果后续要改loss、改结构,YOLOv5的源码组织更直观。

5.3 训练日志怎么看:loss下降和mAP上升的合理节奏

训练过程中终端会实时打印每个epoch的box_loss、cls_loss、dfl_loss,并在验证集上计算mAP50和mAP50-95。不要只盯着最终mAP,节奏比终值更重要。

box_loss应当稳定下降,前20个epoch下降幅度大,后面逐渐平缓,如果box_loss降到一定值后反弹,说明学习率过大或数据里有异常标签。cls_loss对于单类别数据集通常下降最快,因为二分类比多分类简单,如果分类损失一直不降,检查TXT的类别id是否全部正确。mAP50在油罐检测任务上,训练正常的话应该在0.85以上;mAP50-95比mAP50低0.2到0.3是正常的,因为小目标对IoU的微小变化非常敏感,不要因为这个差距去怀疑模型有问题。

训练到一半发现效果不对,先停掉改参数,不要等100个epoch跑完再调整。还有一个细节:观察前5个epoch的mAP曲线,如果从第一个epoch就开始上涨,说明预训练权重和数据集对接顺利;如果前5个epoch为0,先检查标签是否加载成功,而不是继续等下去。

6. 从改进到部署:油罐小目标的NWD与TensorRT加速

6.1 NWD改进思路:用归一化高斯距离替代IoU

油罐检测场景和YOLO小目标优化是天然强相关的:圆形目标密集、像素面积小、排列紧密,IoU对小目标的位置偏差特别敏感,偏移两三个像素,IoU就可能从0.7掉到0.3,梯度信号弱且震荡大。改进YOLO时,一个常见的落点是换回归损失,用归一化高斯距离替代IoU,把边界框视为二维高斯分布,通过Wasserstein距离度量两个分布的形态差异。

def nwd_loss(pred, target, alpha=2): # pred, target: [..., 4],格式为x_center, y_center, w, h center_dist = (pred[..., 0] - target[..., 0]) ** 2 + \ (pred[..., 1] - target[..., 1]) ** 2 wh_dist = (pred[..., 2] - target[..., 2]) ** 2 + \ (pred[..., 3] - target[..., 3]) ** 2 wd = alpha * center_dist + wh_dist nwd = torch.exp(-torch.sqrt(wd) / 2) return (1 - nwd).mean()

逻辑说明:center_dist计算两个框中心点的距离平方,wh_dist计算宽高差异平方,alpha控制中心距离和尺度差异的权重比。整个表达式把Wasserstein距离归一化到0到1之间,两个框完全不重叠时,IoU为0导致梯度消失,NWD仍然能提供微小的梯度信号,让漏检的小目标在后续迭代里有的学。参数说明:alpha的典型取值在2到4之间,油罐这类形状规整的目标取2就够,遥感场景下如果目标尺度变化大,可以适当调大到3或4。

NWD在油罐数据集上的效果通常能提升2到5个点的mAP50,但收益集中在小目标部分,如果你的目标普遍大于64x64像素,这个改动的意义就不大。这也是我一直强调的:模型改进要对着数据短板打,而不是反过来套一个热门改进就期待涨点。

6.2 导出ONNX与TensorRT:小目标模型的部署落地

训练收敛之后,下一步是部署。先把PyTorch权重导出为ONNX:

yolo export model=best.pt format=onnx opset=12 simplify=True

导出成功的ONNX可以在onnxruntime里直接推理验证。在英伟达平台上,TensorRT通常比onnxruntime快一截,转成engine文件:

trtexec --onnx=best.onnx --saveEngine=best.engine --fp16

trtexec默认batch为1,如果你计划做多路RTSP视频流并行推理,需要在导出ONNX时就设置动态batch维度,并用maxBatch参数指定最大batch数。推理时把多路视频的帧拼成一个batch统一送入engine,GPU利用率会明显高于逐路推理。我经常被问到T4显卡能带多少路1080p视频,这个问题的答案取决于预处理耗时、后处理耗时和batch大小,单看engine推理时间没有意义,必须把整条pipeline一起算。

6.3 最后一个建议:把数据清单当作项目的第一份产出

这个压缩包给你的不只是1000张图和标签,而是一个完整的产业起点。我每次拿到新数据集,第一件事永远是建一张清单:图片数量、每个类别的目标数量、标签格式、划分比例,写成一个表格放在项目根目录。后续所有的实验记录、模型迭代都围绕这张表展开。数据本身的干净程度比模型算法重要十倍,YOLO结构再怎么更新,都抵不上你认真排查一遍训练集里有没有漏标目标、有没有坐标越界。我自己在这条路上踩过最大的坑,就是拿到数据集后迫不及待地开训,默认的640分辨率训了50个epoch才发现小目标全漏了,回头一查才发现目标面积中位数只有十几个像素,白白浪费了时间。希望这些排过的雷、试过的参数,能帮你在拿到这个油罐数据集之后少走弯路,快速跑通一个可用模型。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询