ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

PyTorch高分遥感语义分割实战:从数据到推理全流程

PyTorch高分遥感语义分割实战:从数据到推理全流程 简介基于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 Dataset2.1 高分影像和普通图片的本质差异大多数人第一次接触遥感影像都会用cv2.imread直接读图然后就是各种报错或者颜色怪异。这里有个绕不开的底层差异高分遥感影像通常是多波段 GeoTIFF除了 RGB 三个可见光波段往往还带着近红外NIR波段部分数据源甚至有红边波段。PyTorch 的预训练模型比如 ImageNet 权重默认接受 3 通道输入所以第一步就需要考虑波段选取策略。常见做法有两种一种是直接取 RGB 三个波段好处是能加载 ImageNet 预训练权重迁移学习效果稳定另一种是使用 RGBNIR 四个波段信息量更足但此时就不能直接使用预训练模型的第一个卷积层需要做通道维度的权重初始化调整。对于毕业设计来说我一般建议先走 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_size512, stride256): 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:youtput_size, x:xoutput_size] lab_tile lab[y:youtput_size, x:xoutput_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, augmentFalse): 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.longPyTorch 的交叉熵损失要求标签是长整型。归一化用的是 ImageNet 的均值和标准差虽然遥感影像的光谱分布和自然图像有差异但对于使用预训练权重的模型来说这是一个合理起点。如果你用的是多波段数据这组归一化参数就不适用了需要自己统计训练集的均值和标准差。3. 模型选型DeepLabV3 还是 UNet各自的边界在哪里3.1 分割模型的基本构成编码器、解码器、空洞卷积PyTorch 里实现语义分割有两种路径自己搭模型或者用 torchvision 里的现成实现。自己做毕业设计的话我建议两条路都走一遍——先用现成模型跑通基线再尝试改结构做对比实验。先理解分割模型的组成。所有现代语义分割模型都可以分为编码器和解码器两段。编码器通常是图像分类网络去掉全连接层比如 ResNet50、ResNet101负责提取从底层边缘到高层语义的多尺度特征。解码器负责把低分辨率的特征图恢复回输入分辨率逐像素预测类别。UNet 的特色是引入了跳跃连接把编码器各层的特征直接拼到解码器对应层这让它在医学图像和遥感影像这种边界精细的任务上特别管用。DeepLabV3 则依靠空洞卷积Atrous Convolution在不降低分辨率的情况下扩大感受野并在解码端使用 ASPPAtrous 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_classes6, pretrainedTrue): model segmentation.deeplabv3_resnet50( weightssegmentation.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_size1 ) return model这里替换的是model.classifier[4]因为 torchvision 的 DeepLabV3 分类头是一个 Sequential 容器最后一位是 1×1 卷积负责把特征图投影到类别数维度。pretrainedTrue表示加载在 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, 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 SimpleUNet(nn.Module): def __init__(self, in_channels3, num_classes6): 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, stride2) self.dec3 DoubleConv(512, 256) self.up2 nn.ConvTranspose2d(256, 128, 2, stride2) self.dec2 DoubleConv(256, 128) self.up1 nn.ConvTranspose2d(128, 64, 2, stride2) 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], dim1)) d2 self.dec2(torch.cat([self.up2(d3), e2], dim1)) d1 self.dec1(torch.cat([self.up1(d2), e1], dim1)) 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_channels3, num_classes6) deeplab build_deeplabv3(num_classes6, pretrainedFalse) print(fUNet 参数量: {count_parameters(unet) / 1e6:.2f}M) print(fDeepLabV3 参数量: {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(), minlengthnum_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_classes6) criterion nn.CrossEntropyLoss(weightweights)这里最后一步把权重做了归一化让权重均值为 1避免整体损失数值过大。加1e-6是防止某些类别在 batch 里完全没出现导致除零。4.2 优化器、学习率和 Batch Size 的具体推荐遥感分割任务的训练配置比较成熟以下是一组经过大量项目验证的参数参数推荐值说明优化器SGD with momentum0.9Adam 收敛快但容易停在 sharp local minimum初始学习率0.01SGD/ 0.0001AdamW预训练模型用 0.001 更安全Batch Size8-16512×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_lr0.001, power0.9): return base_lr * (1 - epoch / max_epochs) ** power还有一个特别容易被忽略的参数num_workers和数据加载。遥感影像分块后单个样本读取慢如果 CPU 加载跟不上 GPU 计算速度训练效率会大幅下降。在 DataLoader 里至少要设置num_workers4以上并且启用pin_memoryTrue否则你会发现 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_area50): 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, iterations2) class_mask cv2.morphologyEx(class_mask, cv2.MORPH_OPEN, kernel, iterations2) # 过滤面积过小的连通域 num_labels, labels, stats, _ cv2.connectedComponentsWithStats(class_mask, connectivity8) 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_size512, stride256, devicecuda): model.eval() h, w full_image.shape[1:] # [C, H, W] # 记录每个像素被预测次数用于取平均 count_map torch.zeros(1, h, w, devicedevice) prob_map torch.zeros(1, patch_size, patch_size, devicedevice) # 完整实现需要累积概率并取 argmax # 这里展示的思路是重叠区域的输出取平均值后 argmax完成预测得到整幅标签图后最后一步是把结果转成 GeoTIFF 保留地理坐标信息。用gdal创建输出文件并写入投影信息这样标注结果可以直接叠加到原始影像上做可视化检查ArcGIS 或 QGIS 里都能直接打开。这一步在答辩演示时尤其加分——评审看到的不只是一张 numpy 数组可视化而是带地理坐标的矢量级成果。毕业设计文档里实验部分除了放 mIoU、Kappa 系数这些指标表格建议再附上每个类别的混淆矩阵。遥感地物分类里最容易混淆的是林地与草地、裸地与道路这类光谱相似的类别混淆矩阵能直接告诉评审你模型的错误集中在哪里也为后续改进方向提供依据——往往看一眼混淆矩阵就能知道该往哪个类别加训练样本。本文还有配套的精品资源点击获取
返回列表