简介:这份资源面向计算机、电子信息工程、数学等专业的学生与算法初学者,提供一套可直接投入训练的遥感目标检测数据集,解决自建数据标注耗时、格式不统一的问题。数据源自NWPU VHR-10,覆盖飞机、轮船、储罐、棒球场、网球场、篮球场、地面跑道、港口、桥梁和车辆共10个类别,图像与标注一一对应,省去清洗与转换环节。压缩包共1600个文件,由800张jpg图像与800个xml标注文件组成,xml可直接对接YOLO等主流检测框架的解析流程,整体约73.71MB,体积轻便便于本地加载与迁移。目前已有1395人学习下载,适合课程设计、期末大作业与毕业设计等场景,读者可据此快速搭建训练与验证流程,把精力集中在模型调参、结构改进与结果分析上,也可作为遥感检测入门到进阶的练手素材。
1. 遥感目标检测为什么总在“小目标”上翻车:从 NWPU VHR-10 说起
拿到一份标注好的遥感数据集,第一反应往往是“直接上 YOLO 跑一遍不就行了”。真跑起来才发现,飞机、储油罐、棒球场这些目标在 800 张图像里分布极不均衡,小目标密集、背景复杂、尺度跨度大,模型很容易把操场认成网球场,把港口里的船漏掉一半。NWPU VHR-10 就是这样一个典型的遥感检测数据集:10 个类别,包含飞机、舰船、储油罐、棒球场、网球场、篮球场、田径场、港口、桥梁、车辆,图像来自高分辨率卫星和航空影像,每张图都有对应的 xml 标注文件。它适合谁?适合已经跑通过 COCO 或 VOC 上 YOLO 流程、想切进遥感目标检测方向的工程师,也适合做毕设或课程项目、需要一份“拿来就能训”的标注数据的学生。这一章先把这份数据的脾气讲清楚,后面再动手。
2. 把 NWPU VHR-10 的 xml 喂给 YOLO:格式转换与目录组织
2.1 先看清 xml 里到底标了什么
NWPU VHR-10 的标注文件是标准 Pascal VOC 风格的 xml,每个文件对应一张图像,内部结构大致如下:
<annotation> <folder>positive</folder> <filename>001.jpg</filename> <size> <width>1024</width> <height>768</height> <depth>3</depth> </size> <object> <name>airplane</name> <bndbox> <xmin>120</xmin> <ymin>45</ymin> <xmax>260</xmax> <ymax>180</ymax> </bndbox> </object> <!-- 可能还有多个 object --> </annotation>关键点有三个:一是<name>里的类别名是英文,且不同来源的 xml 可能大小写不一致;二是<bndbox>的坐标是左上角和右下角的绝对像素值,不是归一化坐标;三是有些图像可能包含多个同类目标,转换时要逐个 object 处理,不能只取第一个。我一般会先写一个统计脚本,把 800 张图里每个类别的框数量、宽高分布、是否有空标注跑一遍,心里有数再转格式。
2.2 转成 YOLO 需要的 txt:脚本与四个边界坑
YOLO 训练需要的是每张图一个 txt,每行class_id x_center y_center width height,全部归一化到 0~1。下面这个脚本是我常用的转换逻辑,直接改路径就能跑:
import os import xml.etree.ElementTree as ET # 类别顺序必须和训练时的 data.yaml 完全一致 CLASSES = ['airplane', 'ship', 'storage_tank', 'baseball_diamond', 'tennis_court', 'basketball_court', 'ground_track_field', 'harbor', 'bridge', 'vehicle'] CLASS_MAP = {name: i for i, name in enumerate(CLASSES)} def convert_xml_to_txt(xml_path, txt_path, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall('object'): name = obj.find('name').text.strip().lower() # 处理常见别名,避免类别对不上 if name == 'storagetank': name = 'storage_tank' if name not in CLASS_MAP: print(f'跳过未知类别: {name} in {xml_path}') continue cls_id = CLASS_MAP[name] bnd = obj.find('bndbox') xmin = float(bnd.find('xmin').text) ymin = float(bnd.find('ymin').text) xmax = float(bnd.find('xmax').text) ymax = float(bnd.find('ymax').text) # 边界裁剪,防止标注越界导致归一化后为负 xmin = max(0, min(xmin, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) xmax = max(0, min(xmax, img_w - 1)) ymax = max(0, min(ymax, img_h - 1)) if xmax <= xmin or ymax <= ymin: print(f'跳过无效框: {xml_path}') continue x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f'{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}') with open(txt_path, 'w') as f: f.write('\n'.join(lines)) # 批量处理时,图像尺寸从 xml 的 size 里读,不要硬编码逻辑说明:先建立类别名到 id 的映射,遇到未知类别直接跳过并打印,避免训练时标签越界。坐标裁剪是血泪经验,遥感标注里偶尔有框超出图像边界,不裁剪会导致归一化后出现负数,YOLO 训练时直接报错或学出诡异框。参数方面,CLASSES的顺序一旦确定就不要改,否则之前训的权重全废;x_center保留 6 位小数足够,再多了也没意义。
2.3 目录结构怎么摆才不返工
YOLO 官方推荐的结构是 images 和 labels 分开,train/val 各自成对。我一般会这样组织:
dataset/ images/ train/ (约 640 张) val/ (约 160 张) labels/ train/ val/ data.yamldata.yaml内容如下:
path: ./dataset train: images/train val: images/val nc: 10 names: ['airplane', 'ship', 'storage_tank', 'baseball_diamond', 'tennis_court', 'basketball_court', 'ground_track_field', 'harbor', 'bridge', 'vehicle']注意names的顺序必须和转换脚本里的CLASSES完全一致,差一个顺序,模型学到的就是错位的类别。划分 train/val 时不要随机打散,遥感图像里同一区域的多张图如果同时出现在训练和验证集,验证指标会虚高。我一般按图像来源或地理位置做分组划分,没有来源信息时至少保证同一场景的图不跨集。
3. 用 YOLOv8 在 NWPU VHR-10 上跑通第一个 baseline
3.1 环境配置与最小训练命令
环境这块,Ultralytics 的 YOLOv8 是目前上手最快的选择,pip 装完就能用。我一般会建一个干净的 conda 环境,避免和之前的 torch 版本打架:
conda create -n nwpu_yolo python=3.10 -y conda activate nwpu_yolo pip install ultralytics装完后直接命令行训练:
yolo detect train \ data=./dataset/data.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ project=runs/nwpu \ name=baseline参数说明:model=yolov8s.pt是预训练权重,遥感数据量不大时从预训练起步比从头训收敛快很多;imgsz=640是默认输入尺寸,NWPU VHR-10 原图普遍在 1000 像素以上,直接缩到 640 会丢小目标细节,后面会讲怎么调;batch=16在 8G 显存上比较稳,显存不够就降到 8 或 4;device=0指定第一块 GPU,CPU 训练会慢到怀疑人生。
3.2 训练日志里该盯哪几个数
跑起来之后,控制台会输出每一轮的 box_loss、cls_loss、dfl_loss 和 mAP50、mAP50-95。遥感检测里最该盯的是 mAP50-95 和每一类的 AP,因为小目标类(比如 vehicle)的 AP 往往远低于大目标类(比如 ground_track_field)。如果 box_loss 一直不降,先检查标签格式是不是有负数或超出 1 的值;如果 cls_loss 震荡,可能是类别不均衡太严重,车辆和飞机的框数量可能差一个数量级。我一般会在训练中途用yolo detect val单独跑一次验证,看看混淆矩阵里哪些类互相误判,比如网球场和篮球场在低分辨率下确实容易混。
3.3 推理与可视化:确认模型到底学到了什么
训练完拿一张验证集图像跑推理:
yolo detect predict \ model=runs/nwpu/baseline/weights/best.pt \ source=./dataset/images/val \ conf=0.25 \ save=Trueconf=0.25是默认置信度阈值,遥感小目标建议先调到 0.1 看看召回,再根据误报情况往上加。推理结果会保存在runs/detect/predict下,重点看两类问题:一是密集小目标有没有被漏掉,二是大目标有没有被重复框。如果发现储油罐这种圆形目标框得歪歪扭扭,说明数据里的标注本身可能就不够紧,这时候要么清洗标注,要么在训练时加一点旋转增强。
4. 遥感小目标检测的避坑与排查清单
4.1 现象:mAP 看着还行,但小目标全漏
原因:imgsz=640把原图缩小后,车辆、小船这类目标只剩十几个像素,YOLO 的 stride 下采样后特征几乎消失。解决:把imgsz提到 1024 或 1280,同时batch相应降到 4 或 2;如果显存实在不够,可以用切片推理,把大图裁成重叠的小块分别检测再合并。
4.2 现象:训练 loss 正常,验证 mAP 为 0
原因:data.yaml里的names顺序和转换脚本里的CLASSES不一致,或者 labels 目录路径写错导致读不到标签。解决:先跑一遍yolo detect train看它打印的数据集统计,确认nc和每类实例数;再用几行 Python 检查一个 txt 文件里的 class_id 是否在 0~9 之间。
4.3 现象:模型把港口认成桥梁
原因:这两个类在遥感图像里都有长条形结构,且训练样本中港口和桥梁的上下文高度重叠。解决:不要只靠 YOLO 的默认增强,加入 mosaic 和 mixup 的同时,针对性地对这两类做难例挖掘,把误判的图单独拿出来重新标注或加权重。
4.4 现象:训练到一半显存爆了
原因:imgsz调大后没有同步降batch,或者workers开太多导致内存泄漏。解决:按imgsz翻倍、batch减半的原则调整;workers在 Linux 上设 8 左右,Windows 上设 0 或 2,避免多进程读图卡死。
4.5 现象:验证集指标远高于测试集
原因:划分数据时同一区域的图被分到了两边,模型其实在“背答案”。解决:按图像来源或地理区块做分组划分,确保验证集里的场景在训练集中没有出现过。如果数据量实在少,至少做 5 折交叉验证,看指标是否稳定。
5. 把 NWPU VHR-10 用出更多价值:从 baseline 到可交付模型
5.1 用类别权重和损失函数调优小目标
YOLOv8 默认的分类损失是 BCE,对类别不均衡没有额外处理。如果车辆类的 AP 明显拖后腿,可以在训练时给不同类别加权重,或者换用 focal loss 的思路。Ultralytics 没有直接暴露类别权重参数,但可以通过复制车辆类样本、或在数据集中对车辆密集区域做过采样来间接实现。我一般会先跑一版 baseline,看每类 AP 的差距,再决定要不要动损失函数,而不是一上来就改。
5.2 验证模型是否真的可交付:三个检查动作
第一,拿一批完全没有参与训练的遥感图跑推理,看漏检和误报是否在可接受范围;第二,用yolo export format=onnx导出 ONNX,确认推理速度和精度损失;第三,把模型放到实际业务的分辨率下测试,比如原始影像不缩放直接切片推理,看小目标召回是否达标。这三个动作做完,才能说这个模型不是“只在验证集上好看”。
5.3 一个具体技巧:切片推理合并框
遥感图像往往很大,直接缩放会丢小目标。我常用的做法是把原图按 640×640 切片,重叠 128 像素,每片单独推理,再把所有框映射回原图坐标,最后用 NMS 合并。这样小目标的召回能提升一大截,代价是推理时间线性增加。如果业务对实时性要求不高,这个技巧性价比很高。
import cv2 import numpy as np from ultralytics import YOLO model = YOLO('runs/nwpu/baseline/weights/best.pt') img = cv2.imread('test_large.jpg') h, w = img.shape[:2] tile, overlap = 640, 128 all_boxes = [] for y in range(0, h, tile - overlap): for x in range(0, w, tile - overlap): crop = img[y:y+tile, x:x+tile] if crop.shape[0] < tile or crop.shape[1] < tile: crop = cv2.copyMakeBorder(crop, 0, tile-crop.shape[0], 0, tile-crop.shape[1], cv2.BORDER_CONSTANT, value=(0,0,0)) results = model(crop, conf=0.15, verbose=False) for box in results[0].boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() all_boxes.append([x1+x, y1+y, x2+x, y2+y, box.conf[0].item(), box.cls[0].item()]) # 这里接一个 NMS 做全局合并,IoU 阈值建议 0.5逻辑说明:切片时保证每块和相邻块有重叠,避免目标被切一半导致漏检;conf调低到 0.15 是为了先拿高召回,后面再用 NMS 压误报。参数上,tile和overlap要根据目标尺寸调,车辆多就减小 tile,飞机多可以适当放大。这个方案我在地理信息相关的项目里反复用过,比直接缩放整图稳得多。
最后说个习惯:每次拿到新的遥感数据集,我都会先花半小时把标注可视化一遍,随机抽 20 张图把框画出来看。这一步能提前发现大量标注错误和类别混淆,比训完再回头查省事得多。希望帮到你。
本文还有配套的精品资源,点击获取