
简介本资源为面向遥感图像目标检测的NWPU VHR-10标注数据集适合计算机、电子信息工程、数学等专业学生用于课程设计、期末大作业与毕业设计也可供YOLO算法入门者快速搭建训练与验证流程。数据集覆盖飞机、轮船、储罐、棒球场、网球场、篮球场、地面跑道、港口、桥梁和车辆共10个类别图像与标注一一对应省去自行标注的时间成本。压缩包共1600个文件包含800张jpg图像与800个xml标注文件xml可直接对接YOLO等主流检测框架的数据读取与格式转换流程整体约73.71MB体积轻便便于本地训练与迁移实验。目前已有1395人学习下载配套代码采用参数化编程参数修改方便、思路清晰、注释详细读者可据此完成数据加载、类别统计、模型训练与结果评估并在此基础上开展多类别遥感检测的对比实验与改进尝试。1. 遥感小目标检测的起点为什么 NWPU VHR-10 值得先跑通遥感图像目标检测和自然图像检测最大的区别在于尺度。同样一张 800×800 的图自然图像里一辆车可能占 200×200 像素遥感图里一架飞机可能只有 30×30 像素背景还全是纹理相似的停机坪或海面。NWPU VHR-10 这个数据集恰好把这个问题摆到台面上10 个类别800 张图像对应已标注的 xml 文件覆盖飞机、舰船、储油罐、棒球场、网球场、篮球场、田径场、港口、桥梁、车辆。它不算大但类别跨度大、目标尺度差异大、背景复杂度高非常适合拿来验证 YOLO 系列在遥感场景下的真实表现。很多人拿到这个压缩包的第一反应是「直接解压就能训」但实际跑起来会发现两个问题xml 是 PASCAL VOC 格式YOLO 要的是归一化 txt遥感图像里小目标密集默认 anchor 和输入尺寸会让召回率掉得很难看。这篇笔记就围绕「拿到 800 张图 xml 之后怎么一步步变成 YOLO 能吃的数据集怎么选训练参数怎么判断模型是真的学到了东西而不是在背背景」来展开。适合已经跑过 YOLO 自然图像检测、想切到遥感方向的人也适合手里有类似标注数据、想找一套可复现流程的从业者。2. 从 xml 到 YOLO txt格式转换的完整脚本与边界处理2.1 为什么不能直接拿 xml 去训 YOLOYOLO 系列从 v5 开始训练时的标签读取逻辑就是固定的每张图对应一个同名 txt每行格式为class_id x_center y_center width height且四个坐标都是相对于图像宽高的归一化值范围在 0 到 1 之间。xml 里存的是绝对像素坐标的xmin ymin xmax ymax还带图像宽高信息。如果不做转换训练脚本要么直接报错找不到标签要么把绝对坐标当归一化值用导致所有框挤在图像左上角一个极小的区域里loss 一开始就崩。常见做法是写一个转换脚本遍历 xml 目录解析出每个 object 的类别名和 bbox再按类别映射表转成 0 到 9 的整数 id。这里有一个容易被忽略的点NWPU VHR-10 的 xml 里类别名大小写和空格不一定完全统一比如oil tank和oil_tank可能混着出现转换前必须先做一次类别名清洗否则映射表会漏掉某些样本训练时这些图就变成无标签背景图模型会把目标当背景学。2.2 转换脚本从 xml 到归一化 txt下面这个脚本我用了很多次核心逻辑是「先收集所有类别名再统一映射最后逐图写 txt」。脚本里加了类别名清洗和空标注跳过避免生成空 txt 文件干扰训练。import os import xml.etree.ElementTree as ET # 类别名到 id 的映射顺序要和训练时的 data.yaml 一致 CLASS_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 clean_name(name): # 统一小写、去首尾空格、把下划线换成空格 return name.strip().lower().replace(_, ) def convert_xml_to_txt(xml_dir, txt_dir, img_dir): os.makedirs(txt_dir, exist_okTrue) skipped 0 for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() # 读取图像宽高用于归一化 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): raw_name obj.find(name).text cls_name clean_name(raw_name) if cls_name not in CLASS_MAP: # 类别名不在映射表里跳过并记录 skipped 1 continue cls_id CLASS_MAP[cls_name] bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 裁剪到图像边界内防止标注越界 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) if xmax xmin or ymax ymin: continue # 转成 YOLO 格式中心点 宽高全部归一化 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}) if lines: txt_name os.path.splitext(xml_file)[0] .txt with open(os.path.join(txt_dir, txt_name), w) as f: f.write(\n.join(lines)) else: skipped 1 print(f转换完成跳过 {skipped} 个无有效标注的样本) if __name__ __main__: convert_xml_to_txt( xml_dirNWPU_VHR10/annotations, txt_dirNWPU_VHR10/labels, img_dirNWPU_VHR10/images )逻辑说明脚本先解析 xml 的size节点拿到图像宽高再遍历每个object把类别名清洗后查映射表。bbox 做了边界裁剪防止个别标注框超出图像范围导致归一化后坐标大于 1。最后按 YOLO 格式写入 txt文件名和图像名保持一致。参数方面CLASS_MAP的顺序必须和后续data.yaml里的names列表完全一致否则类别 id 会错位。clean_name函数里的替换逻辑可以根据实际 xml 里的命名习惯调整核心是保证同一类别的不同写法能映射到同一个 id。2.3 划分训练集和验证集时要注意的坑转换完 txt 之后下一步是划分 train/val。遥感数据集有一个特点同一张图里可能同时出现多个类别而且不同类别的样本数量极不均衡。NWPU VHR-10 里 vehicle 和 ship 的样本数远多于 bridge 和 harbor。如果直接随机按 8:2 划分验证集里可能某些类别一个样本都没有导致验证时这些类别的 mAP 直接是 0你根本不知道是模型没学好还是验证集没覆盖。我一般会先统计每个类别的图像数量然后按类别分层抽样。具体做法是对每个类别把包含该类别的图像单独拿出来按比例划分最后合并去重。这样能保证验证集里每个类别至少有一定数量的样本。如果某个类别总样本数太少比如少于 20 张图那就把它全部放进训练集验证时只看其他类别避免验证指标失真。3. YOLO 训练参数怎么设遥感小目标的 anchor 与输入尺寸3.1 输入尺寸不是越大越好但太小一定不行遥感小目标检测里输入尺寸直接决定小目标在特征图上的像素占比。以 NWPU VHR-10 为例很多车辆目标在原图里只有 20×20 像素左右。如果输入尺寸设成 416经过 32 倍下采样后特征图上只剩 13×13 的区域再经过几次卷积小目标的特征基本就糊掉了。常见做法是把输入尺寸设到 640 或 800让下采样后的特征图保留更多空间信息。但尺寸也不是无脑拉大。800 的输入相比 640显存占用大约增加 1.5 倍训练速度下降明显。如果你的显卡只有 8G 显存800 的输入配 batch size 8 可能直接 OOM。我一般会先试 640看验证集上小目标类别的召回率如果 vehicle 和 ship 的召回率明显低于其他类别再考虑上 800 或 1024。另外YOLO 系列支持矩形训练也就是按 batch 内图像的最大宽高做 padding而不是统一缩放到正方形。遥感图像很多是宽幅的矩形训练能减少无效 padding对小目标更友好。3.2 anchor 聚类用你的数据重新算一遍YOLO 默认的 anchor 是在 COCO 上聚类出来的那套 anchor 针对的是自然图像里的大中型目标。直接拿来用在遥感小目标上会出现 anchor 尺寸远大于实际目标尺寸的情况导致正样本匹配时很多目标匹配不到合适的 anchor召回率上不去。常见做法是用 k-means 在自己的训练集 bbox 上重新聚类 anchor。具体步骤是先把所有训练集的 txt 标签读出来还原成绝对宽高然后跑 k-meansk 取 9对应 3 个尺度各 3 个 anchor。聚类时用 IoU 作为距离度量而不是欧氏距离这样聚出来的 anchor 更符合检测任务的需求。下面是一个简化的聚类脚本import numpy as np def kmeans_iou(boxes, k9, max_iter100): # boxes: N x 2 的数组每行是 (w, h) n boxes.shape[0] # 随机初始化 k 个聚类中心 indices np.random.choice(n, k, replaceFalse) centers boxes[indices].copy() for _ in range(max_iter): # 计算每个 box 和每个中心的 IoU ious np.zeros((n, k)) for i in range(k): # 以中心为 anchor计算 IoU inter_w np.minimum(boxes[:, 0], centers[i, 0]) inter_h np.minimum(boxes[:, 1], centers[i, 1]) inter inter_w * inter_h union boxes[:, 0] * boxes[:, 1] centers[i, 0] * centers[i, 1] - inter ious[:, i] inter / (union 1e-9) # 每个 box 分配给 IoU 最大的中心 labels np.argmax(ious, axis1) new_centers np.zeros_like(centers) for i in range(k): if np.sum(labels i) 0: new_centers[i] boxes[labels i].mean(axis0) else: new_centers[i] centers[i] if np.allclose(new_centers, centers, atol1e-4): break centers new_centers # 按面积排序方便分配到不同尺度 areas centers[:, 0] * centers[:, 1] order np.argsort(areas) return centers[order] # 读取所有训练集标签提取宽高 def load_boxes(label_dir): boxes [] for f in os.listdir(label_dir): if not f.endswith(.txt): continue with open(os.path.join(label_dir, f)) as fp: for line in fp: parts line.strip().split() if len(parts) 5: w float(parts[3]) h float(parts[4]) boxes.append([w, h]) return np.array(boxes) boxes load_boxes(NWPU_VHR10/labels/train) anchors kmeans_iou(boxes, k9) print(聚类得到的 anchor归一化宽高) print(anchors)逻辑说明脚本用 IoU 作为相似度度量把每个 bbox 分配给 IoU 最大的聚类中心然后更新中心为簇内均值。最后按面积排序方便你把小 anchor 分配给高分辨率特征图大 anchor 分配给低分辨率特征图。参数方面k9是 YOLO 系列的默认值对应三个检测尺度各三个 anchor。如果你的数据集目标尺度特别集中也可以试 k6减少 anchor 数量降低匹配复杂度。聚类结果里的宽高是归一化值直接填到模型配置的 anchor 字段里即可。3.3 损失函数里小目标的权重问题YOLO 的损失函数通常由三部分组成box 回归损失、objectness 损失、分类损失。在遥感小目标场景下box 回归损失对小目标不敏感因为小目标的绝对坐标误差本身就小CIoU 或 DIoU 算出来的 loss 值也小梯度贡献有限。常见做法是给 box 损失加一个和面积相关的权重面积越小的目标权重越大。另一种做法是调整 objectness 损失的正负样本比例遥感图像里背景占绝大多数负样本太多会淹没正样本的梯度可以适当降低负样本的权重。我一般会先跑一轮默认参数看训练日志里 box loss 和 obj loss 的下降曲线。如果 box loss 降得很慢而 obj loss 降得很快说明模型在学背景而不是学目标这时候就要调损失权重。具体调法因 YOLO 版本而异v5 和 v8 的损失实现不同但思路是一样的让小目标的梯度在总损失里占更大比例。4. 训练过程排查loss 不降、mAP 为零、显存爆炸怎么查4.1 现象loss 从第一轮就不降一直卡在同一个值附近原因通常有三个标签格式不对、类别映射错位、学习率太大。先检查生成的 txt 里坐标是不是都在 0 到 1 之间如果有大于 1 的值说明归一化时用的宽高和实际图像宽高不一致。再检查data.yaml里的names顺序和转换脚本里的CLASS_MAP是否完全一致顺序错一位就会导致所有类别标签错乱。如果这两项都没问题把学习率降到 1e-4 再试一轮有时候默认的 1e-2 对遥感小目标来说太大梯度直接炸了。4.2 现象训练 loss 正常下降但验证集 mAP 一直是 0这种情况多半是验证集的标签路径没配对或者验证集图像和标签文件名不一致。YOLO 在验证时会按图像名去找同名 txt如果 txt 文件名多了后缀或者少了前缀就找不到标签所有验证样本都被当成无标签背景mAP 自然是 0。另一个可能是验证集里某些类别的样本数为 0导致这些类别的 AP 无法计算拉低整体 mAP。排查方法是先单独跑一张验证图的推理看输出的框和类别是否正常再检查验证集每个类别的样本数。4.3 现象训练到一半显存爆炸报 CUDA out of memory遥感图像分辨率高如果输入尺寸设成 800 以上batch size 又没相应调小很容易 OOM。解决方法是先把 batch size 降到 4 或 2看能不能跑起来。如果还是 OOM检查是不是开了多尺度训练多尺度训练会在训练过程中随机改变输入尺寸最大尺寸可能远超你的预期。另外YOLO 的某些版本在验证时会缓存所有验证集图像的特征如果验证集太大也会导致显存缓慢增长直到爆炸。可以把验证频率调低或者把验证集拆成多个小批次。4.4 现象模型在训练集上表现很好验证集上一塌糊涂这是典型的过拟合。NWPU VHR-10 只有 800 张图如果训练轮数太多模型会把训练集的背景纹理也背下来。常见做法是加数据增强遥感图像常用的增强包括随机旋转、随机缩放、随机裁剪、色彩抖动。注意旋转增强要同时旋转图像和 bbox而且旋转后的 bbox 要重新计算不能直接用原来的坐标。另外可以加早停策略验证集 mAP 连续 10 轮不提升就停止训练保留最好的权重。4.5 现象推理时框的位置对但类别全是错的这种一般是类别映射表在训练和推理时不一致导致的。训练时data.yaml里的names顺序是 A推理时加载的权重对应的顺序是 B模型输出的类别 id 就会按 B 来解释结果全错。排查方法是把推理结果里的类别 id 打印出来和data.yaml里的顺序对照看是不是错位。解决方法是保证训练和推理用同一份data.yaml不要中途改类别顺序。5. 验证模型真的学到东西从 mAP 到可视化排查5.1 只看 mAP 不够要分类别看 AP整体 mAP 会被样本多的类别拉高掩盖样本少的类别的问题。NWPU VHR-10 里 vehicle 和 ship 的 AP 可能到 0.8 以上但 bridge 和 harbor 的 AP 可能只有 0.3。这时候你要单独看每个类别的 AP如果某个类别 AP 特别低先检查这个类别的训练样本数是不是太少再检查这个类别的目标尺度是不是和其他类别差异太大导致 anchor 匹配不上。分类别 AP 可以在验证脚本里输出也可以自己写脚本从推理结果里统计。5.2 可视化验证把预测框和真实框画在一起mAP 是一个聚合指标看不出具体哪些图检测得好、哪些图漏检。我习惯在验证集里随机抽 20 张图把真实框和预测框画在同一张图上真实框用绿色预测框用红色置信度标在框上方。这样一眼就能看出模型是漏检了小目标还是把背景误检成了目标还是框的位置偏了。下面是一个简单的可视化脚本import cv2 import os import random def visualize_prediction(img_path, label_path, pred_boxes, save_path): img cv2.imread(img_path) h, w img.shape[:2] # 画真实框绿色 if os.path.exists(label_path): with open(label_path) as f: for line in f: parts line.strip().split() if len(parts) 5: cls_id, xc, yc, bw, bh map(float, parts) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) # 画预测框红色带置信度 for box in pred_boxes: x1, y1, x2, y2, conf, cls_id box cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2) cv2.putText(img, f{conf:.2f}, (int(x1), int(y1) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) cv2.imwrite(save_path, img) # 使用示例pred_boxes 是模型推理输出的列表 # 每项格式为 (x1, y1, x2, y2, confidence, class_id)逻辑说明脚本先读原图再把真实框按归一化坐标还原成绝对坐标画绿框然后把预测框画红框并标置信度。参数方面pred_boxes里的坐标要是绝对像素值如果你的推理输出是归一化的先乘以宽高再传进来。置信度阈值可以先设 0.25看哪些低置信度的框是误检再决定要不要调高阈值。5.3 用混淆矩阵看类别之间的误判遥感图像里有些类别天然容易混比如 storage tank 和 basketball court 在低分辨率下都是圆形bridge 和 harbor 都包含长条形结构。混淆矩阵能直观看出模型把 A 类误判成 B 类的比例。如果某一对类别的混淆特别严重可以考虑在损失函数里给这两个类别加更大的分类权重或者检查标注里是不是有把 A 类标成 B 类的错误样本。标注错误在遥感数据集里很常见尤其是小目标密集的区域人工标注时很容易漏标或错标。6. 把 800 张图用到极致小数据集的增强与迁移策略800 张图在深度学习里算小数据集直接从头训 YOLO 很容易过拟合。我一般会走迁移学习的路子先在 COCO 或 ImageNet 上预训练的权重基础上微调冻结 backbone 的前几层只训检测头和后几层。这样模型能复用自然图像里学到的边缘、纹理特征收敛更快对遥感小目标也更友好。微调时学习率要比从头训小一个数量级常见的是 1e-4 到 1e-3 之间具体看 loss 下降情况调。数据增强方面除了常规的翻转、缩放、色彩抖动遥感场景有两个增强特别有用。一个是随机旋转 90 度的整数倍因为遥感图像的目标方向是任意的旋转不改变目标的语义。另一个是马赛克增强把四张图拼成一张让模型在一张图里看到更多上下文和更小的目标。但马赛克增强对小目标密集的数据集要慎用拼接后小目标可能被裁掉一半反而引入噪声标签。我一般会先开马赛克跑一轮看验证集 mAP如果比不开还低就关掉。还有一个容易被忽略的点NWPU VHR-10 的图像分辨率不完全一致有的图大有的图小。训练时如果统一缩放到固定尺寸小图会被放大大图会被缩小导致同一类目标在不同图里的像素尺度差异很大。我一般会统计所有训练图的宽高分布选一个能覆盖大多数图像尺寸的输入尺寸剩下的靠 padding 补齐。如果差异实在太大可以按分辨率把数据集分成两组分别训两个模型推理时再按图像分辨率选模型。这个做法听起来麻烦但在遥感场景里往往比强行统一尺寸效果更好。最后说一个我踩过的坑有次训完模型验证集 mAP 到了 0.75我以为成了结果拿几张新的遥感图去测发现模型只对和训练集背景相似的图有效换一个区域的图就崩了。后来才意识到NWPU VHR-10 的图像来源比较集中背景纹理有很强的域特征模型其实是在背背景而不是学目标。解决办法是在训练时加更强的色彩抖动和随机裁剪逼模型关注目标本身的形状而不是背景纹理。如果你手里的数据来源单一这一点一定要提前防。希望帮到你。本文还有配套的精品资源点击获取