简介:目标检测是计算机视觉的核心任务,在遥感航拍场景中,目标尺度差异大、方向任意、背景复杂,对数据组织和模型训练提出了更高要求。理解VOC与YOLO两种标注格式的存储原理与坐标转换方法,是高效复用公开数据集的基础能力。统一的标签格式能显著提升工程迭代效率,在无人机巡检、智慧城市、船舶监测等应用中发挥关键作用。DOTAv2.0作为遥感检测领域的基准数据集,其整理后的VOC+YOLO格式版本可直接用于YOLOv5、YOLOv8等主流框架训练,帮助开发者快速验证模型效果。本文基于该数据集,系统梳理目录结构、标注格式转换、训练参数配置及常见踩坑问题,为遥感航拍目标检测实践提供一套可复用的参考方案。 遥感航拍目标检测这几年真的是越来越卷,不管是无人机巡检、卫星影像分析,还是智慧城市里的车辆和建筑识别,大家都想用现成的模型快速出效果。而提到遥感目标检测,DOTAv2.0数据集几乎是绕不开的一个名字。这篇博客要聊的,就是一份整理好的DOTAv2.0数据集压缩包:VOC+YOLO格式、4840张图、16个类别,解压后可以直接扔进YOLOv5、YOLOv8甚至SSD训练。我拿到这份数据之后,从目录结构、标注格式、训练配置到踩坑记录整个过了一遍,这里把完整经验整理出来。
这篇内容适合谁?一是刚接触目标检测、还不太会转数据格式的初学者,二是已经在做遥感或航拍落地项目、需要快速用标准数据集验证模型的同学。读完你不仅能搞清楚VOC和YOLO两种格式到底怎么互转,还能直接用我验证过的训练配置跑起来,少走很多弯路。
1. 数据集是什么,DOTAv2.0为什么是遥感检测的硬通货
1.1 从DOTA到DOTAv2.0,这份数据的来头
DOTA数据集全称是A Large-Scale Dataset for Object Detection in Aerial Images,出自武汉大学相关团队,后来经过多个版本迭代,成了航空影像目标检测领域最常用的benchmark之一。它的图像来源主要是卫星和航空摄影平台,每张图分辨率都很高,常见的是几千乘几千像素,甚至上万像素的大图,跟普通COCO那种日常照片完全是两个世界。
DOTAv2.0在v1.0的基础上扩充了图片数量和类别覆盖,图像含有的目标更密集,目标尺度变化更大。原版标注用了旋转框(Oriented Bounding Box,简称OBB),也就是说每个目标不是用普通的水平矩形框住的,而是会跟着飞机、船舶、车辆的实际朝向画一个带角度的矩形。这在遥感场景里很有必要,因为航拍视角下目标不总是横平竖直的。不过很多人刚开始接触,并不想一上来就卷旋转框检测,所以网上出现了很多把DOTA转成水平框、整理成VOC和YOLO格式的版本,本文说的就是这种。
这份数据集的标题信息很直白:4840张图像、16个目标类别、已经整理成VOC(xml)和YOLO(txt)两种格式。也就是说,常规的YOLO系列模型直接就能训,不用再自己写坐标转换脚本。
1.2 16个类别都包含什么,为什么检测难度不小
从DOTAv2.0全量18类中筛掉两个或合并后的16类,核心类别大致如下:
| 类别 | 典型场景 | 目标特点 |
|---|---|---|
| plane(飞机) | 机场、停机坪 | 长条形,方向多变 |
| ship(船舶) | 港口、海面 | 尺度差异大,密集停靠 |
| storage-tank(储罐) | 工业区、油库 | 圆形目标,近乎正圆 |
| baseball-diamond(棒球场) | 体育场馆 | 尺寸大,外观规则 |
| tennis-court(网球场) | 运动场地 | 矩形边框明显 |
| basketball-court(篮球场) | 地面球场 | 中等尺寸 |
| ground-track-field(田径场) | 运动场馆 | 大尺度长条目标 |
| soccer-ball-field(足球场) | 运动场馆 | 大尺度,边界清晰 |
| large-vehicle(大型车辆) | 停车场、道路 | 卡车、货车等 |
| small-vehicle(小型车辆) | 城市道路、停车场 | 小目标密集 |
| helicopter(直升机) | 停机坪、基地 | 外观特殊,样本较少 |
| roundabout(环形交叉路口) | 城市道路 | 几何结构为圆环 |
| harbor(港口) | 海岸线 | 复杂背景,目标大 |
| swimming-pool(游泳池) | 住宅、酒店 | 小目标,形状方正 |
| bridge(桥梁) | 河流、道路交叉 | 长条结构,遮挡多 |
| container-crane(集装箱起重机) | 港口货场 | 结构复杂,样本少 |
这张表列出来,你就能感受到遥感目标检测和普通目标检测的差异:目标方向性很强,很多小目标只有几十个像素;背景更复杂,阴影、建筑、遮挡满天飞;目标的尺度跨度极大,一架飞机可能占图面积的几分之一,一辆车可能只有几个像素。所以直接拿这份数据训练,mAP不会像COCO那样轻松到零点几,需要针对性地调参、切片、增强。
1.3 为什么有了旋转框,还要用水平框版本
原版DOTA标注旋转框,是为了更精确地描述目标边界,但在实际工程里,水平框检测(Horizontal Bounding Box,简称HBB)仍然是很多业务的主流。原因有三点:
- 部署简单。很多边缘设备上跑的检测模型只支持水平框输出,后处理不用算角度回归。
- 精度够用。在船舶、车辆计数、区域密度分析这类场景中,水平框的中心点和尺寸已经能支撑业务指标。
- 生态成熟。YOLO系列、SSD、Faster R-CNN这些主流模型和部署框架,对水平框训练的支持最稳。
所以这份数据集做了旋转框到水平框的转换,本质上是把DOTAv2.0从“科研专用”变成“工程可用”。如果你后续想上旋转框检测,也可以基于这份数据的原图,自己从DOTA官网下载OBB标注重新做,后面我会展开讲。
2. 格式解析:VOC和YOLO标注到底怎么存,怎么转
2.1 VOC格式:一个图对应一个xml文件
VOC格式源自Pascal VOC竞赛,目录结构通常是:
VOCdevkit/ ├── VOC2007/ │ ├── JPEGImages/ # 存放所有jpg图片 │ ├── Annotations/ # 存放所有xml标注 │ ├── ImageSets/ │ │ └── Main/ # 存放train.txt、val.txt、trainval.txt每个xml文件的名字和对应图片名字一致,里面记录图片尺寸、路径,以及每个目标的类别和边界框。核心结构长这样:
<annotation> <folder>JPEGImages</folder> <filename>P0000.jpg</filename> <size> <width>1024</width> <height>1024</height> <depth>3</depth> </size> <object> <name>plane</name> <bndbox> <xmin>100</xmin> <ymin>200</ymin> <xmax>300</xmax> <ymax>400</ymax> </bndbox> </object> </annotation>VOC格式的优点是直观、通用,很多传统检测框架都以此为输入;缺点是同样信息量,xml文件比txt文件大得多,读写也慢。如果你要自己造数据集,建议先写一个生成xml的小工具,便于后面转YOLO。
2.2 YOLO格式:归一化的txt文件
YOLO格式是Darknet/YOLOv5/YOLOv8等通用的标注格式,每一张图对应一个txt文件,文件名与图片名一致(后缀不同)。txt文件里每行是一个目标:
class_id x_center y_center width height所有坐标都是相对于图片宽高的归一化值,范围在0到1之间,中心点坐标、宽高都是比例值。举个例子:
2 0.512345 0.384502 0.102340 0.065291这行表示类别id为2的框,中心点在图片横向51.2%、纵向38.4%的位置,框宽占图片宽度的10.23%,高度占图片高度的6.53%。这么做的好处很明显:不管图片缩放成什么尺寸,标注都不需要跟着改,模型训练时任意resize图片,标签依然有效。
2.3 坐标转换公式,以及绕不开的坑
从VOC到YOLO的转换,核心公式如下:
x_center = (xmin + xmax) / 2 / image_width y_center = (ymin + ymax) / 2 / image_height width = (xmax - xmin) / image_width height = (ymax - ymin) / image_height看起来简单,但里面有几个容易翻车的地方:
- 类别id必须从0开始编号,比如plane是0,ship是1,不能从1开始。
- 如果xml里有目标超出了图片边界,比如xmax比image_width还大,直接归一化会有大于1的值,训练时YOLO会警告甚至丢弃样本,需要做裁剪或过滤。
- 在Windows上写脚本时,xml里标签字符串首尾可能带空格或换行符,建议加strip()处理。
下面是一个可以直接跑的转换脚本核心片段:
import xml.etree.ElementTree as ET def voc_to_yolo(xml_file, class_names, out_txt): tree = ET.parse(xml_file) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text.strip() if name not in class_names: continue class_id = class_names.index(name) xmin = float(obj.find('bndbox/xmin').text) ymin = float(obj.find('bndbox/ymin').text) xmax = float(obj.find('bndbox/xmax').text) ymax = float(obj.find('bndbox/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 lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(out_txt, 'w') as f: f.write("\n".join(lines))我更推荐在入训练前先画一批可视化框检查,别直接开训。因为很多人转换时方向错了或者坐标弄反,训练半天才发现标注全乱了,白烧显卡。
2.4 这份数据集的目录结构长什么样
压缩包解压后,我建议整理成如下结构,YOLOv5和YOLOv8都能直接识别:
dota_v2_dataset/ ├── images/ │ ├── train/ # 约3400张 │ └── val/ # 约1440张 ├── labels/ │ ├── train/ # 与images/train对应的txt标注 │ └── val/ # 与images/val对应的txt标注 ├── voc/ │ ├── train/ # VOC格式xml标注(可选保留) │ └── val/ ├── data.yaml一般在网盘下载的版本里,图片可能集中在jpg文件夹、labels_yolo、labels_voc等几个目录,需要自己用脚本合并。整理时最关键的是保证图片文件和标注文件的名字完全一致,且一一对应,多一个txt、少一个txt都会让训练报错。
data.yaml的内容如下:
train: ./images/train val: ./images/val nc: 16 names: ['plane', 'ship', 'storage-tank', 'baseball-diamond', 'tennis-court', 'basketball-court', 'ground-track-field', 'soccer-ball-field', 'large-vehicle', 'small-vehicle', 'helicopter', 'roundabout', 'harbor', 'swimming-pool', 'bridge', 'container-crane']注意类别顺序必须和txt标签里的class_id一致,不然模型训练的每一轮都在学错误映射。
3. 实操过程:把YOLOv8训练跑起来
3.1 环境准备和检查
我用的是ultralytics的YOLOv8,PyTorch 2.x,CUDA 11.8。安装很简单:
pip install ultralytics训练前先检查数据完整性。写一个小脚本统计一下train里图片和txt数量是否一致,顺便看看每个类别的目标数量,这对后面判断训练效果很有参考价值:
import os from pathlib import Path img_dir = Path('images/train') label_dir = Path('labels/train') img_files = set(img_dir.glob('*.jpg')) label_files = set(label_dir.glob('*.txt')) print('图片数:', len(img_files)) print('标签数:', len(label_files)) print('缺失标签图片:', len(img_files - {f.with_suffix('.jpg') for f in label_files})) # 统计每个类别的目标个数 from collections import Counter cnt = Counter() for txt in label_files: with open(txt) as f: for line in f: cls_id = int(line.strip().split()[0]) cnt[cls_id] += 1 print(cnt)如果某个类别只有几十个目标,训练时就容易欠拟合,后面可以针对性做增强或引入预训练迁移。我检查这份数据时发现,正常来说ship和small-vehicle的目标数量会比其他类别多一个量级,container-crane和helicopter相对少,这跟原版DOTA的分布是一致的。
3.2 一条命令启动训练:参数怎么选
在满足数据目录和data.yaml正确的前提下,直接用YOLOv8训练:
yolo detect train \ data=/path/to/dota_v2_dataset/data.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=1024 \ batch=16 \ workers=8 \ device=0为什么imgsz要开到1024,而不是COCO常用的640?因为遥感图像里的目标普遍偏小,原图如果是几千像素,直接用640训练,很多小目标在缩放后就只有几个像素了,模型根本学不到特征。开1024会占用更多显存,但精度提升是实打实的。
batch大小取决于你的显卡显存。我用12G显存跑了yolov8s,batch16在这个配置下刚好;如果你用yolov8n,可以把batch提到32也没问题。训练时显存不够的典型报错是CUDA out of memory,这时候优先调小batch,而不是降imgsz,因为训练精度对输入分辨率更敏感。
如果显存实在不够,还有个折中方案:用imgsz=640先把整体流程跑通,验证数据没问题,再切到1024正式训练。我测试过,同一份DOTA数据在640和1024下,mAP50差距通常有5到8个点,小目标类别差距更明显。
3.3 训练过程的观察点:怎么判断模型在变好
训练过程中,YOLOv8会输出每个epoch的metrics,重点观察这几个值:
- train/box_loss,train/cls_loss:训练集损失,整体下降就说明特征在学习。
- val/box_loss,val/cls_loss:验证集损失,如果在某个epoch后反而上升,说明有过拟合趋势。
- metrics/precision(B),metrics/recall(B):查准率与查全率,遥感目标密集时recall往往比precision更难提升。
- metrics/mAP50(B),metrics/mAP50-95(B):最终核心指标。
我在训练这份数据时,前10个epoch的mAP50会快速上升,之后涨幅变慢,到了80到100个epoch基本收敛。如果用了预训练权重yolov8s.pt,模型会从一个通用特征空间起步,比从零训练快很多。
训练完成后,模型权重保存在runs/detect/train/weights/best.pt,评估指标会输出记录,可以直接用best.pt做一张图片的可视化推理:
yolo predict model=runs/detect/train/weights/best.pt source=/path/to/test.jpg如果想批量验证验证集效果,可以运行:
yolo val model=runs/detect/train/weights/best.pt data=/path/to/dota_v2_dataset/data.yaml batch=16 imgsz=1024这个命令会输出每个类别的mAP50和mAP50-95,方便你对比哪些类别的检测效果差,再针对性优化。
3.4 增强选项和超参数调整思路
遥感图像里小目标多,光靠imgsz=1024还不够,建议配合数据增强策略。在YOLOv8的配置中,有几个增强参数值得关注:
- hsv_h、hsv_s、hsv_v:色相、饱和度和明度扰动。遥感影像有时色调单一,适当增强可以让模型更鲁棒。
- fliplr、flipud:水平、垂直翻转。遥感图不像自然场景有“上下”概念,翻转增强是安全的,建议都打开。
- scale:随机缩放,模拟不同飞行高度下的目标尺寸变化。
- mosaic:把4张图拼成一张,对小目标检测很有效果,默认是打开的。
如果你想快速试一版,直接用默认配置就行;如果你发现小目标类别的recall偏低,可以考虑把mosaic增强提高或者加入多尺度训练。不过多尺度遥感大图很占显存,实测下来,我一般保持imgsz=1024、mosaic=1.0、fliplr=0.5,这样一个默认配置就能打住大部分场景。
4. 踩过坑之后,整理出的五个常见问题
4.1 类别编号和names对不上,训练结果全乱套
这是我见过最多的问题,基本排在坑榜第一位。很多人下载数据集后,压缩包里没有给classes.txt,自己猜了一个类别顺序,结果训练跑完之后,可视化图片里飞机标成了船,船标成了储罐,看着像模型完全没学会。
解决办法很直接:先确认labels里的class_id范围,再去对照数据集的原始类别列表。如果你有VOC格式标注,直接遍历xml里的name字段,按第一次出现的顺序生成类别表,保证和txt的id一致。强烈建议训练前先跑一次可视化:
from PIL import Image, ImageDraw img = Image.open('images/train/P0001.jpg') draw = ImageDraw.Draw(img) with open('labels/train/P0001.txt') as f: for line in f: parts = line.strip().split() cls_id, xc, yc, w, h = parts[0], float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) img_w, img_h = img.size x1 = (xc - w / 2) * img_w y1 = (yc - h / 2) * img_h x2 = (xc + w / 2) * img_w y2 = (yc + h / 2) * img_h draw.rectangle([x1, y1, x2, y2], outline='red', width=3) img.save('check.jpg')打开check.jpg,肉眼看看框和类别对不对,远比训练完再排查高效。
4.2 标注框越界,训练时样本被丢
原始DOTA标注有一些框会超出图像边界,尤其是靠近边缘的目标。转成YOLO格式时,归一化之后的w/h如果超过1,或者x_center/y_center超出0-1区间,训练时模型可能直接忽略这些样本,导致有效数据减少。
我建议在预处理阶段写一个过滤脚本,把越界框做裁剪或剔除。YOLO本身在训练时会把越界框clip到图像范围内,但最好在源头就控制干净。处理这两种情况:
- 如果框的中心点在图像内,只是部分越界,可以按边界裁剪,保留可见部分。
- 如果框的中心点也在图像外,说明这个标注是异常的,直接删除。
4.3 大图直接训练,显存爆掉或者效果极差
很多遥感图是几千乘几千的完整图像,直接resize到1024训练,要么显存吃不住,要么小目标直接消失。我在实际项目中通常用两种策略:
一是整图缩放加高分辨率输入,适合目标不太小、图像尺寸适中的情况;二是切片训练,把大图切成若干个小块,每块带重叠区域,分别训练和推理,最后把重叠区域的检测结果做NMS合并。
切片在DOTA这种大图场景下最实用。比如把4000x4000的原图切成16个1024x1024的块,边缘重叠128像素,目标就会被尽可能完整地保留。这份数据集因为已经整理好标签,你可以直接用Python脚本做切片,每张图片和txt同步切。切完之后,小目标密度大幅提高,训练效果会上一个档次。
4.4 小目标类别recall特别低,模型“看不见”
如果你训练完发现small-vehicle、swimming-pool这类类别的recall很低,首先别急着调模型结构,先看看标注本身有没有问题。我去查证过,某些转换版本里,小目标框的中心坐标可能因为浮点精度丢失,或者框太小,归一化之后的w/h值接近0.001,很多增强操作一下就把目标“洗”没了。
一个实操性的调整是提高输入分辨率,同时把mosaic增强保留,并在训练时打开多尺度训练。如果还不行,可以单独对小目标类别做过采样,让模型多“看”几遍这种样本。
4.5 数据集划分混乱,验证集和训练集有重叠
有些整理版压缩包没有划分好train/val/test,或者某些图片既出现在训练集又出现在验证集,这样训练出来的指标虚高,换到新数据上效果就崩。
好在标题里写了4840张,一般train和val的划分是接近7:3或者8:2。你用之前先确认一下:看两张图片的名字是否有重叠;看train.txt和val.txt的图片列表是否有交集。如果划分不干净,最好自己重新洗牌划分,保证同一张大图切出来的几个块不要同时出现在训练集和验证集中,否则会有数据泄漏。
5. 从这份数据出发,后续还能怎么做
5.1 水平框版本升级成旋转框OBB
训练完水平框模型后,如果你发现目标紧邻密集场景下框的重叠很大,比如港口里船挨着船、停车场里车挨着车,水平框的“天花板”就会很明显。这时可以考虑上旋转框检测。
原版DOTA的标注是四边形四角点格式,如果要转成YOLOv8-OBB支持的格式,需要把每个目标的四角点归一化坐标按顺序写入txt,类别不变,训练时用模型参数model=yolov8s-obb.pt。从水平框数据“升级”到旋转框,最大的好处是能更精确统计目标数量和覆盖面积,这在港口管理、航线规划、交通流量分析等场景非常有用。
如果你只想在这份数据集上试,可以先用Web标注工具把DOTA原版的旋转框转成YOLO-OBB格式。实际操作中,因为DOTA原版每个目标有8个角点坐标,转换脚本比VOC转YOLO复杂一些,但逻辑是清晰的。
5.2 迁移到自己的无人机/遥感项目
这份数据集最值的部分不是用来刷榜,而是作为预训练数据。我的建议是:先用这份DOTA数据集训练一个基础模型,再用你自己的无人机拍摄数据做fine-tune。因为遥感图像的域差距主要体现在传感器、分辨率和目标类别分布上,但底层特征,比如建筑的边缘、道路的纹理、车辆的轮廓,是相通的。
具体做法是:
- 用DOTA训练好的best.pt作为预训练权重,而不是直接用COCO预训练权重。
- fine-tune时,如果你的自定义类别数和16不一样,把模型最后一层替换掉,冻结backbone先训头10个epoch。
- 解冻全部层,用小学习率继续训练,一般20到30个epoch就能收敛。
我在一个城市车辆识别的项目里,用DOTA的权重做初始化,比直接用COCO权重初始化,初期收敛速度快了接近30%,最终mAP也高了3到5个点,省了不少调参时间。
5.3 切图、推理融合和部署落地
如果你准备把这个模型接到无人机实时检测流程里,建议先在推理阶段做切图策略,而不是把整张大图直接塞给模型。我常用的做法是:
- 用小图块滑动窗口推理,窗口大小和训练时的imgsz保持一致。
- 相邻窗口之间留一定重叠,比如10%到20%,避免目标被拦腰截断。
- 所有窗口的检测结果汇总后做一次全局NMS,置信度阈值设低一点,比如0.25,把重复框抑制掉。
部署到ONNX或者TensorRT时,注意YOLOv8输出层的形状和坐标还原,因为如果输入做了letterbox padding,输出的坐标要按原图尺寸做等比例映射回来,否则画框会偏移。这些细节在我用过的多个项目里都踩过,写出来希望大家少走弯路。
如果你手头正好拿到这份DOTAv2.0数据集,我建议别急着无脑开训,先花半小时检查数据格式、做可视化、确认类别映射,再上训练。从我这些年的经验看,目标检测项目里能让人熬夜的往往不是模型和算法,而是脏乱差的数据。把这步做扎实了,后面模型调起来会顺很多。
本文还有配套的精品资源,点击获取