
简介这份资源面向遥感图像处理方向的研究者、工程师及具备一定深度学习基础的学习者提供基于Pytorch实现高分辨率遥感图像语义分割的完整教程与配套数据集帮助解决地物信息提取中从数据预处理到模型训练、评估的全流程问题。压缩包共1029个文件约577.48MB以819个png图像样本、35个py源码脚本为主另含zbak备份、csv标注索引、jpg效果图与md说明文档覆盖数据读取、网络搭建、训练预测及结果可视化等环节。教程从遥感图像基本概念讲起逐步深入到预处理方法、语义分割网络结构选择与优化策略并演示如何操作数据集、设计训练模型及评估分割结果还涉及标注工具制作像素级标签的思路。目前已有94人学习适合希望将语义分割技术落地到环境监测、资源勘探等场景的读者参考实践。1. 高分遥感图像语义分割从数据到推理一条能跑通的 PyTorch 路线拿到一幅 0.5 米分辨率的遥感影像想把建筑、道路、植被、水体逐像素分开靠人工勾画基本不现实。高分遥感图像语义分割要解决的就是这件事给每个像素分配一个类别标签输出一张与原图等大的掩膜。它和自然图像分割最大的差别在于尺度——同一幅图里既有几十像素的小车也有横跨上千像素的农田类别极不均衡且标注成本极高。这篇笔记面向已经会写 PyTorch 训练循环、但没系统做过遥感分割的工程师也照顾刚搭好 pytorch 环境想找第一个实战项目的新手。我会按「数据怎么准备 → 模型怎么选 → 训练怎么调 → 推理怎么落地」的顺序把一条能复现的完整路线讲清楚包括数据集组织、损失函数、评估指标和几个我踩过的坑。2. 高分遥感数据集怎么选、怎么组织成 PyTorch 能吃的格式遥感语义分割的第一步不是写模型而是把数据理顺。很多人卡在这一步下载下来的数据集要么是整幅大图加一张标签图要么是已经切好的小 patch格式五花八门。选数据集时先看三个维度——分辨率、类别体系、标注粒度。分辨率决定你能分到多细的类别类别体系决定模型输出通道数标注粒度决定你是做语义分割还是实例分割。2.1 常见高分遥感分割数据集对比与选型数据集分辨率类别数标注形式适用场景ISPRS Potsdam5cm6逐像素城市地物精细分割ISPRS Vaihingen9cm6逐像素城市场景含高度信息LoveDA0.3m7逐像素城乡跨域域适应研究DeepGlobe0.5m7逐像素道路、建筑、农田自建数据集视传感器自定义逐像素特定业务场景新手建议从 LoveDA 或 DeepGlobe 入手类别数适中、图像尺寸规整。如果做特定业务比如只关心建筑物提取自建数据集反而更直接。遥感图像标注常用工具是 QGIS 配合栅格标注插件或者 labelme 转栅格标注时务必保证标签图和原图严格对齐一个像素的偏移在训练时会被放大成明显的边缘噪声。2.2 把大图切成训练 patch 的滑动窗口脚本高分影像动辄上万像素直接送进网络显存扛不住标准做法是滑动窗口切 patch。切的时候要保证原图和标签图用同一套坐标且相邻 patch 之间留重叠避免边缘目标被切断。import os import numpy as np from PIL import Image def slide_crop(img_path, mask_path, out_dir, crop_size512, stride256): 滑动窗口切图img 和 mask 同步切 crop_size: patch 边长 stride: 步长小于 crop_size 即产生重叠 img np.array(Image.open(img_path)) mask np.array(Image.open(mask_path)) assert img.shape[:2] mask.shape[:2], 原图与标签尺寸不一致 h, w img.shape[:2] idx 0 for y in range(0, h - crop_size 1, stride): for x in range(0, w - crop_size 1, stride): img_patch img[y:ycrop_size, x:xcrop_size] mask_patch mask[y:ycrop_size, x:xcrop_size] # 跳过几乎全为背景的 patch减少无效样本 if (mask_patch 0).mean() 0.05: continue Image.fromarray(img_patch).save( os.path.join(out_dir, img, f{idx:06d}.png)) Image.fromarray(mask_patch).save( os.path.join(out_dir, mask, f{idx:06d}.png)) idx 1 print(f共生成 {idx} 个 patch) slide_crop(raw/image.tif, raw/label.png, dataset, crop_size512, stride256)这段脚本的关键参数是crop_size和stride。crop_size一般取 512 或 1024取决于显存stride取crop_size的一半能保证重叠边缘目标至少在一个 patch 里完整出现。(mask_patch 0).mean() 0.05这行是过滤纯背景 patch遥感图里背景占比往往超过一半不过滤会导致正负样本严重失衡。注意标签图必须是单通道的类别索引图不是 RGB 彩色图如果下载的数据集标签是彩色可视化图需要先做颜色到索引的映射。2.3 自定义 Dataset 与数据增强的落地写法切好 patch 后用Dataset封装训练时做在线增强。遥感图像有个特殊性旋转不变性比自然图像更强因为航拍视角下目标方向随机所以随机旋转、翻转是性价比最高的增强。import torch from torch.utils.data import Dataset import albumentations as A from albumentations.pytorch import ToTensorV2 class RS_SegDataset(Dataset): def __init__(self, root, splittrain, num_classes7): self.img_dir os.path.join(root, split, img) self.mask_dir os.path.join(root, split, mask) self.ids sorted(os.listdir(self.img_dir)) self.num_classes num_classes # 训练增强旋转、翻转、颜色抖动 self.train_tf A.Compose([ A.RandomRotate90(), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomBrightnessContrast(p0.3), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2(), ]) self.val_tf A.Compose([ A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2(), ]) self.tf self.train_tf if split train else self.val_tf def __len__(self): return len(self.ids) def __getitem__(self, i): name self.ids[i] img np.array(Image.open(os.path.join(self.img_dir, name)).convert(RGB)) mask np.array(Image.open(os.path.join(self.mask_dir, name))) aug self.tf(imageimg, maskmask) return aug[image], aug[mask].long()Normalize用的是 ImageNet 统计量因为后面用预训练骨干保持一致能加速收敛。mask转long是因为交叉熵损失要求标签是 int64。增强只对训练集做验证集只做归一化否则评估结果不可信。如果类别极度不均衡可以在__getitem__里做加权采样或者用WeightedRandomSampler这个后面损失函数部分再展开。3. 模型选型从 FCN 到 SegFormer高分遥感该用哪个遥感分割的模型选型核心矛盾是「感受野要够大」和「细节要够细」。高分影像里大目标需要大感受野小目标又要求高分辨率特征。FCN 作为语义分割的开山之作用全卷积替换全连接奠定了编码器-解码器范式但它的上采样太粗糙边缘分割效果一般。U-Net 的跳跃连接把编码器的浅层特征拼到解码器细节恢复好很多在遥感建筑提取里至今是强 baseline。DeepLab 系列用空洞卷积扩大感受野配合 ASPP 多尺度池化对多尺度目标友好。近两年 Transformer 路线兴起SegFormer 用分层 Transformer 编码器加轻量 MLP 解码器在多个遥感数据集上刷到 SOTA且推理速度不慢。3.1 编码器-解码器结构与预训练骨干的选择理由选模型先看骨干。遥感数据集规模通常远小于 ImageNet从头训容易过拟合用 ImageNet 预训练骨干是标配。ResNet 系列稳定、生态好是 U-Net、DeepLab 的默认选择如果追求精度且显存充足Swin Transformer 或 MiTSegFormer 的编码器更强但对小数据集不友好需要更谨慎的微调策略。我一般这样决策数据量小于 5000 张 patch用 ResNet34/50 的 U-Net 或 DeepLabV3数据量上万且类别复杂上 SegFormer-B2 或 Swin-UNet。别一上来就堆最大的模型遥感分割的瓶颈往往在数据和标注质量不在模型容量。3.2 用 segmentation_models_pytorch 三行搭出 U-Net自己写 U-Net 容易在通道数、上采样方式上出错推荐直接用segmentation_models_pytorchsmp它把主流模型和骨干都封装好了。import segmentation_models_pytorch as smp model smp.Unet( encoder_nameresnet34, # 骨干可换 resnet50 / mit_b2 encoder_weightsimagenet, # 预训练权重 in_channels3, # 输入 RGB classes7, # 类别数含背景 )encoder_name决定骨干encoder_weightsimagenet加载预训练classes必须等于你的类别总数含背景。换成 DeepLabV3 只需把smp.Unet改成smp.DeepLabV3Plus参数含义一致。如果想用 SegFormersmp 也支持encoder_namemit_b2。这套封装的好处是损失函数、指标、训练循环都能自己控不会被框架绑死。3.3 损失函数与评估指标的参数设置遥感分割的类别不均衡非常严重背景、植被往往占大头道路、车辆占比极小。纯交叉熵会被大类主导小类学不动。常见做法是交叉熵加 Dice 的组合损失。import torch.nn as nn import segmentation_models_pytorch as smp # 组合损失CE 稳定训练Dice 缓解类别不均衡 ce nn.CrossEntropyLoss(weighttorch.tensor( [0.2, 1.0, 1.0, 1.5, 2.0, 2.0, 1.0])) # 按类别频率反比设权重 dice smp.losses.DiceLoss(modemulticlass) criterion lambda pred, target: ce(pred, target) dice(pred, target) # 评估指标IoU 和 F1按类计算再平均 iou_metric smp.metrics.iou_score f1_metric smp.metrics.f1_scoreCrossEntropyLoss的weight按类别频率反比设置小类给大权重具体数值要根据你的数据统计调整上面只是示例。Dice 损失直接优化重叠度对小类更敏感。评估时不要只看像素准确率pixel accuracy背景占比高时准确率能到 90% 但模型啥也没学到要看 mIoU 和按类的 F1。smp 的指标函数支持reductionmacro做宏平均能真实反映小类表现。4. 训练循环、学习率调度与显存不够时的排查模型和数据都就位后训练循环本身不复杂但遥感分割有几个高频翻车点学习率太大导致 loss 震荡、batch size 太小导致 BN 统计不稳、显存不够被迫降分辨率。这一章把训练配置和排查方法讲透。4.1 训练循环与混合精度标准训练循环加上混合精度AMP能省显存、提速对遥感这种大图任务很实用。import torch from torch.cuda.amp import autocast, GradScaler from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) optimizer AdamW(model.parameters(), lr6e-5, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max50, eta_min1e-6) scaler GradScaler() for epoch in range(50): model.train() for img, mask in train_loader: img, mask img.to(device), mask.to(device) optimizer.zero_grad() with autocast(): # 前向用 fp16 pred model(img) loss criterion(pred, mask) scaler.scale(loss).backward() # 梯度缩放防下溢 scaler.step(optimizer) scaler.update() scheduler.step() # 每个 epoch 后在验证集算 mIoU model.eval() with torch.no_grad(): for img, mask in val_loader: img, mask img.to(device), mask.to(device) pred model(img).argmax(1) # 累积混淆矩阵后统一算 mIoUlr6e-5是 Transformer 骨干的常用起点ResNet 骨干可以到 1e-4。CosineAnnealingLR让学习率余弦下降比阶梯下降更平滑。AMP 的autocast和GradScaler必须成对使用否则 fp16 梯度下溢会让训练直接崩。验证阶段记得model.eval()和torch.no_grad()否则 BN 统计和显存都会出问题。4.2 学习率、batch size 与权重的调参经验这三个参数互相牵制。经验规则batch size 翻倍学习率大致翻倍用预训练骨干时学习率要比从头训小一个量级。遥感 patch 常用 batch size 是 8 或 16再大显存吃不消。如果显存只够 batch size 4可以开梯度累积模拟大 batch。accum_steps 4 # 等效 batch size 4 * 4 16 for i, (img, mask) in enumerate(train_loader): with autocast(): loss criterion(model(img.to(device)), mask.to(device)) / accum_steps scaler.scale(loss).backward() if (i 1) % accum_steps 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad()梯度累积把 loss 除以累积步数保证梯度量级和真大 batch 一致。注意optimizer.zero_grad()要放在累积周期结束时放错位置梯度就白攒了。4.3 显存不足与 loss 不下降的排查清单显存不足时按顺序试降crop_size512 降到 384、降 batch size、开 AMP、用梯度检查点torch.utils.checkpoint。别急着换小模型先确认是不是输入尺寸太大。loss 不下降或震荡先看学习率是不是太大再看数据标签有没有错位。我遇到过一次 loss 死活不降排查半天发现标签图是调色板模式np.array读出来是 RGB 三通道跟模型输出的单通道对不上这种问题不看数据可视化根本发现不了。所以训练前一定把几对 img/mask 叠在一起可视化确认对齐。5. 高分遥感分割的避坑与常见问题排查这一章集中写我踩过的坑每条按现象、原因、解决来。坑一mIoU 很高但小类全丢。现象是整体 mIoU 0.7 以上但道路、车辆类的 IoU 接近 0。原因是类别不均衡加损失权重没调模型学会了「全预测背景」也能拿高分。解决是给CrossEntropyLoss设类别权重加 Dice 损失评估时看宏平均 mIoU 而不是像素准确率。坑二验证集指标远好于测试集。现象是验证 mIoU 0.75换一批影像掉到 0.5。原因是训练和验证 patch 来自同一幅大图空间上高度相关等于变相泄漏。解决是按地理区域划分训练/验证/测试而不是随机切分 patch跨区域才能反映真实泛化。坑三边缘分割毛糙、目标粘连。现象是建筑边界糊成一片相邻目标连在一起。原因是上采样太粗暴或缺少边缘约束。解决是换 DeepLabV3 或加边界损失也可以在解码器加注意力模块。另外确认标签本身边缘是否清晰标注质量差的话模型再强也救不回来。坑四训练到一半 loss 变 NaN。现象是前几个 epoch 正常突然 loss 变 NaN。原因多半是 AMP 下梯度溢出或学习率过大。解决是调小学习率、检查GradScaler是否正确使用必要时关掉 AMP 用 fp32 跑几个 epoch 确认。坑五推理时整幅大图直接送网络导致显存爆。现象是训练好好的推理一张原图就 OOM。原因是推理图尺寸远大于训练 patch。解决是推理也用滑动窗口带重叠拼接重叠区取平均或投票避免拼接缝。6. 推理部署与把模型导出 ONNX 的实操技巧训练完的模型最终要落到推理。遥感影像推理的标准做法是滑动窗口加重叠拼接和切训练 patch 逻辑一致但多了拼接步骤。def inference_large_image(model, img, crop_size512, stride256, num_classes7): model.eval() h, w img.shape[:2] prob_map np.zeros((num_classes, h, w), dtypenp.float32) count_map np.zeros((h, w), dtypenp.float32) for y in range(0, h - crop_size 1, stride): for x in range(0, w - crop_size 1, stride): patch img[y:ycrop_size, x:xcrop_size] tensor torch.from_numpy(patch).permute(2,0,1).float().unsqueeze(0) with torch.no_grad(): out torch.softmax(model(tensor.to(device)), dim1) prob_map[:, y:ycrop_size, x:xcrop_size] out[0].cpu().numpy() count_map[y:ycrop_size, x:xcrop_size] 1 prob_map / np.maximum(count_map, 1) # 重叠区取平均 return prob_map.argmax(0)重叠区取平均能消除拼接缝count_map记录每个像素被覆盖次数。如果追求更快可以把模型导出 ONNX 用 ONNXRuntime 推理尤其在没有 PyTorch 环境的部署机上。torch.onnx.export( model, torch.randn(1, 3, 512, 512).to(device), rs_seg.onnx, input_names[input], output_names[output], opset_version11, dynamic_axes{input: {0: batch}}, )opset_version11兼容性好dynamic_axes让 batch 维可变。导出后务必用onnxruntime跑一遍和 PyTorch 输出对比误差在 1e-3 以内才算成功。我一般会固定随机种子把同一张图分别过 PyTorch 和 ONNX逐像素比对确认没有算子不兼容。最后说个习惯每次实验把配置、数据划分、指标记到一个表格里别靠脑子记。遥感分割调参周期长没有记录的话两周后你根本想不起哪个学习率对应哪个结果。这套路线我从数据切分到 ONNX 部署跑通过多次最深的教训是——数据对齐和划分方式比模型选型重要得多先把这两件事做扎实再谈刷指标。希望帮到你。本文还有配套的精品资源点击获取