ARTICLE DETAIL

资讯详情

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

高分遥感语义分割实战:PyTorch从数据到推理的工程链路

高分遥感语义分割实战:PyTorch从数据到推理的工程链路 简介这份资源面向遥感图像处理方向的研究者、工程师及具备一定深度学习基础的学习者提供基于Pytorch实现高分辨率遥感图像语义分割的完整教程与配套数据集帮助解决地物信息提取中从数据预处理到模型训练、评估的全流程问题。压缩包共1029个文件约577.48MB以819个png图像样本、35个py脚本、171个zbak备份文件为主另含csv标注索引、jpg示例图与md说明文档覆盖数据读取、网络搭建与结果可视化等环节。目前已有94人学习下载。教程从遥感图像基本概念讲起逐步深入到预处理方法、语义分割网络结构选择与优化策略并演示如何操作数据集、设计训练模型及评估分割结果配套数据可让读者亲身体验端到端训练测试流程理解道路、建筑、植被等地物标签的像素级标注与空间分布适合作为遥感语义分割入门与工程实践的参考素材。1. 高分遥感语义分割从数据到推理一条能跑通的工程链路遥感图像语义分割这件事真正上手做过的都知道难点从来不在模型本身。你拿一个 U-Net 或者 DeepLabV3 的 PyTorch 实现改改输入通道数跑通前向传播可能半小时都用不了。但当你真正拿到一幅 10000×10000 像素的高分遥感影像面对几十平方公里范围内地物类别极不均衡、标注数据稀缺、显存随时爆炸的局面才会意识到从 demo 到落地之间隔着一整套工程链路。这篇文章面向的是想用 PyTorch 把高分遥感图像语义分割真正跑起来的从业者——不管你是做国土资源监测、城市变化检测还是农业遥感应用只要你的任务是把遥感影像逐像素分类这里的内容就能直接复用。我会从数据集选择与预处理讲起经过模型搭建、训练调参、推理优化最后落到实际部署中那些只有踩过才知道的坑。整套流程基于 PyTorch 生态代码可以直接抄作业。2. 数据集与预处理遥感语义分割的第一道分水岭2.1 主流高分遥感语义分割数据集怎么选遥感语义分割和自然图像分割最大的区别在于数据获取成本。自然图像你可以用 COCO、VOC标注相对容易遥感图像你得先有高分影像再请懂遥感的人来标注成本高出一个量级。所以选数据集这件事直接决定了你后面训练能不能收敛、模型能不能泛化。目前工程上用得比较多的几个公开数据集我按适用场景列一下数据集分辨率类别数适用场景获取方式ISPRS Potsdam5cm6类城市地物分类官网申请ISPRS Vaihingen9cm6类城市语义分割官网申请LoveDA0.3m7类城乡遥感分割开源下载DeepGlobe0.5m7类土地覆盖分类挑战赛数据iSAID0.5m15类实例语义分割开源下载如果你是做城市地物分类Potsdam 和 Vaihingen 是经典选择6 个类别不透水面、建筑、低矮植被、树木、车辆、背景覆盖了大部分城市遥感需求。但这两个数据集需要向 ISPRS 提交申请流程大概一到两周。如果不想等LoveDA 是个不错的替代7 个类别覆盖城乡两种场景直接开源下载。做土地覆盖分类的话DeepGlobe 的 7 类城市、农业、牧场、森林、水体、 barren、未知比较实用。iSAID 类别更多15 类适合做细粒度分割但标注质量参差不齐用之前得做清洗。提示选数据集时优先看类别定义和你的业务场景是否对齐。比如你做违法建筑监测那建筑这个类别的标注边界是否包含在建工地、临时工棚直接决定模型上线后的误报率。2.2 高分遥感影像的切图策略与数据增强高分遥感影像动辄上万像素直接塞进网络不现实。切图是必须的但怎么切有讲究。最粗暴的方式是按固定步长滑窗切比如 512×512 的窗口步长 256。这样做的好处是实现简单但会产生大量重叠区域训练时重复采样推理时又需要做拼接融合。我一般会根据地物尺度来定窗口大小如果主要目标是建筑物512 够用如果是道路这种长条形地物窗口得开到 1024否则一段路被切成好几截模型学不到连续性。切图代码大概长这样import numpy as np from PIL import Image def sliding_window_crop(image, label, crop_size512, stride256): 滑窗切图返回图像块和对应标签块列表 image: numpy array, shape (H, W, C) label: numpy array, shape (H, W) crop_size: 切图尺寸 stride: 滑动步长 h, w image.shape[:2] patches_img [] patches_lbl [] for y in range(0, h - crop_size 1, stride): for x in range(0, w - crop_size 1, stride): img_patch image[y:ycrop_size, x:xcrop_size, :] lbl_patch label[y:ycrop_size, x:xcrop_size] patches_img.append(img_patch) patches_lbl.append(lbl_patch) # 处理边缘不足一个窗口的情况用反射填充补齐 if h % stride ! 0 or w % stride ! 0: img_pad np.pad(image, ((0, crop_size), (0, crop_size), (0, 0)), modereflect) lbl_pad np.pad(label, ((0, crop_size), (0, crop_size)), modereflect) y h - crop_size x w - crop_size patches_img.append(img_pad[y:ycrop_size, x:xcrop_size, :]) patches_lbl.append(lbl_pad[y:ycrop_size, x:xcrop_size]) return patches_img, patches_lbl这段代码的逻辑很直白双层循环按步长滑动每次取一个 crop_size 的窗口。边缘处理用 reflect 填充而不是补零是因为遥感影像边缘补零会在训练时引入虚假的黑色地物信号模型会学到错误的边界特征。stride 设成 crop_size 的一半是经验值保证相邻窗口有 50% 重叠既不浪费太多存储又能让模型看到足够的上下文。数据增强方面遥感图像和自然图像有个关键区别遥感图像有明确的物理方向性。你旋转 90 度太阳光照方向就变了阴影方向也跟着变。所以水平翻转、垂直翻转、90 度旋转这些操作是安全的但任意角度旋转要慎重除非你的任务对方向不敏感。我常用的增强组合是随机水平翻转 随机垂直翻转 随机 90 度旋转 颜色抖动亮度、对比度、饱和度各 0.2。颜色抖动幅度不能太大遥感图像的辐射特性是有物理意义的抖太狠会破坏地物光谱特征。import albumentations as A train_transform A.Compose([ A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomRotate90(p0.5), A.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05, p0.3), A.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里用了 albumentations 库比 torchvision 的 transforms 灵活支持图像和标签同步变换。Normalize 的均值和方差用的是 ImageNet 的统计值虽然遥感图像分布和 ImageNet 有差异但作为预训练模型的输入标准化这个值是被验证过可用的。如果你从零训练可以自己统计数据集的均值和方差替换掉。2.3 标签处理从 RGB 到类别索引的转换遥感数据集的标签格式五花八门。ISPRS 的标签是 RGB 彩色图每个类别对应一种颜色LoveDA 的标签是单通道灰度图像素值就是类别索引。你得统一转成 PyTorch 能用的格式单通道像素值 0 到 num_classes-1。RGB 转索引的代码import numpy as np def rgb_to_index(label_rgb, color_map): label_rgb: (H, W, 3) RGB 标签图 color_map: dict, {class_index: (R, G, B)} 返回: (H, W) 类别索引图 h, w label_rgb.shape[:2] index_map np.zeros((h, w), dtypenp.uint8) for class_idx, color in color_map.items(): mask np.all(label_rgb np.array(color), axis-1) index_map[mask] class_idx return index_mapcolor_map 需要你根据数据集的说明文档手动定义。比如 Potsdam 的 6 类不透水面是 (255, 255, 255)建筑是 (0, 0, 255)低矮植被是 (0, 255, 255)树木是 (0, 255, 0)车辆是 (255, 255, 0)背景是 (255, 0, 0)。转换完记得检查一下有没有未映射的像素如果有说明 color_map 漏了类别或者标签图有噪声。注意转换后的索引图一定要可视化检查一遍。我遇到过标签图里混入了抗锯齿产生的过渡色导致大量像素被误判为背景。这种问题不检查根本发现不了但会直接拉低模型精度。3. 模型搭建与训练PyTorch 实现高分遥感语义分割3.1 骨干网络选型ResNet、EfficientNet 还是 Swin Transformer遥感语义分割的骨干网络选择核心考量三个因素感受野、计算量、预训练权重可用性。ResNet 系列是最稳妥的选择。ResNet-50 作为 DeepLabV3 的骨干在遥感数据集上表现稳定预训练权重到处都有改起来也方便。缺点是感受野有限对大尺度地物比如大片农田、水体的全局建模能力偏弱。EfficientNet 计算效率更高同样的精度下参数量少很多。如果你的部署环境算力受限EfficientNet-B3 或 B4 是不错的选择。但 EfficientNet 的预训练权重在遥感图像上的迁移效果不如 ResNet 稳定我试过几次收敛速度明显慢一些。Swin Transformer 是这两年的热门全局建模能力强在遥感分割任务上精度确实有优势。但显存占用大训练慢而且对数据量要求高。如果你的数据集只有几千张切图用 Swin 很容易过拟合。我一般建议数据量少于 1 万张切图用 ResNet 或 EfficientNet超过 1 万张再考虑 Swin。实际工程中我大部分时候用 ResNet-50 或 ResNet-101 配 DeepLabV3。这个组合在精度和效率之间平衡得最好社区支持也最完善。import torch import torch.nn as nn from torchvision.models import resnet50 from torchvision.models.segmentation import deeplabv3_resnet50 def build_model(num_classes7, pretrainedTrue): 构建 DeepLabV3 模型替换分类头适配遥感类别数 num_classes: 地物类别数 pretrained: 是否加载 ImageNet 预训练权重 model deeplabv3_resnet50(pretrainedpretrained) # 替换最后的分类卷积层 model.classifier[4] nn.Conv2d(256, num_classes, kernel_size1) # 替换辅助分类器如果存在 if model.aux_classifier is not None: model.aux_classifier[4] nn.Conv2d(256, num_classes, kernel_size1) return model这里用的是 torchvision 内置的 DeepLabV3 实现backbone 是 ResNet-50。classifier[4] 是最后的 1×1 卷积输出通道数从 21COCO 类别数改成你的类别数。aux_classifier 是辅助分类头训练时用来提供额外的梯度信号推理时可以不用。提示如果你用的类别数很少比如 3 类以下可以把 ResNet 的前几层冻结只训练后面几层和分类头。这样收敛更快也不容易过拟合。3.2 损失函数交叉熵不够用的时候怎么办遥感语义分割最头疼的问题之一是类别不均衡。一张城市遥感图里建筑和道路可能占 60% 以上车辆可能只占 0.5%。用标准交叉熵损失模型会倾向于预测多数类少数类基本学不到。解决方案有几种我按实际效果排序第一种是加权交叉熵。给每个类别一个权重权重和类别频率成反比。实现简单效果立竿见影。def get_class_weights(label_list, num_classes): 根据训练集标签统计类别频率计算权重 label_list: 所有训练标签的列表 freq np.zeros(num_classes) for lbl in label_list: for c in range(num_classes): freq[c] np.sum(lbl c) total freq.sum() weights total / (num_classes * freq 1e-6) return torch.FloatTensor(weights)第二种是 Dice Loss 或 Focal Loss。Dice Loss 直接优化预测和标签的重叠度对不均衡数据更鲁棒。Focal Loss 通过降低易分类样本的权重让模型聚焦难样本。我一般会把加权交叉熵和 Dice Loss 按 1:1 组合效果比单用一种好。class CombinedLoss(nn.Module): def __init__(self, weightNone): super().__init__() self.ce nn.CrossEntropyLoss(weightweight) self.dice DiceLoss() def forward(self, pred, target): return self.ce(pred, target) self.dice(pred, target) class DiceLoss(nn.Module): def forward(self, pred, target): pred_soft torch.softmax(pred, dim1) target_onehot torch.nn.functional.one_hot(target, pred.shape[1]) target_onehot target_onehot.permute(0, 3, 1, 2).float() intersection (pred_soft * target_onehot).sum(dim(2, 3)) union pred_soft.sum(dim(2, 3)) target_onehot.sum(dim(2, 3)) dice (2 * intersection 1e-6) / (union 1e-6) return 1 - dice.mean()Dice Loss 的实现里pred 先过 softmax 变成概率target 转成 one-hot。intersection 是预测和标签的重叠部分union 是两者之和。加 1e-6 是防止除零。最后返回 1 减去平均 Dice 系数作为损失。3.3 训练循环与学习率调度训练循环本身不复杂但有几个参数直接影响收敛速度和最终精度。优化器我一般用 AdamW学习率 1e-4权重衰减 1e-4。SGD 也能用但需要更精细的学习率调参AdamW 对新手更友好。学习率调度用 CosineAnnealingLR从 1e-4 降到 1e-6配合 Warmup 前 500 步线性升温。from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR, LinearLR, SequentialLR def build_optimizer(model, lr1e-4, weight_decay1e-4): optimizer AdamW(model.parameters(), lrlr, weight_decayweight_decay) warmup LinearLR(optimizer, start_factor0.01, total_iters500) cosine CosineAnnealingLR(optimizer, T_max50000, eta_min1e-6) scheduler SequentialLR(optimizer, schedulers[warmup, cosine], milestones[500]) return optimizer, schedulerWarmup 的作用是让模型在训练初期不要因为学习率太大而震荡。遥感图像和 ImageNet 的分布差异大预训练权重迁移过来后前几百步的梯度方向可能和预训练时完全不同Warmup 给模型一个缓冲期。训练循环的核心代码def train_one_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss 0 for images, labels in dataloader: images images.to(device) labels labels.to(device) optimizer.zero_grad() outputs model(images)[out] loss criterion(outputs, labels) loss.backward() # 梯度裁剪防止梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() return total_loss / len(dataloader)梯度裁剪这步在遥感分割里很有必要。高分影像的切图块之间差异大某些 batch 的梯度可能异常大不裁剪容易导致训练发散。max_norm 设 1.0 是常用值如果你发现训练 loss 震荡厉害可以降到 0.5。注意验证集上的 mIoU 才是真正反映模型好坏的指标。训练 loss 降得再低验证 mIoU 不涨就是过拟合。我一般每训练 5 个 epoch 验证一次保存验证 mIoU 最高的模型权重。4. 推理与部署从模型输出到可用分割图4.1 大图推理的滑窗拼接策略训练时切图推理时也得切图。但推理的切图和训练不一样训练时切图可以重叠采样增加数据量推理时切图必须保证拼接后无缝且无重复计算。我用的策略是滑窗步长等于窗口大小相邻窗口不重叠但每个窗口推理时向外扩展一定像素比如 64 像素只取中心区域的结果。这样做的原因是卷积网络在边界处的预测会受 padding 影响边缘像素的精度明显低于中心区域。向外扩展再裁剪能有效消除拼接缝。def inference_large_image(model, image, crop_size512, overlap64, num_classes7): 大图推理滑窗预测后拼接 image: (H, W, C) numpy array 返回: (H, W) 类别索引图 model.eval() h, w image.shape[:2] result np.zeros((h, w), dtypenp.uint8) count np.zeros((h, w), dtypenp.float32) stride crop_size - 2 * overlap for y in range(0, h, stride): for x in range(0, w, stride): y1 max(0, y - overlap) y2 min(h, y crop_size - overlap) x1 max(0, x - overlap) x2 min(w, x crop_size - overlap) patch image[y1:y2, x1:x2, :] # 填充到 crop_size patch_pad np.pad(patch, ((0, crop_size - patch.shape[0]), (0, crop_size - patch.shape[1]), (0, 0)), modereflect) with torch.no_grad(): input_tensor torch.from_numpy(patch_pad).permute(2, 0, 1).unsqueeze(0).float().cuda() output model(input_tensor)[out] pred output.argmax(dim1).squeeze().cpu().numpy() # 只取有效区域 valid_h y2 - y1 valid_w x2 - x1 result[y1:y2, x1:x2] pred[:valid_h, :valid_w] count[y1:y2, x1:x2] 1 return result这段代码的关键在 stride 的计算stride crop_size - 2 * overlap。overlap 是向外扩展的像素数实际有效区域是 crop_size - 2 * overlap。这样每个像素至少被预测一次边界区域被预测两次取第一次的结果即可。4.2 模型导出与推理加速PyTorch 训练完的模型直接推理速度往往不够看。尤其是高分遥感影像一幅图几万像素滑窗推理可能要几分钟。加速手段有几个最直接的是转 ONNX 再用 TensorRT 推理。ONNX 导出import torch.onnx def export_onnx(model, save_path, input_size(1, 3, 512, 512), num_classes7): model.eval() dummy_input torch.randn(*input_size).cuda() torch.onnx.export( model, dummy_input, save_path, input_names[input], output_names[output], dynamic_axes{input: {0: batch, 2: height, 3: width}}, opset_version11 )dynamic_axes 设了 batch 和空间维度动态这样导出的 ONNX 模型可以接受任意尺寸输入不用每次改输入大小都重新导出。opset_version 用 11 是兼容性最好的选择TensorRT 8 对 opset 11 支持完善。导出后可以用 onnxruntime 或 TensorRT 推理。TensorRT 的加速比大概在 2 到 5 倍取决于模型结构和 GPU 型号。如果部署环境没有 NVIDIA GPUonnxruntime 的 CPU 推理也比原生 PyTorch 快不少。提示转 ONNX 之前一定要确认模型里没有 PyTorch 特有的算子。比如自定义的 Dice Loss 里用了 one_hot这个在推理时用不到但导出时如果模型 forward 里包含了就会报错。导出前把模型切到 eval 模式确保 forward 只走推理路径。5. 避坑指南高分遥感语义分割的五个血泪教训5.1 坑一标签噪声导致模型学偏现象训练 loss 正常下降验证 mIoU 也还行但模型在实际影像上推理时某些类别大面积误判。原因标签图里有噪声。遥感数据集的标注质量参差不齐尤其是开源数据集标注边界模糊、类别混淆的情况很常见。模型会把这些噪声当成真实信号去学。解决训练前做标签清洗。具体做法是对每个类别统计标签区域的连通域大小去掉面积过小的连通域比如小于 50 像素的这些大概率是标注噪声。另外用训练好的模型在训练集上推理一遍找出预测和标签差异大的区域人工检查是不是标签错了。这个过程很枯燥但能提升 3 到 5 个点的 mIoU。5.2 坑二显存爆炸的三种触发方式现象训练到一半突然 OOM或者推理时 batch size 设为 1 还是爆显存。原因显存爆炸通常来自三个地方。一是输入尺寸太大512×512 的图配 ResNet-101batch size 4 就要 12G 显存。二是模型里某些操作产生了中间变量没释放比如在 forward 里做了多次 permute 和 reshape。三是验证时忘了加 torch.no_grad()验证集的梯度也存下来了。解决训练时用混合精度AMP显存占用能降 30% 到 40%。验证和推理必须包在 torch.no_grad() 里。如果还爆用梯度累积batch size 设小累积几步再更新一次参数。from torch.cuda.amp import autocast, GradScaler scaler GradScaler() for images, labels in dataloader: with autocast(): outputs model(images)[out] loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() optimizer.zero_grad()5.3 坑三数据增强把地物特征增强没了现象训练时 mIoU 很高验证时掉一大截模型泛化能力差。原因数据增强过度。遥感图像的光谱特征是地物分类的核心依据颜色抖动幅度太大、模糊、cutout 这些操作会破坏光谱特征。模型学到的是增强后的虚假特征换到真实影像上就失效了。解决遥感语义分割的数据增强要克制。几何变换翻转、旋转可以大胆用颜色变换要保守。亮度、对比度调整幅度不超过 0.2不要用模糊和 cutout。如果数据量实在少用 Copy-Paste 增强把一类地物抠出来贴到另一张图上比颜色变换有效得多。5.4 坑四类别不均衡导致小类消失现象模型对建筑、道路预测很准但车辆、水体这些小类几乎预测不出来mIoU 被小类拉得很低。原因标准交叉熵损失下模型优化的是整体像素准确率。小类像素少对整体 loss 贡献小模型自然不关注。解决加权交叉熵 Dice Loss 组合。权重按类别频率的倒数设置但不要设得太极端否则模型会过度关注小类导致大类精度下降。我一般把权重限制在 1 到 10 之间。另外采样时用 WeightedRandomSampler让包含小类的切图块被采到的概率更高。5.5 坑五推理拼接缝明显现象大图推理结果拼接后窗口边界处有明显的直线痕迹地物被切断。原因卷积网络在图像边界处的预测受 padding 影响边缘像素的感受野不完整预测结果和中心区域不一致。滑窗拼接时相邻窗口的边缘预测直接拼在一起就出现了拼接缝。解决用重叠推理 中心裁剪。每个窗口向外扩展 overlap 像素只取中心区域的结果。overlap 设 64 到 128 像素能消除大部分拼接缝。如果还有可以在拼接处做加权融合边缘像素的权重低中心像素的权重高按权重加权平均。6. 进阶技巧用测试时增强把 mIoU 再提两个点模型训练完推理阶段还有免费的精度提升空间就是测试时增强TTA。原理很简单对同一张输入图像做多种变换翻转、旋转分别推理然后把结果逆变换后融合。模型对同一地物的不同视角预测会有差异融合后能抵消部分随机误差。我常用的 TTA 组合是原图 水平翻转 垂直翻转 水平垂直翻转四种变换各推理一次取平均。实现上就是在推理循环里加一层变换和逆变换def tta_inference(model, image_tensor): TTA 推理四种翻转组合结果取平均 image_tensor: (1, C, H, W) 返回: (1, num_classes, H, W) 融合后的概率图 model.eval() preds [] with torch.no_grad(): # 原图 preds.append(torch.softmax(model(image_tensor)[out], dim1)) # 水平翻转 preds.append(torch.flip(torch.softmax(model(torch.flip(image_tensor, [3]))[out], dim1), [3])) # 垂直翻转 preds.append(torch.flip(torch.softmax(model(torch.flip(image_tensor, [2]))[out], dim1), [2])) # 水平垂直翻转 preds.append(torch.flip(torch.softmax(model(torch.flip(image_tensor, [2, 3]))[out], dim1), [2, 3])) return torch.stack(preds, dim0).mean(dim0)这段代码的逻辑是对输入做四种翻转分别推理得到概率图然后把概率图逆翻转回去最后在 batch 维度取平均。注意是先 softmax 再翻转不是先翻转再 softmax顺序不能错。TTA 的代价是推理时间变成 4 倍。如果你的场景对延迟不敏感比如离线处理遥感影像TTA 是性价比最高的精度提升手段。我在 LoveDA 数据集上试过不加 TTA 的 mIoU 是 52.3加了之后到 54.1涨了 1.8 个点。对于已经调了很久的模型这几乎是白捡的。还有一个技巧是多尺度推理。把输入缩放到 0.75 倍、1.0 倍、1.25 倍分别推理后融合。这个对尺度差异大的地物特别有效比如同时存在小车辆和大片农田的场景。但计算量更大一般是 TTA 的 3 倍加上翻转就是 12 倍推理时间。我一般只在最终提交结果或者精度要求极高的场景才用。实际工程中我的习惯是训练阶段把能调的参数都调到位推理阶段先上 TTA如果精度还不够再考虑多尺度。不要一上来就堆技巧先把基础打牢。遥感语义分割这件事数据质量决定上限模型和技巧只是逼近上限的手段。希望帮到你。本文还有配套的精品资源点击获取
返回列表