简介:RSOD-Dataset遥感图像数据集面向遥感图像处理与计算机视觉方向的研究者、开发者及学生,用于目标检测、图像分类与变化检测等任务的模型训练与验证。资源包共487个文件,以189个jpg图像、149个xml标注文件和149个txt说明文件为主,压缩包约309.44MB,图像与标注一一对应,便于直接接入YOLO、Faster R-CNN、Mask R-CNN等检测框架,也可用于SVM、随机森林或CNN的分类实验。playground子文件夹提供样例图像、预处理脚本、标注文件及使用指南,帮助读者快速完成数据探索与模型开发。目前已有3769人学习下载,适合希望系统掌握遥感图像分析流程、积累标注数据与算法实践经验的读者参考使用。
1. 遥感图像数据集 RSOD:从标注格式到训练落地的完整拆解
做遥感目标检测的同行多半有过这种体验:模型结构调了又调,学习率从 1e-2 试到 1e-5,mAP 纹丝不动,最后发现问题出在数据标注上——类别对不上、坐标越界、小目标被 resize 抹掉。RSOD-Dataset 就是在这个环节里被反复提起的一份遥感图像数据集,它面向遥感场景下的目标检测任务,覆盖飞机、油罐、立交桥、操场等典型地物类别,标注以水平框为主,适合用来验证检测模型在俯视视角、小目标密集、背景复杂条件下的真实表现。这份资源适合三类人:刚接触遥感检测、需要一份能跑通全流程的数据练手的新手;想对比不同检测头在遥感域差异的熟手;以及需要快速搭一套可复现 baseline 的工程人员。下面按「它是什么 → 怎么读 → 怎么转 → 怎么训 → 坑在哪」的顺序拆开讲。
2. 遥感图像数据集的结构与标注格式解析
遥感数据和常规自然图像数据集最大的区别不在分辨率,而在成像视角和标注习惯。俯视视角意味着目标没有明显的「上下」语义,旋转不变性变得重要;同时单张图里目标尺度跨度极大,一架飞机可能只占 20 像素,一个操场可能横跨半张图。理解 RSOD 的结构,本质是理解这些特性怎么体现在目录和标注文件里。
2.1 目录组织与文件命名规律
RSOD 常见的组织方式是按类别分文件夹存放图像,标注单独放一个目录,图像和标注通过文件名主干对应。典型结构如下:
RSOD-Dataset/ ├── aircraft/ │ ├── 00001.jpg │ └── ... ├── oiltank/ ├── overpass/ ├── playground/ └── annotations/ ├── 00001.xml └── ...这里有几个容易忽略的点。第一,类别目录名不一定和标注里的类别名完全一致,比如目录叫oiltank,标注里可能写成oil_tank或storage-tank,训练前必须做一次映射对齐,否则类别数会凭空多出来。第二,图像格式以 JPG 为主,但遥感图压缩伪影对边缘目标影响明显,如果做精细分割任务,建议先检查是否有 PNG 版本或原始无压缩源。第三,文件名主干是唯一的关联键,一旦出现00001.jpg对应00001_a.xml这种带后缀的情况,脚本按主干匹配就会漏标。
我一般拿到数据集先跑一遍统计,确认图像数、标注数、类别分布三者是否自洽:
import os from collections import Counter import xml.etree.ElementTree as ET img_dir = "RSOD-Dataset/aircraft" ann_dir = "RSOD-Dataset/annotations" imgs = {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith(".jpg")} anns = {os.path.splitext(f)[0] for f in os.listdir(ann_dir) if f.endswith(".xml")} print("图像数:", len(imgs)) print("标注数:", len(anns)) print("有图无标:", imgs - anns) # 这些图训练时会被跳过 print("有标无图:", anns - imgs) # 这些标注是脏数据 # 统计类别分布,看是否严重长尾 cls_counter = Counter() for f in os.listdir(ann_dir): if not f.endswith(".xml"): continue tree = ET.parse(os.path.join(ann_dir, f)) for obj in tree.findall("object"): cls_counter[obj.find("name").text] += 1 print(cls_counter)这段脚本做了三件事:用集合差找出图像和标注的不匹配项,用Counter统计每个类别的实例数。参数上,img_dir和ann_dir要按实际路径改;如果标注是 JSON 而非 XML,把ET.parse换成json.load即可。跑完如果发现某个类别实例数只有个位数,基本可以判断是标注遗漏或类别名写错,别急着训,先修数据。
2.2 标注字段含义与坐标体系
RSOD 的标注以 PASCAL VOC 风格的 XML 为主,核心字段是bndbox下的xmin/ymin/xmax/ymax。这里有两个坐标系问题必须搞清楚。一是原点位置,VOC 约定左上角为原点,x 向右、y 向下,和 OpenCV、PIL 一致,但和某些 GIS 工具的经纬度坐标系完全不同,混用会导致框整体偏移。二是坐标是否包含边界,VOC 的框是闭区间,xmax指向目标最右像素,转 YOLO 归一化时要用(xmax - xmin) / width,如果误用(xmax - xmin + 1)会引入一个像素的系统误差,小目标上这个误差占比不小。
另外要留意difficult和truncated字段。遥感图里被云层遮挡、被建筑截断的目标很常见,如果训练时把difficult=1的样本也当正样本,模型会学到一堆模糊边界;但全丢掉又会让召回率下降。常见做法是训练时保留但降权,验证时按标准协议决定是否计入。这个取舍没有标准答案,取决于你的下游任务更怕误检还是更怕漏检。
3. 从 VOC 到 YOLO:格式转换与数据校验
拿到 RSOD 后,多数人不会直接用 VOC 格式训,因为主流检测框架要么吃 YOLO 的 txt,要么吃 COCO 的 json。转换本身不难,难的是转完之后怎么确认没转错。这一章把转换脚本、校验方法和参数含义讲透。
3.1 转换脚本与归一化细节
下面这段脚本把 VOC XML 转成 YOLO 的class x_center y_center w h格式,全部归一化到 0~1:
import os import xml.etree.ElementTree as ET # 类别名到索引的映射,顺序一旦定下就不能改 CLASS_MAP = {"aircraft": 0, "oiltank": 1, "overpass": 2, "playground": 3} def voc_to_yolo(xml_path, img_w, img_h, out_path): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in CLASS_MAP: continue # 未登记类别直接跳过,避免索引错乱 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, xmax = max(0, xmin), min(img_w, xmax) ymin, ymax = max(0, ymin), min(img_h, ymax) if xmax <= xmin or ymax <= ymin: continue # 裁剪后无效的框丢弃 xc = (xmin + xmax) / 2.0 / img_w yc = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{CLASS_MAP[name]} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines))逻辑上,CLASS_MAP是全局唯一真相,训练配置里的names必须和它逐项对应,顺序错了模型会把油罐认成飞机。坐标裁剪那两行是血泪经验:遥感标注里偶尔有框超出图像边界的,不裁的话归一化后会出现大于 1 或小于 0 的值,某些框架直接报错,某些框架静默接受但训练时梯度异常。if xmax <= xmin的判断是兜底,防止裁剪后框退化成一条线。归一化保留 6 位小数足够,再多是浪费。
3.2 转换后的三重校验
转完不校验,等于没转。我一般做三层检查。第一层是数量校验,转换前后实例总数应该一致(除非主动丢弃了无效框),差太多说明有解析失败。第二层是可视化抽检,随机抽 20 张把框画回图上,肉眼看有没有整体偏移或缩放错误。第三层是数值范围校验,扫描所有 txt 确认坐标都在 [0,1] 内。
import glob bad = [] for txt in glob.glob("labels/*.txt"): with open(txt) as f: for i, line in enumerate(f): parts = line.strip().split() if len(parts) != 5: bad.append((txt, i, "字段数不对")) continue vals = list(map(float, parts[1:])) if any(v < 0 or v > 1 for v in vals): bad.append((txt, i, "坐标越界")) print("异常条目:", bad[:10], "共", len(bad))可视化那步用 OpenCV 或 PIL 都行,关键是别省。我见过太多人跳过抽检,训了两天才发现框整体偏了半个目标,回头查是img_w和img_h传反了。这种错误数值校验查不出来,只有眼睛能发现。
提示:转换脚本里的
img_w、img_h必须从对应图像实际读取,不要用固定值。遥感图尺寸不统一是常态。
4. 训练配置与常见踩坑排查
数据准备好之后,训练本身反而是相对标准化的环节。但遥感域有几个特有的坑,不提前知道会浪费大量算力。这一章把配置要点和排查清单一起给出来。
4.1 输入尺寸、锚框与类别不平衡
遥感小目标多,输入尺寸直接决定小目标还剩几个像素。常见做法是把长边缩到 1024 或 1280,而不是自然图像的 640。缩得太狠,20 像素的飞机变成 5 像素,特征图上几乎消失。锚框方面,通用 COCO 预训练锚框偏向中大目标,直接拿来训遥感会欠拟合小目标,建议用 k-means 在自己的标注上重新聚类一遍锚框尺寸。
类别不平衡在 RSOD 上也很典型,飞机和油罐实例数往往远多于立交桥。处理方式有三种:过采样稀有类别、在 loss 里给稀有类加权、或者用 focal loss 这类自带难例挖掘的损失。我一般先看分布,长尾不严重就不动,严重了再上加权,避免一上来就改损失函数把问题复杂化。
# 训练配置片段,以常见 YOLO 风格为例 train: RSOD-Dataset/images/train val: RSOD-Dataset/images/val nc: 4 names: ["aircraft", "oiltank", "overpass", "playground"] imgsz: 1024 batch: 8 epochs: 100nc和names必须和转换时的CLASS_MAP严格一致,这是最容易翻车的地方。imgsz调到 1024 后显存占用会明显上升,batch要相应下调,否则 OOM。如果显存实在不够,可以用梯度累积模拟大 batch,但要注意 BN 层的统计会受影响。
4.2 避坑清单:五条真实踩坑记录
现象一:训练 loss 正常下降,但验证 mAP 始终接近 0。原因:验证集的标注路径没配对,框架读到了空标签,把全部预测当误检。 解决:单独跑一遍验证集的数据加载,打印每个 batch 的标签数量,确认非零。
现象二:模型把背景大量误检成目标,置信度还不低。原因:负样本(纯背景图)太少,或者数据增强里 mosaic 拼接引入了不合理的上下文。 解决:补充背景图,或降低 mosaic 概率,先确认误检是不是增强策略导致的。
现象三:同一张图里密集小目标只检出零星几个。原因:NMS 的 IoU 阈值设太高,密集目标互相抑制;或输入尺寸太小。 解决:把 NMS IoU 从 0.7 降到 0.5 左右试,同时确认输入尺寸没被意外缩小。
现象四:训练到一半 loss 突然变 NaN。原因:标注里有宽高为 0 的退化框,计算宽高损失时除零。 解决:回到 3.2 的校验脚本,把所有w或h为 0 的条目清掉。
现象五:换了台机器重新训,结果对不上。原因:类别索引顺序变了,或者随机种子没固定。 解决:把CLASS_MAP和随机种子写进配置文件,随代码一起版本管理。
这五条覆盖了数据、增强、后处理、数值稳定性和可复现性,基本能解释遥感检测里八成的「玄学」问题。遇到指标异常,按这个顺序排查,比盲目调参高效得多。
5. 进阶技巧:用切片推理榨干小目标召回
遥感图分辨率高、目标小,整图直接推理时小目标在特征图上只剩一两个像素,召回率上不去。一个实用技巧是切片推理(slicing inference):把大图切成带重叠的小块分别推理,再把结果映射回原图做全局 NMS。这个思路在遥感社区里被反复验证有效,代价是推理时间成倍增加。
实现上,切片大小和重叠率是两个关键参数。切片太小,单块里目标占比反而合理,但块数暴增;切片太大,又回到小目标消失的问题。我一般让切片尺寸接近训练时的imgsz,重叠率取 20% 到 25%,保证跨块目标至少完整出现在一个块里。
def slice_image(img, slice_size=1024, overlap=0.2): h, w = img.shape[:2] stride = int(slice_size * (1 - overlap)) slices = [] for y in range(0, h, stride): for x in range(0, w, stride): x2 = min(x + slice_size, w) y2 = min(y + slice_size, h) x1 = max(0, x2 - slice_size) # 保证边缘块也是完整尺寸 y1 = max(0, y2 - slice_size) slices.append((img[y1:y2, x1:x2], x1, y1)) return slicesstride由切片尺寸和重叠率共同决定,重叠越大 stride 越小、块数越多。x1 = max(0, x2 - slice_size)这行是为了让靠边的块也保持完整尺寸,避免出现半块导致目标被切。每块推理完,把框坐标加上(x1, y1)偏移映射回原图,最后统一做一次 NMS。这里 NMS 的 IoU 阈值要比常规低一些,因为重叠区域会产生大量重复框。
验证切片推理是否真的有效,别只看整体 mAP,要单独统计小目标(面积小于 32×32)的召回率。我一般会跑两组对照:整图推理 vs 切片推理,固定同一个模型权重,只改推理方式。如果小目标召回提升明显而大目标基本不变,说明切片策略生效;如果大目标反而下降,多半是全局 NMS 阈值没调好,重复框把正确框压掉了。
从那以后我每次上遥感检测任务,都强制先跑一遍切片推理对照,再决定要不要把它写进部署流程。这个习惯帮我避开了好几次「模型明明训得不错、上线却漏检严重」的翻车。希望帮到你。
本文还有配套的精品资源,点击获取