简介:卫星遥感领域的目标检测训练集NWPU VHR-10已整理为YOLO可直接使用的格式,包含800张来自Google Earth与Vaihingen数据源的高分辨率卫星图像,手动标注覆盖飞机、轮船、储罐、棒球场、网球场、篮球场、地面跑道、港口、桥梁和车辆共10个类别,适合计算机、电子信息等专业学生用于课程设计、期末大作业或毕业设计中的目标检测实验。压缩包共2612个文件,以650张JPG图像、650个XML标注文件及1307个TXT辅助文件为主,压缩后约61.15MB,XML/VOC格式便于接入YOLOv5、YOLOv8等主流框架,TXT文件可用于标签或划分说明,另附缓存与MD说明文档。目前已有321人学习下载,资源内还包含参数化编程源码思路,可直接修改参数运行,降低上手门槛,利于快速复现与二次扩展。
1. 卫星图像目标检测的开局:一份可直接落地的 NWPU VHR-10 数据集
做目标检测的人,多半被两件事劝退过:一是找不到合适的数据集,二是数据拿到了但标注格式跟训练框架对不上。卫星图像目标检测尤其明显,自然场景的数据集好找,到了遥感图像就稀缺,能看到的要么没标注,要么类别体系不统一。这份 NWPU VHR-10 数据集解决的是这个缺口:800 张高分辨率卫星图像,来源包括 Google Earth 和 Vaihingen 数据,所有目标经过手动标注,覆盖飞机、舰船、储油罐、棒球场、网球场、篮球场、田径场、港口、桥梁、车辆十个类别。压缩包里图像和已标注文件一起给出,解压后把目录整理一下就能进入 YOLO 训练流程。对正在做遥感目标检测课程设计、期末大作业或毕业设计的同学,以及想快速验证 YOLO 改进点的从业者,这套数据能省掉大量收集和标注的时间。
2. 数据集拆解:十个类别、图像来源与 cache 文件的真相
2.1 十个类别清单与检测难点
遥感图像目标检测和自然图像检测的第一个直观区别是目标尺寸。COCO 数据集里一个目标往往占图像的 5% 到 20%,而卫星图像里的飞机、车辆、储油罐常常只占 1% 甚至不到,视觉上就是一堆十几个到几十个像素的小色块。NWPU VHR-10 的十个类别恰好覆盖了遥感场景里最典型的目标形态,类别之间既有形状差异明显的(桥梁、港口),也有形状接近、单纯靠尺寸和位置区分的(网球场与篮球场)。
我把各类别的检测重点逐一说明:
| 类别 | 英文标签 | 典型尺度 | 识别难点 |
|---|---|---|---|
| 飞机 | airplane | 中大型目标,边缘清晰 | 停机坪上密集排列,互相遮挡,白色机身与浅色地面难区分 |
| 舰船 | ship | 中型目标 | 靠岸船只与港口集装箱、码头纹理连成一片 |
| 储油罐 | storage tank | 中小型目标 | 白色圆形罐顶与周围圆形建筑顶相似 |
| 棒球场 | baseball diamond | 大型目标 | 场地内部线条复杂,菱形结构容易被切碎 |
| 网球场 | tennis court | 中型目标 | 矩形边界与周边颜色接近,分割不清晰 |
| 篮球场 | basketball court | 中型目标 | 与网球场尺寸接近,位置关系是重要线索 |
| 田径场 | ground track field | 大型目标 | 操场在分辨率低时退化成一条环形线 |
| 港口 | harbor | 大型目标 | 水域与陆地交界,岸线曲折,背景干扰大 |
| 桥梁 | bridge | 大型跨越结构 | 桥体与道路在灰度上难以区分 |
| 车辆 | vehicle | 小目标 | 像元数少,密集区域互相覆盖 |
这样的类目设计对课程设计、毕业设计的要求很友好:既包含大目标也包含小目标,既能跑简单 baseline 也能做难例挖掘。
2.2 图像来源:Google Earth 与 Vaihingen 的影像特征
数据集的图像来自 Google Earth 和 Vaihingen 两个数据源。Google Earth 影像以光学卫星影像为主,彩色丰富、地物纹理真实但分辨率不完全一致;Vaihingen 数据则偏航空遥感,灰度一致性好、边缘锐利度高。两种来源混在一起,对模型的泛化是件好事,相当于在训练阶段就做了天然的域增强,模型不至于对某一种成像质量产生过拟合。
不过这也埋了一个隐患:图像尺寸和空间分辨率并不是统一的。有的图片里车辆能有 60 到 90 像素,有的图片里只有 25 像素。因此直接套用自然图像检测的默认输入尺寸 640 会吃亏,训练时把分辨率抬到 960 或 1280,mAP 会肉眼可见地提升。遥感图像目标检测里,这一点比 anchor 设置的影响还大。
2.3 cache 文件不是标注文件:文件结构怎么认
解压资源包后,第一眼会看到几个 jpg 图像和一组 cache 文件:train.cache、test.cache、val.cache,还有名为 positive image set.cache3 的缓存。这里必须提醒一句:cache 文件不是标注文件。
Darknet 系 YOLO 训练框架在第一次扫描数据集时,会把图片路径、标签状态打包成 .cache 文件,用来加速后续启动。train.cache 对应训练集、test.cache 对应测试集、val.cache 对应验证集,里面的内容大多数是 hash 索引和标记位,不是研究者想要的坐标数据。标注文件的正确形态是:图片同名加 .txt 或 .xml。比如 084.jpg 对应 084.txt(YOLO 格式),或者 084.xml(VOC 格式)。判断方法非常简单,随机挑一张图后打开同名文件的内容,如果每行是“类别ID 中心x 中心y 宽 高”这样的五个数字,说明是 YOLO 格式,可以直接用;如果内容是<annotation>开头的 XML 结构,说明是 VOC 标注,需要转成 txt 再进入 YOLO 流程。
注意:解压后先把 cache 文件原样放好,不要手动改后缀。它们不会影响训练,如果训练时框架提示 cache 失效,直接删除让框架重新生成即可。
目标检测数据集处理的第一步,不是急着训练,而是把文件结构看明白,这个成本远比最后返工低。
3. 标注转换:把原始标注变成 YOLO 可直接训练的 txt 格式
3.1 目录规划:images 与 labels 分离
YOLO 系列框架对数据路径的约定很统一:images 和 labels 必须在同一个父目录下,而且 train、val、test 三个子目录的名字一致。这样框架才能根据图像路径自动找到对应的标签文件。
我常用的目录结构是这样的:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── data.yaml └── nwpu_vhr10_split/ ├── train.txt ├── val.txt └── test.txtimages 目录下放 jpg,labels 目录下放与之同名的 txt。Ultralytics 系列的检索逻辑是读取 images/train 下的某张图后,去同级目录找 labels/train 下的同名 txt,因此两个目录层级必须对齐。如果图片名字是 084.jpg,标签就必须是 084.txt,不能有大小写或空格差异。我习惯把划分后的路径清单也存一份到 txt 索引文件,方便后面调试时直接读取。
3.2 XML 转 TXT 脚本:坐标归一化与类别映射
如果标注文件是 XML(VOC 格式),需要先做转换。目标检测数据集处理,最繁琐的往往不是算法调参,而是把标注格式理顺。我一般会在转换前随机抽两个 XML 打开看一眼,确认 name 标签里的类名拼写,再跑下面的脚本:
import xml.etree.ElementTree as ET import os # 类别名单,顺序必须与后续 data.yaml 中的 names 完全一致 class_list = ['airplane', 'ship', 'storage tank', 'baseball diamond', 'tennis court', 'basketball court', 'ground track field', 'harbor', 'bridge', 'vehicle'] def voc_xml_to_yolo(xml_dir, out_dir): if not os.path.exists(out_dir): os.makedirs(out_dir) for xml_name in os.listdir(xml_dir): if not xml_name.endswith('.xml'): continue xml_path = os.path.join(xml_dir, xml_name) tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') if size is None: continue img_w = int(size.find('width').text) img_h = int(size.find('height').text) txt_name = os.path.splitext(xml_name)[0] + '.txt' out_lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_list: continue class_id = class_list.index(name) box = obj.find('bndbox') x1 = float(box.find('xmin').text) y1 = float(box.find('ymin').text) x2 = float(box.find('xmax').text) y2 = float(box.find('ymax').text) # 像素坐标转归一化中心坐标 + 宽高 x_center = ((x1 + x2) / 2.0) / img_w y_center = ((y1 + y2) / 2.0) / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h out_lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") if out_lines: with open(os.path.join(out_dir, txt_name), 'w') as f: f.write('\n'.join(out_lines) + '\n') voc_xml_to_yolo('Annotations', 'labels')脚本逻辑很直接:遍历 Annotations 目录下的所有 XML,读取图像宽高,把每个目标的 xmin、ymin、xmax、ymax 换算成比例值,再按 class_list 的顺序写入类别索引。这里最关键的是 class_list 必须和最终训练的 data.yaml 对齐,否则会出现模型把所有类别都识别成错位野框的情况。另外注意,VOC 坐标如果出现越界,有些标注会超出图像边界一点,这个脚本不会做裁剪,建议转换后检查一下是否出现 w 或 h 大于 1.0 的行。
3.3 数据集划分:按类别均衡地划分训练、验证、测试
800 张图不算多,如果随机打乱划分,可能把某一类目标全部留到一个集合里。稳妥的做法是把类别信息也纳入划分逻辑:
import os import random from collections import defaultdict random.seed(42) label_dir = 'labels' image_dir = 'images' train_ratio, val_ratio = 0.7, 0.15 # 按类别收集样本:一张图含多个类别时,会同时记入多个桶 samples_by_class = defaultdict(list) for f in os.listdir(label_dir): if not f.endswith('.txt'): continue with open(os.path.join(label_dir, f)) as fd: classes = {line.split()[0] for line in fd if line.strip()} for c in classes: samples_by_class[c].append(f) # 每个类别单独按比例抽取,最后取并集 train_set, val_set, test_set = set(), set(), set() for c, files in samples_by_class.items(): random.shuffle(files) n_train = int(len(files) * train_ratio) n_val = int(len(files) * val_ratio) train_set.update(files[:n_train]) val_set.update(files[n_train:n_train + n_val]) test_set.update(files[n_train + n_val:]) for split, sample_set in zip(['train', 'val', 'test'], [train_set, val_set, test_set]): with open(f'{split}.txt', 'w') as fd: for img_name in sorted(sample_set): fd.write(os.path.join(image_dir, img_name.replace('.txt', '.jpg')) + '\n')这段脚本把每个类别的样本单独分桶,先按 7:1.5:1.5 的比例抽取,再把多个桶的并集写入最终的 txt 索引。使用 set 是为了避免一张包含多个类别的图片被重复写入。缺点是类别之间可能有少量重叠,导致 train_set 和 val_set 同时出现同一张图,这是可以接受的,因为样本总量不大。如果不希望重叠,可以用更严格的按文件主键划分,但代价是某些小类别在验证集中数量过少,评估时数值波动会很大。
提示:上面的脚本把划分结果写成了 txt 索引,如果你更习惯目录式管理,也可以直接把 train/val/test 这类索引路径写进 data.yaml,不强制复制图片。
4. 训练实战:YOLOv5 环境配置、超参数设定与日志判断
4.1 环境准备与 data.yaml 配置
以 YOLOv5 为例,这是绝大多数课程设计和毕业设计最容易上手的版本。先用 yolo 预训练模型下载和官方仓库把环境准备好:
git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txtYOLOv5 的依赖以 PyTorch 和 OpenCV 为核心,requirements.txt 里已经把 torch、opencv-python、matplotlib 这些常用库封装好了。目录规划完成之后,把数据配置写在 data.yaml 里:
# dataset/nwpu_vhr10.yaml train: dataset/images/train val: dataset/images/val test: dataset/images/test nc: 10 names: ['airplane', 'ship', 'storage tank', 'baseball diamond', 'tennis court', 'basketball court', 'ground track field', 'harbor', 'bridge', 'vehicle']train、val、test 路径以你执行训练命令的工作目录为参考,我习惯把 data.yaml 放在 dataset 目录下,路径相对书写。names 列表的顺序必须和第 3 章转换脚本里的 class_list 一致,否则类别索引错位后,你会发现飞机被标成储油罐且很难排查。类名里有空格的如 storage tank,在 YOLOv5 里处理正常,但部分可视化脚本在展示时可能截断,建议用下划线代替,写成 storage_tank。
4.2 训练命令解析与核心超参数
数据配置准备好后,训练代码这样启动:
python train.py \ --data dataset/nwpu_vhr10.yaml \ --weights yolov5s.pt \ --img 1280 \ --batch 8 \ --epochs 150 \ --hyp data/hyps/hyp.scratch-low.yaml \ --name nwpu_vhr10逐项说明参数的意义:
- --weights 使用官方预训练权重 yolov5s.pt。第一次运行会自动下载,这份权重会给模型一个很强的初始特征,训练收敛速度明显比从头训练快,这也是答辩时可以说清的一个细节。
- --img 1280 是遥感数据集最重要的设置。目标太小,输入分辨率提到 1280 比加深模型更容易带来收益。显存不够时可以降到 960,但别用 640 起步。
- --batch 8 是 8G 显存下的中间值。如果用的是 24G 显存卡,直接提到 16,同一份数据下能更早收敛;显存不足时优先降 batch,而不是急着换大模型。
- --epochs 150 是相对保守的值。800 张图的规模,100 到 150 轮足够看到 mAP 曲线变平甚至略降,继续加轮数意义不大。
- --hyp hyp.scratch-low.yaml 使用低学习率曲线,适合少量数据,避免早期训练发散。
如果数据集目标尺寸分布与 COCO 差距很大,YOLOv5 会在训练前做 anchor 自适应计算,默认开关保留即可。想确认目标尺寸分布,可以先跑几步观察日志里打印的 anchor 与当前数据集的匹配程度。
4.3 训练日志判断:YOLO 损失函数、mAP 与早停
训练时屏幕每隔一段时间刷新一条状态,我习惯关注这四个维度:
Epoch gpu_mem box obj cls labels img_size 89/150 7.83G 0.0421 0.0214 0.0253 18 1280box 是包围框回归损失,obj 是置信度损失,cls 是分类损失。YOLO 的损失函数就是这三部分的加权组合,遥感图像目标检测里 box 通常比自然场景下降更慢,因为小目标对框的精确回归难度大。三条曲线应该在 100 轮内稳定下行,如果某一项在后期不降反升,说明增强太强或者学习率需要继续衰减。
训练结束后,重点看两个指标:
- mAP@0.5:IoU 阈值 0.5 下的平均精度,课程设计主要用这个指标,能到 0.85 以上就是不错的 baseline。
- mAP@0.5:0.95:对框质量要求更严格,如果这个值和 mAP@0.5 差距明显,说明定位精度不够,优先加大 --img 而不是加深网络。
YOLOv5 会在 runs/train/nwpu_vhr10/weights 下保留 best.pt 和 last.pt。best.pt 是验证集上 mAP 最高的权重,测试和复现都用它。如果训练中断想接着跑,直接在命令里--weights runs/train/nwpu_vhr10/weights/last.pt --resume就行,不用重新从预训练开始。
5. 卫星图像目标检测避坑指南:五个高频翻车点
5.1 现象一:cache 文件被误认成标注文件
现象:解压后没找到 txt 标注,看到 train.cache、test.cache 这些文件,以为改名成 txt 就能用。训练直接报错“无法读取标注”,或者不报错但训练出来的模型几乎把所有区域判断为背景。
原因:cache 文件是训练框架的路径缓存,里面没有坐标内容。它存的是图片路径、hash、标签是否完整的标记位,强行改名后框架解析不到合法文本,自然读不到目标信息。
解决:先确认资源包里是否有与 jpg 同名的 xml 或 txt。有 xml 就走第 3 章的转换脚本;有 txt 就检查每行是否符合“类别ID 中心x 中心y 宽 高”的格式。cache 文件原样保留,训练时框架会自动重建,不需要人工维护。
5.2 现象二:小目标漏检,飞机和车辆检不出来
现象:训练过程损失下降正常,但 val 结果里 airplane 和 vehicle 的召回率很低,一张有 18 架飞机的停机坪图片只检出 6 架。
原因:卫星图像里的飞机车辆往往只有几十像素,输入分辨率 640 时目标缩到 10 像素左右,特征在骨干网络下采样时丢失严重。
解决:把 --img 提到 1280,配合 batch 降到 8 或 4;如果显存仍然吃紧,用切片思路做训练前分块。另一个有效操作是让框架重新计算 anchor,autoanchor 默认对 640 输入是合理的,但 1280 输入下目标尺寸分布会变,重新计算是标准操作。先在日志里看一眼 anchor 的输出值,再决定是否调整。
5.3 现象三:类别不平衡造成 mAP 一边高一边低
现象:训练结束后,车辆、储油罐这类目标多的类别 mAP 很高,棒球场、桥梁这类目标少的类别 mAP 明显偏低,指标波动很大。
原因:NWPU VHR-10 虽然总标注质量高,但十个类别样本数量并不均衡,车辆目标数以千计,桥梁可能只有几十个。模型对少数类拟合不足,评估时样本少也导致数值方差大。
解决:训练前用第 3 章的统计代码打印每个类别的目标数量,对少数类做拼接增强或复制粘贴增强,多加入一些包含该类别的训练样本。也可以对损失加权,比如给少数类更高的 cls 权重,但课程设计阶段用数据增强更直接,代码侵入性更小。
5.4 现象四:验证 mAP 高,整幅大图推理却翻车
现象:val 里 mAP@0.5 有 0.88,信心满满地拿单张 4000×4000 的遥感大图进 detect.py,结果检测框稀疏,很多小目标完全没出现。
原因:训练和验证都是在切好的小图上进行的,整幅大图直接缩放后会变成 1/3 甚至 1/6 的缩略图,小目标信息被急剧压缩,原有的特征尺度失效。
解决:推理阶段采用切片推理,把一张大图切成 1280×1280 的窗口,重叠率设置 0.2,逐窗推理后再做 NMS 合并。常见工具直接用 SAHI,省去自己写滑动窗口的时间。具体参数放第 6 章,这里先记住结论:训练怎么切,推理就怎么切。
5.5 现象五:loss 变 nan 或 BN 参数崩溃
现象:训练到 30 轮到 40 轮,box_loss 突然变成 nan,后续怎么调 batch 和迭代都不恢复。查看模型权重发现 BN 层统计量异常,推理输出无数个低置信度框。
原因:常见诱因有三个:学习率初始值过高,碰到个别样本的尺寸或标签异常,导致梯度爆炸;数据里有损坏的 jpg,读取失败但 num_workers 没报错;空标签文件或坐标越界到负值,让回归目标计算出现异常值。
解决:先把 hyp 文件里的 lr0 调低一半,比如从 0.01 降到 0.005,重新跑。然后遍历一遍 labels 目录,过滤零字节 txt 和 w/h 大于 1.0 或小于 0 的行。最后用图像解码库逐个检查 jpg 是否能正常打开。这类问题靠调参基本救不回来,必须回到数据层面,已经不是玄学能解决的范畴。
注意:这三个诱因按出现概率排序,实际排查时先查数据再调学习率,比盲目改超参省时间。
6. 验证与进阶:多尺度测试、混淆矩阵与大图切片推理
6.1 多尺度验证:--augment 与输入分辨率
训练完先别急着拿 val 结果交差,遥感目标尺寸范围大,多尺度测试往往能多救回几个点。YOLOv5 的 val.py 自带 augment 开关:
python val.py \ --data dataset/nwpu_vhr10.yaml \ --weights runs/train/nwpu_vhr10/weights/best.pt \ --img 1280 \ --augment--augment 在推理阶段做多尺度融合,把输入缩放几个尺寸再取平均,相当于 test-time augmentation。配合 --img 1280 以后,mAP 通常会有 1 到 2 个百分点的提升,虽然推理速度慢一些,但用于课程设计的核心指标足够划算。
6.2 混淆矩阵与错误分析
YOLOv5 在 runs/train/nwpu_vhr10 下会生成 confusion_matrix.png。遥感数据集里最容易出现两类问题:车辆被误判成飞机,或者小型储油罐被误判成车辆,因为灰度上都是亮色圆形或矩形目标。另外要注意,混淆矩阵的归一化数值按行计算,总和不一定等于 100%,这是 YOLO 训练框架的常见表现,不是代码错误。
看懂混淆矩阵之后,下一步该做的不是立刻调模型,而是把误判最多的两个类别拉出来做针对性数据增强。比如对误判成飞机的车辆样本做亮度扰动,把模型注意力从颜色拉回到几何形状。在课程设计答辩时,能讲清楚这一步,给人留下的印象比单纯报一个 mAP 数字扎实很多。
6.3 大图切片推理:把训练技巧延伸到推理阶段
最后给一个能直接落地的推理思路。对大尺寸遥感图,我习惯用 SAHI 这类现成库,参数封装得比较直观:
from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction model = AutoDetectionModel.from_pretrained( model_type="yolov5", model_path="runs/train/nwpu_vhr10/weights/best.pt", confidence_threshold=0.35, image_size=1280, ) result = get_sliced_prediction( image="large_scene.png", detection_model=model, slice_height=1280, slice_width=1280, overlap_height_ratio=0.2, overlap_width_ratio=0.2, ) result.export_visuals(export_dir="output/")这里的主要参数是切片尺寸和重叠率。切片尺寸与训练输入保持一致,效果最好;重叠率 0.2 到 0.3 可以避免目标被切在两片边缘时整体丢失。切片推理的缺点是把单张推理变成了几十片推理,速度明显变慢,但换来的是小目标召回率的稳定提升。如果项目只做离线检测,这个成本完全可以接受。
从那以后,我跑遥感目标检测任务多了一道规范流程:训练完先多尺度验证,再瞄一眼混淆矩阵,最后拿整幅大图切片推理一次,确认模型在真实任务尺度下真能工作,而不是只在评估集上好看。这套流程帮我挡掉过不少答辩现场的尴尬问题,希望帮到你。
本文还有配套的精品资源,点击获取