基于CNN的Landsat遥感影像地物分类全流程实战
2026/9/23 3:07:37 网站建设 项目流程

简介:这套面向遥感影像地物分类的CNN深度学习Python工程,基于PyTorch实现,专用于Landsat数据的高效处理与分类建模。工程包含影像切片、模型训练与新增数据预测三个核心Python脚本,并附有预训练模型权重(.h5)、示例遥感影像(.tif)及其坐标/元数据辅助文件(.tfw/.xml),以及说明文档,可支撑从数据预处理到分类结果输出的完整流程。全部文件共10个,压缩包大小14.88MB,结构紧凑、目录清晰。资源重点覆盖Landsat多波段数据处理和基于CNN的地物类型识别,适合遥感、人工智能、自动化等专业学生作为课程设计、毕业设计或科研入门参考,也可用于快速验证深度学习方法在遥感场景下的迁移效果。目前已有87人学习浏览,代码经过测试可运行,遇到配置问题可联系作者交流。

1. CNN深度学习遥感影像地物分类:先搞清这个zip到底能帮你解决什么

手头攒了一堆Landsat影像,想分个地物类型出来,结果卡在第一步:数据怎么处理、样本怎么标、CNN怎么训、精度怎么验,每一步都像隔着一层黑匣子。这个标题给出的方案,核心就是一条用Python串起来的完整流水线——从Landsat原始影像开始,经过预处理、样本制作、CNN模型训练,最后输出一张地物分类图。它不是教你背理论,而是把影像数据变成模型能吃的格式,再让模型把地物类别吐出来。适合两类人:一是遥感专业的本科生、研究生,论文里需要一张分类结果图;二是自然资源调查、农业估产、生态监测这类岗位的从业者,需要把Landsat数据自动化处理成地物分类成果。前提是你会一点Python,装过深度学习框架,剩下的事情,这个方向都能给你一套可以照做的路径。

2. 从Landsat到能喂进CNN的数据:预处理必须抠的四个环节

2.1 数据源选择:Collection 2 Level-2的SR产品为什么能直接进模型

Landsat数据目前最常用的两个传感器是OLI(Landsat 8)和OLI-2(Landsat 9),分辨率30米,多光谱波段覆盖了可见光到短波红外。下载数据时你会看到两种产品:Level-1的原始DN值产品,和Level-2的大气校正产品(也叫SR产品,Surface Reflectance)。新手最容易犯的错,是下载了Level-1的数据然后花大量时间自己跑大气校正,其实USGS已经帮你把这件事做完了。Collection 2 Level-2的SR产品直接提供地表反射率,不需要再走一遍辐射定标和大气校正流程,省掉的不只是时间,还有一堆自己校正带来的误差。

我一般会直接下载Collection 2 Level-2的SR产品,文件命名里带有“SR”标识,波段文件单独存放,每个波段一个GeoTIFF。注意Landsat的波段命名规则:B2是蓝光、B3是绿光、B4是红光、B5是近红外、B6和B7是短波红外。做地物分类用这几个波段就够了,热红外波段(B10、B11)分辨率是100米重采样到30米,对地物分类提升有限,一般不放进模型。下载时按行列号(Path/Row)检索,覆盖你研究区的那一景或多景影像,如果研究区跨了两景,就下载相邻影像后面做镶嵌。

2.2 辐射定标与大气校正:SR产品为什么不用自己跑

很多人一上来就找大气校正的代码,其实这是被旧教程带偏了。早期Landsat数据都是Level-1的DN值,必须自己做辐射定标转成辐射亮度,再做大气校正得到地表反射率。现在的Collection 2 Level-2产品已经把这两步做完了,你拿到的就是地表反射率,数值范围一般在0到1之间,存储的时候乘以10000变成整型。用的时候除以10000还原成反射率,这个缩放因子在MTL元数据文件里有明确标注。

如果你确实拿到的是Level-1数据(比如某些镜像站只提供Level-1),那就需要自己处理。常见做法是用ENVI的FLAASH模块或 seis 的开源大气校正,但这条路径费时费力,精度还不一定比得上官方产品。我的建议很直接:优先用Level-2,把时间省下来花在样本标注和模型调参上,这两个环节对最终精度的影响比大气校正大一个量级。在Python里读取SR产品时,不需要管辐射定标系数,直接读反射率,做归一化或者标准化即可。

2.3 波段组合与裁剪:别把整景影像直接喂网络

Landsat一景影像大约是185公里×180公里,直接整景喂给CNN是不可能的,显存装不下,计算量也不现实。常见的做法是先把影像裁剪成小块(patch),比如256×256像素或者512×512像素,然后按patch来训练和预测。裁剪之前,先确定波段组合。一般地物分类用6个多光谱波段(B2-B7),加上NDVI、MNDWI这类指数波段,构成多通道输入。指数波段能增强植被和水体的区分度,对分类精度有实际帮助。

另一个必须处理的问题是云和云阴影。Landsat的SR产品自带QA波段(Quality Assessment波段),里面记录了每个像素的云、云阴影、雪等标记。预处理时把这个波段读出来,生成一个掩膜,把有云和云阴影的像素标记为无效,后面做样本标注和精度验证时避开这些区域。很多人在这一步偷懒,结果模型学了一堆云的纹理,分类图上一片一片的云区被分成了各种奇怪的类别。

2.4 批量预处理脚本:从原始tif到可直接训练的npy数组

预处理我用Python的rasterio库来读写GeoTIFF,numpy做数组运算。核心流程是:读取波段→拼接成多通道数组→裁剪成patch→保存为npy或直接在做训练时实时读取。下面是一个最小可用的预处理脚本,把一景Landsat影像裁成256×256的patch并保存。

import rasterio import numpy as np import os from tqdm import tqdm # 配置参数 tif_path = "LC08_L2SP_119038_20230601_SR.tif" # 原始SR影像(多波段合成) out_dir = "patches" patch_size = 256 overlap = 0 # 裁剪时的重叠像素数 # 读取影像和QA波段 with rasterio.open(tif_path) as src: bands = src.read([2, 3, 4, 5, 6, 7]) # B2-B7共6个波段 transform = src.transform height, width = bands.shape[1], bands.shape[2] profile = src.profile # 把DN值转成反射率(SR产品缩放因子10000) reflectance = bands.astype(np.float32) / 10000.0 # 计算NDVI和MNDWI,作为额外特征 np.seterr(divide='ignore', invalid='ignore') ndvi = (reflectance[3] - reflectance[2]) / (reflectance[3] + reflectance[2] + 1e-8) mndwi = (reflectance[1] - reflectance[4]) / (reflectance[1] + reflectance[4] + 1e-8) ndvi = np.nan_to_num(ndvi, nan=0.0, posinf=0.0, neginf=0.0) mndwi = np.nan_to_num(mndwi, nan=0.0, posinf=0.0, neginf=0.0) # 合并所有特征通道:6个原始波段 + NDVI + MNDWI = 8通道 features = np.concatenate([reflectance, ndvi[None, :, :], mndwi[None, :, :]], axis=0) # 按patch_size裁剪,保存为npy os.makedirs(out_dir, exist_ok=True) patch_idx = 0 for y in range(0, height - patch_size + 1, patch_size - overlap): for x in range(0, width - patch_size + 1, patch_size - overlap): patch = features[:, y:y+patch_size, x:x+patch_size] # 过滤掉全是0值或云掩膜占比过高的patch if np.mean(patch == 0) > 0.5: continue np.save(f"{out_dir}/patch_{patch_idx:06d}.npy", patch) patch_idx += 1 print(f"共生成 {patch_idx} 个patch,每个shape为 {features.shape[0]}x{patch_size}x{patch_size}")

这段代码把预处理和patch生成合成了一步。逻辑说明:先用rasterio读取6个多光谱波段,除以10000还原成反射率;然后计算NDVI和MNDWI两个指数,拼接到特征通道里;最后按指定窗口滑动裁剪,跳过全是0值的无效区域。参数说明:patch_size决定每个样本的空间范围,256像素在30米分辨率下对应7.68公里,既能覆盖足够的地物上下文又不会让CNN感受野太小;overlap控制相邻patch之间的重叠,一般设0即可,做预测时再设重叠来消除边界拼接痕;tif_path指向的是多波段合成文件,如果下载的是分波段tif,需要用rasterio.merge或GDAL先合成再处理。

做完这一步,你的数据就从“一景tif”变成了“一堆npy数组”,每张npy是一个8通道的patch,直接可以作为CNN的输入。第一批patch生成后,建议抽查几张,用matplotlib把RGB合成波段(B4-B3-B2)画出来,确认波段顺序没搞错、反射率数值范围正常、没有条纹状噪声。这一步花不了五分钟,但能避免后面训练时才发现数据有问题,白跑好几个小时。

3. 样本制作与数据集划分:标注决定分类精度上限

3.1 每类地物到底要多少样本:按类均衡比像素更重要

CNN训练需要带标签的patch。很多人问“每类要多少个样本”,答案不是固定的,但有个经验下限:每类至少300个patch,也就是300×256×256个像素的标注区域。如果类别不平衡,比如水体只标了50个patch,农田标了2000个,模型会严重偏向农田,水体就经常被漏分。常见做法是先做类别统计,保证样本量最少的类别不低于最多的类别的三分之一。

标注工具怎么选也有讲究。简单的方案是Labelme,它画多边形打标签很方便,但对遥感大影像不太友好,需要先裁剪成patch再标,效率不高。更好的方案是在QGIS里直接打开Landsat影像,参考更高分辨率的谷歌影像或者Google Earth截图,手动矢量化为面;然后把矢量栅格化成和影像分辨率一致的标签图,再按同样的裁剪逻辑切成patch。栅格化的像素值和类别ID对应起来,比如0是背景、1是水体、2是植被、3是农田、4是建筑、5是裸地。类别不要设太多,五六类到头了,类别越多标注工作量越大,类别之间的混淆也越严重。

3.2 类别体系怎么定:从地表覆盖类型反推标签名称

标什么类别,首先要看你的应用目标。做土地利用调查,就按分类体系走(比如GB/T 21010里的耕地、林地、草地、水域、建设用地);做生态监测,就把重点放在植被覆盖度和水体范围上。Landsat的30米分辨率决定了它区分不了太细的类别,比如不同农作物种类,但区分水体、植被、农田、建筑、裸地这类一级类效果很好。我一般会建议先做一个六类分类:水体、植被、农田、建筑、裸地、其他。类别越多,标注难度和分类误差都会显著上升,先跑通六类再看需不需要合并或细分。

类别合并是另一个容易踩坑的点。比如“植被”这个概念,在影像上包括林地、草地、灌丛,它们的反射率光谱差异不小,如果全标成“植被”,会让类内方差过大,CNN学到的是一个模糊的“植被”概念。反过来,如果你的研究区只有一片林地和一小块草地,强行分两类会导致草地样本不足,训练时直接崩掉。合理的做法是:参考你的研究区实际地物分布,结合分类目标的粒度,把光谱相似、应用上不需要区分的类别合并。

3.3 训练/验证/测试集划分:按空间不按像素

很多人在这一步犯了一个隐蔽但后果严重的错误:随机把patch分到训练集和测试集。如果同一个区域的不同patch同时出现在训练集和测试集里,模型实际上已经见过这些像素了,测试精度会虚高。遥感影像有很强的空间自相关性,相邻patch的纹理、光谱高度相似,随机划分等于数据泄漏。正确的做法是按空间区域划分:把整个研究区先划分成几个大的子区域,比如东西两块,一块取训练patch,另一块取测试patch,保证训练集和测试集在地理位置上完全分开。

具体操作上,我会先画一个研究区的矢量网格,比如每个格网1公里×1公里,按格网来采样patch。先把格网按照“训练/验证/测试=7/1/2”的比例随机分配到三个集合,然后在每个格网里生成patch。这样既保证了空间独立性,又让每个集合覆盖整个研究区的地物多样性。验证集用来调超参数,测试集只在最后评估一次,不要反复拿测试集试,否则测试集也会被你“训练”。

3.4 标签编码与样本增强:从矢量到patch标签的完整代码

标注完成后的标签图是单通道的,每个像素的值代表类别ID。需要把标签图切成和影像patch完全对应的patch标签,组成(x, y)对。这里最关键的是保证裁剪的起始像素坐标完全一致,否则影像和标签对不上,训练时模型学到的是噪声。

import rasterio import numpy as np import os from sklearn.model_selection import train_test_split # 配置 img_patches_dir = "patches" # 预处理阶段生成的影像patch label_tif_path = "labels.tif" # QGIS中栅格化后的标签图 out_train_dir = "train_data" os.makedirs(out_train_dir, exist_ok=True) # 读取标签图 with rasterio.open(label_tif_path) as src: label_img = src.read(1).astype(np.int16) # 标签和影像patch的裁剪起始坐标保持一致 patch_size = 256 img_files = sorted(os.listdir(img_patches_dir)) sample_list = [] for fname in img_files: img_path = os.path.join(img_patches_dir, fname) img = np.load(img_path) h, w = img.shape[1], img.shape[2] # 从文件名解析patch的起始坐标(在预处理时按规则写入) # 这里约定文件名格式为 patch_yx_{y}_{x},也可以用其他方式记录 parts = fname.split("_") y0 = int(parts[2]) x0 = int(parts[3].split(".")[0]) label_patch = label_img[y0:y0+h, x0:x0+w] # 过滤掉包含大量未标注像素(值为-9999)的patch if np.sum(label_patch < 0) > (patch_size * patch_size * 0.2): continue # 过滤掉类别单一且背景占比过高的patch unique, counts = np.unique(label_patch, return_counts=True) if len(unique) < 2: continue # 保存样本对,数据集文件名加前缀x_和y_方便后续读取 sample_id = fname.replace(".npy", "") np.save(f"{out_train_dir}/x_{sample_id}.npy", img) np.save(f"{out_train_dir}/y_{sample_id}.npy", label_patch) sample_list.append(sample_id) # 按空间格网分配训练/验证/测试集(这里简化成随机分配,实际应按格网) # 实际项目中建议先生成格网ID,再按格网ID划分,避免空间泄漏 train_ids, temp_ids = train_test_split(sample_list, test_size=0.3, random_state=42) val_ids, test_ids = train_test_split(temp_ids, test_size=0.33, random_state=42) # 保存划分结果 with open(f"{out_train_dir}/train_ids.txt", "w") as f: f.write("\n".join(train_ids)) with open(f"{out_train_dir}/val_ids.txt", "w") as f: f.write("\n".join(val_ids)) with open(f"{out_train_dir}/test_ids.txt", "w") as f: f.write("\n".join(test_ids)) print(f"train: {len(train_ids)}, val: {len(val_ids)}, test: {len(test_ids)}")

这段代码的重点在于“坐标对齐”和“样本过滤”。逻辑说明:影像patch是从预处理阶段生成的,标签图需要按完全相同的起始坐标裁剪,才能得到一一对应的标签patch;两个过滤条件分别去掉未标注区域过多的patch和只有单一类别的patch,后者在训练时无法提供有意义的监督信号。参数说明:random_state=42固定随机种子保证实验可复现;test_size=0.3表示先分出30%作为验证+测试,再从这30%里分出1/3做测试,最终比例约为训练70%、验证10%、测试20%。这里演示的是简化版随机划分,实际项目里请按照3.3节说的按空间格网划分。

样本增强方面,遥感patch常用的增强方式包括:随机旋转(90度的倍数)、上下左右翻转、随机亮度扰动、随机裁剪。不要用随机缩放或任意角度旋转,因为地物有明确的方向性(建筑、道路),破坏了方向就改变了语义。可以用torchvision.transformsalbumentations库实现,在训练时动态增强,增强后的patch不落盘,节省存储空间。

4. 训练一个CNN地物分类模型:模型结构与参数怎么定

4.1 为什么选CNN而不是前馈神经网络或Transformer

图像处理为啥用CNN不用前馈神经网络,这个问题很多入门者都问过。核心原因有两个:参数效率和局部性。全连接网络把每个像素当成独立特征,256×256×8通道的输入展开成50万个特征,第一层全连接的参数量就是天文数字,训练不动也严重过拟合。而CNN通过卷积核共享权重,只关注局部邻域的像素关系,参数数量大幅减少。更关键的是,卷积操作天然符合图像信号的特点——相邻像素高度相关,远处的像素相关性弱。

那和Transformer比呢?Transformer靠自注意力机制也能捕捉全局依赖,但需要海量数据来训练,在遥感小样本场景下很容易欠拟合。CNN的归纳偏置(局部连接、权重共享、平移等变性)让它在几千个patch的小数据集上就能训练出可用的模型。做Landsat地物分类,数据量通常不会超过几万个patch,CNN是最稳的选择。等你积累了几百万个patch,再考虑Swin Transformer或ConvNeXt这类大模型不迟。

4.2 一个能跑通的轻量CNN结构:U-Net还是简单分类网络要看输出

这里要区分两种任务:如果只是给每个像素分类,常见做法是用全卷积网络,输入patch输出同样尺寸的标签图,U-Net是经典选择;如果先提取特征再接全连接层做分类,输出的是整个patch的类别,那是图像分类思路,并不适合地物制图。地物分类需要逐像素的预测结果,所以用全卷积网络。下面给出一个简化的U-Net结构,输入8通道256×256的影像,输出6类概率图。

import torch import torch.nn as nn import torch.nn.functional as F class SimpleUNet(nn.Module): """轻量级U-Net:4次下采样+4次上采样,适配Landsat多光谱输入""" def __init__(self, in_channels=8, num_classes=6): super().__init__() # 编码器 self.enc1 = self.conv_block(in_channels, 32) self.enc2 = self.conv_block(32, 64) self.enc3 = self.conv_block(64, 128) self.enc4 = self.conv_block(128, 256) self.pool = nn.MaxPool2d(2) # 瓶颈层 self.bottleneck = self.conv_block(256, 512) # 解码器 self.up4 = nn.ConvTranspose2d(512, 256, kernel_size=2, stride=2) self.dec4 = self.conv_block(512, 256) self.up3 = nn.ConvTranspose2d(256, 128, kernel_size=2, stride=2) self.dec3 = self.conv_block(256, 128) self.up2 = nn.ConvTranspose2d(128, 64, kernel_size=2, stride=2) self.dec2 = self.conv_block(128, 64) self.up1 = nn.ConvTranspose2d(64, 32, kernel_size=2, stride=2) self.dec1 = self.conv_block(64, 32) self.out_conv = nn.Conv2d(32, num_classes, kernel_size=1) def conv_block(self, in_c, out_c): return nn.Sequential( nn.Conv2d(in_c, out_c, kernel_size=3, padding=1), nn.BatchNorm2d(out_c), nn.ReLU(inplace=True), nn.Conv2d(out_c, out_c, kernel_size=3, padding=1), nn.BatchNorm2d(out_c), nn.ReLU(inplace=True), ) def forward(self, x): # 编码 e1 = self.enc1(x) e2 = self.enc2(self.pool(e1)) e3 = self.enc3(self.pool(e2)) e4 = self.enc4(self.pool(e3)) b = self.bottleneck(self.pool(e4)) # 解码 + 跳跃连接 d4 = self.up4(b) d4 = self.dec4(torch.cat([d4, e4], dim=1)) d3 = self.up3(d4) d3 = self.dec3(torch.cat([d3, e3], dim=1)) d2 = self.up2(d3) d2 = self.dec2(torch.cat([d2, e2], dim=1)) d1 = self.up1(d2) d1 = self.dec1(torch.cat([d1, e1], dim=1)) out = self.out_conv(d1) return out

这个U-Net把输入从8通道降到32通道起步,每下采样一次通道翻倍,在256×256输入下,显存占用大约4到6GB,消费级显卡能跑。逻辑说明:编码器逐级提取从细到粗的特征,解码器逐级恢复空间分辨率,跳跃连接把编码器的细节特征拼到解码器,弥补池化损失的空间信息,这对地物边界保持很有帮助。参数说明:in_channels=8要和前面预处理生成的特征通道数一致(6个波段+NDVI+MNDWI);num_classes=6对应6个地物类别;BatchNorm在batch_size较小(小于8)时效果会变差,如果显存不够减小patch_size而不是减batch_size。

4.3 训练关键参数:lr、batch_size、loss函数怎么设

训练参数的选择直接影响收敛速度和最终精度。我常用的初始化配置如下表,这些值经过多个遥感数据集验证,作为起点基本不会翻车。

参数推荐值说明
优化器AdamWAdam的改进版,权重衰减更干净
初始学习率1e-3用余弦退火调度器逐步衰减到1e-5
batch_size8256×256×8通道下显存约5GB,不够就减到4
loss函数CrossEntropyLoss多分类标配,配合类别权重处理不平衡
epoch数50-80早停机制看验证集loss,连续10个epoch不降就停

类别不平衡的问题要在loss函数里处理。简单做法是给每个类别一个权重,样本少的类别权重高。权重一般取N_total / (N_classes * N_class_i),即总像素数除以该类像素数再除以类别数,这样各类的损失贡献基本均衡。PyTorch的CrossEntropyLoss直接支持weight参数,传入一个长度为类别数的Tensor即可。

训练监控方面,每5个epoch在验证集上算一次OA(总体精度)和Kappa系数,这两个指标比loss更直观。loss下降但精度不升,一般是过拟合信号,需要加大数据增强或降低模型容量;loss和精度都停滞不前,可能是学习率太小或数据有问题,先确认预处理和标签对齐没毛病再动模型。

4.4 训练脚本:数据加载器、训练循环与模型保存

训练循环本身不复杂,难的是把数据加载、模型前向、反向传播、验证、保存这几步组织得不出错。下面是一个训练脚本的最小骨架,用PyTorch实现,读取上一步生成的npy样本对。

import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader import numpy as np import os class LandsatDataset(Dataset): def __init__(self, ids_file, data_dir, augment=False): with open(ids_file) as f: self.ids = f.read().strip().split("\n") self.data_dir = data_dir self.augment = augment def __len__(self): return len(self.ids) def __getitem__(self, idx): sample_id = self.ids[idx] x = np.load(f"{self.data_dir}/x_{sample_id}.npy").astype(np.float32) y = np.load(f"{self.data_dir}/y_{sample_id}.npy").astype(np.int64) # 数据增强:90度旋转和翻转 if self.augment: k = np.random.randint(0, 4) x = np.rot90(x, k, axes=(1, 2)).copy() y = np.rot90(y, k, axes=(0, 1)).copy() if np.random.rand() > 0.5: x = np.flip(x, axis=2).copy() y = np.flip(y, axis=1).copy() # 转成PyTorch张量,x归一化到[0,1] x_tensor = torch.from_numpy(x) y_tensor = torch.from_numpy(y) return x_tensor, y_tensor # 训练配置 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = SimpleUNet(in_channels=8, num_classes=6).to(device) train_ds = LandsatDataset("train_data/train_ids.txt", "train_data", augment=True) val_ds = LandsatDataset("train_data/val_ids.txt", "train_data", augment=False) train_dl = DataLoader(train_ds, batch_size=8, shuffle=True, num_workers=4) val_dl = DataLoader(val_ds, batch_size=8, shuffle=False, num_workers=4) # 类别权重,按像素占比的反比计算 class_weights = torch.tensor([1.0, 1.5, 0.8, 1.0, 1.2, 1.5]).to(device) criterion = nn.CrossEntropyLoss(weight=class_weights) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50) for epoch in range(50): model.train() total_loss = 0.0 for x_batch, y_batch in train_dl: x_batch = x_batch.to(device) y_batch = y_batch.to(device) optimizer.zero_grad() out = model(x_batch) # (B, 6, H, W) y_batch = y_batch.long() loss = criterion(out, y_batch) loss.backward() optimizer.step() total_loss += loss.item() # 验证 model.eval() correct = 0 total = 0 with torch.no_grad(): for x_batch, y_batch in val_dl: x_batch = x_batch.to(device) y_batch = y_batch.to(device) out = model(x_batch) pred = torch.argmax(out, dim=1) # 只统计有效像素(标签>=0) mask = y_batch >= 0 correct += (pred[mask] == y_batch[mask]).sum().item() total += mask.sum().item() val_acc = correct / max(total, 1) print(f"Epoch {epoch+1:02d}, Loss: {total_loss/len(train_dl):.4f}, Val Acc: {val_acc:.4f}") # 保存最佳模型 if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "best_model.pth") scheduler.step()

这段代码覆盖了数据加载、增强、训练和验证全流程。逻辑说明:数据加载器负责从npy文件读取样本对,增强只在训练集启用,验证集保持原始数据;类别权重让样本少的类别获得更大的损失贡献;验证时mask = y_batch >= 0过滤掉无效像素(未标注区域,标签设为-9999或-1)。参数说明:num_workers=4是数据加载的并行进程数,Windows下建议设为0避免多进程报错;T_max=50要和总epoch数一致,否则余弦退火的学习率曲线不完整;weight_decay=1e-4做权重正则化,抑制过拟合。

5. 遥感影像分类避坑指南:五个让新手翻车的细节

5.1 精度明明很高,分类图却一片椒盐噪声

现象:验证集OA跑到90%以上,但把整景影像预测出来一看,地物边界像是被啃过,单个像素孤立地出现错误类别,图面非常碎。

原因:逐像素分类的本质决定了每个像素独立决策,没有考虑邻域一致性。模型在特征空间里学到的是光谱模式,对单个像素的噪声敏感,边界区域更容易出现零星错分。

解决:两招组合用。第一招是预测时做重叠滑窗与概率平均,相邻patch重叠区域多次预测取平均,能显著抑制边界噪声;第二招是分类后处理,用众数滤波或多数投票,我这里一般用scipy.ndimage.median_filter或者形态学开闭运算,窗口大小3×3或5×5,注意不要过度平滑把细小水体抹掉了。

5.2 训练集和测试集精度都很高,换到新影像上精度暴跌

现象:在A区域训练的模型,测试精度85%,直接用到相邻的B区域影像上,精度掉到60%以下。

原因:这是遥感深度学习最常见的泛化问题。不同时相、不同季节的影像,地表反射率分布差异很大;不同传感器(Landsat 8和Landsat 9)之间也有细微的光谱响应差异;甚至太阳高度角不同,阴影区的特征都完全不同。

解决:没有一劳永逸的办法,靠数据策略缓解。一是训练集里加入多时相样本,把不同季节、不同年份的patch都放进去增强光谱多样性;二是在应用前对目标影像做直方图匹配,让它和训练数据的光谱分布对齐;三是在训练时做光谱增强,对反射率做随机线性变换,模拟不同光照条件。

5.3 水体和阴影永远分不清

现象:分类图里的山体阴影区被成片分成水体,或者水体被分成阴影,两者互相混淆。

原因:水体和阴影在可见光波段光谱相似,都是低反射率,从红光到短波红外的曲线形态都很接近。单靠光谱信息区分它们非常困难,这也是遥感分类的经典难题。

解决:增加辅助特征。MNDWI指数对水体非常敏感,水体在绿光反射率高、在短波红外反射率低,而阴影区域的光谱随下垫面变化,没有稳定的指数特征;所以在特征通道里保留MNDWI能帮模型区分。另外,如果研究区地形起伏大,把坡度或山体阴影掩膜作为额外输入通道也有效。从模型角度,增加上下文信息(用更大的patch_size)也能帮助模型学到“旁边的山体导致阴影”这种空间关系。

5.4 训练时报错“CUDA out of memory”

现象:batch_size设成16,一跑训练就报显存不足,换成batch_size=4才勉强跑动。

原因:256×256×8通道的输入本身就不小,U-Net的中间特征图占显存尤其大。很多人误以为是模型参数太多,实际上激活值(中间特征图)占了大头。

解决:按顺序尝试三个办法。第一,减小batch_size到4甚至2,配合梯度累积(accumulation_steps)模拟大batch;第二,减小patch_size到128或192,显存占用按patch_size的平方下降;第三,用混合精度训练,PyTorch的torch.cuda.amp能把显存占用砍半。patch_size不建议小于128,否则感受野太小,地物上下文信息不足,分类精度会下降。

5.5 模型训练loss不下降或直接nan

现象:训练到第10个epoch,loss一直停留在2.0左右不降;或者loss突然变成nan,之后所有参数都变成nan。

原因:loss不降通常是学习率太大或太小,或者标签里有未处理好的异常值;loss变成nan则几乎可以肯定是数据里有nan或inf,比如NDVI计算时除以0,或者某个patch全是0值导致BatchNorm收到全零输入。

解决:先查数据再调参数。把第一个batch的数据打印出来,检查是否有nan和inf、反射率是否在合理范围(0到1之间)、标签ID是否超出num_classes-1。数据没问题再调学习率:从1e-3开始,如果loss震荡就降到1e-4,如果loss不掉就升到3e-3试试。一个实用的做法是写一个torch.autograd.set_detect_anomaly(True),PyTorch会直接告诉你哪个算子导致了nan,定位非常快。

6. 用训练好的模型做整景预测:滑窗推理与精度验证的落地技巧

训练完模型,最后一步是把它应用到整景Landsat影像上生成分类图。因为模型输入是固定大小的patch,所以整景影像需要通过滑窗推理。窗口大小和训练时的patch_size保持一致,但步长不能等于patch_size,否则相邻预测结果之间会有明显的拼缝。我一般用步长等于patch_size的一半,这样每个像素会被多个窗口覆盖,最后取多个预测概率的平均值作为最终输出。

推理时还有一个细节要注意:对于测试集中已经见过的区域,不要重复参与精度评估,否则结果虚高。正确流程是,先用训练好的模型预测整景影像,生成分类结果图;然后拿测试集里的标签patch和预测结果做逐像素对比,计算混淆矩阵、总体精度(OA)和Kappa系数。Kappa系数能反映分类结果和真实标签的一致性,0.8以上说明分类效果优秀,0.6到0.8说明可用,低于0.6就需要回头找原因。

最后我的习惯是把分类结果图用rasterio写成GeoTIFF,带上原始影像的地理变换信息和投影坐标,这样可以直接在QGIS或ArcGIS里打开叠加分析。写成GeoTIFF的过程不难,核心是从原始影像的profile里复制transform和crs信息,把预测结果的背景值设为0或者255。一份完整的Landsat地物分类工作流,到这里就走完了:从数据预处理、样本制作、模型训练,到整景预测和精度评估,每个环节都有对应的Python实现和参数调试经验。这条路径我走过很多遍,踩过的坑基本都在前面几章列全了。按照这个流程做下来的Landsat分类项目,哪怕研究区换了、类别换了一两个,整体的框架都不用动,改改参数和样本就能复现。希望这些从实际项目里攒下来的经验,能帮你把第一个遥感地物分类任务顺利跑通。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询