简介面向遥感地物分类应用场景该资源提供基于卷积神经网络的Landsat影像地物分类Python源码与项目说明适合遥感科学与技术、地理信息科学、计算机视觉等专业方向学生用于课程设计、毕业设计或科研入门。压缩包内共10个文件包含3个Python脚本分别实现影像切片、模型训练、新影像预测、训练好的卷积模型权重、Landsat样例影像、辅助地理定位的XML与TFW文件以及Markdown格式的说明文档总大小14.89MB目录结构紧凑便于按流程复现实验。当前已有970人学习下载。源码经过测试可正常运行完整覆盖从训练样本制作、模型训练到对新增遥感影像进行地物分类的主要环节可直接调整类别参数与数据路径后使用也可作为迁移学习或遥感图像分类项目的改造基础。1. 基于 CNN 的遥感 Lands at 影像地物分类为什么值得自己动手跑一遍接到一个区域地物分类任务最怕的不是影像难下载而是用传统分类器反复调参数精度还是卡在 80% 上下不去。基于 CNN 深度学习的遥感 landsat 影像地物分类其实就是在 Landsat 多光谱影像上用卷积神经网络做逐像素分类把水体、植被、建设用地、裸地这些类别自动标出来。它的价值在于CNN 能自己学习光谱与空间纹理特征不用手动设计光谱规则换一个区域时重新标注少量样本就能迁移这比决策树和随机森林的泛化能力更稳。这篇文章适合手里有 Landsat 影像、想用 python 源码把分类模型跑起来的人也适合刚入门深度学习遥感分类、想知道整套流程哪里容易翻车的学生和从业者。下文直接按数据准备、模型搭建、训练调参、落地出图的顺序讲最后给出我踩过的坑。2. 从 Landsat 影像到训练样本波段、裁剪与标签对齐2.1 先把 Landsat 数据整理成模型能读的格式做 CNN 分类前最花时间的不是模型而是把影像整理成模型能读的数组。Landsat 数据建议直接下载 Collection 2 Level-2 产品它已经做过大气校正输出的是地表反射率省去自己跑大气校正的步骤。分类一般用 6 个波段蓝B2、绿B3、红B4、近红外B5、短波红外 1B6、短波红外 2B7空间分辨率统一是 30 米。热红外 B10/B11 在城市热岛研究里才有用地物分类通常不加入全色 B8 是 15 米做融合会引入额外的配准误差新手不建议碰。拿到影像后第一步是裁到研究区范围同时统一投影和分辨率。Landsat 的 Level-2 产品坐标系是 UTM如果你的矢量边界是 WGS84 经纬度直接用 rasterio 裁剪时要注意先做投影转换。下面是常见的裁剪脚本输入是原始 tif 和矢量边界 shp输出是裁剪后的 tif。import rasterio from rasterio.mask import mask from rasterio.warp import calculate_default_transform, reproject, Resampling import geopandas as gpd def crop_landsat(src_tif, shp_file, out_tif): # 读取影像与矢量保证两者投影一致 with rasterio.open(src_tif) as src: gdf gpd.read_file(shp_file) if gdf.crs ! src.crs: gdf gdf.to_crs(src.crs) # mask 裁剪裁掉无数据的背景区域 out_image, out_transform mask(src, gdf.geometry, cropTrue, nodata0) out_meta src.meta.copy() out_meta.update({ driver: GTiff, height: out_image.shape[1], width: out_image.shape[2], transform: out_transform }) with rasterio.open(out_tif, w, **out_meta) as dst: dst.write(out_image)这段代码的核心是把矢量边界和影像放到同一个坐标系里再裁剪避免出现裁出来是黑图的尴尬。mask(src, gdf.geometry, cropTrue)是 rasterio 的关键函数它按矢量范围裁出最小外接矩形。nodata 设置成 0是因为反射率影像的无效值在 Level-2 产品里已经标记为 0后续归一化时要把它从样本里排除。裁剪完成之后还要检查影像和标签的像元是否一一对应参考系不一致会导致模型学到错误的空间对应关系这一步在 5.2 节会展开。2.2 标签怎么做从公开土地覆盖产品到本地矢量化分类任务必须有一套像素级标签。最省事的做法是直接用现成的土地覆盖产品比如 GlobeLand30 或者 ESA WorldCover这类产品覆盖全球且分类体系基本一致缺点是类别和你要分的不一定对得上。另一种做法是在影像上人工勾画矢量再栅格化成标签图。无论哪种方式最后都要把标签转成 0 到 N-1 的整数编码例如 0水体、1植被、2建设用地、3裸地。标签和影像对齐这一步很容易忽略。土地覆盖产品的分辨率不一定和 Landsat 一样GlobeLand30 是 30 米分辨率WorldCover 是 10 米直接拿来用要重采样到 Landsat 的 30 米格网。重采样方法建议用 nearest不要用 bilinear 或 cubic因为标签是类别号插值会产生不存在的类别。下面这段代码把任意来源的标签 tif 重采样并重映射成训练可用的格式。import rasterio from rasterio.enums import Resampling import numpy as np def align_label(label_tif, ref_tif, out_label_tif): # 以影像为基准把标签重采样到完全一致的网格 with rasterio.open(ref_tif) as ref: label_meta ref.meta.copy() with rasterio.open(label_tif) as lab: # 计算标签在目标网格下的重采样结果 data lab.read( 1, out_shape(ref.height, ref.width), resamplingResampling.nearest ) # 类别重映射把 WorldCover 的类别号映射成自己的编码 remap {10: 1, 20: 1, 30: 2, 40: 2, 50: 3, 60: 3, 80: 3, 90: 4, 100: 0} out np.zeros_like(data, dtypenp.uint8) for old, new in remap.items(): out[data old] new label_meta.update({dtype: uint8, nodata: 255}) with rasterio.open(out_label_tif, w, **label_meta) as dst: dst.write(out, 1)out_shape是 rasterio 提供的按目标尺寸重采样方式配合Resampling.nearest保证类别编码不被打乱。重映射的字典要根据你自己的类别体系调整如果源标签里有些类别你不需要比如冰雪和湿地可以在这一步直接归并或设为忽略。这类对齐工作做完后再用滑动窗口把大影像切成小块比如 256×256 的 patch训练时逐 patch 输入模型。切块时要保证 patch 之间有 overlap否则推理出图阶段会出现明显的接缝具体做法在第 6 章说。3. CNN 怎么接在地物分类上patch 模型与 U-Net 路线取舍3.1 patch-CNN 与 U-Net两种落地路线的对比CNN 做地物分类有两条主流路线。一条是 patch 分类把一个像素周围 N×N 的像元块作为输入输出这个中心像素的类别模型本质是一个图像分类器。另一条是语义分割路线用 U-Net 这类编码器-解码器结构整张影像进、逐像素分类结果出。对于 Landsat 这种 30 米中分辨率影像两种路线都能用区别在于工程代价和效果。patch-CNN 的好处是模型简单显存占用小源码包跑起来不挑机器缺点是推理时要逐像素滑窗整景影像几十亿像素跑一次要很久而且相邻像素的重复计算非常多。U-Net 输出直接是一整张概率图推理快但显存占用高训练数据要求更严格。我的倾向是如果在本地做实验、显卡显存 8G 以下或者只想验证分类思路用 patch-CNN如果要做整景出图或者区域制图直接用 U-Net。下面用一个表格把差异列清楚。对比项patch-CNNU-Net输入形式中心像素周围 N×N patch整幅或分块影像输出形式单像素类别逐像素类别图显存占用低高推理速度慢重复计算多快适合场景小范围实验、低显存机器整景制图、大区域分类工程复杂度低中等如果用 U-Netpatch 大小一般取 256 或 512batch size 跟着显存走我通常先设 batch4跑通后再往上加。patch 太大导致类别边界被过度平滑patch 太小则感受野不足建设用地和裸地这种光谱相近的类别容易混。Landsat 波段空间分辨率是 30 米一个 256×256 patch 覆盖 7.68 km×7.68 km对地物空间纹理来说足够。3.2 用 PyTorch 搭一个轻量 U-Net 做地物分类从零写一个能跑的分类网络并不复杂关键在于把通道数、卷积核大小和输入波段数对应好。下面是一个轻量 U-Net 的 PyTorch 实现输入是 6 波段影像输出是 5 类地物的概率图。结构上砍掉了原版 U-Net 的部分深度让它在小数据集上不容易过拟合。import torch import torch.nn as nn import torch.nn.functional as F class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x) class LightUNet(nn.Module): def __init__(self, in_channels6, num_classes5): super().__init__() # 编码器部分每层通道数翻倍 self.enc1 DoubleConv(in_channels, 32) self.enc2 DoubleConv(32, 64) self.enc3 DoubleConv(64, 128) self.pool nn.MaxPool2d(2) # 解码器部分每层通道数减半 self.up3 nn.ConvTranspose2d(128, 64, 2, stride2) self.dec3 DoubleConv(128, 64) self.up2 nn.ConvTranspose2d(64, 32, 2, stride2) self.dec2 DoubleConv(64, 32) self.out nn.Conv2d(32, num_classes, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) d3 self.up3(e3) d3 torch.cat([d3, e2], dim1) d3 self.dec3(d3) d2 self.up2(d3) d2 torch.cat([d2, e1], dim1) d2 self.dec2(d2) return self.out(d2)in_channels6对应 2.1 节选的 6 个 Landsat 波段如果加了光谱指数做输入这里要同步改成对应的通道数。编码器用卷积加池化逐步压缩空间尺寸解码器用转置卷积恢复分辨率中间的 skip connection 把低层细节拼回来这对地物边界恢复特别重要。num_classes5对应 2.2 节的重映射编码。实际使用中如果发现训练集很小可以把初始通道数从 32 降到 16减少过拟合风险。显存不足时优先调小 batch size 而不是减小 patch 大小patch 减小会直接削弱模型对连续地物纹理的感知能力。4. 把模型训起来损失函数、学习率与类别不均衡的调参清单4.1 损失函数与类别权重处理水体少、植被多的样本失衡遥感地物分类里最典型的问题是类别不均衡一个区域里植被和水体可能占了一半以上建设用地和裸地只占几个百分点。如果不处理模型会学成“什么都预测成植被”因为这样损失值最低总体精度看着还挺高。解决思路有两个一是算损失时给少数类更高的权重二是在切 patch 的时候限制每类的样本比例。两个方法可以一起用损失权重是兜底采样控制是治本。加权交叉熵的权重一般按类别频率的倒数来算。比如某类占比 5%权重设为 1/0.0520占比 50% 的类别权重设为 2。实际用的时候不建议直接套倒数因为少数类几乎都是难分样本权重过大会让模型对噪声标签过度敏感。我会把权重压缩一下比如w 1 / sqrt(freq)压缩后的权重更稳。下面给出加权交叉熵加 Dice 损失的组合Dice 损失对小类别更友好。import torch import torch.nn.functional as F def weighted_ce_dice_loss(pred, target, weights): # pred: [B, C, H, W] logits # target: [B, H, W] 类别索引 # weights: [C] 各类别权重 # 加权交叉熵部分 ce F.cross_entropy(pred, target, weightweights, ignore_index255) # Dice 损失部分逐类别计算 pred_soft F.softmax(pred, dim1) target_onehot F.one_hot(target, num_classespred.size(1)).permute(0, 3, 1, 2).float() smooth 1.0 dice 0.0 for c in range(pred.size(1)): inter (pred_soft[:, c] * target_onehot[:, c]).sum() union pred_soft[:, c].sum() target_onehot[:, c].sum() dice (2 * inter smooth) / (union smooth) dice_loss 1 - dice / pred.size(1) return ce dice_loss组合损失里交叉熵负责梯度稳定Dice 负责让少数类被模型“看见”。ignore_index255对应 2.2 节里设为 nodata 的像素这些像素不参与损失计算。weights 的构造在训练脚本里依据训练集标签的像素频率统计得到建议统计一次后写死不要每个 epoch 动态更新否则损失权重会抖动。加 Dice 损失之后训练会变慢一点但对小地物类别的改善很明显。4.2 训练参数的基准值与验证指标怎么读训练超参不需要反复试我直接给一套在大多数 Landsat 分类场景下能跑通的参数。优化器用 Adam 或 AdamW初始学习率 1e-3训练 30 到 50 个 epoch。数据增强用随机水平翻转、垂直翻转和 90 度旋转这类增强对遥感影像有效因为地物不存在“上下颠倒”的概念。饱和度增强和色彩抖动不建议用Landsat 是定量遥感数据光谱值被人为扰动会破坏反射率的物理意义。参数建议值调整方向优化器AdamW损失不降时换 SGD动量初始学习率1e-3收敛慢就降到 5e-4batch size4-8显存不足优先减半patch size256类别混分严重时加大到 512训练轮数30-50验证指标不再升就早停数据增强翻转旋转不要加色彩抖动验证指标不要只盯总体精度遥感分类的通行做法是看混淆矩阵、每类 IoU 和 Kappa 系数。总体精度会被大类主导容易出现“精度 90%裸地完全没分出来”的情况。下面这段代码计算验证集上的逐类 IoU 和总体精度。import numpy as np def compute_metrics(pred_all, label_all, num_classes): # pred_all 和 label_all 都是展平的一维数组 ious [] for c in range(num_classes): pred_mask (pred_all c) label_mask (label_all c) inter np.logical_and(pred_mask, label_mask).sum() union np.logical_or(pred_mask, label_mask).sum() ious.append(inter / (union 1e-6)) oa (pred_all label_all).sum() / len(label_all) # Kappa 系数 n len(label_all) p0 oa pe 0.0 for c in range(num_classes): gt_count (label_all c).sum() pred_count (pred_all c).sum() pe (gt_count * pred_count) / (n * n) kappa (p0 - pe) / (1 - pe 1e-6) return ious, oa, kappa训练过程中我一般把每个 epoch 的验证 mIoU 打印出来如果 mIoU 连续 5 个 epoch 不涨就把验证集里错分的图单独存下来看。错分图往往比指标更有说服力指标只是数值图能直接告诉你是阴影被分成了水体还是裸地被分成了建设用地。保存错分图这个习惯省了我很多调参时间强烈建议养成。5. 避坑指南Landsat 分类项目里最常见的 5 个翻车现场5.1 整景推理爆显存输出还有明显接缝现象训练时 batch size 能到 8推理时用整景影像直接进模型显存立刻溢出改成裁块推理后输出的分类图块与块之间色调不一致接缝明显。原因整景 Landsat 影像几万乘几万像素远超模型输入尺寸分块推理时没有重叠区边界处的卷积感受野缺失导致同一地物在接缝两侧被分到不同类别。解决推理阶段把影像裁成和训练 patch 相同大小的块块与块之间加 overlap一般取 patch 的 1/8 到 1/4。推理完成后接缝处取重叠区的平均概率而不是硬切一刀。overlap 越大接缝越淡但推理耗时也随之增加256 的 patch 用 32 像素重叠即可。提示推理脚本里的预处理必须和训练时完全一致包括归一化方式、波段顺序和 dtype。我遇到过训练时用 float32推理时 tif 读出来是 int16 没转换导致所有概率输出偏向某一类排查了很久才发现。5.2 影像和标签“看起来对齐”实际错位半个像元现象训练损失降得很低验证精度也不错但把预测结果叠加到影像上地物边界普遍偏移了半个到一个像元建渣边缘和道路边界最明显。原因标签重采样时基准格网和影像格网不一致。比如影像的左上角坐标是 300000.0标签的左上角是 300015.0两者在 30 米分辨率下只差半个像元肉眼几乎看不出来但逐像素训练时模型学到了一个固定的空间偏移。解决做训练样本前先打印影像和标签的 transform 信息确认左上角坐标、像元大小和宽高完全一致。不一致的一定要用 2.2 节的out_shape对齐不要用 ArcGIS 里随手一导的结果。检查方法很简单把标签叠加到影像上沿一条明显的道路或河流边界看 20 个点如果全偏同一侧就是基准格网的问题。5.3 总体精度高图斑却碎成“雪花”现象验证集 OA 超过 90%Kappa 也不错但生成的分类图上一类像素零散地穿插在另一类中间建设用地里一个点一个点的水体图面没法看。原因逐像素分类天然不考虑空间一致性每个像素独立判断加上 Landsat 混合像元多地物边界处天然存在光谱过渡于是出现椒盐噪声。这种情况说明模型学到了光谱却没有学到纹理结构。解决两步走。第一步在训练阶段确认 U-Net 里的 skip connection 没被裁掉它能帮助恢复空间细节第二步在后处理加一个 Majority Filter用 3×3 或 5×5 窗口取众数滤波能直接去掉孤立像元。多数滤波会损失细碎地物如果研究区需要保留细小水体滤波窗口不宜超过 3×3。5.4 验证分数高换一景影像就崩现象在训练那一景影像上验证 mIoU 有 85%换到相邻时相或相邻区域精度掉到 60%图面惨不忍睹。原因训练和验证 patch 来自同一景影像随机划分后两边在空间上高度相关模型其实把这一景影像的成像条件和大气状态背下来了。Landsat 不同时相的太阳高度角、土壤湿度和植被物候都不一样模型没见过这些变化。解决划分训练集和验证集一定按空间分块不要按像素随机划分。常见做法是把研究区按 2-3 公里格网切块取一部分格网做训练另一部分做验证保证验证数据和训练数据空间上不相邻。如果还要跨区域迁移最好在训练数据里加入多个时相的影像哪怕类别标签只标注了其中一景。5.5 水体和阴影、裸地和建筑混分不停现象混淆矩阵里阴影大量被分到水体裸地被分到建设用地查了光谱曲线发现两个类别的均值确实接近怎么调结构都没用。原因Landsat 只有 6 个多光谱波段没有短波红外以外的辅助信息山体阴影和清澈水体的光谱特征非常相似裸地和建设用地的区别主要靠纹理如果 patch 太小模型看不到足够大的空间范围。解决给模型加特征通道。常见做法是把 NDVI、NDWI、NDBI 三个指数作为额外的输入波段拼到原始 6 波段后面这样输入变成 9 通道。NDWI 能直接拉开水体和阴影的差距NDBI 给裸地和建设用地提供额外区分度。效果不够就再加一个纹理特征比如用 3×3 窗口计算近红外波段的标准差作为第 10 个通道输入。注意增加输入通道后模型第一层的in_channels要改成对应的数字训练前统计训练集的均值和标准差做标准化而不是直接用固定值。这一点最容易忘。6. 从验证集精度到整景出图把模型用起来的三个落地技巧6.1 把光谱指数叠进波段通道少调参白捡精度第一招是把光谱指数直接拼进输入。Landsat 的 B2-B7 六个波段之间相关性高CNN 虽然能自动学习特征但显式的指数特征能帮它更快区分水体和阴影、裸地和建设用地。通道拼接顺序建议为B2、B3、B4、B5、B6、B7、NDVI、NDWI、NDBI其中 NDVI (B5-B4)/(B5B4)NDWI (B3-B5)/(B3B5)NDBI (B6-B5)/(B6B5)。计算时注意分母加一个极小值防止除零Landsat 反射率已经除以 10000范围在 0-1 之间指数值会被压缩得比较小所以输入前要重新做一次标准化不要让指数通道和反射率通道的数值范围差太多。加完指数后我实际对比过水体 IoU 平均能提升 3 到 5 个百分点阴影误分明显减少。6.2 重叠推理叠加众数滤波把分类图做成能交付的成果第二招是推理阶段用重叠窗口加概率平均出图后做一次众数滤波。推理代码写成两层循环每次取 patch 大小的影像块预测得到的概率图存到全局概率矩阵里窗口移动 stride 小于 patch 大小时重叠区域自动累加概率最后取 argmax 得到分类结果。之后用 3×3 的 Majority Filter 清掉孤立像元这一步在 scipy 里可以直接调scipy.ndimage.generic_filter开 3×3 窗口用众数函数。这两个操作加在一起分类图交付给规划部门时基本不用二次修图接缝消失图斑也干净。6.3 按混淆矩阵做类别合并比再训一轮模型更划算第三招是输出成果前看一遍逐类 IoU把表现差且光谱相近的类别合并。我做过一个区域项目裸地和建设用地逐类 IoU 都只有 60 上下两者互相错分严重但研究区的最终需求是估算不透水面面积。于是直接把裸地和建设用地合并成一类重新统计面积精度一次性提到 90% 以上交付需求完全满足。分类体系不是死的分类树要服务于最终应用硬撑着区分两个对用户没价值的类别只会让整张图的精度被拖下去。这几次踩坑下来我最大的习惯改变是训练结束不只看 OA 和 loss 曲线而是把错分图叠加到影像上沿着地物边界放大逐类看所有“模型为什么这么分”的答案都在图里。调参调的是训练策略而真正决定成果质量的往往是数据对齐、类别体系和后处理这几件不起眼的小事。希望这篇笔记能让你在这个方向上少走我走过的弯路祝跑通。本文还有配套的精品资源点击获取