
简介面向计算机相关专业正在完成课程设计或期末大作业的学生项目完整实现了基于图像分割对卫星遥感图像进行国土分类的方案涵盖数据加载与预处理、PSPNet、DeepLabV3及DeepLabV3等主流分割模型并附有两份训练日志与26组Poster展示图可直观呈现模型效果与调优过程。压缩包共12个文件以8个Python脚本为主体分别负责数据预处理、模型搭建与训练另有2个log训练记录、1个数据说明文档和1张展示图片整体大小仅2.49MB结构清晰便于按需查阅和移植复用。项目经过严格调试下载即可直接运行适合需要快速搭建遥感图像分类系统、深入掌握图像分割实战流程的开发者。目前已有156人学习下载可作为课程设计答辩演示、期末报告素材或科研入门的参考范例。1. 遥感国土分类的课程设计为什么一定要用语义分割遥感国土分类这个题目最容易做歪把“基于图像分割”误读成“基于图像分类”。真实任务是逐像元输出地物标签水体、耕地、林地、裸土、不透水面边界闭合面积可统计普通分类网络交不出这种东西。资源包把 PSPNet、DeepLabV3、DeepLabV3 三套分割模型和完整预处理链封装在一起训练入口是 deecamp_train.py附 pspnet.log 与 deeplab_v3.log 两份日志和 26 组 poster 展示图数据和训练、日志、展示能闭合成一个做课程设计足够用的工程。对赶期末大作业的同学按下面拆的顺序把数据换进去就能复现对想在项目里做出差异化的从业者水体预分类和两份日志里的模型差异才是值得停下来看的地方。2. 数据侧preprocess 系列脚本与 data_load 迭代器的取数逻辑2.1 preprocess_water.py水体预分类为什么值得单独写遥感影像的土地覆盖分类里水体是个很矛盾的类别。它光谱特征稳定肉眼几乎不会错认但对分割网络又不够友好水面波纹、云影、高差很大的建筑阴影都会让网络在边界附近来回摇摆。把水体单独拿出来处理是所有遥感分割工程里常见的第一刀。资源包里的 preprocess_water.py 干的就是这件事常见实现是用 NDWI归一化差异水体指数做硬阈值。# preprocess_water.py 中水体掩膜的核心逻辑 import numpy as np def ndwi_mask(green, nir, threshold0.1): # green/nir 分别为绿波段与近红外波段形状需一致 g green.astype(np.float32) n nir.astype(np.float32) ndwi (g - n) / (g n 1e-6) return (ndwi threshold).astype(np.uint8)NDWI 用绿光-近红外/绿光近红外这一比值把水体亮度提升到 0 以上、非水体压到 0 以下。阈值 0.1 是常见起点沙质水体和浑浊水体边缘会低于它需要小幅下调山地阴影多时则往上调否则阴影会被当成水。这个脚本的产出不是最终标签而是一份 0/1 掩膜之后要与分割网络输出做按位或网络只负责陆地类水体和陆地交界处才不会出现明显的锯齿边。换句话说preprocess_water.py 的存在是在给网络减负。国土分类中的类别不平衡问题经常表现为水体占比极高或极低当一片研究区有 40% 面积是水面时模型只要把像素都推给水体就能拿到很高的像素准确率水面很少时水体类 IoU 又低到没法看。先把水体拿走后面训练损失就不会被这个强先验带偏剩余类别的不平衡程度也会缓和得多。2.2 preprocess.py通道选择、裁剪与归一化preprocess.py 是通用预处理入口承担从原始遥感影像到训练切片的转换。遥感原始数据通常以多波段 TIFF 形式存在不能直接用 cv2 读成三通道常见做法是先选波段再切块。一个 x 波段影像里可见光 RGB 负责纹理近红外波段负责植被和水体区分很多分割网络只吃三通道输入所以波段取舍直接决定任务上限。# preprocess.py 中波段读取与切块的通用写法 from osgeo import gdal def load_and_crop(path, band_indices(1, 2, 3, 4), patch_size512): ds gdal.Open(path) if ds is None: raise ValueError(fGDAL 无法打开 {path}) data ds.ReadAsArray() # 形状 (B, H, W) data data[list(band_indices)].astype(np.float32) patches [] # 无重叠切块stride 等于 patch_size for y in range(0, data.shape[1] - patch_size 1, patch_size): for x in range(0, data.shape[2] - patch_size 1, patch_size): patches.append(data[:, y:y patch_size, x:x patch_size]) return np.stack(patches, axis0)band_indices 用 1 起始的 GDAL 波段编号第四个波段留给近红外如果你只保留 RGB后续 NDWI 和植被指数都会失掉信息来源。patch_size 设 512 是在精度与显存之间的折中课程设计机器上 8GB 显存跑 512 输入比较稳妥调到 1024 则训练显存成倍上涨。切块之后通常还要做按样本统计的均值方差归一化而不是按整张大图算否则不同日期的影像光照差异会把网络逼得反复震荡。归一化后的数组建议缓存成 npydata_load.py 直接读取省掉每次训练都重复解译 TIFF 的开销。2.3 data_load.py把 npy 缓存喂进数据迭代器data_load.py 的职责是把预处理产物组织成 PyTorch 的 Dataset 和 DataLoader。为了避免每轮迭代重复做几何矫正、归一化常见做法是上一节输出的 npy 文件这里只负责路径对应关系和类型转换。# data_load.py 中的 Dataset 封装 import glob import numpy as np import torch from torch.utils.data import Dataset class LandCoverDataset(Dataset): def __init__(self, image_dir, mask_dir): self.images sorted(glob.glob(f{image_dir}/*.npy)) self.masks sorted(glob.glob(f{mask_dir}/*.npy)) assert len(self.images) len(self.masks), 影像与标签数量必须一致 def __getitem__(self, idx): image np.load(self.images[idx]) # (C, H, W), float32 mask np.load(self.masks[idx]) # (H, W), 类别从 0 开始 return torch.from_numpy(image), torch.from_numpy(mask).long()类别编号必须从 0 开始连续编码0 通常是背景或未分类1、2、3 分别对应耕地、林地、建设用地等。如果标注文件里类别从 1 起步训练时要额外减 1否则 CrossEntropyLoss 的类别总数会整体多一位IoU 也会平白多出个“零类”。train/val 划分建议按大图单位切分而不是在所有切片里随机抽否则同一区域的大量重叠切块会同时出现在训练和验证集里mIoU 虚高而实测崩坏。DataLoader 侧需要注意的是 num_workers 要和机器核数匹配Windows 下还要把代码包进if __name__ __main__否则多进程取数会反复触发训练脚本。2.4 高精数据不随包附带时如何替换数据集资源包里有一句“高精卫星保密数据无法开源”翻译成实操语言就是工程骨架齐全影像数据得自己准备。课程设计阶段不建议去啃那些重达几十 GB 的原始影像更稳妥的是用公开土地覆盖数据集或者对 Sentinel-2 单时相影像自己标注几类地物。替换数据时有四个对齐项必须检查波段顺序、切块尺寸、类别编号、投影信息。前三个决定训练是否能跑通最后一个决定你在答辩时能不能把结果叠回地图上。如果原工程设定输入是 RGB 三通道而你换了四波段数据必须在调用 preprocess.py 时同步改 band_indices不要在 data_load 里悄悄加通道否则第一轮 forward 就会报 shape mismatch。3. seg_zoo 与三套分割骨干PSPNet、DeepLabV3、DeepLabV3 的模块差异3.1 seg_zoo.py把模型选择收敛到一个入口seg_zoo.py 从命名看就是“分割模型动物园”它本身不包含训练逻辑只提供一个按名字取模型的统一入口。课程设计里维护这样一个文件的价值在于实验对比时不用在训练脚本里到处改 import写个配置字符串就能切换模型。# seg_zoo.py 常见结构的模型选择函数 def build_seg_model(model_name, num_classes, backboneresnet50): if model_name pspnet: return PSPNet(num_classesnum_classes, backbonebackbone) elif model_name deeplabv3: return DeepLabV3(num_classesnum_classes, backbonebackbone) elif model_name deeplabv3_plus: return DeepLabV3Plus(num_classesnum_classes, backbonebackbone) else: raise ValueError(funknown model: {model_name}可用: pspnet/deeplabv3/deeplabv3_plus)这个设计的边界条件很明确新加网络时只需要在动物园里注册一个分支训练脚本不动。做消融实验或者并行跑多组对比就非常方便。如果你准备把 unet 作为基线模型加进来对比也是在这一层加分支而不是去改动训练入口。3.2 PSPNet金字塔池化模块怎么覆盖不同尺度地物PSPNet 的核心是金字塔池化模块PPM。耕地地块、林地连片区域、城市街道纹理的尺度差异很大只用最后一层特征图直接上采样小尺度和全局上下文会被同时压缩。PPM 的思路是把特征图同时池化成多个分辨率再上采样回原尺寸拼在一起。class PyramidPoolingModule(nn.Module): def __init__(self, in_channels, pool_sizes(1, 2, 3, 6)): super().__init__() inner in_channels // 4 self.branches nn.ModuleList() for size in pool_sizes: self.branches.append(nn.Sequential( nn.AdaptiveAvgPool2d(size), # 池化到 size x size nn.Conv2d(in_channels, inner, 1, biasFalse), nn.BatchNorm2d(inner), nn.ReLU(inplaceTrue))) def forward(self, x): h, w x.size(2), x.size(3) out [x] for branch in self.branches: f branch(x) out.append(F.interpolate(f, (h, w), modebilinear, align_cornersFalse)) return torch.cat(out, 1)pool_sizes 固定成 1、2、3、6 是 PSPNet 论文里的默认配置含义与卷积核尺寸不同它表示自适应池化的输出边长。1×1 分支聚合整张影像的全局信息适合识别湖泊、大块农田2×2 和 3×3 负责中等尺度的林班和建设用地组团6×6 保留更多位置细节。把四个尺度的输出与原始特征拼接后通道数变为原来的两倍后续分类层看到的既是局部纹理又是全局上下文。课程设计里改 pool_sizes 的收益往往不如改主干明显刻意调成 1、2、4、8 只会增加显存占用对 mIoU 的提升很难超过一个百分点。3.3 DeepLabV3 与 DeepLabV3膨胀卷积替换池化DeepLab 系列与 PSPNet 最大的设计分歧在于它不靠池化而用膨胀卷积扩大感受野。池化会丢掉位置信息膨胀卷积在保持特征图分辨率的前提下让卷积核看到更远的像素。ASPP 模块使用 1×1 卷积加三个不同膨胀率的 3×3 卷积分别抓取不同距离的语义关系。class ASPP(nn.Module): def __init__(self, in_channels, rate_list(6, 12, 18)): super().__init__() self.heads nn.ModuleList() # 1x1 卷积分支等价于膨胀率 1 self.heads.append(nn.Sequential( nn.Conv2d(in_channels, 256, 1, biasFalse), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue))) for rate in rate_list: self.heads.append(nn.Sequential( nn.Conv2d(in_channels, 256, 3, paddingrate, dilationrate, biasFalse), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue))) def forward(self, x): feats [head(x) for head in self.heads] return torch.cat(feats, dim1)rate_list(6, 12, 18) 是构建在 ImageNet 预训练权重上的经验值输入分辨率普遍在 513 左右可确定。换成 512 输入时18 的膨胀率会让卷积核的感受野超过特征图边长边缘特征被边界截断表现为道路边缘出现撕裂状伪影。若你的影像尺寸偏小需要把膨胀率整体下调比如 4、8、12。DeepLabV3 和 DeepLabV3 的编码器部分一致多出的 decoder 是把高层输出与浅层特征拼接再细化对恢复道路、田埂这类细边界贡献明显。资源包里 deeplabv3.py 与 deeplabv3_plus.py 并存正好能验证这一点。表三套骨干在国土分类场景下的定位对比网络上下文提取方式主要调参点容易出的问题PSPNetPPM 池化 1/2/3/6池化分支数、骨干网络大面积类别稳定细小道路边界被池化抹掉DeepLabV3ASPP 膨胀率 6/12/18膨胀率、输出步长分辨率太小时膨胀卷积越界DeepLabV3ASPP decoder低层拼接层数decoder 特征对齐差会引入棋盘格伪影把 unet 也拉进对比是很多答辩老师期待看到的扩展。unet 的编码-解码结构在医学图像分割里被验证过放到遥感上也能直接跑只是缺少多尺度池化和膨胀卷积对大范围连片地物的上下文建模稍弱。加这个对比不需要改训练主流程只要在 seg_zoo.py 里注册一个 unet 分支把它和 PSPNet 的结果放到同一张海报上就能把课程设计的深度往上抬一档。4. 训练入口与日志判读deecamp_train.py、pspnet.log、deeplab_v3.log4.1 训练脚本的组装方式与关键超参数deecamp_train.py 是工程里的训练入口Deccamp 竞赛类项目常用这个命名方式它把数据集读取、网络构建、损失计算和日志输出串在一起。跑通它之前要先准备依赖遥感数据读写至少要装 gdal 和 numpy分割模型需要 torch。创建虚拟环境后执行项目文件的顺序是先跑 preprocess 生成 npy 切片再运行训练脚本。# 一个能够承接该工程的 python 环境创建方式 conda create -n rsseg python3.8 -y conda activate rsseg pip install torch torchvision opencv-python numpy gdal训练循环本身没有特殊魔法值得照抄的是一个稳定的前向反向骨架# deecamp_train.py 中训练一个 epoch 的核心流程 def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, total_num 0.0, 0 for images, masks in loader: images images.to(device) masks masks.to(device) logits model(images) # (B, C, H, W) loss criterion(logits, masks) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * images.size(0) total_num images.size(0) return total_loss / total_num常见的优化器配置是 AdamW学习率 1e-4权重衰减 1e-4如果追求和论文一致的收敛行为换成 SGDmomentum0.9weight_decay5e-4并配多项式学习率衰减语义分割跑出来的边界会更干净。损失函数用 CrossEntropyLoss同时要设置 ignore_index255把标签里无效区域的像素排除在梯度外。显存管理上batch_size4 是课程设计机器的保守值16GB 显存可以上到 8crop_size512 与 preprocess.py 的切块尺寸必须一致否则候选块数量对不上。4.2 pspnet.log 与 deeplab_v3.log 怎么读两份日志放在根目录说明作者在选型阶段做过一轮横向对比。一般训练脚本每个 epoch 会输出学习率、损失、像素准确率、mIoU 和目前最优 mIoUEpoch [32/100] lr0.00034 loss0.3124 pixAcc0.9362 mIoU0.6941 best0.7030重点不是绝对值而是变化趋势。loss 发愁不看先看是不是前 20 个 epoch 没有降那是学习率太高导致的震荡降到 3e-5 再试pixAcc 很容易上 90但水域占比高的数据集里 pixAcc 接近 95 也可能只说明模型学出了“全都预测成背景”的捷径mIoU 才是类别间均衡的硬指标遇到类别不平衡不能只盯 loss。表两份日志在课程设计报告里可以做哪些对比对比维度pspnet.logdeeplab_v3.log结论写作方向收敛速度前 10 轮 loss 降幅较大同等轮次下略慢PPM 对全局结构更敏感显存占用特征拼接通道多ASPP 多条膨胀卷积并行日志里可额外记录显存边界质量大块地物稳细边抖动膨胀卷积保住细边结合可视化单独说明最终 mIoU数据相关数据相关关注“最优 epoch”而不是末轮读日志时还要确认随机种子是否固定。两个模型对比时如果 dataset shuffle 顺序不同最优 epoch 的位置会有偏差这不代表模型优劣。把随机种子在训练脚本开头固定住见 log 和 deeplab 的比较才站得住。日志里出现 loss 突然跳到 NaN优先查两种情况一是近红外波段参与归一化后存在 0 方差像素二是学习率配合 BN 在 batch 太小时梯度爆炸。4.3 训练阶段最容易翻车的三个细节显存不够是第一个常见问题。resnet50 加 ASPP输入 512 时单卡 8GB 只能扛 batch_size4 附近一旦把 DeepLabV3 的 decoder 打开显存会再涨一截。缓解办法是减少 batch、把数据加载的 pin_memory 关闭、降低 num_workers尽量不要动 crop_size因为预处理脚本和训练脚本的裁剪尺寸不一致会引发运行时错误。类别顺序错位是第二个问题。替换数据集后标签里类别编号可能从 1 开始而代码默认从 0 开始。训练前用np.unique(mask)扫一遍训练集确认类别索引的连续范围否则 CrossEntropyLoss 的类别数会对不上。第三个问题是类别不平衡对损失函数的影响。水域已经被 preprocess_water.py 拿走剩余类别里裸土和建设用地经常严重稀缺这时普通交叉熵会把稀缺类别压成噪声加权交叉熵更合适# 类别加权交叉熵权重根据训练集各类像素占比的倒数归一化 class_weights torch.tensor([0.8, 1.0, 1.0, 2.5, 3.0]).to(device) criterion nn.CrossEntropyLoss(weightclass_weights, ignore_index255)权重设置的原则是让稀缺类获得更高的惩罚权重但不能高出平均值一个数量级否则网络会牺牲大类精度来讨好小类整体 mIoU 不升反降。一般以各类像素占比的倒数做底再截断到 [0.5, 5] 区间内。5. 把掩膜换算成可展示成果IoU 评估、面积统计与海报组织5.1 用逐类 IoU 替代整图准确率答辩时评委最容易问的一句话是“你怎么证明分割结果好”。像素准确率在这里没有说服力一个全是农田的测试切片里把像素全预测成农田就能拿 95% 以上。要展示的是逐类 IoU它统计每类的预测与真实标签的交集、并集比值各类得分分开看才能暴露短板。# 逐类 IoU 计算pred 与 target 都是整数掩膜 def per_class_iou(pred, target, num_classes): ious [] for c in range(num_classes): p (pred c) t (target c) inter (p t).sum() union (p | t).sum() ious.append(float(inter) / (float(union) 1e-6)) return np.array(ious)这里的逐类遍历虽然朴素但胜在不会用错混淆矩阵的轴。假设数据集共 5 类评估后给出数组分别是背景、耕地、林地、草地、建设用地的 IoU。报告里通常把五个类别的 IoU 做成柱状图再在图上标注水体是预分类拿到的结果。更好的做法是同时输出混淆矩阵它能让答辩老师一眼看出哪些类互相混分比如草地和耕地边界处大片错分说明训练切片里两类的光谱纹理太接近需要补充该区域的样本。5.2 从分割掩膜换算国土面积统计分类结果最终要落到“面积”才像国土分类。以一景已知地面采样距离的影像为例分割掩膜每个像素代表一个真实的物理范围。gsd 为 1 时一个像素对应 1 平方米0.5 时对应 0.25 平方米。统计各类像素数并乘上单像素面积就能得到各类地物的物理占地。# 根据掩膜统计各类面积单位 km² import numpy as np def class_area_km2(mask, num_classes, gsd1.0): pixel_area_m2 gsd * gsd # 单像素对应平方米数 counts np.bincount(mask.ravel(), minlengthnum_classes) return counts * pixel_area_m2 / 1e6gsd 必须与数据说明一致不能统一填 1.0。如果你的数据来源是 10 米分辨率的卫星影像一个像素是 100 平方米面积统计会差两个数量级。做完面积统计要顺手把类别占比列成表格耕地占比、林地占比、不透水面占比再画饼图或横向条形图。面积统计表放在分割图下面就构成了“图上是什么地物”到“这些地物有多少”的完整证据链。5.3 26 组 poster 展示图怎么组织成答辩材料资源包里的“26 组poster展示.png”大概率是一张集合了多组对比结果的大图作报告答辩材料时可以直接复用。组织手法推荐三行一组第一行放大原始影像和真值标签第二行放三种模型的预测掩膜第三行放预测与真值的差异叠加图用红色标出误分类像素。三套模型都出现时这样一组图就能回答“为什么选 PSPNet 或 DeepLabV3”。最后一行放面积统计表把每家模型输出的面积与真值面积的相对误差也列出来视觉上既直观又带量化。海报配色要注意色盲友好性水体和建设用地分别用蓝、红是默认习惯草地和耕地不要都用绿色系否则打印成黑白稿后会完全无法分辨。答辩汇报顺序也就按“预处理切块、水体预分类、模型对比、面积统计”这条线走评委看到的每个细节都能对应到工程文件课程设计的完整度就立住了。本文还有配套的精品资源点击获取