news 2026/9/23 18:43:07

高分遥感语义分割实战:PyTorch实现地物分类与面积估算全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
高分遥感语义分割实战:PyTorch实现地物分类与面积估算全流程

简介:这是一份面向遥感与计算机视觉学习者的项目实践资源,以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 在 1080Ti(11GB)上训练 U-Net,batch size 设 8 没有压力;如果切到 1024x1024,batch 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_size=512, stride=384): 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_ok=True) os.makedirs(out_dir + '/label', exist_ok=True) 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, train=True): 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, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True) ) def forward(self, x): return self.conv(x) class UNetLight(nn.Module): def __init__(self, in_ch=3, out_ch=6, base=16): 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_factor=2, mode='bilinear', align_corners=False) 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], dim=1) d2 = self.dec2(self.up(d3)) d2 = torch.cat([d2, e2], dim=1) d1 = self.dec1(self.up(d2)) d1 = torch.cat([d1, e1], dim=1) return self.out(d1)

这段代码的核心设计是三层下采样加一个中心层,通道数从 16 递增到 128,解码时通过跳跃连接把编码器特征拼接回来。为什么遥感任务不追求更深的网络?因为高分影像中的很多地物只有几十个像素,下采样次数过多会直接把小目标“压没”。三层下采样对应 8 倍分辨率降低,对 512x512 输入,最后特征图是 64x64,足以保留大部分有效信息。Upsample使用双线性插值而不用转置卷积,是因为转置卷积在上采样时会引入额外的可学习参数,在遥感数据量有限时更容易过拟合。

如果你希望直接套用成熟实现,可以安装 segmentation-models-pytorch 这个库,它提供了Unet(encoder_name='resnet34', encoder_weights='imagenet', classes=6)这样的接口,几行代码就能加载预训练编码器。遥感影像若包含近红外波段,输入通道不是 3 时,预训练权重必须去掉第一层重新初始化,这会损失一部分预训练优势。

3.3 输出层设计与类别不均衡处理

语义分割的最后一层输出每个像素在 N 个类别上的 logits,通常用交叉熵损失训练。遥感地物分类里一个非常突出的问题是类别极度不均衡:大片的耕地和裸地占据了绝大多数像素,道路和小水体可能只占 1% 以下。直接用标准交叉熵,模型会发现“把所有像素都预测为耕地”就能获得很低的损失,于是小类别几乎全部丢失。解决这个问题的两种思路是:加权交叉熵和组合损失。

加权交叉熵按类别像素占比的倒数给每个类别分配权重。实现上只要计算每个类别在训练集中出现的频率,然后在损失中给每个像素乘上对应权重即可。Focal Loss 是交叉熵的改良版,它让模型把注意力集中在难分样本上,对遥感里像素数少又难分的类别特别有效。

class WeightedFocalLoss(nn.Module): def __init__(self, alpha=None, gamma=2.0, ignore_index=255): 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, dim=1) probs = torch.exp(log_probs) one_hot = torch.eye(num_classes, device=logits.device)[target] one_hot = one_hot.permute(0, 3, 1, 2) pt = (one_hot * probs).sum(dim=1) 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(dim=1) * focal_weight return loss[~ignore_mask].mean()

Focal Loss 的两个关键参数:gamma=2.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 可以跑到 8,DeepLabv3+ 建议降到 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_ch=3, out_ch=6).cuda() optimizer = AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = PolynomialLR(optimizer, total_iters=60, power=0.9) criterion = nn.CrossEntropyLoss(ignore_index=255) 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_classes=6) if miou > best_miou: best_miou = miou torch.save(model.state_dict(), 'best_model.pth')

这段训练循环里的total_iters=60不是 epoch 次数,而是 scheduler 的迭代单位。PolynomialLR 支持按 epoch 或按 step 更新,我这里写成 epoch 维度,方便理解和调试。ignore_index=255对应数据预处理里的边界填充值——切块后的边缘区域可能包含无效像素,统一置为 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(dim=1) 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, gsd=0.8, tile_size=512, stride=384): img = np.array(Image.open(image_path).convert('RGB')) h, w = img.shape[:2] prob_map = np.zeros((h, w, num_classes), dtype=np.float32) count_map = np.zeros((h, w, 1), dtype=np.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), dim=1).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(axis=2) # 按类别统计像素数,乘上 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 分数更直观,能看到模型在城市阴影、河岸交界等复杂区域的真实表现。这个检查步骤虽然不是必需,但在交付给非技术背景的需求方时,一张叠加图的说服力远胜于十张指标表。遥感语义分割的项目,效果靠指标撑,质量靠人眼验——千万别省掉这一步。

另一个值得说到做到的细节是类别颜色的统一约定。地物分类输出结果时,最好固定一套颜色映射表:耕地绿色、建筑红色、水体蓝色、道路灰色、裸土棕色。否则每个人看图时都会有自己的解读习惯,验收时容易出现“为什么这块地是红色”的质疑。项目交付的稳定性,往往由这些不起眼的规范决定。

希望这篇从数据到面积统计的完整路径能帮到你,也祝你在地物分类这条路上少踩一个边界伪影的坑。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 18:42:16

G6 节点(Node)体系全解析:内置类型、数据结构与样式配置实战

数据可视化前端图表库 【免费下载链接】G6 ♾ A Graph Visualization Framework in JavaScript. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/g6/G6 点击查看 免费下载 节点是图可视化中最核心的构成单元。本文以 G6 官方文档《节点总览》为主线&#xff0c;结合 G6…

作者头像 李华
网站建设 2026/9/23 18:41:14

BUCK电路环路补偿设计与Saber仿真验证:从传递函数到相位裕量

简介&#xff1a;这是一份面向开关电源研发工程师的环路设计专题资料&#xff0c;聚焦BUCK电路从环路计算、补偿参数设计到仿真验证的完整流程。资料从自动控制理论中的乃奎斯特稳定性判据切入&#xff0c;讲解穿越频率、相位裕量、增益裕量、静态增益与动态响应等关键概念&…

作者头像 李华
网站建设 2026/9/23 18:40:21

单通道脑电睡眠分期实战:Python从EDF到分类模型

简介&#xff1a;这份资源面向计算机、人工智能、通信工程、自动化、电子信息等专业的在校学生与教师&#xff0c;以及希望入门生理信号处理的企业员工&#xff0c;提供一套基于单通道脑电信号实现自动睡眠分期的完整Python项目。项目围绕EEG信号预处理、数据集构建、深度网络建…

作者头像 李华
网站建设 2026/9/23 18:38:55

用C#解析STEP文件:从ISO-10303-21文本到B-Rep拓扑提取

简介&#xff1a;基于C#的STEP文件解析器完整源码与项目说明&#xff0c;属于本科毕设项目&#xff0c;主要面向计算机相关专业毕业生及需要工程实战的C#学习者。项目围绕STEP中性文件解析展开&#xff0c;实现了对文件中各组成元素的类型识别、详细信息提取&#xff0c;以及拓…

作者头像 李华
网站建设 2026/9/23 18:37:09

Postman Linux ARM64 国产化适配实战指南

简介&#xff1a;Postman Linux ARM64 版本&#xff08;v10.20.3&#xff09;是专为基于 ARM 架构的 Linux 系统&#xff08;如树莓派、国产信创平台等&#xff09;优化的接口测试工具&#xff0c;面向后端开发、API 测试工程师及嵌入式系统开发者&#xff0c;解决跨平台 API 调…

作者头像 李华