简介:面向遥感图像分类任务,WHU-RS19土地利用类型遥感卫星图像分类数据集是一套已标注的基准数据,包含机场、海滩、桥梁、商业区、沙漠、农田等19类典型地物,总量约1000张遥感影像,覆盖常见土地利用场景。数据适合高校学生、科研人员及深度学习入门者,既可用于CNN分类、迁移学习的快速上手,也可作为算法精度对比的公共测试集。压缩包共1016个文件,以1006张jpg图像为主体,另有标签映射json、可视化脚本py和系统缩略图db,整体约100MB;资源已预先划分训练集与测试集,同类图片按类别存放,能直接接入分类项目的数据管道,省去自行整理标注的时间。目前已有199人学习下载。附带show脚本可快速浏览各类别样例,帮助核对标注与数据分布;结合作者博客中的CNN分类、图像分割和YOLOv5改进等系列项目,还可延伸到目标检测、语义分割等场景,一站式掌握遥感数据从组织、训练到效果评估的完整流程。
1. WHU-RS19 遥感图像分类数据集:19 类标注、约 1000 张图,第一份实验数据
做遥感图像分类的人,起步阶段最头疼的不是模型选型,而是手里没有一份能直接用的标注数据。自己从 Google Earth 截图再手工标注,一天能凑出三五十张就算快的,标完还得担心类别分布歪了、尺寸不统一。WHU-RS19 就是拿来解决这个问题的:武汉大学发布的遥感卫星图像分类数据集,覆盖 19 种土地利用类型,包含 airport、beach、bridge、commercial、desert、farmland 等常见地物类别,总计约 1000 张已标注图像,并已划分好训练集与测试集,每个类别的图片独立存放在对应文件夹中。你不需要再写复杂的解析脚本,PyTorch 的 ImageFolder 直接能读。资源里还带了一个 show 脚本,可以可视化整个数据集。适合课程设计、毕设预研、算法对比,也适合想快速跑通 CNN 分类流程的从业者。
2. 数据目录与标注结构:先看 labels.json 再谈训练
很多人拿到数据集第一件事就是直接开训,结果跑到一半才发现类别顺序对不上、训练测试划分跟预期不一致。花十分钟把目录结构和标签映射理清楚,后面能省下大量排查时间。
2.1 19 个类别的全景:从 airport 到 viaduct
WHU-RS19 的 19 个类别覆盖了遥感图像中最常见的土地利用场景。除了摘要里提到的 airport、beach、bridge、commercial、desert、farmland,还包括 forest、industrial、meadow、mountain、park、parking、playground、pond、port、railwayStation、resort、river、viaduct。全部类别如下表:
| 类别名 | 含义 | 类别名 | 含义 |
|---|---|---|---|
| airport | 机场 | parking | 停车场 |
| beach | 海滩 | playground | 操场 |
| bridge | 桥梁 | pond | 池塘 |
| commercial | 商业区 | port | 港口 |
| desert | 沙漠 | railwayStation | 火车站 |
| farmland | 农田 | resort | 度假村 |
| forest | 森林 | river | 河流 |
| industrial | 工业区 | viaduct | 高架桥 |
| meadow | 草地 | mountain | 山地 |
| park | 公园 |
每个类别大约 50 张图像,总体接近 1000 张。这个分布比较均匀,不像 ImageNet 那种长尾分布,对新手友好。需要特别注意的是,pond、park、meadow 这三类外观有重叠,比如公园里可能有草地和水塘,模型容易混淆,后面评估时要重点盯这几类。
2.2 目录结构拆解:train/test 双目录加类别子文件夹
资源解压后的目录结构大致如下:
WHU-RS19/ ├── train/ │ ├── airport/ │ │ ├── airport_001.jpg │ │ ├── airport_002.jpg │ │ └── ... │ ├── beach/ │ ├── bridge/ │ └── ... ├── test/ │ ├── airport/ │ │ └── ... │ ├── beach/ │ └── ... ├── show.py └── labels.jsontrain 和 test 下各自按类别建文件夹,同一个类别的图像全部放在对应目录中。labels.json 里记录了类别名称与索引的映射关系,具体内容以资源实际文件为准。这种目录组织方式与 COCO、VOC 那种带独立 annotation 文件的结构完全不同——它不需要坐标框、不需要分割掩码,目录名就是标签。PyTorch 里用torchvision.datasets.ImageFolder可以直接读取,连自定义 Dataset 都不用写。
2.3 训练集与测试集的划分逻辑
资源已经按类别将图像划分到了 train 和 test 目录,常见的划分比例在 4:1 到 8:2 之间,具体以实际目录里的文件数量为准。这意味着你不需要再手动做train_test_split,省了一步操作。
但这个"省事"背后有一个隐患:划分是固定的,同一景观下不同角度拍摄的图像可能同时出现在训练集和测试集里。模型如果记住了拍摄区域的光照、纹理背景,测试指标会虚高。这个坑我在第五章会专门展开。如果你要做严谨的实验,建议在固定划分基础上再做一次 StratifiedKFold 交叉验证,确认模型的泛化能力不是来自数据划分的偶然性。
3. 用 show 脚本做可视化检查:训练前 5 分钟的数据体检
数据拿到手,先别急着上模型。我一般会强制自己先跑一遍可视化脚本,把每类图像都看一遍,确认数据质量、标注一致性、图像尺寸分布是否符合预期。这个习惯帮我挡掉过好几次"训练到一半才发现数据是坏的"的翻车现场。
3.1 show.py 的运行方式与参数设置
资源里的 show 脚本可以直接可视化数据集,常见做法是在命令行指定数据目录和每类抽样数量:
python show.py --data_dir ./WHU-RS19/train --num_samples 5脚本会从每个类别中随机抽取指定数量的图像,拼接成网格显示。这里的--data_dir指向包含类别子文件夹的根目录,--num_samples控制每类显示多少张图像。如果你的脚本不支持命令行参数,直接打开文件修改顶部路径变量也可以。运行前先确认环境里装了 matplotlib,否则会报ModuleNotFoundError。
3.2 可视化能发现的三类问题
跑完可视化,重点检查三个方面。
第一,图像尺寸是否接近。WHU-RS19 的图像大多在 600×600 像素附近,但不同来源的图像会有裁切差异。如果某类图像的尺寸明显偏离主流值,说明采集来源不一致,训练时需要用 Resize 统一。
第二,类别内图像风格是否一致。比如 commercial 类别里如果混入了 residential 的图像,模型学到的就不纯。第三,标注是否有明显错误——farmland 里出现密集建筑物、river 里出现大面积陆地,这类问题肉眼一看就能发现,但如果不做可视化,它会成为模型训练里的黑匣子,你永远不知道 loss 不降是模型问题还是数据问题。
3.3 补充一个图像尺寸统计脚本
show 脚本只能看单张图,整体尺寸分布还得靠统计脚本。我一般会额外写一个快速统计:
import os from PIL import Image base_dir = './WHU-RS19' size_counter = {} for split in ['train', 'test']: split_dir = os.path.join(base_dir, split) for cls_name in os.listdir(split_dir): cls_dir = os.path.join(split_dir, cls_name) if not os.path.isdir(cls_dir): continue for img_name in os.listdir(cls_dir): img_path = os.path.join(cls_dir, img_name) with Image.open(img_path) as img: size = img.size size_counter[size] = size_counter.get(size, 0) + 1 for size, count in sorted(size_counter.items(), key=lambda x: -x[1])[:10]: print(f'{size}: {count} 张')这段代码遍历 train 和 test 下所有类别的 jpg 文件,用Image.size读取宽高并计数。逻辑很简单,但能直接告诉你数据集的尺寸分布是否集中。如果出现多种尺寸混在一起,训练时的 Resize 策略就要格外注意;如果所有图像尺寸一致,则预处理可以省掉 Resize 这一步。
4. 把 WHU-RS19 接进 PyTorch 跑 CNN 分类:加载、增强与评估参数设置
WHU-RS19 的目录结构决定了它在 PyTorch 里非常好用。这一章我按实际训练流程,从数据加载到评估指标逐段说清参数怎么设、为什么这么设。
4.1 用 ImageFolder 加载数据集并构建标签映射
torchvision.datasets.ImageFolder会自动按子目录名生成标签,省去手写 Dataset 的麻烦:
from torchvision import datasets, transforms train_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) test_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_dataset = datasets.ImageFolder('./WHU-RS19/train', transform=train_transform) test_dataset = datasets.ImageFolder('./WHU-RS19/test', transform=test_transform) print(train_dataset.class_to_idx)逻辑说明:ImageFolder扫描./WHU-RS19/train下的子目录,按字母序为每个类别分配索引,class_to_idx里存的就是映射关系。Resize((224, 224))把遥感图统一到模型输入尺寸;RandomHorizontalFlip做随机水平翻转,是一种极低成本的增强;ToTensor把 PIL 图像转成张量并归一化到 0-1;Normalize用 ImageNet 的均值方差做标准化。
这里有个容易被忽略的细节:Normalize的 mean/std 虽然是 ImageNet 统计值,但遥感图像和自然图像在底层特征上是共享的,直接用这个标准化不会出问题。不建议因为"遥感是特殊域"就自定义统计值,除非你的验证指标明确显示有必要。
4.2 模型主干与训练参数设计
WHU-RS19 每类只有 50 张图,这个数据量撑不起 ResNet50 或更深的网络。我一般用 ResNet18 做主干,利用 ImageNet 预训练权重:
import torch import torch.nn as nn from torchvision import models model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.fc = nn.Linear(model.fc.in_features, 19) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)参数说明:model.fc = nn.Linear(..., 19)把最后一层全连接改成 19 维输出,匹配类别数。lr=1e-3是 Adam 在迁移学习场景下的常用起点,如果 loss 震荡可以降到 5e-4。StepLR每 10 个 epoch 把学习率乘 0.1,帮助收敛末期更稳定。
关键点在于batch_size的设置。训练集总共约 800 张,如果batch_size=64,每个 epoch 只有 12 次迭代,梯度更新太稀疏。我一般固定batch_size=16,加上 shuffle,让每个 epoch 有足够的更新步数。
4.3 数据增强的取舍:遥感图与自然图的差异
数据集规模小,增强策略直接决定过拟合程度。WHU-RS19 是俯视角遥感图,与 ImageNet 的自然图相比,没有严格的"上下"概念,所以旋转增强非常适合:
train_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(degrees=30), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])RandomRotation(degrees=30)在 ±30 度范围内随机旋转,对遥感图像来说几乎没有语义损失。ColorJitter调节亮度对比度,模拟不同光照条件下的成像差异。这里不推荐使用RandomResizedCrop随机裁剪,因为 WHU-RS19 的类别语义分布在整个图像中,比如机场需要看到跑道整体结构才能判断,裁剪过度反而会丢失关键判别信息。
4.4 评估指标:别只看总准确率
19 类分类任务,总 accuracy 只是及格线。WHU-RS19 里 pond、park、meadow 外观相似,commercial 和 industrial 也有重叠,必须看 per-class 指标和混淆矩阵:
from sklearn.metrics import confusion_matrix, classification_report import numpy as np model.eval() all_preds = [] all_labels = [] with torch.no_grad(): for inputs, labels in test_loader: outputs = model(inputs) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) print(classification_report(all_labels, all_preds, target_names=list(test_dataset.class_to_idx.keys()))) cm = confusion_matrix(all_labels, all_preds) print(cm)逻辑说明:classification_report输出每个类别的 precision、recall、f1-score,confusion_matrix打印具体错分关系。如果发现 pond 和 park 互相错分严重,这个网络的判别力不够,要么换更大的模型,要么增加对应类别的训练样本。
5. 避坑指南:WHU-RS19 使用中的 5 个真实踩坑记录
这一章全部来自实际训练中的教训,每一条都是真金白银换来的。我之前带学生跑这个数据集,几乎每个人都至少踩中一条。
5.1 测试集指标虚高:训练 95%,测试只有 70%
现象:训练集准确率冲到 95% 以上,测试集只有 70% 出头,明显过拟合却查不到原因。
原因:WHU-RS19 的 train/test 划分是固定的,某些类别的测试图与训练图来自同一景观区域,模型学到的其实是拍摄位置的光照与背景特征,而不是地物语义特征。
解决:不要只依赖固定划分做评估。用StratifiedKFold做 5 折交叉验证,每次用 4 折训练、1 折验证,综合 5 次结果。如果交叉验证的均值和固定划分差很多,说明固定划分的结果不可信,后续实验以交叉验证为准。
5.2 show 脚本在 Windows 上一闪而过
现象:双击运行show.py,图片窗口闪一下就消失了,什么都看不清。
原因:脚本没有阻塞等待用户查看,plt.show()执行完进程就结束了,Windows 控制台随之关闭。常见做法是在脚本末尾加input('Press Enter to exit...')保证窗口停留。
解决:从命令行运行python show.py,或者在脚本里加入阻塞等待。如果脚本本身不报错,看到的窗口一闪而过,全都是这个问题。
5.3 训练中途报 PIL.UnidentifiedImageError
现象:训练到第 n 轮,突然抛PIL.UnidentifiedImageError,进程中止,且每次报错的文件可能相同。
原因:数据在下载或解压过程中某个 jpg 文件头损坏,PyTorch 的 DataLoader 在读取时才解码,平时不触发,一遇到就崩。
解决:在训练前先用verify()全量扫描一遍,不要用load(),因为verify()只校验文件完整性,速度快得多:
from PIL import Image import os for root, dirs, files in os.walk('./WHU-RS19'): for f in files: if f.lower().endswith('.jpg'): path = os.path.join(root, f) try: Image.open(path).verify() except Exception as e: print(f'损坏文件: {path},错误: {e}')扫描后把损坏文件移到单独目录,不要直接删除。万一后面需要追溯数据来源,还有后悔药可吃。
5.4 类别映射错位:预测标签和实际类别对不上
现象:训练正常,但推理时保存的类别名与图片实际内容不符,比如把 river 预测成了 pond,输出的标签还是错的。
原因:ImageFolder的类别顺序按文件夹名字母序排列,airport 自然排在最前面,索引为 0。但如果你自己写了标签映射,用的是 labels.json 里的顺序,两边的索引就对不上了。
解决:始终以dataset.class_to_idx为准。训练完保存模型时,同时把class_to_idx存成 json,推理时加载同一个映射文件。不要自己硬编码类别与索引的关系,也不要直接读 labels.json 自带的顺序。
5.5 batch 维度不一致:stack expects each tensor to be equal size
现象:DataLoader 在 collate 时报错RuntimeError: stack expects each tensor to be equal size。
原因:一部分图像没有走 Resize,或者 transform 顺序错误——把Resize放在了ToTensor之后。ToTensor已经把 PIL 图像转成 Tensor,Tensor 上不能再做Resize。
解决:检查 transform 顺序,必须是Resize → ToTensor → Normalize。再确认传入ImageFolder的 transform 不是 None,并打印一张图像的 shape 做验证:
sample = train_dataset[0][0] print(sample.shape) # 期望 torch.Size([3, 224, 224])6. 进阶技巧:把分类数据集改造成伪检测数据的做法
WHU-RS19 是图像级分类标注,没有目标边界框。但如果你想在这个数据集上跑目标检测的预实验,有一个低成本改造方案:把每张 600×600 的图切成 4 个 300×300 的 patch,每个 patch 继承原图的类别标签,得到一个"伪检测"数据集。这个做法解决的是检测任务冷启动问题——在真实检测标注数据不足时,先验证检测网络的收敛能力。
import cv2 import os def split_image(src_path, out_dir, size=300): img = cv2.imread(src_path) h, w = img.shape[:2] for i, (y, x) in enumerate([(0, 0), (0, w // 2), (h // 2, 0), (h // 2, w // 2)]): patch = img[y:y + size, x:x + size] out_name = f'{os.path.basename(src_path)[:-4]}_{i}.jpg' cv2.imwrite(os.path.join(out_dir, out_name), patch) # 使用示例 split_image('./WHU-RS19/train/airport/airport_001.jpg', './patches/airport')逻辑说明:(0, 0)取左上角,(0, w // 2)取右上角,(h // 2, 0)取左下角,(h // 2, w // 2)取右下角。每块 300×300 继承原图的机场标签,四张 patch 都算作正样本。这样做出的数据集可以用于训练 patch-level 分类器,相当于在特征图上做滑窗定位,是检测的雏形。如果后续配套 YOLO 的标签格式,就只需要把每个 patch 对应的图像坐标换算成边界框坐标。
我从那以后每次拿到新的遥感数据集,都会强制走一遍 show 脚本看数据、统计尺寸分布、跑一次小模型基线、再上正式训练的流程。这个习惯帮我挡掉了至少两次数据损坏和一次标签错位导致的翻车。希望帮到你。
本文还有配套的精品资源,点击获取