简介这是一份面向遥感与计算机视觉学习者的项目实践资源以PyTorch为基础实现高分遥感影像语义分割解决地物分类任务。资源基于GF2影像样本数据覆盖模型设计、数据加载、训练验证与推理预测全流程并重点展开膨胀预测、后处理、半监督伪标签策略及TensorBoardX可视化输出等技术点适合具备一定深度学习基础的高校学生、科研人员或竞赛选手系统掌握遥感地物分割完整链路。压缩包内含858个文件以819张PNG遥感切片图为主配合35个Python脚本、CSV标注清单、README说明文档等整体约548MB目录按数据处理、模型训练、预测评估等模块组织便于定位与复用。已有3040人学习下载。通过该资源可获得可直接运行的PyTorch工程了解主流分割模型在遥感数据上的训练调优思路并参考膨胀预测与后处理的可视化脚本从样本组织到成果输出形成完整方案。1. 高分遥感语义分割为什么自然影像的模型一上来就翻车把在 Cityscapes 或 VOC 上训练好的语义分割模型直接扔到高分遥感影像上做地物分类大多数学到的第一个教训就是边界碎成渣小目标丢光精度掉十几个点。原因不是模型不行而是高分遥感影像和自然影像在三个维度上根本不是一回事空间分辨率从“看到一棵树”变成“看到树冠里的单个像素”近红外等额外波段没有对应通道地表类别之间的光谱差异远比“人、车、路”要细微。PyTorch 实现高分遥感语义分割地物分类这个项目核心不是跑通一个 U-Net而是学会围绕遥感数据本身重新设计输入、标签、损失函数和评估方式。这篇文章按“数据 → 模型 → 训练 → 避坑 → 推理落地”的顺序把一条能复现的完整链路讲清楚。适合三类人高校里拿遥感做课程设计或毕设的学生刚接触地物分类想找基准方案的研究生以及需要快速产出地类面积成果的 GIS 从业者。2. 遥感数据准备标注质量决定精度上限模型只是逼近这个上限2.1 高分影像的核心属性分辨率、波段和投影做高分遥感语义分割第一步不是选模型而是先读懂影像头文件。高分影像区别于自然影像的关键参数有三个。第一是空间分辨率即单个像素对应的地面实际尺寸。常见的高分二号全色影像分辨率在 0.8 米左右多光谱在 3.2 米左右部分商业卫星或无人机航拍能做到 0.5 米以内。分辨率直接决定了一个地物目标占据多少像素一栋 10 米宽的房子在 0.5 米分辨率下约 20 个像素宽在 2 米分辨率下只有 5 个像素宽。对象越小语义分割越吃细节对模型下采样次数的限制就越严格。第二是波段数。大多数公开遥感数据集提供红、绿、蓝三个可见光波段少数带近红外。近红外波段对植被检测的价值极大——健康植被在近红外波段反射率非常高水体则几乎完全吸收。带近红外时模型输入要设计成 4 通道不带时我们通常只取 RGB。这里有一个常见的“白嫖”做法当影像只有单波段全色数据时可以复制成三通道喂给在 ImageNet 上预训练的模型但精度会打折扣。第三是投影坐标系。遥感影像的像素坐标对应真实地理坐标做面积统计时依赖 GSD地面采样距离参数。这个值在影像元数据里通常有定义单位是米/像素。后面做地物面积估算时直接用像素个数乘以 GSD 的平方即可不需要做投影转换。2.2 标注方案的三个约定类别体系、矢量化规范、标签栅格化高分遥感语义分割的标注工作量大、主观性强标注方式直接影响模型训练效果。我用过的标注方案中最稳健的一套约定如下。类别体系控制在 5~8 类。常见的地物分类任务围绕耕地、建筑、水体、道路、林地、裸地展开。类别太多会显著增加标注难度和类别混淆度比如“草地”和“林地”在可见光下经常会互相误判。这里借热搜词“耕地识别 sam语义分割”说一句SAM 等大模型可以辅助预标注但单个类别的分界线仍需要人工修正别指望端到端全自动。矢量化标注时遵循“贴着边界走、保留过渡像素”的原则。在 QGIS 或 Labelme 里画多边形时不要沿着目标边缘精确到像素因为人工不可能标得绝对准留出 1~2 个像素的过渡带反而能让模型学会更平滑的边界。标注完成后以影像左上角为原点、影像宽高为范围把矢量栅格化成单通道 8-bit PNG像素值对应类别 ID。PyTorch 里的标准做法是用 0~N-1 的整数类别标签不使用调色板索引。栅格化这一步最容易出现“标注错位”问题矢量范围与影像范围不匹配、栅格分辨率与影像分辨率不一致都会导致训练时模型看到的地物和标签对不上。所以我在每次标注完成之后都会做一次叠加检查把标签 PNG 以半透明方式叠加在原图上肉眼抽查 20~30 个固定点位。这一步不需要写代码用 QGIS 即可。2.3 切块策略为什么 512x512 是默认尺寸高分影像动辄上万像素宽直接整幅送进 GPU 不现实。切块是标准做法PyTorch 里通常用“滑窗 带重叠采样”的方式。切块尺寸的选择有三个考量。一是感受野。遥感语义分割常用 U-Net 或 DeepLabv3 这类模型5 次下采样后特征图尺寸会缩小 32 倍。输入 512x512输出概率图是 16x16每个输出像素对应的感受野远大于这个范围。切块大小至少要让最大地物目标的直径不超过输入尺寸的一半。对常见的高分影像512x512 是无人机航拍和高分卫星影像之间的折中值。二是显存约束。512x512 在 1080Ti11GB上训练 U-Netbatch size 设 8 没有压力如果切到 1024x1024batch size 通常要降到 2。除非显存非常充裕或模型非常轻量否则没必要上来就切大块。三是重叠滑窗能避免目标被切成两半后完全丢掉上下文。我一般按 0.25 倍数重叠切块也就是 stride 设为 size 的 75%。训练阶段增加数据量推理阶段用重叠切块做拼图还可利用多块重叠区域的平均预测来消除边界伪影。2.4 用 PyTorch 实现可复现的数据加载与切块逻辑在 PyTorch 中遥感切块通常先写一个离线预处理脚本把大影像一次性切好存盘训练时只做在线增强。这样能减少训练阶段的 CPU I/O 压力也便于直观检查切块结果。import cv2 import numpy as np from glob import glob import os def split_tiles(image_path, label_path, out_dir, tile_size512, stride384): img cv2.imread(image_path, cv2.IMREAD_COLOR) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) label cv2.imread(label_path, cv2.IMREAD_UNCHANGED) h, w label.shape os.makedirs(out_dir /img, exist_okTrue) os.makedirs(out_dir /label, exist_okTrue) idx 0 for y in range(0, h - tile_size 1, stride): for x in range(0, w - tile_size 1, stride): tile_img img[y:y tile_size, x:x tile_size] tile_lab label[y:y tile_size, x:x tile_size] # 过滤掉全背景的块减少无效训练 if (tile_lab 0).mean() 0.95: continue cv2.imwrite(f{out_dir}/img/{idx}.png, cv2.cvtColor(tile_img, cv2.COLOR_RGB2BGR)) cv2.imwrite(f{out_dir}/label/{idx}.png, tile_lab) idx 1 print(tiles saved:, idx)脚本的逻辑很简单以固定步长在大图上滑窗切块同时保存影像块和对应的标签块。关键参数有两个。stride控制块与块之间的重叠率384 对应 512 大小的 75% 步长重叠率 25%。重叠越大样本量越大但相邻块内容高度相似会造成训练集冗余。全背景过滤阈值设 0.95意思是标签中 95% 以上像素为背景类的块直接跳过避免大量无意义的纯背景训练样本。在实际使用中如果某些区域的地物特别稀疏可以把这个阈值降到 0.9 甚至 0.8确保训练集中保留足够的“负样本”让模型学会不误报。这里要提醒一句切块后要按数据集随机划分不要按图幅划分如果同一幅图的相邻块恰好落在训练集和验证集里验证分数会虚高上面提到的“数据泄漏”问题在遥感切块里极其常见。2.5 在线增强遥感专属的旋转与光谱扰动切块完成后在线增强是决定模型泛化能力的胜负手。遥感影像的增强与自然影像最大的区别在于旋转和翻转是“免费”的增强因为地物没有固定的上下方向而色彩抖动需要格外保守因为地物的光谱特征是分类依据的一部分。import torch from torch.utils.data import Dataset from torchvision import transforms from PIL import Image import random class RemoteSegDataset(Dataset): def __init__(self, img_dir, label_dir, trainTrue): self.img_paths sorted(glob(f{img_dir}/*.png)) self.label_dir label_dir self.train train def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img Image.open(self.img_paths[idx]).convert(RGB) lab Image.open(f{self.label_dir}/{idx}.png) # 训练时随机 0/90/180/270 旋转 if self.train: k random.choice([0, 1, 2, 3]) img img.rotate(90 * k) lab lab.rotate(90 * k) # 随机水平翻转与垂直翻转二选一 if random.random() 0.5: img img.transpose(Image.FLIP_LEFT_RIGHT) lab lab.transpose(Image.FLIP_LEFT_RIGHT) img_tensor transforms.ToTensor()(img) lab_tensor torch.from_numpy(np.array(lab)).long() return img_tensor, lab_tensor这里用 PIL 的rotate而不是 OpenCV 的warpAffine是因为 PIL 旋转默认不改变图像尺寸而且插值行为对标签更友好但Image.rotate每转一次都会重新采样像素。对标签做同样的几何变换时必须使用与影像一致的旋转参数和插值方式否则标签和图像会出现一个像素级别的错位。注意我没有在这里做亮度、对比度、饱和度扰动——植被、水体、裸土的区分高度依赖光谱值扰动过强相当于人为破坏了地物的光谱特征。如果你非要做建议亮度扰动控制在 ±10% 以内。3. 模型选型U-Net 是遥感语义分割的最佳起始点DeepLabv3 是精度上限的参考线3.1 为什么首选 U-Net编码器-解码器结构天然适配遥感遥感语义分割模型的选型绕不开两个名字U-Net 和 DeepLabv3。U-Net 原本为医学图像设计但它在遥感上表现同样出色原因在于其对称的编码器-解码器结构和跳跃连接。编码器逐层下采样每次下采样都在扩大感受野的同时保留了不同尺度的特征解码器逐层上采样恢复分辨率跳跃连接把编码器每层的浅层特征直接拼接到解码器对应层补偿了下采样造成的细节丢失。在高分遥感里U-Net 的跳跃连接价值巨大。道路、房屋边界、小水体这类地物的几何细节对分割结果影响极大而这些细节恰恰是深层次语义特征最容易丢失的信息。U-Net 的跳跃连接相当于给解码器提供了“看得清细节”的捷径。同时U-Net 的结构对称、参数可解释性强、训练相对稳定低位显存也能跑。对于第一次接触遥感语义分割的人来说U-Net 是容错率最高、迭代最快的基线模型。DeepLabv3 的核心是空洞卷积Atrous Convolution它用不同的膨胀率在不降低特征图分辨率的前提下扩大感受野。这对需要长距离上下文信息的场景有帮助比如判断一个像素究竟属于耕地还是裸地时需要考虑周围更大范围的地形关系。DeepLabv3 最大的问题是模型偏大、训练更消耗显存而且对高分影像中小目标的边界保持效果并不总是优于 U-Net。我的建议是先用 U-Net 跑通基线如果发现大块地物区域经常被误分再换 DeepLabv3 做对比。3.2 轻量 U-Net 的 PyTorch 实现能跑通是最低要求这里给出一段不依赖外部模型库、可以独立运行的 U-Net 实现。它把通道数压缩到 16 起步以减少显存占用同时保留了完整结构。import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x) class UNetLight(nn.Module): def __init__(self, in_ch3, out_ch6, base16): super().__init__() self.enc1 DoubleConv(in_ch, base) self.enc2 DoubleConv(base, base * 2) self.enc3 DoubleConv(base * 2, base * 4) self.pool nn.MaxPool2d(2) self.center DoubleConv(base * 4, base * 8) self.dec3 DoubleConv(base * 8, base * 4) self.dec2 DoubleConv(base * 4 base * 4, base * 2) self.dec1 DoubleConv(base * 2 base * 2, base) self.up nn.Upsample(scale_factor2, modebilinear, align_cornersFalse) self.out nn.Conv2d(base, out_ch, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) c self.center(self.pool(e3)) d3 self.dec3(self.up(c)) d3 torch.cat([d3, e3], dim1) d2 self.dec2(self.up(d3)) d2 torch.cat([d2, e2], dim1) d1 self.dec1(self.up(d2)) d1 torch.cat([d1, e1], dim1) return self.out(d1)这段代码的核心设计是三层下采样加一个中心层通道数从 16 递增到 128解码时通过跳跃连接把编码器特征拼接回来。为什么遥感任务不追求更深的网络因为高分影像中的很多地物只有几十个像素下采样次数过多会直接把小目标“压没”。三层下采样对应 8 倍分辨率降低对 512x512 输入最后特征图是 64x64足以保留大部分有效信息。Upsample使用双线性插值而不用转置卷积是因为转置卷积在上采样时会引入额外的可学习参数在遥感数据量有限时更容易过拟合。如果你希望直接套用成熟实现可以安装 segmentation-models-pytorch 这个库它提供了Unet(encoder_nameresnet34, encoder_weightsimagenet, classes6)这样的接口几行代码就能加载预训练编码器。遥感影像若包含近红外波段输入通道不是 3 时预训练权重必须去掉第一层重新初始化这会损失一部分预训练优势。3.3 输出层设计与类别不均衡处理语义分割的最后一层输出每个像素在 N 个类别上的 logits通常用交叉熵损失训练。遥感地物分类里一个非常突出的问题是类别极度不均衡大片的耕地和裸地占据了绝大多数像素道路和小水体可能只占 1% 以下。直接用标准交叉熵模型会发现“把所有像素都预测为耕地”就能获得很低的损失于是小类别几乎全部丢失。解决这个问题的两种思路是加权交叉熵和组合损失。加权交叉熵按类别像素占比的倒数给每个类别分配权重。实现上只要计算每个类别在训练集中出现的频率然后在损失中给每个像素乘上对应权重即可。Focal Loss 是交叉熵的改良版它让模型把注意力集中在难分样本上对遥感里像素数少又难分的类别特别有效。class WeightedFocalLoss(nn.Module): def __init__(self, alphaNone, gamma2.0, ignore_index255): super().__init__() self.alpha alpha self.gamma gamma self.ignore_index ignore_index def forward(self, logits, target): num_classes logits.shape[1] target target.clone() ignore_mask target self.ignore_index target[ignore_mask] 0 log_probs torch.log_softmax(logits, dim1) probs torch.exp(log_probs) one_hot torch.eye(num_classes, devicelogits.device)[target] one_hot one_hot.permute(0, 3, 1, 2) pt (one_hot * probs).sum(dim1) focal_weight (1 - pt) ** self.gamma # 将忽略区域权重置 0 focal_weight focal_weight * (~ignore_mask).float() if self.alpha is not None: alpha_map torch.zeros_like(pt) for i, a in enumerate(self.alpha): alpha_map (target i).float() * a focal_weight focal_weight * alpha_map loss (-log_probs * one_hot).sum(dim1) * focal_weight return loss[~ignore_mask].mean()Focal Loss 的两个关键参数gamma2.0时对置信度高的样本pt 接近 1降权明显模型被迫把更多注意力放在难分类像素上gamma过大如 5 以上会让训练前期梯度过于集中在少数样本上反而导致振荡。alpha是每个类别的权重一般取类别像素占比补数的归一化占比越低权重越高。遥感地物分类实践中我推荐weighted cross entropy Dice loss组合使用而不是严格只用 Focal Loss。Dice Loss 直接优化类别像素集合的重叠率对小目标更友好但对大目标区域梯度更新偏弱所以两者各占一半权重是常见做法。4. 训练流程与参数如何设计一次不浪费 GPU 时间的训练4.1 超参数基线学习率、Batch Size、Epoch 与验证策略遥感语义分割训练并没有特别神秘的超参数但有几条基线值值得直接抄作业。学习率采用多项式衰减或 Cosine Annealing。我不建议在遥感语义分割中采用固定学习率或 Step Decay因为不同类别的收敛速度差异会让固定策略要么收敛慢要么后期振荡。PyTorch 中直接使用torch.optim.lr_scheduler.PolynomialLR初始学习率设为 1e-3总训练轮次结束时衰减到 1e-5 附近。Batch Size 受显存制约。512x512 输入在 11GB 显存下 U-Net 可以跑到 8DeepLabv3 建议降到 4。Batch Size 太小比如 2时BatchNorm 统计量不稳定训练震荡明显。遥感语义分割几乎总用 BatchNorm所以 Batch Size 下限建议 4。如果显存实在不够把输入切到 384x384 比强行把 Batch Size 降到 2 更可取。这里提醒一个常见做法采用 AdamW 优化器而不是 SGD。遥感数据标注本身就带噪声SGD 对噪声的敏感度高于 AdamW并设置 weight_decay 为 1e-4 防止边缘模糊处的过拟合。Epoch 数量方面如果数据集是 5000 张 512 切片训练 60~80 个 epoch 基本收敛。关键是每个 epoch 后都在验证集上计算 mIoU保存验证分数最高的模型权重作为最终结果。不要迷信最后一轮权重遥感数据里模型过拟合出现得很晚最优点往往在训练中段。4.2 一次完整的训练循环示意import torch import torch.nn as nn from torch.utils.data import DataLoader from torch.optim import AdamW from torch.optim.lr_scheduler import PolynomialLR model UNetLight(in_ch3, out_ch6).cuda() optimizer AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler PolynomialLR(optimizer, total_iters60, power0.9) criterion nn.CrossEntropyLoss(ignore_index255) best_miou 0.0 for epoch in range(60): model.train() for imgs, labels in train_loader: imgs, labels imgs.cuda(), labels.cuda() optimizer.zero_grad() logits model(imgs) loss criterion(logits, labels) loss.backward() optimizer.step() scheduler.step() model.eval() miou compute_miou(model, val_loader, num_classes6) if miou best_miou: best_miou miou torch.save(model.state_dict(), best_model.pth)这段训练循环里的total_iters60不是 epoch 次数而是 scheduler 的迭代单位。PolynomialLR 支持按 epoch 或按 step 更新我这里写成 epoch 维度方便理解和调试。ignore_index255对应数据预处理里的边界填充值——切块后的边缘区域可能包含无效像素统一置为 255 后让模型忽略。compute_miou是你要自己实现的验证函数逻辑见 4.3。这里的一键抄作业点是如果数据集太小或类别极度不均衡把CrossEntropyLoss换成 3.3 中的WeightedFocalLoss或者0.5 * CE 0.5 * DiceLoss。4.3 mIoU 评估指标别只看 Accuracy地物分类任务里Accuracy 是极具欺骗性的指标。假设 90% 的像素是耕地把所有像素预测为耕地Accuracy 也能达到 90%但道路、水体等关键小类别全部丢失。mIoU平均交并比是遥感语义分割事实上的标准指标它先按类别计算 IoU再对所有类别取平均避免了类别不平衡对性能评估的掩盖。def compute_miou(model, val_loader, num_classes): model.eval() iou_list [] with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.cuda(), labels.cuda() logits model(imgs) preds logits.argmax(dim1) for cls in range(num_classes): pred_cls (preds cls) label_cls (labels cls) inter (pred_cls label_cls).sum().item() union (pred_cls | label_cls).sum().item() if union 0: iou_list.append(inter / union) return float(np.mean(iou_list))这段代码按每个 batch 累加所有类别的 IoU最后统一取平均比每个 batch 单独计算 mIoU 再求平均更准确因为后者会给小 batch 更多权重导致方差偏大。注意遥感里常用“每个类别单独报告 IoU 整体 mIoU”的方式mIoU 用于模型选型每类 IoU 用于判断模型具体在哪些类别上出现问题。如果验证集 mIoU 在 50% 以下先别调模型结构回去检查标注是否错位、类别是否失衡这两点对 mIoU 的影响远大于模型结构的微小改动。5. 遥感语义分割避坑高分影像独有的 5 个陷阱5.1 现象模型把河流和阴影区全部分成同一类原因水体在可见光波段反射率极低与山体阴影的光谱特征非常相似加上高分影像中水体常常伴随植被阴影模型很容易将二者混淆。解决在训练数据中刻意增加阴影区域样本。如果训练影像里阴影区域占比很低可以手动采集包含阴影的切片做额外增强并在损失函数中提高水体类别的权重。推理后可用后处理规则修正水体通常是连通且平坦的区域阴影区域常紧邻高大地物边缘用形态学开运算消除零星误判块。这里补充一个更实用的小技巧如果影像带近红外波段水体在近红外波段极暗而阴影在近红外波段仍有一定反射能让模型多学到一个波段这项误判会显著下降。5.2 现象切块边缘出现明显的块状伪影拼图后地物接不上原因模型在切片边缘没有看到完整的上下文导致同一地物在不同切片里的预测不一致。解决推理阶段采用重叠滑窗。具体做法是以训练时相同的步长重新滑窗把每张切片预测出的概率图按位置累加重叠区域的概率取平均值。这样拼图得到的边界更平滑。还有一个技巧训练时输入尺寸固定为 512推理时也保持 512不要为了“提分辨率”改成 640 或 768 之类模型对未见过的输入尺寸泛化能力会下降。5.3 现象训练损失不断下降但验证 mIoU 始终在低位徘徊原因最大的嫌疑是训练集和验证集来自同一幅影像的相邻切片即 2.4 中提到的数据泄漏。相邻切片之间内容高度相似模型在训练中“背”下了这些区域的特征验证集测不出真实泛化能力。解决按图幅而不是按切片划分数据。如果数据集只有 3~5 张大图宁可把整幅影像留作验证集也不要打乱切片再划分。数据量紧张时可采用 K 折交叉验证每次用其中一折做验证。这在遥感地物分类里尤其重要因为同一区域的影像在光照、季节、土壤湿度上高度一致模型很容易“记住场景”而不是“学会识别地物”。5.4 现象训练 loss 波动剧烈验证集分数忽高忽低原因Batch Size 太小导致 BatchNorm 统计量不稳定或者学习率过大导致优化过程在局部最优附近震荡。解决把 Batch Size 升到至少 4如果显存不够考虑梯度累积。梯度累积的代码实现不复杂核心是在多个 batch 上累积梯度后再执行一次optimizer.step()accum_steps 2 optimizer.zero_grad() for i, (imgs, labels) in enumerate(train_loader): imgs, labels imgs.cuda(), labels.cuda() logits model(imgs) loss criterion(logits, labels) / accum_steps loss.backward() if (i 1) % accum_steps 0: optimizer.step() optimizer.zero_grad()梯度累积相当于把 Batch Size 从 4 提升到 8而显存占用不变但训练速度会略慢。这个技巧在 BatchNorm 层较多的模型中需要特别注意BN 的统计量仍然是按每个小 batch 计算的梯度累积只能缓解优化稳定性并不能完全等价于增大 Batch Size。5.5 现象推理结果里道路断裂、房屋边界“糊”在一起原因模型在深层特征中丢失了高频细节信息。U-Net 的跳跃连接已经缓解了一部分问题但如果下采样层数太多细节信息仍然有损。解决使用带空洞卷积的解码器结构或者对输入做“多尺度裁剪拼接”推理。最常见的做法是把原始输入分别以 0.5、1.0、1.5 倍缩放后送进模型把三张概率图缩放到原始尺寸后取平均。这种多尺度推理能有效提升边界质量代价是推理时间增加 2~3 倍。或者用形态学后处理中的“骨架提取”对道路做联通性修复但这个方法在交叉路口容易引入伪分支我很少单独依赖它。6. 从语义分割到面积估算推理脚本与地物统计的落地细节语义分割训练的终点不是生成一张彩色分类图而是产出可量化的地物面积。面向“地物面积估算系统”这类高频需求推理阶段可以直接写一个统计脚本把像素计数换算成真实面积。import numpy as np import torch from PIL import Image def inference_and_area(model, image_path, gsd0.8, tile_size512, stride384): img np.array(Image.open(image_path).convert(RGB)) h, w img.shape[:2] prob_map np.zeros((h, w, num_classes), dtypenp.float32) count_map np.zeros((h, w, 1), dtypenp.float32) model.eval() with torch.no_grad(): for y in range(0, h - tile_size 1, stride): for x in range(0, w - tile_size 1, stride): tile img[y:y tile_size, x:x tile_size] tile_tensor torch.from_numpy(tile).permute(2, 0, 1).unsqueeze(0).float().cuda() probs torch.softmax(model(tile_tensor), dim1).squeeze(0).permute(1, 2, 0).cpu().numpy() prob_map[y:y tile_size, x:x tile_size] probs count_map[y:y tile_size, x:x tile_size] 1 prob_map / np.maximum(count_map, 1) pred prob_map.argmax(axis2) # 按类别统计像素数乘上 GSD^2 即得面积平方米 area_per_pixel gsd * gsd for cls in range(num_classes): count int((pred cls).sum()) area_m2 count * area_per_pixel area_ha area_m2 / 10000.0 print(f类别{cls}: {count} 像素, {area_m2:.2f} 平方米, {area_ha:.4f} 公顷) return pred, prob_map这段脚本的核心是重叠滑窗推理和概率平均。每一个切块预测出的概率图按位置累加同时维护一个计数矩阵最终将累加值除以计数得到每个像素的平均概率。这样拼接处不会出现刺眼的块状分界面积统计的误差也会随之减小因为单个切块的预测偏差会在重叠区域被平均掉。面积换算是遥感里最容易踩单位坑的环节。gsd的单位是米/像素面积计算完成后 1 公顷等于 10000 平方米。耕地面积通常按公顷汇报建筑占地面积按平方米汇报。如果影像本身带地理参考也可以读取投影信息做精确面积计算但基于 GSD 的像素计数法在精度已足够高的情况下是最快且最不容易出错的路径。我在实际项目里还有一个习惯推理完成后把概率图和标签叠加输出成一张带透明度的 PNG人工抽查几个典型区域。这比单看 mIoU 分数更直观能看到模型在城市阴影、河岸交界等复杂区域的真实表现。这个检查步骤虽然不是必需但在交付给非技术背景的需求方时一张叠加图的说服力远胜于十张指标表。遥感语义分割的项目效果靠指标撑质量靠人眼验——千万别省掉这一步。另一个值得说到做到的细节是类别颜色的统一约定。地物分类输出结果时最好固定一套颜色映射表耕地绿色、建筑红色、水体蓝色、道路灰色、裸土棕色。否则每个人看图时都会有自己的解读习惯验收时容易出现“为什么这块地是红色”的质疑。项目交付的稳定性往往由这些不起眼的规范决定。希望这篇从数据到面积统计的完整路径能帮到你也祝你在地物分类这条路上少踩一个边界伪影的坑。本文还有配套的精品资源点击获取