简介:遥感图像分类是理解地表覆盖与土地利用的关键技术,而高质量标注数据集则是模型训练的基础。WHU-RS19作为武汉大学发布的开源遥感影像数据集,包含约1000张覆盖19类地物的图像,虽然规模不大,却足以支撑完整的深度学习分类流程。本文从数据集的下载、目录结构与TIFF读取细节出发,探讨了遥感数据切分防泄漏、预处理规范等工程实践,并给出基于PyTorch和ResNet的微调训练方案。借助混淆矩阵与分类报告,可以精准定位bridge与overpass等易混淆类别,避免评估指标的虚高。该数据集既能作为入门深度学习的练习平台,也能为遥感语义分割、传统方法对比等进阶研究提供验证。通过严谨的数据划分与分阶段微调,小数据集同样能产出可靠且可复现的实验结论。
1. WHU-RS19 是什么:1000 张图、19 类地物,够不够用、值不值得上手
做遥感图像分类的人,很容易陷入两个极端:要么手里数据多到要跑几天几夜,要么少到连一个像样的训练集都拼不出来。WHU-RS19 正好卡在中间,它是 WHU(武汉大学)公开的一套老牌遥感卫星图像分类数据集,约 1,000 张已标注图像,覆盖 19 种土地利用类型:飞机、海滩、桥、商业区、农田、森林、高速公路、草地、工业区、湖泊、山地、河流、居民区、海洋、体育场、公园、停车场、火车站。图像来自 Google Earth,统一约 600×600 像素,以 TIFF 格式存盘,标签直接写在目录名里。这个数据量级意味着它解决不了复杂遥感大模型训练,但足够让一个人用一个晚上跑通“数据整理—训练—评估”全流程,看清楚深度学习在遥感数据上的真实边界。它适合刚接触遥感方向的工程师、需要短平快发论文的研究生,以及所有想验证分类模型选型的人。
2. 拿到 WHU-RS19:下载、解压和目录结构一次看清
2.1 下载与解压:先确认压缩格式,再看文件是否完整
WHU-RS19 的原始发布页是武汉大学遥感组的公开页面,这批数据最早在 2010 年前后放出,后来被 GitHub、Kaggle 和不少学术镜像站转载。常见做法是在搜索引擎里直接搜“WHU-RS19 dataset”,或者在数据比赛平台里找已经打包好的副本。我一般会优先找 GitHub 上的镜像,因为原始页面的下载链接有时候是 http 直链,断点续传体验很差;镜像站通常给出 zip 包,几 MB 到几十 MB 不等。拿到压缩包后,第一步不是急着解压,而是看文件类型:
file whu-rs19.zip du -h whu-rs19.zip unzip -l whu-rs19.zip | head -20file命令确认它到底是不是 zip,有些镜像站给的其实是 tar.gz 或 rar;du看体积是否符合“约 1,000 张”的预期,1,000 张 600×600 的 TIFF 压缩后大概几十 MB,如果只有几百 KB,说明文件不完整或被人二次压缩。unzip -l列出压缩包内前 20 个条目,能快速看目录结构是否齐全。解压时我习惯先建一个干净目录,防止把一堆分类文件夹散落到当前目录里:
mkdir -p whu-rs19 unzip whu-rs19.zip -d whu-rs19 cd whu-rs19 ls -lunzip -d指定解压目标,解压后ls -l应该能看到 19 个子目录。如果某个类目缺失,多半是上传副本的人漏掉了部分文件,需要换一个镜像源。注意文件名编码问题:部分副本的目录名带中文,或者文件名经过转码变成airport_001.tif这类英文命名,这对于后续写代码影响不大,但在 Windows 上解压时遇到乱码,先试unzip -O gbk或改用 7zip 处理。
2.2 标注格式:目录名就是标签,别指望有 XML 或 JSON
WHU-RS19 和 ImageNet 一样,没有单独提供 label 文件,标注信息隐藏在目录结构里:airport/目录下都是飞机场图像,beach/目录下都是海滩图像。每个目录内的文件一般命名为airport_001.tif、airport_002.tif这样的规则。这意味着两件事:第一,读数据时不需要解析标注文件,直接拿父目录名做标签;第二,如果自己重新整理数据、复制到别处,极容易把标签弄乱。我踩过最蠢的坑是写了个脚本按文件名前缀分类,结果bridge和overpass两个目录因为单词太长被截断,混进同一批训练样本里,后面查了半天才明白模型为什么一直把这两个类混淆。所以,稳妥做法是在拿到数据后先用脚本做一次“目录名—类别编码”映射,把标签固化成 CSV 或 JSON,不要每次都依赖目录名拼字符串:
| 类别编码 | 目录名 | 中文含义 | 约张数 |
|---|---|---|---|
| 0 | airport | 飞机场 | 约 50 |
| 1 | beach | 海滩 | 约 50 |
| 2 | bridge | 桥 | 约 50 |
| 3 | commercial | 商业区 | 约 50 |
| 4 | farmland | 农田 | 约 50 |
| 5 | forest | 森林 | 约 50 |
| 6 | highway | 高速公路 | 约 50 |
| 7 | grassland | 草地 | 约 50 |
| 8 | industrial | 工业区 | 约 50 |
| 9 | lake | 湖泊 | 约 50 |
| 10 | mountain | 山地 | 约 50 |
| 11 | park | 公园 | 约 50 |
| 12 | parking | 停车场 | 约 50 |
| 13 | playground | 体育场 | 约 50 |
| 14 | residential | 居民区 | 约 50 |
| 15 | river | 河流 | 约 50 |
| 16 | sea | 海洋 | 约 50 |
| 17 | station | 火车站 | 约 50 |
| 18 | overpass | 立交桥 | 约 50 |
这类目表是我按数据集公开说明整理的,具体每类数量会有轻微浮动,总量约 1,000 张。拿到数据后,第一件事永远是用脚本统计每个目录下的文件数,而不是凭感觉:
import os root = "whu-rs19" class_stats = {} for cls_name in sorted(os.listdir(root)): cls_path = os.path.join(root, cls_name) if os.path.isdir(cls_path): n = len([f for f in os.listdir(cls_path) if f.endswith((".tif", ".TIF", ".png"))]) class_stats[cls_name] = n print(f"{cls_name}: {n} 张") print("总样本数:", sum(class_stats.values()))这段代码遍历根目录下每个子目录,只统计图片文件,不统计隐藏文件和临时文件。逻辑很简单,但价值很大:它能第一时间暴露类别缺失、数量悬殊等问题。如果发现某些类只有 30 张,另一些类有 60 张,后续训练时就要考虑加权损失或数据增强。WHU-RS19 本身是相对均衡的,但如果拿到别人重新整理的副本,这种统计就成了一种必要的数据体检。统计完以后,建议顺手把统计结果存成class_stats.json,后面训练脚本和实验记录都会用到。这里有个细节:.tif和.TIF后缀都可能出现,统计时要同时匹配两种后缀,否则会漏数文件。
3. 把 1000 张图变成可训练样本:预处理与数据切分
3.1 图像特征与统一读取:TIFF 会遇到的小麻烦
WHU-RS19 的原始图像是 600×600 的 TIFF,来源是 Google Earth 的卫星视角,空间分辨率大约 2 米。这意味着图像里能看到清晰的建筑物轮廓、道路走向、森林纹理,但和无人机影像、0.5 米级高分辨率商业卫星影像相比,细节还是偏粗。处理这类数据时,第一件事是确认每个文件是几通道、什么位深。直接调用cv2.imread读 TIFF 经常翻车,因为 TIFF 可能是 16 位灰度、带 Alpha 通道,或者色彩空间是 YCbCr,OpenCV 按 BGR 读出来色调会偏掉。我一般用tifffile或skimage.io.imread做第一遍统一读入,输出到 numpy 数组后看shape和dtype:
from skimage import io import numpy as np sample = io.imread("whu-rs19/airport/airport_001.tif") print("shape:", sample.shape) print("dtype:", sample.dtype) print("channel range:", sample.min(), "-", sample.max())skimage.io.imread内部会调用 tifffile 处理多种 TIFF 变体,返回的数组无论是三通道还是单通道,都能直接反映真实情况。打印结果的目的是确认:如果shape末尾没有 3 这个维度,说明读出来的是单通道或伪彩色,这时候用np.stack把它扩成三通道即可;如果dtype是uint16,要转为uint8,否则送入神经网络时数值范围不是 0–255,归一化会出问题。转换时要小心,直接除以 257 或astype(np.uint8)都可以,但astype会截断信息,我习惯用(sample / 257).astype(np.uint8)把 16 位范围压到 8 位,保留更多灰度级。
3.2 数据切分:避免肉眼简单切分带来的数据泄漏
绝大多数入门教程会让用户直接train_test_split随机切分,但加到 WHU-RS19 上会出现一个隐蔽问题:数据集图像本来就是从更大场景里裁切下来的,相邻 patch 之间高度相似。如果随机切分,同一片森林可能既出现在训练集又出现在验证集,模型其实是在“背答案”,而不是在“学习”。这是初学遥感分类最容易犯的错,很多没有遥感背景的人会把自然图像的随机切分习惯带过来,导致验证精度虚高。做遥感数据切分,我建议先按文件名做分组,保证同一来源的 patch 不跨集合:
import os import random from collections import defaultdict def build_sample_list(root, known_prefix): samples = [] for cls_name in sorted(os.listdir(root)): cls_path = os.path.join(root, cls_name) if not os.path.isdir(cls_path): continue for fname in os.listdir(cls_path): if not fname.endswith((".tif", ".TIF")): continue samples.append((os.path.join(cls_path, fname), cls_name)) random.shuffle(samples) train, val, test = [], [], [] for path, cls in samples: r = random.random() if r < 0.7: train.append((path, cls)) elif r < 0.85: val.append((path, cls)) else: test.append((path, cls)) return train, val, test train, val, test = build_sample_list("whu-rs19", "whu-rs19") print(len(train), len(val), len(test))但话要说清楚:这个脚本只保证了按样本随机切分,没有真正解决相邻 patch 重叠问题。要做得更严谨,需要知道每张图在原图上对应的坐标,而 WHU-RS19 自身不带切分前的底板,所以在多数公开复现里,大家退而求其次,直接用随机切分,但会在论文或博客里注明“随机切分可能导致同一地点前后样本重叠,结果有乐观偏差”。我个人的习惯是:先用这种简单切分跑通流程、定网络,等到做最终实验时,换一个更保守的切分方式——把每类样本按文件名编号排序,奇数编号进训练集、偶数编号进测试集,人工打散空间相邻关系。虽然无法根治,但至少让你对泛化能力有个诚实估计。
预处理另一个重点是统一尺寸。原始图是 600×600,直接送网络太大,很多公开代码会用Resize((600, 600))或直接Resize((224, 224))。这里有个容易被忽视的细节:单张 600×600 的 patch,内部地物尺度已经是“场景级”,直接缩到 224×224 会丢失道路纹路和建筑轮廓,而这类判别信息恰恰是 WHU-RS19 分类的关键。我一般先把图缩到 256×256 再随机裁剪 224×224,必要时保留短边等比例缩放后再 pad。这样既能维持 batch 内尺寸统一,又保留了随机裁剪带来的空间扰动。
4. 训练一个分类基线:用 PyTorch 和 ResNet 在 WHU-RS19 上跑通最小流程
4.1 自定义 Dataset:批量读取 WHU-RS19 并动态增强
在 PyTorch 里写数据集类,核心是做一个__getitem__方法,返回(图像, 标签)对。标签从父目录名映射成整数。因为 WHU-RS19 没有附带标注文件,这一步等于把“目录名标注法”转换成训练标准格式。我习惯把第 2 章的类别映射固化成一个字典,然后像下面这样写:
import torch from torch.utils.data import Dataset from PIL import Image import torchvision.transforms as T CLASS_NAMES = ["airport", "beach", "bridge", "commercial", "farmland", "forest", "highway", "grassland", "industrial", "lake", "mountain", "park", "parking", "playground", "residential", "river", "sea", "station", "overpass"] CLASS_TO_IDX = {name: i for i, name in enumerate(CLASS_NAMES)} train_tf = T.Compose([ T.Resize(256), T.RandomResizedCrop(224, scale=(0.8, 1.0)), T.RandomHorizontalFlip(), T.ColorJitter(brightness=0.2, contrast=0.2), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) class WHURS19Dataset(Dataset): def __init__(self, samples, transform=None): self.samples = samples self.transform = transform def __len__(self): return len(self.samples) def __getitem__(self, idx): path, cls_name = self.samples[idx] image = Image.open(path).convert("RGB") label = CLASS_TO_IDX[cls_name] if self.transform: image = self.transform(image) return image, label这个类很短,但有几个值得说的点。第一,Image.open(...).convert("RGB")是必须的,即使原始 TIFF 是灰度图,也得先转成 RGB,否则后续 Normalize 的均值方差通道对不上。第二,训练时的增强组合RandomResizedCrop(224, scale=(0.8, 1.0))比单纯Resize(224)效果好,因为 WHU-RS19 类别里既存在“全局土地利用模式”(农田、海洋),也存在“局部对象模式”(飞机、停车场),随机裁剪能让模型同时看到整体和局部。第三,ColorJitter的幅度要克制,遥感图像颜色相对固定,过强的色彩抖动会让模型学会忽略真实光谱差异,我把 brightness 和 contrast 都压在 0.2。最后,因为测试集和验证集不应该做增强,我会给验证集单独定义一个只包含 Resize、ToTensor、Normalize 的 transform,不再复用train_tf。
4.2 训练脚本:学习率、batch size、冻结策略
模型选择上,WHU-RS19 只有 1,000 张图,从零训练一个 ResNet 不现实,常规做法是加载 ImageNet 预训练的 ResNet18,微调最后一层。这个数据集上 ResNet18 的效果已经很能说明问题,不需要一上来就搬 ResNet50 或更重的网络,否则过拟合风险大、跑起来也慢。下面是一段精简但可跑的训练循环骨架:
import torch.nn as nn from torch.utils.data import DataLoader from torchvision import models model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.fc = nn.Linear(model.fc.in_features, 19) for name, param in model.named_parameters(): if "fc" not in name: param.requires_grad = False optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3) criterion = nn.CrossEntropyLoss() train_loader = DataLoader(train_set, batch_size=32, shuffle=True, num_workers=4) model.train() for epoch in range(30): running_loss = 0.0 for images, labels in train_loader: images, labels = images.cuda(), labels.cuda() optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() print(f"epoch {epoch+1}: loss = {running_loss/len(train_loader):.4f}")这里我先把骨干网络所有参数冻结,只训练最后的全连接层:requires_grad = False是核心操作。这样做的原因是数据集只有 1,000 张,从头微调全部参数极容易过拟合,特别是前几层学到的边缘、纹理特征在 ImageNet 上已经足够泛化,再在遥感小样本上调它们只是浪费时间。用filter(lambda p: p.requires_grad, model.parameters())喂给优化器,保证冻结层不参与权重更新。学习率先给 1e-3,只训练 FC 层时这个学习率没问题;跑完 30 轮后,如果需要进一步提升精度,再解冻最后两层的 Block,并把学习率降到 1e-4 或 5e-5,这就是常见的“两阶段微调”。
Batch size 选 32,在多数单卡上都能跑动。num_workers=4让数据加载多进程并行,但要注意,如果机器是 Windows,num_workers大于 0 时必须在if __name__ == "__main__":里调用训练脚本,否则会无限重启子进程。这是老生常谈,但每次都有新手踩。30 个 epoch 对 WHU-RS19 来说有点保守,实际训练中通常 20 epoch 左右 loss 就开始平缓,如果你观察 val accuracy 连续 5 个 epoch 不涨,可以提前停。
4.3 评估:别只看 accuracy,用混淆矩阵和分类报告定位问题
很多人在 WHU-RS19 上跑到 90% 以上 accuracy 就觉得完事了。但遥感影像分类里,平均准确率是很能骗人的指标,因为 19 类里某些类样本多、容易分对,会把整体数字抬上去。真正能说明问题的是分类报告和混淆矩阵:
from sklearn.metrics import classification_report, confusion_matrix model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for images, labels in test_loader: images, labels = images.cuda(), labels.cuda() outputs = model(images) preds = outputs.argmax(dim=1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) print(classification_report(all_labels, all_preds, target_names=CLASS_NAMES)) cm = confusion_matrix(all_labels, all_preds)classification_report输出每个类别的 precision、recall、F1-score,能一眼看出哪些类别拉低了整体性能。confusion_matrix则显示哪些类之间互相混淆。在这个数据集上,最典型的混淆对是 bridge 和 overpass,其次可能是 mountain 和 forest。这一步得到的结论会直接影响后续调优方向:如果是 bridge/overpass 混淆,说明特征提取不够细腻,可以考虑更大输入分辨率;如果是 sample 数太少的类 recall 低,应该做类别加权或数据增强。不要跳过评估直接改模型,那样你只是在盲调。“高精度遥感”这个目标看着很高大,但实际上就是从这种小数据集上的逐类指标抠出来的。还要注意:评估时验证集 transform 里不能有 ColorJitter 和 RandomResizedCrop,否则每次评估结果都会抖动,失去可比性。
5. WHU-RS19 实战避坑:5 个我踩过且必须绕开的坑
5.1 随机切分让验证集虚高:同一场景的 patch 泄漏了
现象:训练集 accuracy 一直徘徊在 85%,验证集却轻松到 96%,我还以为模型泛化能力极强,结果部署到新数据上,accuracy 掉到 70% 以下。原因:WHU-RS19 里同一类别的图像本身存在近邻场景重叠,随机切分相当于让模型提前记住了验证集样本,我拿到的虚高数字是“背答案”的产物。解决:切分时按文件名编号做分组,让相邻编号进入同一集合;或者在论文里明确写清楚“随机切分,结果偏乐观”,不搞虚假宣传。很多遥感分类文章的复现实验都不提这个点,但这是数据集自带的结构性问题,必须注意。
5.2 直接 Resize(224) 把细节压没了
现象:输入尺寸从 600×600 直接缩到 224×224 后,模型在 beach 和 river 上还能分对,但 bridge、overpass、park 这三类的 recall 明显下降。原因:600×600 里的桥面、公路匝道、公园小径在大比例缩放后变成几根模糊的线条,本来清晰的纹理全没了。解决:不要直接 resize,改用 256×256 缩放加随机裁剪到 224×224,必要时保留原始比例,在 batch 里用 pad 对齐。另一种常见做法是直接训练 384×384 输入,代价是显存翻倍,但精度往往能涨 2–3 个点,特别是在有预训练模型支撑时。
5.3 小数据全量微调必过拟合
现象:loss 在前 10 个 epoch 下降得很快,之后训练 loss 继续降,验证 loss 反而抬头,准确率不再上升。原因:1,000 张图要学 19 类,同时微调一个 1200 万参数的 ResNet18,参数容量远超样本量,模型开始死记硬背训练集。解决:严格按“先冻结骨干,只训练 FC;再解冻最后两层,低学习率微调”两阶段去做。你看到的参数设置在 4.2 里已经给了,requires_grad = False是非常关键的一步。如果解冻后还是过拟合,可以加 dropout(在 FC 前加一层nn.Dropout(0.3)),或者用 Label Smoothing 把硬标签软化,减轻模型对训练集标签的绝对信任。
5.4 bridge 和 overpass 永远互相认错
现象:混淆矩阵里 bridge 的样本大量被预测成 overpass,overpass 又大量被预测成 bridge。原因:这两类在视觉上高度相似,都是“跨越障碍物的长条结构”,WHU-RS19 的标注边界本身也存在一些模糊地带;再加上 Google Earth 分辨率有限,立交桥和跨河桥在高空视角下的差异可能只是一个弧度。解决:首先要正视这是数据集固有难点,不追求 100% 分清;其次可以修改网络输出,把这两个类做层次分类,比如先分大组“交通设施”,再细分子类;最后可以引入语义分割模型的上下文信息,用 SegFormer 这类 Transformer 结构提取全局上下文,比单纯 CNN 分类更敏感。这个坑提醒我:遥感分类里“看似容易的类”往往是最容易翻车的地方,必须提前看混淆矩阵再决定取舍。
5.5 TIFF 读取报错、通道 mismatch
现象:用cv2.imread读某些.tif,显示None,或者读出来后图像颜色怪异,像蒙了一层滤镜。原因:TIFF 格式太开放了,可能是 8 位 RGB、16 位灰度、CMYK,或者带 Alpha 通道;OpenCV 对 TIFF 支持并不完善,部分文件会被默认当成 BGR 甚至按 C 顺序读。解决:统一用tifffile.imread或skimage.io.imread读入,读完后打印shape和dtype;如果是 16 位,先astype(np.float32)归一化旋转再转uint8;不要把多个通道的数据直接塞给模型,输入模型前必须变成(3, H, W)的 float Tensor。这个问题最好在预处理阶段一次解决,不要在训练循环里反复试错。
6. 把 WHU-RS19 用出价值:从分类到语义分割的进阶思路
WHU-RS19 已经发布十几年了,直接拿它当论文卖点已经过时,但它作为“算法验证板”依然能打。最常见的进阶路线是把图像分类任务升级成遥感图像语义分割:WHU-RS19 的 600×600 大图本身很适合切成更小的 patch 做像素级标注。但这里没有现成的分割 ground truth,我通常的做法是先把每类图像对应的整图标签“降级”成粗糙分割标签,用大块区域填充,再用 SegFormer 预训练模型做语义分割迁移。这样做的价值不是让你得到多精确的分割结果,而是验证“从 scene-level 分类到 pixel-level 分割”的迁移能力,这个选题方向在近两年遥感方向的论文里很讨巧,发文门槛也相对友好。
另一条路是把 WHU-RS19 当作传统方法与深度学习的对比基准。很多人写论文时需要一个数据集来证明 CNN 比遥感随机森林强,这个数据集恰恰合适:用随机森林提取纹理与光谱特征做分类,再和 ResNet 微调结果对比,既有表格又有图,审稿人不容易挑刺。做对比实验时记得用同一套训练集,否则结果没有说服力。如果你想投期刊,遥感领域有不少技术门槛适中、接受遥感应用类短文的期刊可以投,比如 PLOS ONE 或一些开源遥感期刊,前提是结论够诚实、实验能复现。第五届遥感与地理信息技术国际学术会议这类会议也收偏应用方向的稿件,用 WHU-RS19 做基础实验章节完全够用。
最后一件事是我个人的血泪经验:我在做这个数据集时,曾为了赶进度直接随机切分、直接 Resize(224),结果提交给导师的验证精度高达 94%,但现场演示时新采的影像准确率只有 60%。后来我把数据重新分组、加了保守切分和分阶段微调,最终沉默地在报告里写了个诚实的 87%。这段经历让我明白一个道理:WHU-RS19 这种小数据集的价值不在于分数好看,而在于逼你把数据工程的基本功做扎实;模型的能力上限由数据切分和预处理决定,调参只是锦上添花。希望帮到你。
本文还有配套的精品资源,点击获取