卫星遥感国土分类从零到全流程:PSPNet与DeepLabV3+源码实战解析
简介面向计算机相关专业课程设计与期末大作业的Python项目实现基于图像分割的卫星遥感图像国土分类。资源适合需要完整可运行代码的学生也适合进阶实战学习者可帮助掌握遥感图像预处理、语义分割模型构建与训练流程。压缩包共十二个文件包含八个脚本文件、两个训练日志、一个说明文档及一张展示图片整体大小约二点五兆。其中脚本覆盖数据加载、预处理、分割网络定义与训练入口日志可辅助分析训练过程说明文档提供必要使用提示。项目经过严格调试下载即可使用已获得一百五十九人学习关注通过源码可了解从遥感数据读取到国土分类输出的完整流程适合作为课设答辩素材或图像分割入门参考。1. 基于图像分割的卫星遥感国土分类一份能跑通全流程的课程设计源码用 Python 做卫星遥感影像的国土分类难点从来不在「训练一个分类器」而在把一张几万像素见方的大影像逐像素判别成水域、耕地、林地、建设用地这些地物类别。这个课程设计项目把整条链路做成了一套可直接运行的 Python 源码包数据预处理、标签加载、模型仓库、PSPNet、DeepLabV3、DeepLabV3 三套分割网络、训练脚本和完整训练日志都在里面。我拿到后完整拆过一遍结论是对正在做课程设计、期末大作业的计算机专业学生来说这是一个非常完整的基线——数据路径改一改就能开始训练答辩需要的对比图和海报素材也是现成的。源码里带了两份真实训练日志和 26 组 poster 展示图说明它不是「能看不能跑」的演示稿而是真产出过训练结果的项目。下面我按模型选型、数据流、训练、踩坑、进阶验证的顺序拆开讲标出我复现时遇到的问题和参数边界。数据保密的部分我也会说清楚避免你拿到手在 data 目录里翻车。2. 模型选型先立住PSPNet 与 DeepLabV3 在国土分类里的取舍拆代码之前先回答一个最容易被忽略的问题为什么这份源码选的是分割网络而不是分类网络这个判断决定了整个项目的走向也决定了课程设计报告的第一章怎么写。2.1 国土分类为什么是像素级任务从「这张图是什么」到「每个像素是什么」国土分类本质上是土地覆盖制图。传统做法是人工目视解译或者用浅层分类器逐像元分类但前者成本高后者对光谱相似的地物几乎无解。语义分割把整张影像端到端映射成逐像素标签正好替代这两条路。为什么普通分类网络不行因为场景级分类输出的是一个标签它对「图里同时有河、有田、有村」这种情况无能为力而国土分类的成果是一张地类分布图河流走向、农田边界、村庄范围都要精确到像素。所以这门课设选分割网络不是炫技是被任务逼的。输出端分割网络最后一层是 num_classes 个通道的 logits取 argmax 得到 H×W 的标签图每个像素对应一个地物类别。评价指标主要看 mIoU对每个类别算预测为该类的像素集合与真实为该类的像素集合的交并比再对全部类取平均。这个指标必须看懂因为它直接决定你读训练日志的方式。比如背景类占 90% 像素的数据模型全预测成背景准确率有 90%mIoU 却可能只有零点几所以日志里 acc 和 mIoU 并存时mIoU 优先。国土分类和通用分割还有一个差别遥感影像是俯视图地物没有天然的上下左右语义随机翻转是安全且高效的数据增强同时地物尺度跨度极大一条河可能横跨几百像素一棵树只有几十像素单一感受野很难同时照顾。源码同时提供 PSPNet 和 DeepLab 系列正是因为它们在多尺度上下文聚合上走了两条不同的技术路线适合做对比实验。2.2 PSPNet 的金字塔池化与 DeepLab 的空洞卷积两份网络代码的核心PSPNet 的核心是金字塔池化模块。骨干网络提取最后一层特征图后PPM 对它做 1×1、2×2、3×3、6×6 四个尺度的自适应平均池化各自经过 1×1 卷积压缩通道再上采样回原特征图尺寸和原特征图拼接输出。import torch import torch.nn as nn class PyramidPoolingModule(nn.Module): PSPNet 的金字塔池化不同 bin 尺寸抓不同尺度上下文。 def __init__(self, in_channels, out_channels512, bin_sizes(1, 2, 3, 6)): super().__init__() self.stages nn.ModuleList() for bin_size in bin_sizes: self.stages.append(nn.Sequential( nn.AdaptiveAvgPool2d(output_sizebin_size), nn.Conv2d(in_channels, out_channels, kernel_size1, biasFalse), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue), )) def forward(self, x): h, w x.shape[2:] feats [x] for stage in self.stages: pooled stage(x) feats.append(nn.functional.interpolate( pooled, size(h, w), modebilinear, align_cornersFalse)) return torch.cat(feats, dim1)参数含义bin_sizes 是池化输出尺寸。1×1 那支把整张特征图平均成一个向量等于把全局上下文塞给每个像素对遥感影像里大面积水体、连片农田这种长程相关地物特别有用6×6 那支保留局部细节。out_channels 控制每个池化层压缩后的通道数通道数越大表达力越强但显存和过拟合风险也同步上升。DeepLabV3 换了一套思路用空洞卷积在不降分辨率的前提下扩大感受野。ASPP 模块用 1×1 卷积、三个空洞率不同的 3×3 卷积和一个图像级池化分支并行采样最后拼接。import torch import torch.nn as nn class ASPP(nn.Module): DeepLabV3 的空洞空间金字塔池化不同空洞率抓不同感受野。 def __init__(self, in_channels, out_channels256, rates(6, 12, 18)): super().__init__() self.branches nn.ModuleList() self.branches.append(nn.Sequential( nn.Conv2d(in_channels, out_channels, 1, biasFalse), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue))) for rate in rates: self.branches.append(nn.Sequential( nn.Conv2d(in_channels, out_channels, 3, paddingrate, dilationrate, biasFalse), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue))) self.branches.append(nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(in_channels, out_channels, 1, biasFalse), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue))) def forward(self, x): size x.shape[2:] out [] for branch in self.branches: y branch(x) out.append(nn.functional.interpolate( y, sizesize, modebilinear, align_cornersFalse)) return nn.functional.relu(torch.cat(out, dim1), inplaceTrue)rates(6,12,18) 是三个空洞率。空洞率越大卷积核覆盖的像素间距越大感受野越大但参数不增。道路、田埂这类细长地物既要大感受野判断上下文又要保留边缘精度ASPP 正好两头兼顾。DeepLabV3 在 V3 基础上加了解码器把 ASPP 输出 4 倍上采样与骨干浅层特征拼接再卷积边缘明显更细。我的经验是答辩要强调边缘精细度就上 deeplabv3_plus要控制训练时间就先用 pspnet三套对比下来报告素材会非常完整。三套模型的取舍我整理成一张表写报告可以直接参考对比项PSPNetDeepLabV3DeepLabV3上下文聚合方式金字塔池化1/2/3/6 binASPP 空洞卷积6/12/18ASPP 解码器边缘细节表现一般较好最好训练开销最低中等最高适合场景大片地物为主地物尺度差异大细长地物、水网、道路骨干网络一般用 ResNet50 或 ResNet101加载 ImageNet 预训练权重对遥感影像依然有明显帮助因为底层的边缘、纹理特征通用。课程设计数据量通常不大强烈建议加载预训练权重而不是从零训练小数据从零训基本是过拟合现场。2.3 seg_zoo.py一个 dict 搞定模型注册换模型不改训练主流程这个包里 seg_zoo.py 的存在感很高它是模型仓库。常见做法是维护一个名字到构造函数的映射字典再提供一个统一的 create_model 入口# seg_zoo.py SEG_MODELS {} def register_model(name): def decorator(builder): SEG_MODELS[name] builder return builder return decorator register_model(pspnet) def build_pspnet(num_classes, backboneresnet50): return PSPNet(num_classesnum_classes, backbonebackbone) register_model(deeplabv3) def build_deeplabv3(num_classes, backboneresnet101): return DeepLabV3(num_classesnum_classes, backbonebackbone) register_model(deeplabv3_plus) def build_deeplabv3_plus(num_classes, backboneresnet101): return DeepLabV3Plus(num_classesnum_classes, backbonebackbone) def create_model(name, num_classes, **kwargs): if name not in SEG_MODELS: raise ValueError(funknown model: {name}, available: {list(SEG_MODELS)}) return SEG_MODELS[name](num_classes, **kwargs)这个设计对课程实验非常实用。训练脚本只需要一个 --model 字符串参数就能在三种网络间切换训练主流程一行不用改。三套模型吃同一份数据、同一个训练器差异只体现在网络结构上对比实验的公平性好报告里也讲得清。seg_zoo 注册表本身就是「系统支持多模型扩展」的代码证据比贴一个写死的 model.py 有说服力得多。想加 U-Net 之类的模型照葫芦画瓢写个 build_unet 注册进去就行这就是这个脚本最大的价值。3. 数据预处理与加载preprocess 脚本和 data_load 的分工数据链路决定了训练能不能开始。这个包把预处理和加载拆成两层preprocess 系列脚本负责把原始影像变成训练样本data_load.py 负责把样本变成张量喂给网络。3.1 preprocess.py 与 preprocess_water.py裁剪、归一化与水体掩膜遥感影像原始尺寸动辄上万像素直接塞进 CNN 不现实。preprocess.py 干的第一件事是把大影像切块。常见做法是按 512×512 或 1024×1024 的窗口滑动裁剪步长小于窗口尺寸形成重叠比如 512 窗口配 256 步长能避免地物正好被切在边界上等于顺带做了数据扩充。切完做归一化按波段均值方差标准化或者把每个波段压缩到 0~1。这一步不能省直接拿原始 DN 值进网络训练初期 loss 曲线会非常难看。预处理参数常见取值说明切块尺寸512×512显存与上下文信息的平衡点滑窗步长256与窗口重叠 50%减少边界断裂归一化方式按波段均值/方差每个波段独立统计不混在一起无效值处理置 0 或剔除检查影像边缘的黑边preprocess_water.py 单独存在说明这个项目在预处理阶段对水体做了专门处理。遥感里提水体最通用的手段是 NDWI 指数绿色波段对水体反射强近红外波段对水体吸收强两者的差除以和能突出水体光谱特征。import numpy as np def calc_ndwi(green_band, nir_band): NDWI (G - NIR) / (G NIR)水体像元一般在 0 以上。 green green_band.astype(np.float32) nir nir_band.astype(np.float32) ndwi (green - nir) / (green nir 1e-8) return (ndwi 0).astype(np.uint8)阈值 0 是常见起点实际项目里要对着影像调有时 0.1 更干净。这个脚本生成的掩膜有两种用法一是当辅助真值参与训练样本筛选二是做后处理投票把网络预测的水体区域和 NDWI 结果做融合减少误分。如果课程数据只有 RGB 三波段、没有近红外波段这个脚本跑不了直接跳过不动主线流程。3.2 data_load.pyDataset 的加载逻辑与增强参数data_load.py 负责把预处理好的影像块和标签块喂给训练器核心是一个继承 torch.utils.data.Dataset 的类import os import cv2 import torch from torch.utils.data import Dataset class RemoteSegDataset(Dataset): 读取影像块和同名标签块输出网络输入张量。 def __init__(self, image_dir, mask_dir, transformNone, class_num6): self.image_paths sorted( os.path.join(image_dir, f) for f in os.listdir(image_dir)) self.mask_paths [p.replace(image_dir, mask_dir) for p in self.image_paths] self.transform transform self.class_num class_num def __getitem__(self, idx): img cv2.imread(self.image_paths[idx]) # 读出来是 BGR img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(self.mask_paths[idx], cv2.IMREAD_GRAYSCALE) mask torch.from_numpy(mask).long() # 转成 Long 索引 if self.transform is not None: img, mask self.transform(img, mask) img torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 return img, mask def __len__(self): return len(self.image_paths)这段代码有三个关键约定凡是改过 data_load 的人基本都在这里吃过亏。第一mask 必须用灰度模式读成单通道索引值从 0 开始且小于 class_num否则 CrossEntropyLoss 直接抛 target 越界错误。第二图像最后要转成 C×H×W 的 float 张量并归一化到 0~1网络里一般不再重复归一化。第三image 和 mask 路径靠 replace 对应要求两个目录文件名完全一致命名对不上会静默读到错图。数据增强建议至少加随机翻转和随机裁剪。翻转对遥感影像非常友好地物不管怎么翻语义不变随机裁剪等于免费扩充样本。增强必须同时作用于 img 和 mask随机状态要一致否则标签和图像错位模型学到的是噪声。注意mask 目录和 image 目录的文件名必须一一对应data_load 的路径替换逻辑不允许两边命名有差异。3.3 数据目录怎么摆这份源码要求的文件组织拿到源码第一步不是读代码而是把目录结构对清楚。基于 data_load 的路径替换逻辑和训练入口的默认路径这个项目预期的数据布局大致是data/ ├── images/ # 预处理切好的影像块 │ ├── patch_0001.tif │ └── ... ├── masks/ # 与影像块同名的标签块 │ ├── patch_0001.png │ └── ... └── 高精卫星保密数据无法开源.txt那个 txt 是整份源码里最该先看的信息原始高精度卫星数据属于保密数据不能随源码开源所以包里的 data 目录是个占位结构。这意味着你拿到包后不可能直接开训必须先用公开遥感数据或课程统一发的数据把 images 和 masks 填上。这不是缺陷而是遥感课程里数据合规的常态课程报告里也要把这个约束交代清楚。nets 目录放训练中保存的模型权重pspnet.log 和 deeplab_v3.log 是两轮训练的完整日志26 组 poster 展示.png 是答辩素材。我建议拿到包先不动代码按上面的结构把数据目录建好再用一个很小的子集跑通全流程这样能最快确认链路是通的后面换大数据只是时间问题。4. 训练脚本与日志解读deecamp_train.py 跑通一个完整训练从文件名看这个脚本应该是某个深度学习训练营项目落地后的产物整体结构是一个标准 PyTorch 训练入口。把它读透了课程设计的「训练与实验」部分就有一半素材了。4.1 训练入口参数epoch、batch size、学习率与类别数deecamp_train.py 用 argparse 接收参数命令行就能控制模型、数据、训练超参。常见写法大概是# deecamp_train.py 训练入口 import argparse parser argparse.ArgumentParser(description遥感影像国土分类训练) parser.add_argument(--model, typestr, defaultpspnet, choices[pspnet, deeplabv3, deeplabv3_plus]) parser.add_argument(--num-classes, typeint, default6, help地物类别数水、耕地、林地、建设用地等) parser.add_argument(--batch-size, typeint, default8) parser.add_argument(--lr, typefloat, default1e-3) parser.add_argument(--epochs, typeint, default100) parser.add_argument(--data-root, typestr, default./data) args parser.parse_args()注意四个参数的联动关系。num_classes 必须和标签真实类别数严格一致多一个少一个都会在训练时报错或者让 mIoU 变成乱码。batch size 直接决定显存占用6 类分割任务、512×512 输入、ResNet50 骨干batch size 8 大约要 11~13GB 显存显卡不够就降到 2 或 4不要硬扛。学习率 1e-3 是 Adam 系优化器的常见起点换 SGD 建议落到 1e-2 再配 momentum。epochs 对课程设计不用追满公开小数据集 30~50 个 epoch 就能看到像样的分割轮廓100 个是给完整数据准备的。提示训练前先用 python deecamp_train.py --help 确认默认参数重点核对 --data-root 是否指向你实际建好的数据目录。还有一个容易被忽略的点训练是否固定随机种子。固定 seed 才能保证多次实验之间可比报告里的对比数字才站得住。训练中保存 checkpoint 的写法也值得看一眼一般会按 epoch 保存最佳 mIoU 和最后一轮两个权重方便中断后断点续训。4.2 训练中途看什么pspnet.log 与 deeplab_v3.log 的 loss 曲线与 mIoU包里带了两份真实日志pspnet.log 和 deeplab_v3.log说明作者用两套网络都完整训过。这类日志是课程设计报告里最有分量的证据比任何截图都能说明「代码真的运行过」。典型日志行大概是epoch: 30/100 | loss: 0.3124 | acc: 0.9210 | mIoU: 0.6842 | lr: 3.2e-4读日志抓三个信号。第一loss 是否趋势向下头几个 epoch 不降很正常几十个 epoch 还不降就要查学习率和归一化。第二train 与 val 的 mIoU 差距训练 mIoU 很高而验证明显掉队这是典型过拟合对应解法是加数据增强、加权重衰减或提前停止。第三loss 是否剧烈震荡正常训练有波动但趋势连续反复大起大落说明学习率偏大或 batch size 太小。两份日志还能支撑一个漂亮的对比实验把每个 epoch 的 mIoU 整理成曲线答辩时直接说「相同数据、相同训练器下DeepLabV3 相比 PSPNet 在 mIoU 上提升了多少」。这个对比的价值在于公平——唯一变量是网络结构结论可信度高比贴一张市面榜单截图强得多。我自己做对比时还会把训练耗时一起记上DeepLabV3 在边缘细节上的收益是用明显更长的训练时间换来的这个 trade-off 写到报告里很加分。4.3 26 组 poster 展示图答辩汇报时怎么用26 组 poster 展示图是这份源码容易被低估的部分。它应该是把原图、预测、真实标签按横向三联的方式排布的对比结果覆盖不同地物场景。答辩时不要从头讲模型结构评委听太多了建议按「问题定义 → 数据链路 → 三套模型定性对比 → 两个典型失败案例」来讲。典型失败案例比漂亮结果更抓人。比如水体边界在阴影区被误分、细长道路出现断裂说明你对模型边界有真实认知比只会背指标强得多。如果课程要求提交实验报告poster 图可以直接复用进实验结果章节按场景分类重新排版如果做答辩 PPT把图按「模型对比」和「场景对比」两个维度重组即可。有一点务必检查图片里如果出现真实卫星影像的敏感区域答辩前要打码处理。5. 避坑与常见问题这套源码最容易翻车的五个环节复现这套源码时我在五个位置翻过车下面按「现象 → 原因 → 解决」逐条记录基本覆盖这个项目大多数会出问题的点。5.1 显存爆掉batch size 与输入尺寸的联动现象启动训练几秒后报 CUDA out of memory程序直接终止查看日志时发现连第一个 epoch 都没跑完。原因遥感影像切块尺寸偏大1024×1024 很常见默认 batch size 8 的组合下显存很容易爆。我见过 24GB 显存的卡在这个组合下也阵亡。解决不要改网络结构先改两个数。把 data_load 里输入 resize 到 512×512batch size 降到 4 或 2。显存占用和输入面积成正比1024 降到 512单样本显存直接变成四分之一。还爆就查是否有别的进程占显存nvidia-smi 先看一眼再说。5.2 标签图按 RGB 读DataLoader 里最隐蔽的错误现象训练一开始就报 Target ... is out of bounds或者 loss 数值正常但可视化结果全花、看不出地物轮廓。原因标签 PNG 是 RGB 三通道真彩色图直接 imread 读出来形状是 H×W×3再转 long 变成三维标签。CrossEntropyLoss 要求标签是 H×W 单通道索引值不能超过类别数RGB 每像元三个通道值当三个标签自然越界。解决读标签强制用灰度模式保证只有单通道。如果原标签是调色板索引 PNG用 PIL 读 P 模式可以保留索引值from PIL import Image mask Image.open(mask_path) # 保持 P 模式不要 convert(RGB) mask torch.from_numpy(np.array(mask)).long()5.3 data 目录里只有 txt保密数据缺失时怎么让训练跑起来现象按默认路径启动训练报 FileNotFoundErrordata/images 下只有那个高精卫星保密数据占位说明。原因高精度卫星影像不能开源资源包只给了一个数据占位文件。没有数据训练无从谈起。解决先拿公开遥感数据替上。DeepGlobe、LoveDA、WHU 这些语义分割基准数据集都能直接下载切块后按第 3 章的目录结构放进 data/images 和 data/masks。先把链路跑通后面再换课程统一发的数据。替换时务必核对文件名一一对应这是 data_load 里最容易踩的暗坑。5.4 loss 卡住不降或剧烈震荡类别不平衡与学习率设置现象训练到 20 个 epochloss 在 1.5 附近横盘mIoU 上不去另一类表现是 loss 每轮大起大落曲线毫无收敛趋势。原因遥感数据类别极不平衡背景和建设用地占比可能超过一半水体、裸地占比很小模型被大类别带偏。另一个常见原因是学习率过大或 batch size 太小梯度更新方向不稳定。解决先确认 num_classes 和标签真实类别数一致不一致时 logits 和标签根本对不上。然后给 CrossEntropyLoss 加类别权重小类别权重抬高拉平各类贡献import torch.nn as nn # 按每个类别像素占比的倒数归一化得到 weight class_weight torch.tensor([1.0, 2.5, 1.2, 3.0, 1.5, 4.0]) criterion nn.CrossEntropyLoss(weightclass_weight)学习率改 poly 衰减训练后期自动缓降比固定学习率收敛顺一些。如果之前用坚实测代码的习惯不检查 learn rate scheduler这里值得专门看一眼。5.5 日志读不到或乱码编码与写入时机现象用记事本打开 pspnet.log 显示乱码或者日志文件停留在某个 epoch 没有后续内容。原因训练脚本写日志的编码和打开工具不一致常见是 GBK 混 UTF-8训练中断时日志缓冲区还没落盘。解决用 UTF-8 编码打开日志文件编辑器里指定编码再打开。看源码里日志写入方式改成逐行 flushwith open(log_path, a, encodingutf-8) as f: f.write(fepoch: {epoch} | loss: {loss:.4f} | mIoU: {miou:.4f}\n) f.flush() # 强制落盘训练中断也不丢flush 是血泪经验换来的训练跑到一半被系统杀掉、日志丢了十几个 epoch 的教训谁遇谁知道。6. 进阶验证用公开数据替换保密数据并产出对比图6.1 替换数据与冒烟测试跑通才算数拿到这份源码先别急着上全量数据。我习惯的做法是先做一次 2 epoch 的冒烟测试用 20 张公开影像切块喂进去确认 DataLoader、网络、损失函数、日志四段链路全部通畅再扩展数据跑完整训练。冒烟测试阶段就把训练集、验证集、测试集分开避免后面报告里的指标没有说服力。如果你下载的就是这份课程设计源码包拿到手先别改任何代码按第 3 章的目录结构把数据建好用 --epochs 2 --batch-size 2 跑一遍能正常出日志再谈完整训练。6.2 三图联排可视化预测、标签、原图对齐输出训练完加载最佳权重对验证集做预测把原图、预测、真实标签拼成三联图。这是分割项目最通用的可视化方法也是 poster 展示图的基本单元import numpy as np import cv2 def save_compare(img, pred, truth, save_path, palette): palette 是每类一个 RGB 值顺序必须和训练索引一致。 pred_rgb palette[pred] truth_rgb palette[truth] grid np.hstack([img, pred_rgb, truth_rgb]) cv2.imwrite(save_path, cv2.cvtColor(grid, cv2.COLOR_RGB2BGR))这里有个细节palette 的类别顺序必须和 num_classes 的索引顺序一致索引 0 是水就全图统一用代表水的颜色否则对比图会误导答辩评委。输出时挑一张表现好的和一张表现差的场景放进报告比十张好看的更有说服力。从那以后我拿到任何分割源码都会先做一轮「公开小数据冒烟测试」再谈完整训练——目录结构、标签索引、显存占用、日志 flush 全在这一轮暴露出来。这个习惯帮我避开了绝大多数翻车现场希望你用这份源码时也先走一遍这个流程希望帮到你。本文还有配套的精品资源点击获取