ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WHU-RS19遥感图像分类实战:从数据预处理到ResNet微调全流程

WHU-RS19遥感图像分类实战:从数据预处理到ResNet微调全流程 简介遥感图像分类是理解地表覆盖与土地利用的关键技术而高质量标注数据集则是模型训练的基础。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 或 rardu看体积是否符合“约 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 或 JSONWHU-RS19 和 ImageNet 一样没有单独提供 label 文件标注信息隐藏在目录结构里airport/目录下都是飞机场图像beach/目录下都是海滩图像。每个目录内的文件一般命名为airport_001.tif、airport_002.tif这样的规则。这意味着两件事第一读数据时不需要解析标注文件直接拿父目录名做标签第二如果自己重新整理数据、复制到别处极容易把标签弄乱。我踩过最蠢的坑是写了个脚本按文件名前缀分类结果bridge和overpass两个目录因为单词太长被截断混进同一批训练样本里后面查了半天才明白模型为什么一直把这两个类混淆。所以稳妥做法是在拿到数据后先用脚本做一次“目录名—类别编码”映射把标签固化成 CSV 或 JSON不要每次都依赖目录名拼字符串类别编码目录名中文含义约张数0airport飞机场约 501beach海滩约 502bridge桥约 503commercial商业区约 504farmland农田约 505forest森林约 506highway高速公路约 507grassland草地约 508industrial工业区约 509lake湖泊约 5010mountain山地约 5011park公园约 5012parking停车场约 5013playground体育场约 5014residential居民区约 5015river河流约 5016sea海洋约 5017station火车站约 5018overpass立交桥约 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 通道或者色彩空间是 YCbCrOpenCV 按 BGR 读出来色调会偏掉。我一般用tifffile或skimage.io.imread做第一遍统一读入输出到 numpy 数组后看shape和dtypefrom 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(brightness0.2, contrast0.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, transformNone): 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(weightsmodels.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()), lr1e-3) criterion nn.CrossEntropyLoss() train_loader DataLoader(train_set, batch_size32, shuffleTrue, num_workers4) 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(fepoch {epoch1}: 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_workers4让数据加载多进程并行但要注意如果机器是 Windowsnum_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(dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) print(classification_report(all_labels, all_preds, target_namesCLASS_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 的样本大量被预测成 overpassoverpass 又大量被预测成 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 这种小数据集的价值不在于分数好看而在于逼你把数据工程的基本功做扎实模型的能力上限由数据切分和预处理决定调参只是锦上添花。希望帮到你。本文还有配套的精品资源点击获取
返回列表