简介:基于PyTorch的高分遥感语义分割(地物分类)项目源码,面向计算机、人工智能、自动化及相关专业学生、教师或从业者,可作为课程设计、大作业与毕业设计的完整参考。资源源自个人毕设,答辩评审98分,代码已调试通过、可直接运行。压缩包共858个文件、约550MB,其中819张png为遥感训练样本与预测可视化结果,35个py为模型训练、推理、评估及预处理脚本,另有类别对照csv、项目说明docx与样例jpg等,结构清晰便于定位学习。目前已有157人学习浏览。通过该项目可获得高分二号影像切块、地物标签预处理、PyTorch语义分割训练与预测、叠加融合出图等整套工作流,并配有答辩文档模板,便于对照复现关键环节,也可在此基础上更换数据集或调整网络进一步拓展。
1. 高分遥感语义分割,为什么值得用 PyTorch 重新做一遍
高分遥感影像里的地物分类,从来不是“把图片丢进网络”那么简单。2 米或者 0.5 米分辨率的影像,单景动辄几亿像素,地物尺度从几米的道路到几百米的农田都有,阴影、云遮挡、同物异谱、异物同谱这些干扰叠在一起,让普通分类网络在遥感数据上经常直接崩掉。语义分割正好是处理这类问题的标准范式——对每一个像素做类别预测,输出和输入同尺寸的密集标注图。而 PyTorch 在遥感分割任务里几乎是事实标准,不只是因为 torchvision 里自带 DeepLabV3 这类现成骨架,更关键的是它的动态图和灵活的 Dataset 机制,能让你在处理超大影像、做分块训练和样本增强时,随时改逻辑而不用重写整个工程。
这篇博客就围绕一条完整可落地的技术路径展开:高分影像怎么做预处理和标签制作、PyTorch 里怎么搭分割模型、训练时要盯哪些参数、推理时怎么处理大图拼接,以及毕业设计文档里怎么把实验数据写扎实。整个过程不需要你手写反向传播,也不需要你有遥感背景,有 Python 基础就能跟下来。
2. 高分影像的数据链路:从 GeoTIFF 到可训练的 PyTorch Dataset
2.1 高分影像和普通图片的本质差异
大多数人第一次接触遥感影像,都会用cv2.imread直接读图,然后就是各种报错或者颜色怪异。这里有个绕不开的底层差异:高分遥感影像通常是多波段 GeoTIFF,除了 RGB 三个可见光波段,往往还带着近红外(NIR)波段,部分数据源甚至有红边波段。PyTorch 的预训练模型(比如 ImageNet 权重)默认接受 3 通道输入,所以第一步就需要考虑波段选取策略。
常见做法有两种:一种是直接取 RGB 三个波段,好处是能加载 ImageNet 预训练权重,迁移学习效果稳定;另一种是使用 RGB+NIR 四个波段,信息量更足,但此时就不能直接使用预训练模型的第一个卷积层,需要做通道维度的权重初始化调整。对于毕业设计来说,我一般建议先走 RGB 路线,把模型跑通,再在改进实验里加入多波段对比。
另外,高分影像的位深通常是 16bit 甚至 32bit,像素值范围不是 0 到 255。如果直接除以 255 做归一化,算出来的值会非常小,模型收敛会变得很慢。需要先做线性拉伸或者百分位截断,让像素分布落到 0 到 255 的区间,再进入常规归一化流程。
2.2 大影像分块:训练集构建的核心逻辑
遥感影像单景尺寸巨大,显卡显存放不下完整输入,这是所有遥感分割项目都必须处理的现实问题。标准方案是滑窗裁剪。假设原始影像尺寸是 10000×10000,标签是同样尺寸的单通道灰度图,类别像素值从 0 到 N-1。你需要在影像和标签上用相同的窗口位置做裁剪,保证像素级对齐。
以下是一个典型的裁剪脚本:
import numpy as np from osgeo import gdal def split_image(image_path, label_path, output_size=512, stride=256): img_ds = gdal.Open(image_path) lab_ds = gdal.Open(label_path) img = img_ds.ReadAsArray() # 形状: [C, H, W] lab = lab_ds.ReadAsArray() # 形状: [1, H, W] if img.ndim == 2: img = img[np.newaxis, :, :] if lab.ndim == 3: lab = lab[0, :, :] bands, height, width = img.shape tiles = [] labels = [] for y in range(0, height - output_size + 1, stride): for x in range(0, width - output_size + 1, stride): img_tile = img[:, y:y+output_size, x:x+output_size] lab_tile = lab[y:y+output_size, x:x+output_size] # 过滤掉标签中全是背景的样本,减少类别不平衡 unique_labels = np.unique(lab_tile) if len(unique_labels) == 1 and unique_labels[0] == 0: continue tiles.append(img_tile) labels.append(lab_tile) return np.stack(tiles), np.stack(labels)这段代码有几个参数需要特别注意。output_size一般取 512 或 256,取决于显存大小和地物尺度。如果你的地物是大片农田,512 的窗口能保留更多上下文信息;如果是道路、房屋这类细碎地物,256 窗口配合高重叠率更合适。stride控制相邻窗口的重叠程度,这里设为 output_size 的一半,也就是 50% 重叠,能增加训练样本量,同时缓解窗口边缘地物被切断的问题。过滤全背景样本是一个很实用的技巧,遥感影像里背景占比往往极高,不过滤的话模型会被背景淹没。
2.3 自定义 Dataset 中的增强策略
数据增强在遥感分割里比在自然图像里更讲究。常规的随机翻转和裁剪可以直接用,但有一个雷区:光谱变换。像颜色抖动、高斯噪声这类增强,对地物分类可能有负面影响——因为遥感影像的光谱值是物理测量结果,你把植被波段的数值随机抖动一下,原本区分植被和土壤的特征可能就被破坏了。
所以,遥感分割项目里我更推荐几何增强和尺度增强的组合:
import torch from torch.utils.data import Dataset import torchvision.transforms.functional as TF import random class RemoteSensingDataset(Dataset): def __init__(self, images, masks, augment=False): self.images = images self.masks = masks self.augment = augment def __len__(self): return len(self.images) def __getitem__(self, idx): image = torch.from_numpy(self.images[idx]).float() mask = torch.from_numpy(self.masks[idx]).long() if self.augment: # 随机水平翻转 if random.random() > 0.5: image = torch.flip(image, dims=[2]) mask = torch.flip(mask, dims=[1]) # 随机旋转90度 - 遥感影像的旋转增强不会破坏地物语义 k = random.randint(0, 3) if k > 0: image = torch.rot90(image, k, dims=[1, 2]) mask = torch.rot90(mask, k, dims=[0, 1]) # 归一化,参数需根据数据统计调整 mean = torch.tensor([0.485, 0.456, 0.406]) std = torch.tensor([0.229, 0.224, 0.225]) image = (image / 255.0 - mean[:, None, None]) / std[:, None, None] return image, mask注意这里 mask 的类型是torch.long,PyTorch 的交叉熵损失要求标签是长整型。归一化用的是 ImageNet 的均值和标准差,虽然遥感影像的光谱分布和自然图像有差异,但对于使用预训练权重的模型来说这是一个合理起点。如果你用的是多波段数据,这组归一化参数就不适用了,需要自己统计训练集的均值和标准差。
3. 模型选型:DeepLabV3+ 还是 UNet,各自的边界在哪里
3.1 分割模型的基本构成:编码器、解码器、空洞卷积
PyTorch 里实现语义分割有两种路径:自己搭模型,或者用 torchvision 里的现成实现。自己做毕业设计的话,我建议两条路都走一遍——先用现成模型跑通基线,再尝试改结构做对比实验。
先理解分割模型的组成。所有现代语义分割模型都可以分为编码器和解码器两段。编码器通常是图像分类网络去掉全连接层,比如 ResNet50、ResNet101,负责提取从底层边缘到高层语义的多尺度特征。解码器负责把低分辨率的特征图恢复回输入分辨率,逐像素预测类别。UNet 的特色是引入了跳跃连接,把编码器各层的特征直接拼到解码器对应层,这让它在医学图像和遥感影像这种边界精细的任务上特别管用。DeepLabV3+ 则依靠空洞卷积(Atrous Convolution)在不降低分辨率的情况下扩大感受野,并在解码端使用 ASPP(Atrous Spatial Pyramid Pooling)模块并联多个不同空洞率的卷积核,捕捉不同尺度地物。
3.2 torchvision 快速搭建 DeepLabV3
如果你只是想快速跑通一个像样的基线,不要自己写模型。以下代码使用 torchvision 自带的 DeepLabV3,主干网络是 ResNet50 的变体:
import torch.nn as nn import torchvision.models.segmentation as segmentation def build_deeplabv3(num_classes=6, pretrained=True): model = segmentation.deeplabv3_resnet50( weights=segmentation.DeepLabV3_ResNet50_Weights.DEFAULT if pretrained else None ) # 替换分类头,适配自己的类别数 in_channels = model.classifier[4].in_channels model.classifier[4] = nn.Conv2d( in_channels, num_classes, kernel_size=1 ) return model这里替换的是model.classifier[4],因为 torchvision 的 DeepLabV3 分类头是一个 Sequential 容器,最后一位是 1×1 卷积,负责把特征图投影到类别数维度。pretrained=True表示加载在 COCO 数据集上预训练过的权重,虽然 COCO 是自然图像,但编码器学到的基础特征对遥感数据同样有很强的迁移价值。
这个模型输入不需要你手动做 stride 处理,DeepLabV3 内部自带空洞卷积来保持输出分辨率,唯一要记住的是输入尺寸建议是 32 的倍数,否则某些中间层会有尺寸对齐问题。
3.3 手写 UNet:为什么它仍然值得作为对比实验
DeepLabV3 在很多 benchmark 上分数更高,但 UNet 在遥感分割里依然是出场率最高的结构之一。原因很朴素:遥感地物边界往往很锐利,而 UNet 的跳跃连接能把底层的高分辨率特征直接送到解码器,帮助恢复精细边界。在 PyTorch 里实现一个简版 UNet 其实不需要多少代码:
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, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True) ) def forward(self, x): return self.conv(x) class SimpleUNet(nn.Module): def __init__(self, in_channels=3, num_classes=6): super().__init__() self.enc1 = DoubleConv(in_channels, 64) self.enc2 = DoubleConv(64, 128) self.enc3 = DoubleConv(128, 256) self.pool = nn.MaxPool2d(2) self.bridge = DoubleConv(256, 512) self.up3 = nn.ConvTranspose2d(512, 256, 2, stride=2) self.dec3 = DoubleConv(512, 256) self.up2 = nn.ConvTranspose2d(256, 128, 2, stride=2) self.dec2 = DoubleConv(256, 128) self.up1 = nn.ConvTranspose2d(128, 64, 2, stride=2) self.dec1 = DoubleConv(128, 64) self.out_conv = nn.Conv2d(64, num_classes, 1) def forward(self, x): e1 = self.enc1(x) e2 = self.enc2(self.pool(e1)) e3 = self.enc3(self.pool(e2)) bridge = self.bridge(self.pool(e3)) d3 = self.dec3(torch.cat([self.up3(bridge), e3], dim=1)) d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=1)) d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=1)) return self.out_conv(d1)这个 UNet 的深度比较浅,编码器只有三层下采样,最终特征图分辨率是原图的 1/8。对于遥感分割来说这个设置已经够用,因为太深的网络配合大窗口输入,显存消耗会快速失控。torch.cat就是跳跃连接的核心操作,把编码器特征和解码器上采样后的特征在通道维度拼接起来,让解码器同时看到高层语义和底层纹理。注意拼接前e3的尺寸必须和self.up3(bridge)一致,这就是前面说输入尺寸最好是 2 的幂次倍的原因。
3.4 模型对比实验怎么设计
毕业设计的核心论证逻辑通常需要两组模型做对比。常见设计是:UNet 作为基准模型,DeepLabV3+ 作为改进方案,两者使用完全一样的数据集、损失函数和训练轮数,只比较网络结构带来的差异。这样写论文时可以从 "改进模型在 mIoU 上提升了 X 个百分点" 来切入,逻辑干净有力。
下面是一个模型参数量的统计方法,用于实验章节的表格展示:
def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) unet = SimpleUNet(in_channels=3, num_classes=6) deeplab = build_deeplabv3(num_classes=6, pretrained=False) print(f"UNet 参数量: {count_parameters(unet) / 1e6:.2f}M") print(f"DeepLabV3 参数量: {count_parameters(deeplab) / 1e6:.2f}M")参数量差距会直接影响推理速度的对比。你在论文里做效率分析时,不仅要报告 mIoU,还要同时报告单张 512×512 影像的平均推理时间,这样审稿老师或者答辩评委才能看出结构改进带来的计算代价是否值得。
4. 训练过程的关键参数:损失函数、学习率与训练策略
4.1 交叉熵和 Dice Loss 该怎么组合
语义分割最常用的损失函数是交叉熵,但遥感影像有个绕不开的问题——类别极度不平衡。比如整幅影像里水体可能只占 0.5%,房屋占 3%,其余全是植被和裸地。普通交叉熵会把梯度注意力几乎全放在大类别上,小类别地物直接学不动。解决办法是两个:修改类别权重,或者叠加 Dice Loss。
类别权重的计算方式通常是对训练集中每个类别的像素占比取倒数或对数倒数:
import torch.nn as nn def compute_class_weights(mask_tensor, num_classes): """ mask_tensor: [N, H, W] 的长整型标签 """ class_counts = torch.bincount(mask_tensor.flatten(), minlength=num_classes).float() total = class_counts.sum() # 用 log 平滑处理,避免少数类权重过大 weights = torch.log(total / (class_counts + 1e-6) + 1.0) return weights / weights.sum() * num_classes weights = compute_class_weights(train_masks, num_classes=6) criterion = nn.CrossEntropyLoss(weight=weights)这里最后一步把权重做了归一化,让权重均值为 1,避免整体损失数值过大。加1e-6是防止某些类别在 batch 里完全没出现导致除零。
4.2 优化器、学习率和 Batch Size 的具体推荐
遥感分割任务的训练配置比较成熟,以下是一组经过大量项目验证的参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 优化器 | SGD with momentum=0.9 | Adam 收敛快但容易停在 sharp local minimum |
| 初始学习率 | 0.01(SGD)/ 0.0001(AdamW) | 预训练模型用 0.001 更安全 |
| Batch Size | 8-16(512×512 输入) | 取决于显存,小于 8 时要用梯度累积 |
| 训练轮数 | 50-100 | 配合 Early Stopping |
| 学习率调整 | Poly 衰减或 CosineAnnealing | 比 StepLR 更适合分割任务 |
Poly 衰减是分割任务里最常用的学习率策略,计算公式是lr = base_lr * (1 - iter / total_iter) ** 0.9。它让模型在训练前期快速学习,后期缓慢精细调优。PyTorch 里可以这样实现:
from torch.optim.lr_scheduler import LambdaLR import math def poly_lr(epoch, max_epochs, base_lr=0.001, power=0.9): return base_lr * (1 - epoch / max_epochs) ** power还有一个特别容易被忽略的参数:num_workers和数据加载。遥感影像分块后单个样本读取慢,如果 CPU 加载跟不上 GPU 计算速度,训练效率会大幅下降。在 DataLoader 里至少要设置num_workers=4以上,并且启用pin_memory=True,否则你会发现 GPU 利用率只有百分之二三十。
4.3 训练过程中的监控指标:不要只盯 Loss
训练时很多人只盯着 loss 曲线下降就以为万事大吉,这在分割任务里非常危险。因为交叉熵 loss 下降可能只是模型把所有像素都预测成了背景,mIoU 依然是零。建议每个 epoch 结束直接在验证集上计算 mIoU,并打印每个类别的 IoU。
下面这段代码实现训练加验证的完整过程,适合直接放进项目主文件:
def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() total_loss = 0 for images, masks in dataloader: images = images.to(device) masks = masks.to(device) optimizer.zero_grad() outputs = model(images) # torchvision 的 DeepLabV3 输出是 dict,需要取 'out' if isinstance(outputs, dict): outputs = outputs['out'] loss = criterion(outputs, masks) loss.backward() optimizer.step() total_loss += loss.item() return total_loss / len(dataloader) def compute_miou(preds, masks, num_classes): """ preds: [N, H, W] 预测类别索引 masks: [N, H, W] 真实标签 """ ious = [] preds = preds.cpu().numpy() masks = masks.cpu().numpy() for cls in range(num_classes): pred_mask = (preds == cls) true_mask = (masks == cls) intersection = (pred_mask & true_mask).sum() union = (pred_mask | true_mask).sum() if union == 0: ious.append(float('nan')) # 该类别在验证集中未出现 else: ious.append(intersection / union) return np.nanmean(ious) # 忽略未出现的类别np.nanmean在这里非常关键。如果验证集某个小类别一张图里都没出现,union为 0,直接除零会产生 inf,导致整个 mIoU 失效。忽略这些类别求均值,才能真实反映模型的有效类别的表现。
4.4 GPU 内存不够时的三个应急预案
高分遥感分割最常见的硬件问题就是显存溢出。12GB 以下显存跑 512×512 输入加 ResNet50 编码器,batch size 稍微调大就直接 OOM。有三个方案可以按顺序尝试。第一,降低输入 patch 尺寸到 256×256,这是最直接的改动但对小地物可能有影响。第二,启用梯度累积,模拟更大的 batch size,不增加显存占用。第三,使用混合精度训练,PyTorch 原生的自动混合精度(AMP)能减少约 50% 显存占用,同时保证精度基本不损失。
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() optimizer.zero_grad() accumulation_steps = 4 # 模拟 4 倍 batch size for i, (images, masks) in enumerate(dataloader): images = images.to(device) masks = masks.to(device) with autocast(): outputs = model(images) if isinstance(outputs, dict): outputs = outputs['out'] loss = criterion(outputs, masks) / accumulation_steps scaler.scale(loss).backward() if (i + 1) % accumulation_steps == 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad()注意混合精度下 loss 除以accumulation_steps是为了让累积多步的梯度均值与正常 batch size 下的梯度量级一致。scaler.scale(loss)和scaler.step(optimizer)必须配对使用,不能用常规的optimizer.step()替代。
5. 推理后处理与毕业设计验证技巧
推理阶段有一个训练时完全没遇到的新问题:预测的标签图里有大量椒盐噪声和碎斑。这在高分遥感上尤其明显,因为高分辨率影像的细节太丰富,单个像素很容易被误分类。常见的后处理手段是用形态学开闭运算去除小碎块,这个在 OpenCV 里一行代码就能实现:
import cv2 import numpy as np def postprocess_mask(pred_mask, min_area=50): """ pred_mask: [H, W] 整数标签 """ cleaned = np.zeros_like(pred_mask) kernel = np.ones((3, 3), np.uint8) for cls in np.unique(pred_mask): class_mask = (pred_mask == cls).astype(np.uint8) # 先闭运算填补内部空洞,再开运算去掉碎斑 class_mask = cv2.morphologyEx(class_mask, cv2.MORPH_CLOSE, kernel, iterations=2) class_mask = cv2.morphologyEx(class_mask, cv2.MORPH_OPEN, kernel, iterations=2) # 过滤面积过小的连通域 num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(class_mask, connectivity=8) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] >= min_area: cleaned[labels == i] = cls return cleaned参数min_area要根据你的影像分辨率设定。0.5 米分辨率下,一个 50 像素的区域实际只有 12.5 平方米,通常不是有效地物;如果是 2 米分辨率,50 像素就对应 200 平方米了,这时候阈值要相应调大。这个后处理本质上是在精度和召回之间找平衡,论文里可以作为一个对比提出来。
大影像推理时的滑窗重叠策略也值得注意。如果你的训练 patch 是 512×512 且 stride 为 256,那推理时也应该用相同的参数。取重叠区域预测结果的平均概率而非只取中心预测,能明显减少拼接缝隙处的类别突变:
def sliding_predict(model, full_image, patch_size=512, stride=256, device='cuda'): model.eval() h, w = full_image.shape[1:] # [C, H, W] # 记录每个像素被预测次数,用于取平均 count_map = torch.zeros(1, h, w, device=device) prob_map = torch.zeros(1, patch_size, patch_size, device=device) # 完整实现需要累积概率并取 argmax # 这里展示的思路是:重叠区域的输出取平均值后 argmax完成预测得到整幅标签图后,最后一步是把结果转成 GeoTIFF 保留地理坐标信息。用gdal创建输出文件并写入投影信息,这样标注结果可以直接叠加到原始影像上做可视化检查,ArcGIS 或 QGIS 里都能直接打开。这一步在答辩演示时尤其加分——评审看到的不只是一张 numpy 数组可视化,而是带地理坐标的矢量级成果。
毕业设计文档里,实验部分除了放 mIoU、Kappa 系数这些指标表格,建议再附上每个类别的混淆矩阵。遥感地物分类里最容易混淆的是林地与草地、裸地与道路这类光谱相似的类别,混淆矩阵能直接告诉评审你模型的错误集中在哪里,也为后续改进方向提供依据——往往看一眼混淆矩阵,就能知道该往哪个类别加训练样本。
本文还有配套的精品资源,点击获取