公司动态

基于PyTorch与U-Net的高分辨率遥感影像语义分割实战指南

📅 2026/8/28 7:51:31
基于PyTorch与U-Net的高分辨率遥感影像语义分割实战指南
简介语义分割是计算机视觉的核心任务之一旨在为图像中的每个像素分配一个类别标签实现像素级的场景理解。其原理在于通过深度神经网络特别是编码器-解码器架构学习从图像到密集预测的映射。这项技术的价值在于能够自动化、精细化地解析视觉内容广泛应用于自动驾驶、医疗影像分析和遥感解译等领域。在遥感影像分析中高分辨率影像带来了丰富的地物细节但也伴随着类别不平衡和边界模糊等挑战。针对这些挑战本文聚焦于利用PyTorch框架结合U-Net网络架构与混合损失函数如Dice Loss和Lovasz Loss构建一个鲁棒的遥感影像智能解译系统详细阐述了从环境配置、数据预处理、模型实现到训练调优的全流程工程实践。1. 项目概述从遥感影像到像素级理解的跨越高分辨率遥感影像解译听起来是个高大上的学术名词但说白了就是让计算机学会“看懂”卫星或航拍照片。想象一下你拿到一张覆盖几十平方公里的城市影像传统方法需要人工拿着放大镜一点点勾画出哪里是建筑、哪里是道路、哪里是水体费时费力还容易出错。我们这个项目的核心目标就是用深度学习特别是语义分割技术让机器自动、快速、准确地完成这项工作把影像中的每一个像素都打上正确的标签。为什么这件事有价值在城市规划中快速统计建筑密度和绿地覆盖率在农业监测中精准识别作物类型和生长状况在灾害评估中及时勾勒出洪水淹没范围或山体滑坡区域——这些应用场景都离不开对遥感影像的精细化理解。而“高分辨率”意味着影像细节极其丰富地物边界复杂这对模型的识别精度提出了近乎苛刻的要求。基于PyTorch框架来实现这个项目是一个相当务实的选择。PyTorch的动态计算图和直观的编程风格让研究和实验迭代变得非常高效。当你需要尝试一个新的损失函数或者调整网络结构时PyTorch能让你像搭积木一样快速验证想法这对于探索如何更好地处理遥感影像这类复杂数据至关重要。项目的技术核心很明确采用经典的U-Net架构作为主干网络并重点研究如何通过组合SoftCrossEntropyLoss、Dice Loss和Lovasz Loss这三种损失函数来提升模型在遥感影像语义分割任务上的表现。这不仅仅是调个参那么简单它背后是对遥感影像分割中“类别不平衡”、“边界模糊”等老大难问题的系统性攻坚。接下来我们就一层层剥开这个项目的技术细节看看如何从零开始构建一个真正能用的高分辨率遥感影像智能解译系统。2. 核心思路与方案选型为什么是U-Net与混合损失函数面对高分辨率遥感影像语义分割任务第一个要回答的问题就是用什么网络模型这几年分割模型层出不穷从DeepLab系列到PSPNet各有千秋。但我们最终选择了U-Net这个2015年就提出的“老将”绝非偶然而是基于遥感影像特点的深思熟虑。2.1 U-Net架构的适配性分析U-Net最标志性的就是它的“U型”对称结构一个下采样编码器路径捕获上下文信息一个上采样解码器路径精确定位再加上中间的“跳跃连接”将浅层的高分辨率特征与深层的语义特征融合。这套设计几乎是为医学图像和遥感影像这类需要精细边界分割的任务量身定做的。高分辨率遥感影像中道路、河流、建筑物的边缘往往错综复杂。单纯的深层网络虽然语义理解能力强但经过多次下采样后空间细节损失严重预测出的边界会像毛边一样粗糙。U-Net的跳跃连接机制相当于在解码器“绘制”分割图时不断参考编码器早期阶段捕捉到的原始边缘、纹理细节从而实现了“既见森林又见树木”。在实际操作中我通常会对原始U-Net进行一些微调比如将编码器的主干网络从简单的卷积块替换为ResNet或EfficientNet等预训练模型这能显著提升特征提取能力相当于站在了巨人的肩膀上。2.2 损失函数组合的战略意义选好了模型骨架接下来就是驱动模型学习的“指挥棒”——损失函数。在语义分割中单纯使用标准的交叉熵损失CrossEntropyLoss经常会遇到瓶颈尤其是在遥感场景中。SoftCrossEntropyLoss这是我们对标准交叉熵的一个改进。标准交叉熵要求标签是硬性的“0”或“1”one-hot形式但遥感影像中很多像素点可能处于混合类别边界比如植被和土壤的交界处硬标签会引入噪声。SoftCrossEntropyLoss允许我们使用软标签概率分布让模型学习更平滑、更不确定的类别过渡这更符合现实世界的复杂性。在数据标注质量不高或存在歧义时这个损失函数能提供更强的鲁棒性。Dice Loss它的设计初衷直接面向我们评估模型好坏的核心指标——Dice系数或称F1分数。Dice Loss关注的是预测区域和真实区域之间的重叠度。它有一个关键特性对前景像素我们关心的目标类别如建筑物的数量不敏感。这意味着即使建筑物在整张图片中只占很小一部分极端类别不平衡Dice Loss也能迫使模型努力去“找全”它而不是一味地偏向背景类别。这对于遥感影像中常见的小目标如车辆、单个树木检测至关重要。Lovasz Loss这是一个基于子模优化的“黑科技”它直接优化的是分割任务在评估时另一个核心指标——IoU交并比的松弛形式。与Dice Loss类似它也对类别不平衡不敏感但理论上能提供更光滑的优化梯度。我的实践经验是在处理边界极其不规则、形状多变的地物如云朵、不规则水体时Lovasz Loss有时能带来比Dice Loss更精准的边缘贴合度。那么为什么要把它们组合起来道理很简单没有一种损失函数是万能的。SoftCrossEntropyLoss从像素分类的统计特性上驱动学习Dice Loss从区域重叠的宏观层面进行优化Lovasz Loss则从评估指标本身出发。将它们以加权和的形式结合例如总损失 α * SoftCE β * Dice γ * Lovasz相当于让模型同时接受多重目标的训练往往能获得比单一损失函数更稳健、更强大的性能。权重的设置需要根据你的数据集特点进行微调通常可以从[1.0, 0.5, 0.5]这样的比例开始尝试。注意损失函数组合虽好但也会增加训练的不稳定性和调参复杂度。建议初期先使用Dice Loss或Focal Loss另一种处理不平衡的损失函数之一与交叉熵结合待模型稳定后再引入Lovasz Loss进行精细调优。3. 实战环境搭建与数据准备理论谈得再多不如动手搭环境。一个稳定、高效的开发环境是项目成功的基石尤其是深度学习项目环境配置上的一个疏忽可能让你浪费数天时间。3.1 PyTorch与CUDA环境配置要点我的主力机是Ubuntu系统搭配NVIDIA显卡。以下是我的标准配置流程其中坑点已经为你标出确认CUDA版本首先在终端输入nvidia-smi查看显卡驱动支持的最高CUDA版本。记住这里是“最高支持”你安装的CUDA版本不能高于这个值。安装对应版本的CUDA Toolkit前往NVIDIA官网下载并安装与你驱动匹配的CUDA Toolkit。例如nvidia-smi显示CUDA Version: 12.4那么你可以安装CUDA 12.4或更低的12.x版本。通过Conda创建虚拟环境这是至关重要的一步它能完美隔离不同项目所需的包版本。conda create -n rs_seg python3.9 # 建议使用Python 3.8或3.9兼容性最好 conda activate rs_seg安装PyTorch前往 PyTorch官网 使用官网提供的安装命令选择器。根据你的CUDA版本、操作系统和包管理工具我们选Conda生成命令。例如对于CUDA 12.1conda install pytorch torchvision torchaudio pytorch-cuda12.1 -c pytorch -c nvidia安装完成后务必在Python中验证import torch print(torch.__version__) # 查看PyTorch版本 print(torch.cuda.is_available()) # 必须返回True print(torch.cuda.get_device_name(0)) # 显示你的显卡型号实操心得如果你在Windows系统上强烈建议使用WSL2Windows Subsystem for Linux来配置环境能避开无数原生Windows下的路径和编译坑。如果只能用纯Windows请严格使用Anaconda Prompt作为终端并优先选择由PyTorch官网提供的、带CUDA的conda安装命令避免使用pip安装可能遇到的VC编译问题。3.2 遥感数据预处理流水线拿到遥感数据通常是GeoTIFF格式的多波段图像不能直接扔给模型。我们需要构建一个标准化的预处理流水线。读取与波段选择使用rasterio或GDAL库读取影像。遥感影像通常包含多个波段如RGB真彩色、近红外等。你需要根据任务决定使用哪些波段。对于地物分类RGB近红外共4波段是常见组合。代码示例import rasterio with rasterio.open(image.tif) as src: # 假设我们读取前4个波段 img src.read([1,2,3,4]) # 形状为 (C, H, W) img np.transpose(img, (1,2,0)) # 转换为 (H, W, C) 方便后续处理归一化Normalization每个波段的数值范围差异巨大例如光学波段0-255红外波段可能上千。必须进行归一化通常采用“最小-最大值归一化”或“标准差归一化”。# 最小-最大值归一化到[0,1] def min_max_normalize(band): return (band - band.min()) / (band.max() - band.min() 1e-8) # 对每个波段分别处理 for i in range(img.shape[-1]): img[..., i] min_max_normalize(img[..., i])图像切片Tiling高分辨率影像动不动就是上万像素的尺寸远超GPU显存。必须将其切割成固定大小如256x256或512x512的小块进行训练。切割时要有重叠预测时也需要用滑动窗口并融合边缘以避免接缝。数据增强Data Augmentation遥感影像的数据增强至关重要因为实地采集数据昂贵且有限。除了常规的旋转、翻转、缩放对于遥感数据色彩抖动模拟不同光照、随机裁剪和弹性形变尤其有效。我习惯使用albumentations库它针对图像分割任务的数据增强非常强大。import albumentations as A transform A.Compose([ A.RandomRotate90(p0.5), A.Flip(p0.5), A.RandomBrightnessContrast(p0.2), A.ElasticTransform(alpha1, sigma50, alpha_affine50, p0.1), ], additional_targets{mask: mask}) augmented transform(imageimg, maskmask) img_aug, mask_aug augmented[image], augmented[mask]构建DataLoader最后将处理好的图像块和对应的标注掩码Mask配对用PyTorch的Dataset和DataLoader封装准备好批量输入给模型。4. 模型实现构建增强版U-Net有了数据和环境我们来搭建模型的核心。这里我们不满足于原始U-Net而是实现一个加强版。4.1 编码器-解码器结构实现我们将编码器部分替换为预训练的ResNet34利用其在ImageNet上学到的强大特征。import torch import torch.nn as nn from torchvision import models import torch.nn.functional as F class UNetResNet(nn.Module): def __init__(self, n_channels4, n_classes6, pretrainedTrue): super(UNetResNet, self).__init__() # 使用预训练的ResNet34作为编码器 self.encoder models.resnet34(pretrainedpretrained) # 修改第一层卷积适配我们的输入波段数如4波段 self.encoder.conv1 nn.Conv2d(n_channels, 64, kernel_size7, stride2, padding3, biasFalse) # 获取ResNet的中间层输出作为跳跃连接的特征 self.enc1 nn.Sequential(self.encoder.conv1, self.encoder.bn1, self.encoder.relu) # 初始卷积块 self.enc2 nn.Sequential(self.encoder.maxpool, self.encoder.layer1) # 下采样后layer1 self.enc3 self.encoder.layer2 self.enc4 self.encoder.layer3 self.enc5 self.encoder.layer4 # 解码器部分 self.up1 UpBlock(512, 256) # 对应enc4 self.up2 UpBlock(256, 128) # 对应enc3 self.up3 UpBlock(128, 64) # 对应enc2 self.up4 UpBlock(64, 64) # 对应enc1 # 最终输出层将通道数映射为类别数 self.outc nn.Conv2d(64, n_classes, kernel_size1) def forward(self, x): # 编码器前向传播保存各阶段特征 e1 self.enc1(x) # [B, 64, H/2, W/2] e2 self.enc2(e1) # [B, 64, H/4, W/4] e3 self.enc3(e2) # [B, 128, H/8, W/8] e4 self.enc4(e3) # [B, 256, H/16, W/16] e5 self.enc5(e4) # [B, 512, H/32, W/32] # 解码器前向传播融合跳跃连接特征 d4 self.up1(e5, e4) # 融合e4 d3 self.up2(d4, e3) # 融合e3 d2 self.up3(d3, e2) # 融合e2 d1 self.up4(d2, e1) # 融合e1 # 最终输出用双线性插值上采样回原始输入尺寸或所需尺寸 logits self.outc(d1) return F.interpolate(logits, sizex.shape[2:], modebilinear, align_cornersTrue) # 定义一个上采样块包含上采样、特征拼接和卷积 class UpBlock(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.up nn.ConvTranspose2d(in_channels, out_channels, kernel_size2, stride2) self.conv DoubleConv(in_channels, out_channels) # 假设DoubleConv是一个两层卷积模块 def forward(self, x1, x2): # x1: 来自上一解码层的特征空间尺寸小 # x2: 来自跳跃连接的特征空间尺寸大 x1 self.up(x1) # 处理尺寸可能不匹配的情况由于步长和padding导致 diffY x2.size()[2] - x1.size()[2] diffX x2.size()[3] - x1.size()[3] x1 F.pad(x1, [diffX // 2, diffX - diffX//2, diffY // 2, diffY - diffY//2]) # 沿通道维度拼接 x torch.cat([x2, x1], dim1) return self.conv(x)4.2 深度可分离卷积的引入为了在保持精度的同时降低模型计算量我们可以在解码器的DoubleConv中用深度可分离卷积Depthwise Separable Convolution替换部分标准卷积。这能显著减少参数量对于未来部署到边缘设备如无人机机载计算机非常有意义。class DepthwiseSeparableConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size3, padding1): super().__init__() self.depthwise nn.Conv2d(in_channels, in_channels, kernel_sizekernel_size, paddingpadding, groupsin_channels) self.pointwise nn.Conv2d(in_channels, out_channels, kernel_size1) def forward(self, x): x self.depthwise(x) x self.pointwise(x) return x # 在DoubleConv中可以用它替换第二个标准卷积 class DoubleConv(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_channels, out_channels, 3, padding1), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue), DepthwiseSeparableConv(out_channels, out_channels), # 替换这里的标准卷积 nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x)5. 损失函数模块的详细实现与组合策略模型输出的是每个像素属于各个类别的原始分数logits损失函数的作用就是计算这个预测与真实标签之间的差距并指导模型更新。5.1 SoftCrossEntropyLoss实现标准的交叉熵损失要求标签是整数索引或one-hot形式。SoftCrossEntropyLoss则允许标签是概率分布这被称为标签平滑Label Smoothing的一种形式对模型有正则化效果能防止过拟合。class SoftCrossEntropyLoss(nn.Module): def __init__(self, reductionmean): super().__init__() self.reduction reduction def forward(self, input, target): input: 模型输出的logits形状 [N, C, H, W] target: 可以是one-hot编码的软标签形状 [N, C, H, W]也可以是硬标签类别索引形状 [N, H, W] log_probs F.log_softmax(input, dim1) # 计算对数概率 if target.dim() input.dim(): # target是软标签 [N, C, H, W] loss -torch.sum(target * log_probs, dim1) else: # target是硬标签 [N, H, W]转换为one-hot loss F.nll_loss(log_probs, target, reductionnone) if self.reduction mean: return loss.mean() elif self.reduction sum: return loss.sum() else: return loss # none5.2 Dice Loss 与 Lovasz Loss 实现Dice Loss 直接优化Dice系数。需要注意的是为了避免分母为零通常会加上一个平滑项smooth。class DiceLoss(nn.Module): def __init__(self, smooth1e-6): super().__init__() self.smooth smooth def forward(self, input, target): # input: 经过softmax的概率图 [N, C, H, W] # target: one-hot编码的标签 [N, C, H, W] input_flat input.contiguous().view(-1) target_flat target.contiguous().view(-1) intersection (input_flat * target_flat).sum() union input_flat.sum() target_flat.sum() dice (2. * intersection self.smooth) / (union self.smooth) return 1 - diceLovasz Loss的实现相对复杂因为它基于子模优化的理论。我们可以直接使用现有的开源实现如pytorch-lovasz-softmax包这里给出其调用方式的概念# 假设已安装 lovasz_losses 包: pip install lovasz-losses-pytorch from lovasz_losses import lovasz_softmax class LovaszLoss(nn.Module): def __init__(self): super().__init__() def forward(self, input, target): # input: 模型输出的logits [N, C, H, W] # target: 类别索引标签 [N, H, W] # lovasz_softmax 期望probs在[0,1]区间和类别索引 probs F.softmax(input, dim1) loss lovasz_softmax(probs, target, ignoreNone) return loss5.3 组合损失函数与动态权重调整将三者组合起来并尝试一种简单的动态权重策略在训练初期让SoftCrossEntropyLoss占主导帮助模型快速建立基本的分类能力中后期逐渐提高Dice Loss和Lovasz Loss的权重以优化分割的几何特性。class CombinedLoss(nn.Module): def __init__(self, ce_weight1.0, dice_weight0.5, lovasz_weight0.5, dynamicFalse): super().__init__() self.ce SoftCrossEntropyLoss() self.dice DiceLoss() self.lovasz LovaszLoss() self.w_ce ce_weight self.w_dice dice_weight self.w_lovasz lovasz_weight self.dynamic dynamic self.epoch 0 def forward(self, input, target): # 假设target是类别索引需要为Dice Loss转换为one-hot num_classes input.shape[1] target_onehot F.one_hot(target, num_classes).permute(0,3,1,2).float() loss_ce self.ce(input, target) loss_dice self.dice(F.softmax(input, dim1), target_onehot) loss_lovasz self.lovasz(input, target) # 简单的动态权重随着训练进行降低CE权重提高Dice/Lovasz权重 if self.dynamic: # 例如训练100个epochCE权重从1.0线性降到0.5Dice/Lovasz从0.5升到1.0 progress min(self.epoch / 100.0, 1.0) w_ce self.w_ce * (1 - 0.5*progress) w_dice self.w_dice * (1 1.0*progress) w_lovasz self.w_lovasz * (1 1.0*progress) else: w_ce, w_dice, w_lovasz self.w_ce, self.w_dice, self.w_lovasz total_loss w_ce * loss_ce w_dice * loss_dice w_lovasz * loss_lovasz return total_loss def step(self): # 每个epoch后调用 self.epoch 16. 模型训练、验证与调优全流程搭建好模型和损失函数后就进入了最关键的训练循环阶段。这个过程充满了各种“坑”需要细致的监控和调整。6.1 训练循环的关键配置import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR from torch.utils.tensorboard import SummaryWriter def train_model(model, train_loader, val_loader, device, num_epochs100): model.to(device) # 优化器AdamW是目前更受推崇的选择相比Adam有更好的权重衰减处理 optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) # 学习率调度器余弦退火让学习率从初始值平滑下降到0 scheduler CosineAnnealingLR(optimizer, T_maxnum_epochs) # 组合损失函数 criterion CombinedLoss(ce_weight1.0, dice_weight0.5, lovasz_weight0.5, dynamicTrue) writer SummaryWriter(runs/experiment_1) # 用于TensorBoard可视化 best_val_iou 0.0 for epoch in range(num_epochs): model.train() train_loss 0.0 for batch_idx, (images, masks) in enumerate(train_loader): images, masks images.to(device), masks.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, masks) loss.backward() # 梯度裁剪防止梯度爆炸在RNN中常见但在深层CNN中也有用 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() # 验证阶段 model.eval() val_metrics evaluate(model, val_loader, device, num_classes) val_iou val_metrics[mean_iou] # 记录到TensorBoard writer.add_scalar(Loss/train, train_loss/len(train_loader), epoch) writer.add_scalar(IoU/val, val_iou, epoch) writer.add_scalar(LR, optimizer.param_groups[0][lr], epoch) # 学习率调度 scheduler.step() # 更新组合损失函数的epoch如果使用动态权重 criterion.step() # 保存最佳模型 if val_iou best_val_iou: best_val_iou val_iou torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), val_iou: val_iou, }, best_model.pth) print(fEpoch {epoch1}/{num_epochs}, Train Loss: {train_loss/len(train_loader):.4f}, Val IoU: {val_iou:.4f}) writer.close()6.2 多指标验证与模型评估训练时不能只看损失分割任务需要多维度评估。我通常会计算以下几个核心指标交并比IoU每个类别的预测区域与真实区域交集与并集的比值是核心指标。平均IoUmIoU所有类别IoU的平均值反映整体性能。像素准确率Pixel Accuracy所有像素中分类正确的比例但在类别不平衡时参考价值有限。F1分数Dice系数与Dice Loss对应关注预测区域与真实区域的重叠。验证函数evaluate需要计算这些指标def evaluate(model, dataloader, device, num_classes): model.eval() conf_matrix torch.zeros(num_classes, num_classes).to(device) # 混淆矩阵 with torch.no_grad(): for images, masks in dataloader: images, masks images.to(device), masks.to(device) outputs model(images) preds torch.argmax(outputs, dim1) # 获取预测类别 # 更新混淆矩阵 for t, p in zip(masks.view(-1), preds.view(-1)): conf_matrix[t.long(), p.long()] 1 # 从混淆矩阵计算各项指标 iou_per_class [] for i in range(num_classes): tp conf_matrix[i,i] fp conf_matrix[:, i].sum() - tp fn conf_matrix[i, :].sum() - tp iou tp / (tp fp fn 1e-8) iou_per_class.append(iou.item()) mean_iou sum(iou_per_class) / num_classes pixel_acc conf_matrix.diag().sum() / conf_matrix.sum() return { confusion_matrix: conf_matrix.cpu().numpy(), iou_per_class: iou_per_class, mean_iou: mean_iou, pixel_accuracy: pixel_acc.item() }6.3 超参数调优与实验管理深度学习项目一大部分时间都在调参。我的策略是先固定一个基线使用AdamW优化器初始学习率1e-4余弦退火batch size根据显存尽可能大如16或32。系统性地网格搜索或随机搜索对学习率1e-3, 1e-4, 1e-5、权重衰减1e-2, 1e-4, 0、损失函数权重组合进行搜索。使用Ray Tune或Weights Biases这类工具可以自动化这个过程并可视化结果。重点关注学习率学习率是最重要的超参数。如果训练损失震荡不降说明学习率太大如果下降极其缓慢说明学习率太小。使用学习率查找器如torch-lr-finder可以快速找到一个合适的范围。早停Early Stopping监控验证集mIoU如果连续10或20个epoch没有提升就停止训练避免过拟合。7. 预测推理与后处理优化模型训练好后我们要用它来预测新的、整张的大尺寸遥感影像。这不仅仅是简单的model.eval()和forward。7.1 滑动窗口预测与边缘融合由于训练时用的是小图像块如256x256预测大图时需要采用滑动窗口。直接滑动并拼接会导致明显的“棋盘格”效应或边界不连续。def predict_large_image(model, large_img, window_size256, stride128, devicecuda): 对大尺寸影像进行滑动窗口预测 large_img: 输入大图形状 [C, H, W] window_size: 窗口大小 stride: 滑动步长小于window_size以产生重叠 model.eval() C, H, W large_img.shape # 计算需要填充的像素使得图像尺寸能被stride整除可选简化计算 pad_h (stride - H % stride) % stride pad_w (stride - W % stride) % stride # 使用反射填充避免引入黑色边界 large_img_padded F.pad(large_img.unsqueeze(0), (0, pad_w, 0, pad_h), modereflect).squeeze(0) _, H_pad, W_pad large_img_padded.shape num_classes model.outc.out_channels # 初始化一个全零的概率累加图和权重图 output_prob torch.zeros((num_classes, H_pad, W_pad), devicedevice) count_mat torch.zeros((1, H_pad, W_pad), devicedevice) with torch.no_grad(): for i in range(0, H_pad - window_size 1, stride): for j in range(0, W_pad - window_size 1, stride): patch large_img_padded[:, i:iwindow_size, j:jwindow_size].unsqueeze(0).to(device) patch_pred model(patch) # [1, C, H, W] patch_prob F.softmax(patch_pred, dim1).squeeze(0) # [C, H, W] # 将预测概率累加到对应位置 output_prob[:, i:iwindow_size, j:jwindow_size] patch_prob # 记录每个像素被预测了多少次用于平均 count_mat[:, i:iwindow_size, j:jwindow_size] 1 # 计算平均概率 output_prob / count_mat # 移除填充部分 final_output output_prob[:, :H, :W] final_pred torch.argmax(final_output, dim0).cpu().numpy() # 得到最终的类别索引图 return final_pred7.2 后处理从概率图到优化结果模型输出的分割图往往存在一些小的空洞或孤立的噪声点。我们可以使用一些简单的后处理技术来优化视觉效果连通成分分析使用scikit-image或OpenCV的connectedComponentsWithStats函数找出面积小于某个阈值的孤立区域将其归并到周围的主要类别中。条件随机场CRF虽然现在很多模型声称端到端效果很好但对于边界要求极高的任务如地图绘制使用CRF进行后处理仍然能带来肉眼可见的提升。pydensecrf库可以实现这一点它利用原始影像的颜色和纹理信息对模型输出的概率图进行空间上的平滑和边界细化。形态学操作开运算先腐蚀后膨胀可以去除小噪声点闭运算先膨胀后腐蚀可以填充小的空洞。这适用于对地块完整性要求高的场景。import cv2 import numpy as np def postprocess(pred_mask, original_imageNone, min_area50): pred_mask: 模型预测的类别索引图 [H, W] original_image: 原始RGB图像用于CRF [H, W, 3] min_area: 最小连通区域面积小于此值的区域将被移除 # 1. 连通成分分析去噪 num_labels, labels, stats, centroids cv2.connectedComponentsWithStats((pred_mask0).astype(np.uint8), connectivity8) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] min_area: pred_mask[labels i] 0 # 假设0是背景类 # 2. 形态学操作可选 kernel np.ones((3,3), np.uint8) # 例如对建筑物类别假设类别索引为1进行闭运算填充小洞 building_mask (pred_mask 1).astype(np.uint8) building_mask cv2.morphologyEx(building_mask, cv2.MORPH_CLOSE, kernel) pred_mask[pred_mask1] 0 # 先清除原建筑物区域 pred_mask[building_mask1] 1 # 用处理后的掩码替换 # 3. CRF优化计算量大可选 if original_image is not None: # 此处需要模型输出的概率图prob_map [C, H, W] # 调用pydensecrf库进行优化 # refined_mask apply_crf(original_image, prob_map) # pred_mask np.argmax(refined_mask, axis0) pass return pred_mask8. 项目部署与性能考量让模型从实验室的Jupyter Notebook里走出来变成一个可以处理新数据的稳定服务或工具是项目的最终归宿。8.1 模型导出与优化首先我们需要将训练好的PyTorch模型导出为更通用的格式并可能进行优化以加速推理。导出为TorchScript这是PyTorch自带的序列化格式可以脱离Python环境运行便于C部署。model UNetResNet(n_channels4, n_classes6) checkpoint torch.load(best_model.pth, map_locationcpu) model.load_state_dict(checkpoint[model_state_dict]) model.eval() example_input torch.rand(1, 4, 256, 256) traced_script_module torch.jit.trace(model, example_input) traced_script_module.save(unet_resnet_traced.pt)使用ONNX RuntimeONNX是一个开放的模型格式标准。将模型转为ONNX后可以使用ONNX Runtime进行推理它通常比原生PyTorch有更快的推理速度并且支持多种硬件后端CPU, GPU, NPU等。torch.onnx.export(model, example_input, unet_resnet.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}})模型量化如果部署在资源受限的设备上如嵌入式设备、手机可以考虑模型量化将浮点权重转换为8位整数能大幅减少模型体积和提升推理速度但可能会带来轻微的精度损失。PyTorch提供了动态量化和静态量化工具。8.2 构建推理服务或应用程序根据应用场景可以选择不同的部署方式Web API服务如使用FastAPI适用于中心化服务器处理。用户上传影像服务器调用模型预测返回分割结果图。需要处理好并发请求和GPU资源管理。from fastapi import FastAPI, File, UploadFile import numpy as np from PIL import Image import io app FastAPI() # 加载模型... app.post(/predict/) async def predict(file: UploadFile File(...)): contents await file.read() image Image.open(io.BytesIO(contents)) # 预处理image... with torch.no_grad(): prediction model(image_tensor) # 后处理prediction... # 将结果掩码转换为图片字节流返回 return StreamingResponse(result_bytes, media_typeimage/png)桌面端应用如使用PyQt、Tkinter适合内业人员使用的单机工具。将模型、预处理、后处理逻辑打包提供图形界面让用户选择文件、设置参数、查看结果。边缘设备部署如NVIDIA Jetson、华为Atlas对于需要实时处理或离线作业的场景如无人机实时分析。这需要针对特定硬件平台如ARM架构、特定AI加速芯片编译优化模型通常使用TensorRT针对NVIDIA或MindSpore Lite针对华为昇腾等工具。8.3 性能监控与持续迭代模型部署上线不是终点。需要建立监控机制持续收集模型在新数据上的表现。可以记录预测的置信度对于置信度过低的样本可以将其保存下来作为“困难样本”加入到后续的训练数据集中进行主动学习从而让模型在不断迭代中越来越强。同时也要关注推理速度、内存占用等性能指标确保服务稳定高效。整个项目从数据准备到模型部署是一个完整的闭环。每一个环节都有大量的细节和技巧需要掌握也正是在解决这些具体问题的过程中我们对深度学习、对遥感影像解译的理解才会不断加深。本文还有配套的精品资源点击获取