news 2026/9/11 10:39:01

遥感国土分类语义分割实战:PSPNet与DeepLabV3+全流程解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
遥感国土分类语义分割实战:PSPNet与DeepLabV3+全流程解析

简介:面向计算机相关专业正在完成课程设计或期末大作业的学生,项目完整实现了基于图像分割对卫星遥感图像进行国土分类的方案,涵盖数据加载与预处理、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, threshold=0.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_size=512): ds = gdal.Open(path) if ds is None: raise ValueError(f"GDAL 无法打开 {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, axis=0)

band_indices 用 1 起始的 GDAL 波段编号,第四个波段留给近红外;如果你只保留 RGB,后续 NDWI 和植被指数都会失掉信息来源。patch_size 设 512 是在精度与显存之间的折中,课程设计机器上 8GB 显存跑 512 输入比较稳妥,调到 1024 则训练显存成倍上涨。切块之后通常还要做按样本统计的均值方差归一化,而不是按整张大图算,否则不同日期的影像光照差异会把网络逼得反复震荡。归一化后的数组建议缓存成 npy,data_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, backbone="resnet50"): if model_name == "pspnet": return PSPNet(num_classes=num_classes, backbone=backbone) elif model_name == "deeplabv3": return DeepLabV3(num_classes=num_classes, backbone=backbone) elif model_name == "deeplabv3_plus": return DeepLabV3Plus(num_classes=num_classes, backbone=backbone) else: raise ValueError(f"unknown 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, bias=False), nn.BatchNorm2d(inner), nn.ReLU(inplace=True))) 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), mode="bilinear", align_corners=False)) 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, bias=False), nn.BatchNorm2d(256), nn.ReLU(inplace=True))) for rate in rate_list: self.heads.append(nn.Sequential( nn.Conv2d(in_channels, 256, 3, padding=rate, dilation=rate, bias=False), nn.BatchNorm2d(256), nn.ReLU(inplace=True))) def forward(self, x): feats = [head(x) for head in self.heads] return torch.cat(feats, dim=1)

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膨胀率、输出步长分辨率太小时膨胀卷积越界
DeepLabV3+ASPP + decoder低层拼接层数decoder 特征对齐差会引入棋盘格伪影

把 unet 也拉进对比是很多答辩老师期待看到的扩展。unet 的编码-解码结构在医学图像分割里被验证过,放到遥感上也能直接跑,只是缺少多尺度池化和膨胀卷积,对大范围连片地物的上下文建模稍弱。加这个对比不需要改训练主流程,只要在 seg_zoo.py 里注册一个 unet 分支,把它和 PSPNet 的结果放到同一张海报上,就能把课程设计的深度往上抬一档。

4. 训练入口与日志判读:deecamp_train.py、pspnet.log、deeplab_v3.log

4.1 训练脚本的组装方式与关键超参数

deecamp_train.py 是工程里的训练入口,Deccamp 竞赛类项目常用这个命名方式,它把数据集读取、网络构建、损失计算和日志输出串在一起。跑通它之前要先准备依赖:遥感数据读写至少要装 gdal 和 numpy,分割模型需要 torch。创建虚拟环境后执行项目文件的顺序是:先跑 preprocess 生成 npy 切片,再运行训练脚本。

# 一个能够承接该工程的 python 环境创建方式 conda create -n rsseg python=3.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;如果追求和论文一致的收敛行为,换成 SGD(momentum=0.9,weight_decay=5e-4)并配多项式学习率衰减,语义分割跑出来的边界会更干净。损失函数用 CrossEntropyLoss,同时要设置 ignore_index=255,把标签里无效区域的像素排除在梯度外。显存管理上,batch_size=4 是课程设计机器的保守值,16GB 显存可以上到 8;crop_size=512 与 preprocess.py 的切块尺寸必须一致,否则候选块数量对不上。

4.2 pspnet.log 与 deeplab_v3.log 怎么读

两份日志放在根目录,说明作者在选型阶段做过一轮横向对比。一般训练脚本每个 epoch 会输出学习率、损失、像素准确率、mIoU 和目前最优 mIoU:

Epoch [32/100] lr=0.00034 loss=0.3124 pixAcc=0.9362 mIoU=0.6941 best=0.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_size=4 附近,一旦把 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(weight=class_weights, ignore_index=255)

权重设置的原则是让稀缺类获得更高的惩罚权重,但不能高出平均值一个数量级,否则网络会牺牲大类精度来讨好小类,整体 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, gsd=1.0): pixel_area_m2 = gsd * gsd # 单像素对应平方米数 counts = np.bincount(mask.ravel(), minlength=num_classes) return counts * pixel_area_m2 / 1e6

gsd 必须与数据说明一致,不能统一填 1.0。如果你的数据来源是 10 米分辨率的卫星影像,一个像素是 100 平方米,面积统计会差两个数量级。做完面积统计,要顺手把类别占比列成表格:耕地占比、林地占比、不透水面占比,再画饼图或横向条形图。面积统计表放在分割图下面,就构成了“图上是什么地物”到“这些地物有多少”的完整证据链。

5.3 26 组 poster 展示图怎么组织成答辩材料

资源包里的“26 组poster展示.png”大概率是一张集合了多组对比结果的大图,作报告答辩材料时可以直接复用。组织手法推荐三行一组:第一行放大原始影像和真值标签,第二行放三种模型的预测掩膜,第三行放预测与真值的差异叠加图,用红色标出误分类像素。三套模型都出现时,这样一组图就能回答“为什么选 PSPNet 或 DeepLabV3+”。最后一行放面积统计表,把每家模型输出的面积与真值面积的相对误差也列出来,视觉上既直观又带量化。海报配色要注意色盲友好性,水体和建设用地分别用蓝、红是默认习惯,草地和耕地不要都用绿色系,否则打印成黑白稿后会完全无法分辨。答辩汇报顺序也就按“预处理切块、水体预分类、模型对比、面积统计”这条线走,评委看到的每个细节都能对应到工程文件,课程设计的完整度就立住了。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 10:37:10

高通车载平台EDL刷机与QCN恢复实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 10:37:00

大疆无人机视频流传输与MQTT协议适配技术解析

1. 大疆无人机视频流传输技术背景 大疆行业级无人机(如M300 RTK、Mavic 3 Enterprise等)采用的视频流传输系统,本质上是一个经过深度优化的实时多媒体传输管道。其核心由三个技术层构成:物理层的OcuSync/O3图传系统负责无线信道管…

作者头像 李华
网站建设 2026/9/11 10:34:41

Maestro 移动 UI 自动化测试入门指南:从 YAML 流程到 AI 断言

Maestro 移动 UI 自动化测试入门指南:从 YAML 流程到 AI 断言 【免费下载链接】Maestro Painless E2E Automation for Mobile and Web 项目地址: https://gitcode.com/GitHub_Trending/ma/Maestro Maestro 是一个开源的移动与 Web 端 UI 自动化测试框架。你用…

作者头像 李华