
简介面向目标检测方向的学生与算法工程师这份卫星图像数据集拥有800张高分辨率的遥感影像及配套标注文件涵盖飞机、船舶、储罐、棒球场、网球场、篮球场、田径场、港口、桥梁、车辆共10个类别适合用于YOLO等模型的训练与效果验证也可直接支撑课程设计、期末大作业和毕业设计等场景。资源包共2612个文件以jpg原图、xml标注、txt标签为核心还包含数据划分缓存与说明文档整体大小61.15MB目录结构清晰、标注格式规范便于接入常见检测流程。目前已有321人浏览学习。相比从零开始采集和标注这份数据能显著节省时间成本尤其适合需要快速产出实验结果的初学者或需要标准数据集验证算法的研究者配合作者在大厂多年的算法工程经验资料实用性和可靠性都有保障。1. YOLO 目标检测 卫星图像数据集NWPU VHR-10 这 800 张已标注图为什么值得落到本地遥感图像的目标检测比自然场景更考验数据目标小、背景乱、类别之间尺度差异大很多时候模型精度上不去不是网络不够深而是数据集本身没有对齐。NWPU VHR-10 是遥感领域最常用的公开数据集之一800 张高分辨率卫星图像全部手动标注覆盖飞机、轮船、储罐、棒球场、网球场、篮球场、地面跑道、港口、桥梁、车辆十类目标。这份 rar 资源把图像和已经标注好的文件打包在一起解压后理论上可以直接走 YOLO 训练流程但因为里面混杂着 train.cache、test.cache 这类自动生成的缓存以及 positive image set 这种原始标注目录想真正跑通要先花十几分钟把文件结构理顺。对做课程设计、期末大作业或毕业设计的同学来说这是性价比很高的数据起点对刚接触小目标检测的工程师也可以拿它做 baseline 验证。2. 解压后先别急着训练读懂 NWPU VHR-10 的文件结构和 cache 文件2.1 压缩包内的文件构成与角色划分解压这份 rar 之后你看到的不是想象中整齐的 images/labels 两级目录而是几个 .cache 文件、一个 positive image set 目录以及零散的 .jpg 图像。这里先说结论train.cache、test.cache、val.cache 是 YOLO 训练时自动生成的缓存文件作用是把图像路径、尺寸、哈希值和标注文件的校验值记录下来避免每次启动训练都重新扫描整个数据集。也就是说它们在压缩包里反而说明有人用这份数据跑过一次 YOLO但它们并不属于数据本体。文件用途是否需要保留train.cache / test.cache / val.cacheYOLO 数据集缓存可删除训练时会重建positive image set.cache3正样本子目录缓存可删除positive image set原始图像与标注目录保留数据本体084.jpg / 311.jpg 等正样本图像保留用于整理需要考虑的一件重要事情是cache 文件与生成它的目录路径强绑定。如果你把 rar 解压到另一个路径YOLO 检测到路径不一致就会自动重建缓存。这一步通常要花几秒钟到十几秒不要误认为是程序卡死。我自己的习惯是拿到资源后先删除所有 .cache 文件再重新整理目录让 YOLO 从零扫描一次避免读到无效路径后出现奇怪的报错。positive image set 目录是 NWPU VHR-10 原始结构中的正样本区里面每张 JPG 图像通常对应一个同名的 .txt 标注文件。负样本在不含目标的图像也就是 negative image set 里。但这份 rar 里没有单独列出 negative 目录所以先按正样本数据来处理负样本可以在训练阶段用背景图补充或者从原数据集补全。2.2 原始标注 txt 的解析类名带空格和坐标格式NWPU VHR-10 的原始标注文件是纯文本格式每行代表一个目标框格式大致是airplane 420 180 560 235 storage tank 90 300 150 350 ship 620 410 700 470每一行第一个字段是类别名称后面四个数字分别是左上角 x、左上角 y、右下角 x、右下角 y 的像素坐标。这个格式有几个坑值得提前说明。第一个坑是类别名里有空格。storage tank 是两个单词如果你用 Python 的line.split( )直接切会得到五个字段类别名被拆成 storage 和 tank 两段坐标全部错位。正确做法是先切出前两个字段作为“可能带空格的类名”和第一个坐标或者根据已知的类别列表做匹配。第二个坑是坐标是整数像素值而且原始标注没有归一化直接喂给 YOLO 会报标签格式非法的错误。第三个坑是不同版本的 NWPU VHR-10 标注坐标的参考系可能有差异有的从 0 开始有的从 1 开始影响不大但转换时统一处理即可。下面是一段读取原始标注并打印统计信息的代码先确认数据能正确解析再动手转换import os from collections import defaultdict label_dir positive image set cls_counter defaultdict(int) with open(os.path.join(label_dir, 0001.txt), r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue tokens line.split() if len(tokens) 5 and tokens[0].isdigit() is False: # 已知类别名才进入统计避免把坐标当类别 cls_name .join(tokens[:2]) if tokens[0] storage else tokens[0] x1, y1, x2, y2 map(int, tokens[-4:]) cls_counter[cls_name] 1 print(f{cls_name}: ({x1}, {y1}) - ({x2}, {y2})) else: print(f[skip] 无法解析行: {line}) print(类别统计:, dict(cls_counter))这段代码的核心逻辑是逐行读取标注文件对每行做切分并判断字段数是否为 5其中第一个字段是类名后四个是坐标。代码里对 storage 这类多词类名做了特殊处理避免坐标错位。输出类别统计能让你快速确认这份 rar 里的标注是否覆盖完整十类如果某一类的计数为 0说明要么是数据不完整要么是解析逻辑还有问题。2.3 目录重整把散装 JPEG 整理成 YOLO 标准 images/labels 分支YOLO 官方训练时对数据目录没有强制要求但惯例是数据集根目录下分 images 和 labels 两个分支各自再按 train、val、test 划分。图像的 jpg 文件和标注 txt 文件名一一对应只是后缀不同。这个结构对后续写 data.yaml 特别友好否则你需要在配置文件里写一长串路径还容易出错。整理脚本的思路是遍历 positive image set 目录中的全部图像复制到 dataset/images/train同时把同名 txt 复制到 dataset/labels/train。如果你打算划分验证集可以在复制前对文件名列表做一次随机打乱按 8:1:1 或其他比例切片。这里给出一个只处理 train 部分的简化版本import os import shutil SRC_DIR positive image set DST_IMG dataset/images/train DST_LBL dataset/labels/train os.makedirs(DST_IMG, exist_okTrue) os.makedirs(DST_LBL, exist_okTrue) for fname in sorted(os.listdir(SRC_DIR)): if not fname.lower().endswith(.jpg): continue stem os.path.splitext(fname)[0] img_src os.path.join(SRC_DIR, fname) lbl_src os.path.join(SRC_DIR, stem .txt) if not os.path.exists(lbl_src): print(f[warning] {fname} 没有对应标注文件) continue shutil.copy(img_src, os.path.join(DST_IMG, fname)) shutil.copy(lbl_src, os.path.join(DST_LBL, stem .txt)) print(整理完成图像数:, len(os.listdir(DST_IMG))) print(标注数:, len(os.listdir(DST_LBL)))这段脚本先遍历源目录里的 JPG 文件通过文件名 stem 去配对同名 txt如果图像没有标注文件就跳过并打印警告避免把无标签的图混进训练集导致 YOLO 训练中断。复制完成后打印两个目录的文件数检查数量是否一致这是最直接的完整性校验手段。目录重整这一环节容易遇到的问题是原始数据里可能存在一些非 JPG 格式的图像比如 PNG 或 TIFFYOLO 本身支持这些格式但脚本里如果只写了.jpg会漏掉。我一般会把后缀判断改成一个集合比如{.jpg, .jpeg, .png, .bmp, .tif}这样能覆盖遥感数据里的常见格式。另一个问题是文件名中有空格或中文虽然 YOLO 能处理但在 Linux 服务器上容易出现转义问题建议顺手重命名为纯数字或纯英文文件名。import re def safe_name(fname): stem, ext os.path.splitext(fname) stem re.sub(r[^0-9a-zA-Z], _, stem) return stem ext.lower()用这个函数在复制时重命名能省掉后续大量路径引用的麻烦。整理完目录之后下一步就是标注格式转换这是整份资源能不能直接训练的关键。3. 标注格式转换从 NWPU 到 YOLO 的坐标归一化与类别映射3.1 两种格式的核心差异与转换思路NWPU VHR-10 的标注是绝对像素坐标单位是像素表达的是“框的左上角”和“框的右下角”而 YOLO 使用的标签格式是归一化的中心坐标加宽高即class_id cx cy w h其中 cx、cy 是目标框中心点相对于图像宽高的比例w、h 是框的宽高占比取值范围都在 0 到 1 之间。这个差异决定了你不可能把原始 txt 直接复制到 labels 目录里完事必须做一次换算。换算公式很简单。假设图像宽度为 W高度为 H原始标注为 x1、y1、x2、y2那么box_w (x2 - x1) / W box_h (y2 - y1) / H box_cx (x1 x2) / 2 / W box_cy (y1 y2) / 2 / H这两个公式里要特别注意的就是除以图像宽高这一步很多新手漏掉归一化生成的标签里出现大于 1 的数值YOLO 在训练时会直接过滤掉这些目标框导致模型什么都没学到但训练日志看起来一切正常。类别映射同样关键。YOLO 的类别是从 0 开始的整数而 NWPU 原始标注里是字符串。你需要建立一个固定的映射表例如把 airplane 映射为 0、ship 映射为 1依此类推并且这个顺序必须与 data.yaml 里的 names 列表完全一致。否则会出现标注文件里是飞机训练时模型按轮船去学的情况这种错位在训练日志里根本看不出来只有最后评估时才能发现。3.2 转换脚本读 txt、归一化并写出 labels下面是一段完整的转换脚本处理的是整理后放在 train 目录中的原始标注import os category_map { airplane: 0, ship: 1, storage tank: 2, baseball diamond: 3, tennis court: 4, basketball court: 5, ground track field: 6, harbor: 7, bridge: 8, vehicle: 9, } def convert_label(txt_path, out_path, img_w, img_h): with open(txt_path, r, encodingutf-8) as f_in, \ open(out_path, w, encodingutf-8) as f_out: for line in f_in: line line.strip() if not line: continue tokens line.split() # 处理带空格的类别名 if tokens[0] storage and len(tokens) 6: cls_name storage tank x1, y1, x2, y2 map(float, tokens[1:5]) elif len(tokens) 5: cls_name tokens[0] x1, y1, x2, y2 map(float, tokens[1:5]) else: continue if cls_name not in category_map: print(f[skip] 未知类别 {cls_name} in {txt_path}) continue box_w (x2 - x1) / img_w box_h (y2 - y1) / img_h box_cx (x1 x2) / 2.0 / img_w box_cy (y1 y2) / 2.0 / img_h # 越界检查YOLO 对超出 [0,1] 的框会直接忽略 box_cx min(max(box_cx, 0.0), 1.0) box_cy min(max(box_cy, 0.0), 1.0) box_w min(max(box_w, 0.0), 1.0) box_h min(max(box_h, 0.0), 1.0) cls_id category_map[cls_name] f_out.write(f{cls_id} {box_cx:.6f} {box_cy:.6f} {box_w:.6f} {box_h:.6f}\n) print(f转换完成: {txt_path} - {out_path})这段脚本先按行读取原始标注对 storage tank 这种多词类名做单独判断再按公式计算归一化坐标。写文件时保留 6 位小数既保证精度又能让文件体积可控。越界检查的目的有两个一是防止标注框超出图像边界导致 YOLO 报警二是如果原始标注坐标本身有错误这里的截断至少不会让整个训练崩溃。实际转换时img_w 和 img_h 需要从图像文件读取推荐用 OpenCV 或 PIL不要直接从文件名推断尺寸。3.3 转换后的自检画框验证与三类常见错误转换脚本跑完不等于数据没有问题这一步最容易被跳过也是很多人训练出来模型完全不收敛的真正原因。自检方法很直接把转换后的 YOLO 标签画回图像上人工看一眼框的位置是否合理。脚本如下import cv2 import numpy as np def draw_yolo_boxes(img_path, label_path, class_names): img cv2.imread(img_path) h, w img.shape[:2] with open(label_path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id, cx, cy, bw, bh map(float, parts) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) color (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, class_names[int(cls_id)], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) cv2.imwrite(check.jpg, img) print(f画框完成: {img_path} - check.jpg)画框验证时重点看三类问题。第一类是框整体偏离目标通常是归一化公式写错比如把 x1、y1 直接当中心点用了。第二类是框宽高为负说明 x1 大于 x2 或 y1 大于 y2原始标注里框的顶点顺序可能不是标准的左上右下需要做一次排序纠正。第三类是多个框重叠在一起大概率是同名 txt 里出现了重复行需要做去重处理。这三类错误如果靠肉眼逐张检查 800 张图不现实我的建议是随机抽 30 到 50 张画框查看只要这批没有问题大概率整体没有系统性错误。4. 训练配置data.yaml 的写法、命令行参数调整与小目标优化4.1 data.yaml 的最小完整配置数据准备好之后第一个要写的是数据集配置文件。YOLO 系列从 v5 到 v8 都支持通过 YAML 文件描述数据集位置和类别信息。一个最小可用的 data.yaml 如下path: ./dataset train: images/train val: images/val test: images/test nc: 10 names: 0: airplane 1: ship 2: storage tank 3: baseball diamond 4: tennis court 5: basketball court 6: ground track field 7: harbor 8: bridge 9: vehiclepath 字段写数据集根目录的路径可以是相对路径也可以是绝对路径。train、val、test 分别是指向图像目录的相对路径。nc 是类别总数必须与 names 列表长度一致。这里最隐蔽的坑是 names 索引顺序它必须严格对应转换时用的 category_map。如果你在转换时把 airplane 设为 0但在 data.yaml 里把 ship 放在第 0 位模型学到的输出顺序就会与真实类别错位最终评估时混淆矩阵会乱成一团。另一个容易忽略的点是 path 字段的使用。如果你的训练命令在子目录下执行建议 path 写成相对当前工作目录的路径或者在命令行直接传data/绝对路径/data.yaml。我看到不少人在服务器上把 home 目录下的数据集配置写到 container 里路径没有同步映射训练一开始就报找不到图片。用绝对路径能减少这类问题代价是换机器后要改配置。我习惯写相对路径配合一个软链接或环境变量指向实际数据位置这样代码和配置都能保持可迁移。4.2 训练命令参数逐个拆解以 YOLOv5 为例训练命令一般长这样python train.py \ --data data/nwpu_vhr10.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --cache ram \ --workers 8 \ --project runs/nwpu \ --name experiment_01这里每个参数都有讲究。--img 640是输入图像尺寸训练时会先把原图等比缩放再 pad 成 640×640。卫星图像的分辨率一般远高于这个值比如 1024×1024 甚至更高直接缩到 640 会把小目标缩没这是遥感目标检测里最典型的精度杀手。--batch 16是批大小直接受显存制约如果你的卡只有 8G从 8 起步比较稳。--cache ram表示把图像加载到内存加速训练但 800 张图大约占几个 GB 内存内存紧张时可以去掉这个参数。--weights yolov5s.pt是预训练权重s 代表 small 版本。这里有一个常见误区用 COCO 预训练权重做迁移学习时如果新数据集的类别数和 COCO 的 80 类完全不同模型最后一层输出会被自动重置这是 YOLO 源码内部处理的不需要手动修改权重文件。你真正要关心的是骨干网络的参数是否适合小目标如果有资源用yolov5m或yolov5l往往比 s 效果好。训练日志里最值得看的两行是all 320前的 mAP 值以及分类损失和回归损失曲线的下降趋势。如果 loss 在前 20 个 epoch 没有明显下降先怀疑数据问题而不是网络结构第一步检查标注是否为空、类别是否倾斜第二步检查学习率第三步再考虑换模型。4.3 小目标场景的 img_size 与 tile 策略针对 NWPU VHR-10 这种以飞机、车辆为主的小目标数据集直接设--img 640会损失大量细节。常见做法是两个方向一是提高--img到 1280 或更高配合较大的 batch 撑住显存二是把原图切割成若干小块再训练即 tile 策略。提高输入尺寸最直接显存足够时收益明显但推理速度会下降。以 YOLOv5 为例训练时设--img 1280推理时保持同样尺寸mAP 通常能比 640 高出 3 到 8 个百分点代价是单张推理时间翻倍。如果你的显卡是 3090 或以上优先用这个方案。python train.py \ --data data/nwpu_vhr10.yaml \ --weights yolov5m.pt \ --img 1280 \ --batch 8 \ --epochs 150 \ --cache ram \ --rect--rect是一个容易被忽视的参数它让 YOLO 在训练时把长宽比相近的图像打包进同一个 batch减少 padding 带来的计算浪费对小数据集训练更友好。如果你的数据集里图像尺寸差异很大这个参数能让训练显存占用降低不少同时不损失精度。tile 策略实现上是在数据预处理阶段把大图按 512×512 或 640×640 的 tile 切成小块保留原来的标注坐标只做裁切不缩放。切分时注意目标是否落在 tile 边界被切开常见做法是允许 10% 的宽高重叠。这个方案对小目标特别有效相当于让模型在原始分辨率下看到目标而不是把目标缩成几个像素。5. 避坑排查cache 失效、类别错位、显存溢出与背景误检5.1 cache 文件路径失效导致反复重建现象训练启动时每次都显示Scanning dataset...耗时十几秒甚至更久数据集只有 800 张图却像卡住一样。第二次训练时又重复同样的扫描过程。原因train.cache 文件记录的是之前生成它的绝对路径。数据集移动过目录后缓存里的路径全部失效YOLO 确认失效后删除缓存并重新生成。每次启动都重建的最常见原因是数据集路径没有固定或者是多个训练脚本在不同工作目录下执行。解决拿到资源后直接删除所有 .cache 文件并把数据集固定在一个目录下。启动训练时使用相同的相对路径或绝对路径缓存生成后第二次启动就会秒过。如果数据集目录本身被挂载在多个位置训练时只走其中一个路径避免反复切换。5.2 类别 id 错位导致训练不收敛现象训练 loss 下降但 mAP 始终在 0.1 到 0.2 之间徘徊画出预测框发现框的位置基本正确但类别乱标例如把飞机框标成轮船。原因转换脚本里的 category_map 与 data.yaml 的 names 顺序不一致。比如转换时把 airplane 映射为 0但 data.yaml 里第 0 位写的 ship模型学会的输出与正确类别完全错位。这类错误不会报错训练日志里 loss 照常下降但验证集评估时精度极差。解决写一个校验脚本随机取 10 张训练图把 YOLO label 的第一列数字还原成类别名与原图人工比对。如果对不上先统一 category_map 和 names 的顺序重新转换标注。另外可以在 data.yaml 里加注释把每个 id 对应的类别名写清楚避免下次改配置时搞混。5.3 显存溢出现象训练开始后报CUDA out of memory或者跑几十个 batch 后 OOM 崩溃。这类问题在设置--img 1280后尤其常见。原因batch 太大和输入分辨率太高共同造成显存峰值超限。YOLO 的显存占用随输入尺寸呈平方增长从 640 升到 1280 时显存需求大约翻四倍。设置--cache ram后图像缓存在内存中不会直接影响显存但 dataloader 的缓存队列会额外占一部分内存间接推高整体资源压力。解决先降 batch从 16 降到 8 或 4观察有没有改善。如果还不够把 img 从 1280 降到 1024。另一个实用技巧是启用--noval参数在训练阶段跳过验证集的推理省下不少显存等训练结束再单独做一次评估。多卡用户要注意 batch 参数是单个 GPU 的 batch总 batch 等于 batch 乘卡数不要以为设了 16 就是每张卡 16。5.4 负样本缺失导致背景误检现象训练完的模型在纯背景区域乱框比如大面积农田或裸地也被识别成储罐或车辆误检率奇高。NWPU VHR-10 原始数据里有一部分负样本图即完全不含目标的背景图但如果你的 rar 解压后没有这些负样本模型实际上只在正样本里见过目标和背景的搭配从未见过完全无目标的场景。原因正样本图里目标周围的背景往往和该类别强相关比如飞机在停机坪、船在水面。模型学到的是“停机坪纹理 → 飞机”这种相关性而不是飞机本体的特征。测试时遇到类似纹理就会误触发。解决从其他遥感数据集里收集负样本或者手动截取一批不含任何目标的背景图放在训练集和验证集中。最简单的方式是用 YOLO 自带的背景图目录labels 目录里放空 txt 文件即可。建议负样本比例控制在正样本的 10% 到 20%加得太多会让模型对真正目标也变得保守降低召回率。5.5 标签框越界与坐标归一化异常现象训练日志里出现大量WARNING: ignoring corrupt label或者某个类别的目标数量明显少于标注数量。原因原始标注的坐标完全等于图像边界归一化后可能出现 w 或 h 为 0 的情况或者标注框的坐标超出了图像宽高YOLO 直接丢弃该框。另一个常见情况是转换时没有正确读取图像尺寸拿到的 W 和 H 与实际不符导致所有坐标整体偏移。解决在转换脚本里加上坐标合法性检查处理 x1 x2、y1 y2、cx 或 cy 超出 [0,1] 的情况。同时建议统计每个 txt 中的目标框数量和原图尺寸做交叉验证。如果发现某张图的目标数明显异常单独拿出来检查。6. 训练完之后的一个习惯用混淆矩阵反推数据集的病灶模型训练完先别急着写报告或部署第一件事是打开runs/nwpu/experiment_01/confusion_matrix.png看混淆矩阵。混淆矩阵能直接告诉你模型把哪些类别混在一起了而这个问题往往不只是模型的问题而是数据集本身存在标注质量或类别分布的问题。例如飞机、车辆这两类如果频繁互混很可能是标注框过小、目标像素太少模型提取不到足够的特征这时优先考虑提升输入分辨率或做 tile 裁剪。储罐和桥梁如果有交叉则要考虑是否存在标注框重叠比如桥梁跨越水面时把水面背景框进来导致模型学会了水面纹理而不是桥梁特征。混淆矩阵里如果某个类别的整行几乎全是背景说明该类别的样本量太少或者负样本太多把真阳性压下去了需要回看训练集里该类别的目标框数量。第二个习惯是训练完成后随机挑 100 张验证集图像保存模型的预测结果人工逐张看预测框和真实框的差异焦点在哪里。重点不是框得准不准而是模型的错误模式是漏检多还是误检多。漏检集中在特定类别就去补该类别的样本或提高分辨率误检集中在背景区域就去加负样本或提高置信度阈值。我自己的固定做法是调参后重新训练时把训练曲线里的 mAP 变化和混淆矩阵截图存档方便和上一版对比。这个习惯帮我避免了很多次“以为改了有效、实际越改越差”的情况。有一点永远值得反复确认数据集的干净程度直接决定 YOLO 训练的置信度与其花几天调模型结构不如花两小时彻底检查一遍标注质量。希望这份 NWPU VHR-10 的整理和训练思路能帮到你少走一些我走过的弯路。本文还有配套的精品资源点击获取