ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PyTorch高分遥感语义分割实战:从数据裁剪到模型部署

PyTorch高分遥感语义分割实战:从数据裁剪到模型部署 简介面向遥感与深度学习交叉领域开发者这份实战资源以PyTorch为框架完整实现高分遥感影像的语义分割与地物分类流程。内容包含基于GF2影像的样本组织、模型构建、训练调优与预测输出并重点介绍了膨胀预测、后处理以及半监督伪标签方法还接入tensorboardX用于可视化监控训练过程适合希望从零跑通遥感分割任务的算法工程师或研究生参考。资源包总文件数858个其中819个PNG图覆盖影像样本、标签与预测结果35个Python脚本构成核心训练、推理及工具模块另有CSV标注信息、说明文档与样例图片压缩包大小约547.74MB。包内目录按功能划分便于对照README逐步理解数据预处理、模型迭代和结果分析逻辑。已有3041人学习下载。通过该项目可获得一整套可复现的高分遥感地物分类方案包含常用训练细节与后处理思路能够有效缩短遥感语义分割的上手周期。1. 高分遥感地物分类为什么直接套用自然影像分割会翻车把一张2万×2万的国产高分卫星影像喂进在COCO上预训练好的语义分割模型十有八九会得到一张惨不忍睹的预测图大片农田被识别成建筑河流断成数截阴影被归为水体。这不是模型不行而是任务本身换了赛道。高分遥感语义分割面对的不是“猫和狗”而是地物的尺度跨度——一棵树可能占几个像素一片城区却绵延数千像素类别不平衡可以悬殊到背景占98%、目标类别只有0.5%。PyTorch实现这个任务的价值恰恰在于它让你能在同一套框架里把数据裁剪、模型选型、损失函数和推理拼接这些环节全部掌控住。这篇笔记我从数据标注一路写到模型部署适合正在做遥感算法落地、搞自然资源监测或者准备人工智能大作业的开发者照着改就能跑通自己的数据。2. 语义分割数据集制作与裁剪策略先把影像喂给模型的门槛跨过去2.1 裁剪参数窗口大小、重叠率与尺度陷阱高分影像的幅面动辄上亿像素显存不可能整图输入裁剪是第一步也是决定后续训练效果的第一步。裁剪窗口大小和步长是两个必须反复掂量的参数。窗口大小方面512×512和1024×1024是遥感语义分割最常用的两个配置。512的优点是显存友好、批量好设大缺点是感受野太窄街区的完整结构和农田的纹理边界容易被截断1024则让模型更容易看到地物上下文但batch通常只能设为24训练速度慢下来不少。我的落地建议是先看显存12G以下老老实实用51224G或以上直接上1024。步长直接影响样本量步长等于窗口大小时样本之间没有重叠容易丢失图幅边缘信息步长取窗口的50%即512窗口配256步长相当于每个像素被多个裁剪块覆盖训练时模型见过每个位置的不同上下文边界稳定性更好。但重叠也会带来类别重复参与训练的问题后面讲样本均衡时再细说。裁剪还有一个容易被忽略的尺度陷阱同一景影像内部地物比例尺一致但不同时段、不同传感器的影像DN值分布差异极大。如果训练集只有一景影像的裁剪结果验证时换了一景新图mIoU立刻掉十几个点。常见做法是让裁剪窗口跑遍整景影像而不是只裁剪标注过的区域这样模型至少能在无标注区域学会背景建模也便于后续半监督扩展。正确的流程是先铺满整景影像再按标注范围过滤出有效样本。2.2 用GDAL把GeoJSON标注转成掩膜一个可改的转换脚本标注数据在遥感领域最常见的交付格式是GeoJSON或Shapefile里面是一堆多边形矢量每个多边形对应一类地物。模型要的是和影像同尺寸的像素级PNG掩膜因此第一步是把矢量栅格化。下面是常规流程里最核心的转换脚本基于osgeo的GDAL/OGR完成投影信息校验和Rasterize不依赖ArcGIS。# rasterize_vector.py from osgeo import gdal, ogr, osr import numpy as np def vector_to_mask(geo_json_path, ref_tif_path, out_mask_path, attr_fieldclass_id): # 打开矢量数据源读取分类字段 vec_ds ogr.Open(geo_json_path, 0) # 0 表示只读 lyr vec_ds.GetLayer(0) # 用参考影像的空间参考和地理范围作为栅格化基准 ref_ds gdal.Open(ref_tif_path) geo_transform ref_ds.GetGeoTransform() cols ref_ds.RasterXSize rows ref_ds.RasterYSize # 创建单波段掩膜背景默认填充 0 mem_drv gdal.GetDriverByName(GTiff) out_ds mem_drv.Create(out_mask_path, cols, rows, 1, gdal.GDT_Byte) out_ds.SetGeoTransform(geo_transform) out_ds.SetProjection(ref_ds.GetProjection()) # 属性字段必须预先在矢量数据中生成 gdal.RasterizeLayer(out_ds, [1], lyr, options[fATTRIBUTE{attr_field}]) out_ds.FlushCache() out_ds None vec_ds None print(fmask 已写入{out_mask_path}, 大小 {cols} x {rows})这段代码先打开待标注的矢量文件再打开对应的高分影像把影像的投影和地理变换信息“套”到输出掩膜上。这里有个参数容易被低估attr_field必须是矢量图层里已存在的整型字段字符串字段会直接导致RasterizeLayer静默失败只输出全零图。更稳妥的做法是在GIS软件里预先给每个地块赋好类别编码比如建筑1、水体2、农田3再到代码里直接引用。执行完后检查一下掩膜里各类像素数量的分布如果哪一类像素总和只有几百个说明标注覆盖不足这类样本即便训练也基本学不出来了。掩膜和影像是否严丝合缝对齐也是栅格化后必须验证的一步。把掩膜叠加到影像上如果地物边缘出现整体偏移通常是GeoJSON和影像来源坐标系不一致导致的比如矢量是WGS84经纬度、影像却是UTM投影解决办法是把矢量先做投影转换常见的做法是把矢量统一到影像的投影坐标系后再执行RasterizeLayer。2.3 无效像素与类别权重先把数据里的“洞”填平高分遥感影像不是每一块像素都有意义。传感器扫描带外的黑色无值区、云遮挡区域、以及标注方刻意避开的不明确地物都会在掩膜上形成黑洞。如果直接把无值像素标为背景类模型会把“无信号”当成一种地物特征去学上线时一旦遇到真实的无值区预测就完全失控。我一般先统计影像的无效值然后额外生成一个valid mask在损失函数里把这些位置的梯度直接屏蔽掉。这样mask的class_id永远从1开始0一律视为无效区。类别权重是遥感语义分割绕不开的一环。城区影像里建筑可能占20%但泳池、天桥可能只占0.2%。常见做法是按类别像素占比的倒数做加权再加一个中位数频率平衡裁掉长尾def compute_class_weight(unique_labels): 输入为掩膜文件展开后的像素类别值列表 返回每个类别的权重向量供交叉熵损失使用 pixel_counts np.bincount(unique_labels) total_pixels pixel_counts.sum() # 先算中位数频率 median_freq np.median(pixel_counts[pixel_counts 0]) weights np.zeros(len(pixel_counts)) for idx, count in enumerate(pixel_counts): if count 0: weights[idx] median_freq / count # 聚类上限防止极小类别权重被拉到爆炸 weights np.clip(weights, 0.5, 10.0) return weights.astype(np.float32)权重分布的逻辑是出现频率越低的类别梯度放大倍率越高但上限必须设否则个别只有几百个像素的类别会把训练直接带偏。midian freq的本质是用“中位数类别”作为基准避免最高频类别权重降得过低、最低频类别权重升得过高。经验值是clip到0.510之间低于0.5的权重会导致梯度主导类别学不进去高于10则会在训练初期疯狂震荡。3. 语义分割模型选型与PyTorch实现U-Net、DeepLabV3还是SegFormer3.1 选型前先算算力账三个模型的边界在哪里语义分割模型在遥感场景里没有绝对王者只有合不合适。U-Net的编码器-解码器对称结构对中小规模数据集极其友好参数量小、训练快在样本量只有一两万张裁剪图的场景里不容易过拟合缺点是标准U-Net没有预训练骨干从头训练收敛慢所以现在更常见的是ResNet34作为编码器的U-Net变体。DeepLabV3的优势在于ASPP空洞空间金字塔池化模块用不同膨胀率的空洞卷积捕获多尺度上下文对遥感里“建筑群-道路-城市街区”这种大尺度结构有天然优势但解码器对目标边缘的恢复能力弱小地物边界往往偏圆润。SegFormer用Transformer做编码器好处是可以直接用ImageNet-22K的预训练权重并且它的MLP解码器对不同分辨率下的特征做聚合边界锐利度远超DeepLabV3代价是显存占用高、推理速度明显慢于前两者。具体到选型我给一个硬性的判断标准如果你的目标是快速出效果给领导看U-NetResNet是投入产出比最高的起步点如果地物类型以建成区为主、目标是大规模城市制图DeepLabV3的ASPP收益更明显如果标注数据量足够大单类样本超过3万张SegFormer值得换上去试一次。但任何模型都绕不开一个现实约束——显存。1024×1024输入下DeepLabV3的ASPP分支和SegFormer的多头注意力都会让显存飙升有条件的话优先上A100或4090级别的卡否则就退回512窗口。3.2 改输入通道让ResNet编码器吃下五波段影像高分影像不只是RGB三通道。常见的高分二号或高分六号影像包含蓝、绿、红、近红外四个多光谱波段部分还有全色波段。很多人在PyTorch里做迁移学习时踩坑torchvision里下载的ResNet预训练权重是3通道的强行把第一层改成5通道再加载权重就会报shape mismatch。常见做法是保留前三个通道的预训练参数后两个通道对应波段用随机初始化。import torch import torch.nn as nn from torchvision import models def build_unet_encoder(in_channels5, base_modelresnet34): if base_model resnet34: encoder models.resnet34(weightsmodels.ResNet34_Weights.IMAGENET1K_V1) encoder.conv1 nn.Conv2d(in_channels, 64, kernel_size7, stride2, padding3, biasFalse) with torch.no_grad(): # RGB 三通道直接复用预训练卷积权重新增波段置零 new_weight encoder.conv1.weight.clone() new_weight[:, :3, :, :] encoder.conv1.weight[:, :3, :, :] new_weight[:, 3:, :, :] 0.0 encoder.conv1.weight.copy_(new_weight) return encoder这里的关键是预训练权重复用五波段输入时新卷积组的RGB部分继承了ImageNet的浅层纹理提取能力新增的近红外和全色波段从零开始学。如果全部随机初始化浅层特征提取能力会退化得厉害训练前几十个epoch基本在重新学习边缘检测。闪存上还有第二种差异化初始化把新增通道的卷积核置零但不设requires_gradFalse让它在训练中自然更新这样既保留预训练知识又给了新波段足够的拟合自由度。3.3 把DeepLabV3的ASPP参数调成遥感形状DeepLabV3在遥感里经常被念叨“大目标好、小目标糊”。这得怪ASPP模块的膨胀率设置。默认的膨胀率组合61218是针对自然影像设计的输入512×512时感受野覆盖范围基本适配。但遥感影像里一栋建筑占几百像素一条小路只有几个像素同样的膨胀率会把小路信息彻底淹没。常见调整是把ASPP的膨胀率降到4812配合一个更小的辅助分支去专门捕获细地物。下面给出ASPP模块的改法。import torch.nn.functional as F class RemoteASPP(nn.Module): def __init__(self, in_ch2048, out_ch256): super().__init__() self.conv1x1 nn.Conv2d(in_ch, out_ch, 1) self.atrous_4 nn.Conv2d(in_ch, out_ch, 3, padding4, dilation4) self.atrous_8 nn.Conv2d(in_ch, out_ch, 3, padding8, dilation8) self.atrous_12 nn.Conv2d(in_ch, out_ch, 3, padding12, dilation12) self.image_pool nn.AdaptiveAvgPool2d(1) def forward(self, x): feat_1x1 self.conv1x1(x) feat_a4 self.atrous_4(x) feat_a8 self.atrous_8(x) feat_a12 self.atrous_12(x) feat_pool self.image_pool(x) feat_pool F.interpolate(feat_pool, sizex.shape[-2:], modebilinear, align_cornersFalse) out torch.cat([feat_1x1, feat_a4, feat_a8, feat_a12, feat_pool], dim1) out nn.Conv2d(out_ch * 5, out_ch, 1).to(x.device)(out) return outpadding dilation这个对应关系务必保持否则空洞卷积会把特征图的边缘位移弄偏。遥感影像里道路、河流这类线性地物直接在ASPP之后拼一层F.interpolate(..., modebilinear, align_cornersFalse)做上采样比直接反卷积的边界更平滑又不易产生棋盘伪影。这里有个参数取舍哲学遥感地物尺度跨度大ASPP的五路分支里应该让低膨胀率分支和高膨胀率分支的输出通道数保持一致避免小地物特征在拼接时被大地物分支“稀释”。4. 训练策略与四项关键参数让语义分割在遥感图上真正收敛4.1 损失函数组合CrossEntropy加Dice为什么是遥感默认解遥感语义分割的损失函数几乎所有工程落地都从CrossEntropy出发但只靠它十有八九会倒在类别不平衡上。CrossEntropy对像素进行独立惩罚高频类别贡献的梯度天然压过稀有类。于是常用做法是叠加Dice Loss直接优化预测掩膜和真值掩膜的区域重合度它天然对类别频率不敏感。def dice_loss(pred_probs, target_mask, smooth1.0): # pred_probs: [B, C, H, W] 经过 softmax 后的概率 # target_mask: [B, H, W] 类别索引 b, c, h, w pred_probs.shape target_onehot F.one_hot(target_mask.long(), num_classesc).permute(0, 3, 1, 2).float() intersection (pred_probs * target_onehot).sum(dim(2, 3)) union pred_probs.sum(dim(2, 3)) target_onehot.sum(dim(2, 3)) dice (2.0 * intersection smooth) / (union smooth) return 1.0 - dice.mean()逻辑说明Dice系数衡量的是预测区域和标注区域的重叠率系数越接近1越好因此返回1减dice作为损失。smooth参数防止分子分母同时为零的情况遥感图幅边缘经常出现整块都是无效区的情况不加smooth直接NaN。参数取值上smooth常见设1.0如果发现Loss曲线出现剧烈的毛刺试着提到5或10稳定性立刻变好。总损失一般是CrossEntropy和Dice按0.6比0.4混合相加。前几十个epoch权重放CrossEntropy多一点让模型先把语义空间学对等到训练中期再逐步提高Dice权重边界会明显变干净。4.2 类别权重与Focal Loss先按频率算再按表现调如果你已经用了加权交叉熵但模型依然在地物边界上出现成片误分就该上Focal Loss了。Focal Loss在交叉熵基础上引入了“难例聚焦”因子让模型把注意力放在那些当前置信度低、边界模糊的像素上而不是躺在简单的大片背景上躺赢。遥感里典型的高难例就是建筑物阴影边界阴影内的暗色像素和楼体混在一起常规交叉熵会无脑把它们判成背景。def focal_loss(pred_logits, target_mask, gamma2.0, weight_tensorNone): ce F.cross_entropy(pred_logits, target_mask, weightweight_tensor, reductionnone) p torch.exp(-ce) # 当前样本的预测置信度 focal_weight (1 - p) ** gamma return (focal_weight * ce).mean()gamma2.0是Focal Loss的经典取值。gamma越大难例的权重放大越激进但过大超过3会让训练初期全部焦点集中在少数极难的噪声样本上导致训练震荡。配合类别权重使用时weight_tensor用中位数频率权重两个机制是叠加关系类别权重管类别频率Focal管像素难度。遥感里另一个常用技巧是设一个“置信度截断”只对预测概率低于0.8的像素执行Focal高于0.8的像素直接当简单样本处理这样能避免道路等大面积同质区域也对模型梯度持续施压。4.3 学习率策略与冷冻backbone训练曲线不再玄学PyTorch里训练语义分割优化器的选型比损失函数更容易被人忽略。遥感分割任务里AdamW和SGD-WD各有拥趸。AdamW收敛快相对不挑学习率适合在样本不充足、训练时间紧迫的工程场景里快速摸底SGD配合Cosine退火则更可能在后期达到更高的精度上限但需要手工调整初始学习率和动量。我的习惯是第一阶段冻结backbone只训练解码器。让解码器先适应遥感影像的地物分布把mask预测能力拉起来等训练损失下降到平台期后再解冻整个网络做联合微调。这个做法能解决一个新数据集上从头训练常常遇到的“前期爆炸”问题。具体实现里解冻时机用epoch值控制最直接optimizer torch.optim.AdamW(model.parameters(), lr2e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_maxtotal_epochs, eta_min1e-6) for epoch in range(total_epochs): for inputs, masks in train_loader: outputs model(inputs) loss loss_fn(outputs, masks) optimizer.zero_grad() loss.backward() # 梯度裁剪防止遥感影像里极端像素拉爆梯度 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() scheduler.step()CosineAnnealingLR把学习率按余弦曲线从2e-4衰减到1e-6相比StepLR的阶梯式下降语义分割在训练后期edywis更喜欢平缓的衰减因为边界像素从模糊到锐利的细化阶段学习率跳变容易把学好的轮廓打坏。梯度裁剪的max_norm设5.0是防止某些含云层高反射像素的样本把梯度值推到几百上千这一步在遥感任务里几乎是必须的。关于batch sizeU-Net验证时常常会让人“自信”提高batch但语义分割在小batch下本身就容易陷入训练不稳定。单卡训练时512输入下batch8起步1024输入下batch4低于这个数值BatchNorm的统计量会在推理时发生偏移验证mIoU掉35个点是常有的事。如果显存不够优先减输入尺寸而不是减batch。5. 高分遥感推理避坑清单滑窗重叠、归一化与空间泄露5.1 现象边缘锯齿、中心准边缘花——原因滑窗推理无重叠训练时用的裁剪窗口都是50%重叠推理时把大图切成互不重叠的块逐块预测再拼回整张图这是最常见的翻车现场。现象很直观预测结果里图块中心区域地物干净但每个拼接缝两侧出现锯齿状错误道路在接口处断成两截建筑边缘出现规则矩形伪影。原因在于模型推理时每个图块边缘像素的感受野不完整卷积在边缘处缺少上下文预测置信度先天偏低。解决办法是推理时保留图块间的重叠区并在重叠区做加权投票。我常用的参数是推理窗口与训练窗口一致步长设为窗口的1/3到1/2对多次预测的重叠像素按距离图块中心的距离赋予权重越靠近中心的预测越可信。5.2 现象换一景影像预测结果整体发灰——原因归一化参数没按波段固化训练时用了全图的均值和标准差做标准化推理时却只用当前影像自己的统计量去归一化结果就是两景影像亮度不一致时预测掩膜整体漂移。遥感影像的DN值受太阳高度角、大气条件和传感器增益影响极大同一传感器不同日期的两景影像亮度可以差出一倍。解决办法是在训练前就把所有训练影像统一到同一套统计参数上并把归一化参数存成配置文件推理时强制使用训练时的均值方差而不是按每景影像实时计算。# normalize_config.yaml 示例 bands_mean: [127.3, 126.1, 123.7, 118.2, 135.6] bands_std: [48.4, 47.9, 49.1, 42.7, 55.8]参数说明这套mean和std必须在训练集全量统计不能只在一张图上算。推理时用img (img - mean) / std做逐波段处理之后不再做任何二次缩放。还有一个容易漏的配置验证集的mean和std如果和训练集不一致验证mIoU也会出现假性偏高或偏低导致你误判模型好坏。训练、验证、推理必须共用同一套归一化配置文件。5.3 现象验证mIoU 0.78上线只剩0.52——原因空间数据泄露把一景大图随机裁剪成训练块和验证块看似公平实际上训练块和验证块很可能来自同一片城区——同一个小区、同一条道路的重复纹理出现在两边。模型学到了“记住这片屋顶”而不是“学会识别屋顶”验证集里自然分数虚高。这是遥感语义分割里最隐蔽的陷阱也是模型部署后性能大幅缩水的头号原因。解决方式是按图幅分集训练和验证必须来自不同时间、不同地理范围的影像确保验证集的地物在空间上与训练无重叠。更严谨的做法是按经纬度网格划分比如把一个1公里×1公里的网格整体划入验证集。5.4 滑窗推理的PyTorch实现重叠宽度与权重公式下面给出一段能直接改的滑窗推理代码它读入整景影像按固定步长裁剪预测并用三角权重融合重叠区。def sliding_window_inference(model, full_img, window512, stride256): _, H, W full_img.shape # (C, H, W) pred_acc np.zeros((num_classes, H, W), dtypenp.float32) weight_acc np.zeros((H, W), dtypenp.float32) # 三角权重窗口中心权重最高边缘权重线性下降 tri_w np.minimum(np.arange(window), np.arange(window)[::-1]) tri_w np.outer(tri_w, tri_w).astype(np.float32) / (window / 2.0) for y in range(0, H - window 1, stride): for x in range(0, W - window 1, stride): patch full_img[:, y:ywindow, x:xwindow] patch_tensor torch.from_numpy(patch).unsqueeze(0).to(device) with torch.no_grad(): logits model(patch_tensor) probs torch.softmax(logits, dim1).squeeze(0).cpu().numpy() pred_acc[:, y:ywindow, x:xwindow] probs * tri_w weight_acc[y:ywindow, x:xwindow] tri_w # 权重归一化得到最终概率图 pred_acc pred_acc / np.maximum(weight_acc, 1e-6) return pred_acc.argmax(axis0)参数设置上stride256即窗口的50%意味着每个像素至少参与两次预测重叠区的三角权重把中心预测的权重显著抬高。这个推理方式的缺点是速度翻倍一张2万×2万的影像要用近万次前向推理如果没时间等可以把stride放宽到窗口的2/3但边界伪影会回来一部分。另一个实际工程技巧是推理时的batch化把多行patch堆成batch一次性前向显存够用的话整体速度提升35倍。权重归一化前必须把weight_acc的小值区域做下限保护否则图幅的右下边缘可能出现除零NaN。6. 进阶一步从70%的mIoU到可落地的地物分类图当模型在验证集达到70%出头的mIoU之后很多人就卡住了指标不再涨但预测图里细碎噪声仍多矢量化之后在GIS里根本没法看。这时候我会优先尝试三件事。第一是测试时增强TTA推理时把输入做水平翻转和垂直翻转得到三个预测结果取平均。这在遥感里特别有效因为影像没有“上下颠倒”的语义约束翻转不会引入不合理预测。付出三倍推理时间通常能换回1到2个点的mIoU尤其对边界类别的稳定效果显著。第二是多尺度推理把同一窗口缩放到0.75倍和1.25倍分别预测再融合。遥感地物的尺度跨度过大单一分辨率下模型总会在某些尺寸上吃亏多尺度推理在道路和屋顶边缘的完整性上改善极其明显。第三件事是把预测结果做后处理矢量化。PyTorch输出的pred_mask是像素级栅格要变成GIS能用的地物分类图还得走一遍栅格到矢量的转换。常见做法是先用形态学开闭运算去掉孤立噪点再用rasterio.features.shapes提取多边形边界最后按类别字段导出GeoPackage或者Shapefile。这里的实战习惯是不要直接把所有细碎多边形全部导出而是设置面积阈值像素数量少于50的地块直接舍弃否则成图后全是椒盐噪声。做这一步时记得检查输出矢量是否带上了原始影像的投影信息很多人在这步丢失了坐标系到ArcGIS里重新配准才是真正的灾难。最后说一个我的个人习惯每次训练结束我会把训练集、验证集和测试集各随机抽一张大图把模型的预测结果和真值掩膜做成三联图放在一起肉眼检查地物边界的贴合度。mIoU数字会骗人图不会——有些边界区域明明预测错得离谱但因为面积占比太小对mIoU的拖累不到0.1个点只有逐图看才能发现。这个习惯帮我避免过不止一次把没调好的模型草率上线的错误。这套基于PyTorch的语义分割实践数据、模型、训练、推理每个环节都有它自己的坑跑通不难真正做好靠的是脚手架搭得足够稳希望帮到你。本文还有配套的精品资源点击获取
返回列表