简介:面向遥感与深度学习交叉领域开发者,这份实战资源以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通常只能设为2~4,训练速度慢下来不少。我的落地建议是:先看显存,12G以下老老实实用512;24G或以上直接上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_field='class_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=[f"ATTRIBUTE={attr_field}"]) out_ds.FlushCache() out_ds = None vec_ds = None print(f"mask 已写入:{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.5~10之间,低于0.5的权重会导致梯度主导类别学不进去,高于10则会在训练初期疯狂震荡。
3. 语义分割模型选型与PyTorch实现:U-Net、DeepLabV3+还是SegFormer
3.1 选型前先算算力账:三个模型的边界在哪里
语义分割模型在遥感场景里没有绝对王者,只有合不合适。U-Net的编码器-解码器对称结构对中小规模数据集极其友好,参数量小、训练快,在样本量只有一两万张裁剪图的场景里不容易过拟合;缺点是标准U-Net没有预训练骨干,从头训练收敛慢,所以现在更常见的是ResNet34作为编码器的U-Net变体。DeepLabV3+的优势在于ASPP(空洞空间金字塔池化)模块用不同膨胀率的空洞卷积捕获多尺度上下文,对遥感里“建筑群-道路-城市街区”这种大尺度结构有天然优势,但解码器对目标边缘的恢复能力弱,小地物边界往往偏圆润。SegFormer用Transformer做编码器,好处是可以直接用ImageNet-22K的预训练权重,并且它的MLP解码器对不同分辨率下的特征做聚合,边界锐利度远超DeepLabV3+,代价是显存占用高、推理速度明显慢于前两者。
具体到选型,我给一个硬性的判断标准:如果你的目标是快速出效果给领导看,U-Net+ResNet是投入产出比最高的起步点;如果地物类型以建成区为主、目标是大规模城市制图,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_channels=5, base_model='resnet34'): if base_model == 'resnet34': encoder = models.resnet34(weights=models.ResNet34_Weights.IMAGENET1K_V1) encoder.conv1 = nn.Conv2d(in_channels, 64, kernel_size=7, stride=2, padding=3, bias=False) 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_grad=False,让它在训练中自然更新,这样既保留预训练知识,又给了新波段足够的拟合自由度。
3.3 把DeepLabV3+的ASPP参数调成遥感形状
DeepLabV3+在遥感里经常被念叨“大目标好、小目标糊”。这得怪ASPP模块的膨胀率设置。默认的膨胀率组合(6,12,18)是针对自然影像设计的,输入512×512时感受野覆盖范围基本适配。但遥感影像里一栋建筑占几百像素,一条小路只有几个像素,同样的膨胀率会把小路信息彻底淹没。常见调整是把ASPP的膨胀率降到(4,8,12),配合一个更小的辅助分支去专门捕获细地物。下面给出ASPP模块的改法。
import torch.nn.functional as F class RemoteASPP(nn.Module): def __init__(self, in_ch=2048, out_ch=256): super().__init__() self.conv1x1 = nn.Conv2d(in_ch, out_ch, 1) self.atrous_4 = nn.Conv2d(in_ch, out_ch, 3, padding=4, dilation=4) self.atrous_8 = nn.Conv2d(in_ch, out_ch, 3, padding=8, dilation=8) self.atrous_12 = nn.Conv2d(in_ch, out_ch, 3, padding=12, dilation=12) 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, size=x.shape[-2:], mode='bilinear', align_corners=False) out = torch.cat([feat_1x1, feat_a4, feat_a8, feat_a12, feat_pool], dim=1) out = nn.Conv2d(out_ch * 5, out_ch, 1).to(x.device)(out) return outpadding = dilation这个对应关系务必保持,否则空洞卷积会把特征图的边缘位移弄偏。遥感影像里道路、河流这类线性地物,直接在ASPP之后拼一层F.interpolate(..., mode='bilinear', align_corners=False)做上采样,比直接反卷积的边界更平滑,又不易产生棋盘伪影。这里有个参数取舍哲学:遥感地物尺度跨度大,ASPP的五路分支里,应该让低膨胀率分支和高膨胀率分支的输出通道数保持一致,避免小地物特征在拼接时被大地物分支“稀释”。
4. 训练策略与四项关键参数:让语义分割在遥感图上真正收敛
4.1 损失函数组合:CrossEntropy加Dice为什么是遥感默认解
遥感语义分割的损失函数,几乎所有工程落地都从CrossEntropy出发,但只靠它十有八九会倒在类别不平衡上。CrossEntropy对像素进行独立惩罚,高频类别贡献的梯度天然压过稀有类。于是常用做法是叠加Dice Loss,直接优化预测掩膜和真值掩膜的区域重合度,它天然对类别频率不敏感。
def dice_loss(pred_probs, target_mask, smooth=1.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_classes=c).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, gamma=2.0, weight_tensor=None): ce = F.cross_entropy(pred_logits, target_mask, weight=weight_tensor, reduction='none') p = torch.exp(-ce) # 当前样本的预测置信度 focal_weight = (1 - p) ** gamma return (focal_weight * ce).mean()gamma=2.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(), lr=2e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max=total_epochs, eta_min=1e-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_norm=5.0) optimizer.step() scheduler.step()CosineAnnealingLR把学习率按余弦曲线从2e-4衰减到1e-6,相比StepLR的阶梯式下降,语义分割在训练后期edywis更喜欢平缓的衰减,因为边界像素从模糊到锐利的细化阶段,学习率跳变容易把学好的轮廓打坏。梯度裁剪的max_norm设5.0,是防止某些含云层高反射像素的样本把梯度值推到几百上千,这一步在遥感任务里几乎是必须的。
关于batch size,U-Net验证时常常会让人“自信”提高batch,但语义分割在小batch下本身就容易陷入训练不稳定。单卡训练时,512输入下batch=8起步,1024输入下batch=4,低于这个数值,BatchNorm的统计量会在推理时发生偏移,验证mIoU掉3~5个点是常有的事。如果显存不够,优先减输入尺寸而不是减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, window=512, stride=256): _, H, W = full_img.shape # (C, H, W) pred_acc = np.zeros((num_classes, H, W), dtype=np.float32) weight_acc = np.zeros((H, W), dtype=np.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:y+window, x:x+window] patch_tensor = torch.from_numpy(patch).unsqueeze(0).to(device) with torch.no_grad(): logits = model(patch_tensor) probs = torch.softmax(logits, dim=1).squeeze(0).cpu().numpy() pred_acc[:, y:y+window, x:x+window] += probs * tri_w weight_acc[y:y+window, x:x+window] += tri_w # 权重归一化,得到最终概率图 pred_acc = pred_acc / np.maximum(weight_acc, 1e-6) return pred_acc.argmax(axis=0)参数设置上,stride=256即窗口的50%,意味着每个像素至少参与两次预测,重叠区的三角权重把中心预测的权重显著抬高。这个推理方式的缺点是速度翻倍,一张2万×2万的影像要用近万次前向推理,如果没时间等,可以把stride放宽到窗口的2/3,但边界伪影会回来一部分。另一个实际工程技巧是推理时的batch化:把多行patch堆成batch一次性前向,显存够用的话整体速度提升3~5倍。权重归一化前必须把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的语义分割实践,数据、模型、训练、推理每个环节都有它自己的坑,跑通不难,真正做好靠的是脚手架搭得足够稳,希望帮到你。
本文还有配套的精品资源,点击获取