遥感图像语义分割全流程实战:Python实现与踩坑指南

发布时间:2026/9/17 10:07:32
遥感图像语义分割全流程实战:Python实现与踩坑指南 遥感图像语义分割的项目网上代码一抓一大把但真要把“全流程”从数据准备、模型训练一路跑到结果导出中间藏着不少坑。我最早入门时也走过弯路以为把一幅高分影像丢进U-Net就能直接出一个漂亮的逐像素分类图结果被大图内存、标签错位、类别失衡这几个问题磨了好几个通宵。这篇文章想做的就是把我用Python实现遥感图像语义分割全流程的完整思路、关键代码、踩坑排查经验一次性串起来给正在做遥感地物分类、变化检测、目标提取相关工作的朋友一条能直接照着的路线。无论你是刚接触语义分割的初学者还是已经被大图推理折磨过的老手这篇内容都值得认真过一遍。1. 遥感图像语义分割项目一开始就被低估的三件事1.1 遥感图像分割和普通照片分割根本不是一回事很多人上手遥感图像语义分割的第一个误区是把它当成“换个数据集跑一下DeepLabV3”那么简单。事实上遥感影像和日常见到的自然图像差异非常大。首先是尺寸。一幅高分二号或者WorldView级别的标准影像动不动就是几千乘几千像素。以公开的ISPRS Potsdam数据集为例单张正射影像尺寸是6000×6000像素这还只是单片Tile。普通深度学习训练通常用512×512输入这意味着你在训练前就要面对一个“这么大一张图怎么切、切完怎么用”的问题。训练阶段还好说真正痛苦的是推理阶段——你不能把6000×6000的图直接整张丢进显存。其次是光谱信息。普通照片是RGB三波段而遥感影像常见的有四波段RGB 近红外、八波段、甚至十几波段的高光谱。很多地物在可见光下难以区分比如健康植被和干枯植被在RGB里看着都是绿褐色但在近红外波段差异非常明显。如果不利用这些额外通道等于白白浪费了一半以上的信息量。再就是类别分布。遥感场景下背景往往占据绝对多数道路、建筑物、车辆这些感兴趣目标加起来的像素可能只占5%~10%。直接用普通交叉熵训练模型大概率学会“把一切预测成背景”因为这样正确率就已经很高了。1.2 大多数人第一个翻车点没在项目开始前想清楚技术路线我在技术群里见过太多人拿着开箱即用的分割网络下载好公开数据集就开始跑训练脚本最后跑完一看mIoU只有0.2然后开始怀疑人生。这不是模型能力问题而是流程设计问题。遥感图像语义分割的完整链路远不止“训练一个模型”这一步。你至少要考虑下面几个环节数据整理与标签检查掩膜是否是单通道标签图类别编号是否连续影像和标注是否严格对应。裁片策略用什么尺寸切图训练集和验证集如何采样边界信息怎么处理。模型设计与输入适配网络输入通道数是否和影像波段数一致分类头和类别数是否匹配。训练调优损失函数适合不适合类别不均衡学习率计划是否平滑评估指标是否用对。大图推理与后处理如何用滑窗把大图切成块再将预测结果拼回去去掉接缝效应输出业务可用的结果。任何一个环节掉链子最终精度都会大打折扣。你要做的是把每一个环节都当成一件独立的技术活来做而不是“数据标注完成后一键训练”。1.3 我用的技术路线总览这篇文章里我会围绕一套非常务实的技术路线来讲这套方案在公开数据集和实际项目中我都跑通过数据以ISPRS Potsdam、LoveDA这类公开遥感数据集为主裁片训练阶段用随机采样裁片验证和推理用重叠滑窗数据增强albumentations 库做旋转、翻转、光照扰动模型用 segmentation_models_pytorch 搭建 U-Net / DeepLabV3 作为主力损失函数CrossEntropyLoss DiceLoss 组合评估mIoU、类别IoU、混淆矩阵一起看推理重叠滑窗概率拼接得到整幅大图的分割结果导出保存为TIF并可进一步转成GeoJSON向量。这个路线最大的优点是稳定、可控、可解释性高适合工程落地也适合作为学习和改进的基线。下面每个环节我会把核心代码和设计原因一起给出来。2. 数据准备公开数据集怎么选本地目录怎么组织2.1 公开数据集选型五个常见选择的横向对比数据是语义分割项目的地基。选对数据集能让你的调试周期缩短一大截。我把自己用过的几个常用数据集整理成了下面的对照表方便你按需求选型数据集名称影像类型空间分辨率典型尺寸标注类别适合场景ISPRS Potsdam正射影像含IRRG和RGB5cm6000×6000不透水面、建筑、低矮植被、树木、汽车、背景城市地物精细分割新手首推ISPRS Vaihingen正射影像含IRRG9cm约2500×2000不等同上城市分割数据量偏小LoveDA遥感影像含RGB0.3m1024×1024背景、建筑、道路、水、草地、耕地、森林多类别不平衡、多尺度适合做鲁棒性实验DeepGlobe Land Cover卫星影像含RGB0.5m左右2448×2448城市、农业、牧场、森林、水、背景/未知土地利用分类类别较少GIDGaofen Image Dataset高分一号卫星影像约2m4m多尺寸居住地、农业、林地等大规模地物分类如果你想快速跑通整条流程我最推荐ISPRS Potsdam。它数据量大、类别标准化程度高学术界大量论文都用它做基准交流起来方便。如果想测试模型在严重类别不平衡、不同地块大小下的表现LoveDA是非常好的第二选择。2.2 本地目录怎么组织一个不会出错的规范很多人忽略目录规范结果数据集一多就乱套。我的习惯是按“数据集根目录 → 影像目录 / 标注目录”的结构组织datasets/ └── potsdam/ ├── images/ │ ├── top_potsdam_2_10_RGB.tif │ ├── top_potsdam_2_11_RGB.tif │ └── ... ├── masks/ │ ├── top_potsdam_2_10_label.tif │ ├── top_potsdam_2_11_label.tif │ └── ... └── train_val_split.txt有几个细节值得特别注意标注掩膜必须是单通道、像素值等于类别编号的图像比如类别0、1、2、3、4、5不要用调色板RGB图当训练标签。影像和标注文件名要严格一一对应代码里建议写一个健壮的匹配函数不要只靠文件名后缀硬编码。train_val_split.txt 里写明哪些用于训练、哪些用于验证避免每次跑代码都随机切分导致实验结果不可复现。2.3 Dataset类的实现关键代码与设计原因写 Dataset 类是数据环节的入口。下面是我常用的一个实现支持指定波段、自动读取掩膜并做了基础的像素类别检查import os import numpy as np import cv2 from torch.utils.data import Dataset class RemoteSensingDataset(Dataset): def __init__(self, image_dir, mask_dir, file_list, bands[0, 1, 2], patch_size512, transformNone, use_maskTrue): super().__init__() self.image_dir image_dir self.mask_dir mask_dir self.file_list [line.strip() for line in open(file_list, encodingutf-8)] self.bands bands self.patch_size patch_size self.transform transform self.use_mask use_mask def __len__(self): return len(self.file_list) def __getitem__(self, idx): name self.file_list[idx] img_path os.path.join(self.image_dir, name .tif) mask_path os.path.join(self.mask_dir, name _label.tif) # cv2.imread默认读成BGR这里显式以unchanged方式读入保留全部波段 image cv2.imread(img_path, cv2.IMREAD_UNCHANGED) if image.ndim 2: image np.stack([image] * 3, axis-1) if self.bands is not None: image image[:, :, self.bands] if self.use_mask: mask cv2.imread(mask_path, cv2.IMREAD_UNCHANGED) # 某些掩膜是16位或含多个通道的调色板需要转单通道 if mask.ndim 3: mask mask[:, :, 0] mask mask.astype(np.int64) else: mask np.zeros((image.shape[0], image.shape[1]), dtypenp.int64) if self.transform is not None: transformed self.transform(imageimage, maskmask) image transformed[image] mask transformed[mask] # HWC - CHW image image.transpose(2, 0, 1).astype(np.float32) return image, mask这套代码的核心设计点是用IMREAD_UNCHANGED保持原始波段数通过bands参数灵活选择RGB或近红外组合这样后期扩展多光谱输入时不用大改代码。掩膜读取后强制转为int64避免后面算损失时类型不匹配。提示如果数据集的标签类别是从1开始的背景为0目标从1计数你可以在__getitem__里做mask mask - 1把类别编号规整为从0开始否则模型输出通道和标签值对不上损失函数直接报错。3. 大图不能直接训裁片策略、样本采样与数据增强3.1 为什么必须裁片Patch Size 到底怎么定遥感影像动辄几千像素宽直接整图输入任何分割网络都会让显存爆炸。更重要的是语义分割模型通常有固定的下采样倍数常见的是32倍、16倍输入尺寸如果不合适输出尺寸会和标注不一致训练直接报错。常用的Patch Size是256、512、768。怎么选我觉得核心依据是你的目标地物尺度和数据集的影像分辨率。以Potsdam的5cm分辨率为例建筑边长可能几百像素汽车只有几十像素如果patch取256视野太小模型可能看到“墙壁”局部纹理却判断不出这是房子外墙如果patch取1024虽然上下文丰富但显存压力大批次大小也不好调。我实测下来512是精度和效率比较折中的选择。你可以用下面这段逻辑去估算单卡显存占用显存占用 ≈ batch_size × patch_h × patch_w × decoder_channels × 若干倍系数比如输入512×512、batch size 8U-Net ResNet34编码器在12GB显存上可以跑换到768×768batch size就得降到4甚至2。没有经验的话一开始从512 batch_size 8 起步遇到显存溢出再往下降。3.2 三种裁片策略对比训练和推理不能用同一种遥感场景下常见的裁片策略有三类我实际对比后感受如下策略做法适用阶段优缺点无重叠网格切固定尺寸均匀切不重叠弱实现最简单但目标可能被切断预测接缝明显随机采样切每次随机取一个位置裁块训练数据多样性最好相当于隐式数据增强重叠滑窗切窗口按步长滑动步长小于窗口有重叠验证、推理边界信息保留好但计算量稍大我的建议是训练阶段用随机采样因为模型需要“看到”更多不同位置的地物组合随机采样可以把数据分布打散。验证和推理阶段用重叠滑窗重叠部分做多次预测再平均这样能够显著减少接缝效应。下面这段是滑窗采样的核心逻辑可以直接用在推理阶段def sliding_window_crop(image, window_size512, stride256): h, w image.shape[:2] crops [] positions [] for y in range(0, h - window_size 1, stride): for x in range(0, w - window_size 1, stride): crops.append(image[y:y window_size, x:x window_size]) positions.append((y, x)) # 处理右下边界保证覆盖整幅图 if (h - window_size) % stride ! 0: y h - window_size for x in range(0, w - window_size 1, stride): crops.append(image[y:y window_size, x:x window_size]) positions.append((y, x)) if (w - window_size) % stride ! 0: x w - window_size for y in range(0, h - window_size 1, stride): crops.append(image[y:y window_size, x:x window_size]) positions.append((y, x)) if (h - window_size) % stride ! 0 and (w - window_size) % stride ! 0: crops.append(image[h - window_size:h, w - window_size:w]) positions.append((h - window_size, w - window_size)) return crops, positions注意这个实现里步长如果取窗口的一半512窗口 256步长每个像素往往被预测2到4次最后叠加时取平均边缘过渡会自然很多。3.3 数据增强不是简单的翻转而是让模型对遥感特性“免疫”遥感影像增强和自然图像增强思路类似但有两个额外的方向值得投入一是对亮度、对比度做扰动因为不同批次影像的光照条件差异明显二是可以用“通道级别的随机变换”。我平时用 albumentations 来组织增强流程配置如下import albumentations as A train_transform A.Compose([ A.RandomResizedCrop(height512, width512, scale(0.5, 1.0), p1.0), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomRotate90(p0.5), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.8), A.HueSaturationValue(hue_shift_limit10, sat_shift_limit20, val_shift_limit20, p0.5), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ]) val_transform A.Compose([ A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ])有三个细节要特别提醒归一化的均值和标准差默认是针对ImageNet的但遥感影像的分布和自然图像差异很大有条件的话最好用自己数据集的统计量重新计算一遍。Resize或RandomResizedCrop改变尺寸时mask 一定也要跟着变albumentations 会自动处理但如果你自己用 numpy 的 resize 去缩放mask必须使用cv2.INTER_NEAREST否则类别边缘会出现插值产生的伪类别。如果影像有近红外波段数据增强时不要做RGB渲染类操作像HueSaturation这类针对颜色空间的增强只对RGB波段有意义如果把NIR当普通通道一起处理可能会引入奇怪的光谱失真。通常的做法是NIR波段做轻微的Gamma、亮度扰动不做颜色变换。4. 模型选型与训练策略U-Net系为什么是默认答案4.1 三种主流分割模型的选型逻辑语义分割模型这两年迭代很快但在遥感场景下我依然建议团队或新手把主流模型控制在下面三个范围里模型Backbone精度表现显存占用备注U-NetResNet34/ResNet50中上较低结构简单易于调试遥感社区使用最广DeepLabV3ResNet50/101高偏高ASPP模块对多尺度目标有效SegFormerMiT-B2/B3高中Transformer结构依赖大量数据小数据集慎用实际项目中我通常这样决策如果数据集只有几百张图而且类别多、样本小选U-Net ResNet34因为它在小数据上不容易过拟合调试周期也短。如果数据量很充足上千张以上DeepLabV3 或者SegFormer可以拿到更高的上限。Potsdam这个数据量下U-Net和DeepLabV3我也都跑过后者的mIoU通常会高1~2个百分点但对显存和训练时间的要求也更高。4.2 损失函数为什么只用CrossEntropy不够遥感语义分割最常见的痛点是类别不均衡。建筑物、道路这类细长目标或者小目标在像素占比上远小于背景。如果只使用CrossEntropyLoss模型会很自然地偏向多数类少数类几乎学不到特征。实践中我通常采用“CrossEntropyLoss DiceLoss”组合CrossEntropyLoss 提供逐像素的梯度信号数值稳定适合类别间的精细决策DiceLoss 直接优化预测区域和真实区域的重叠度对类别不均衡不敏感能有效提升少数类的分割质量。组合方式很简单import torch import torch.nn as nn class CombinedLoss(nn.Module): def __init__(self, ce_weight0.6, dice_weight0.4, num_classes6): super().__init__() self.ce nn.CrossEntropyLoss(ignore_index255) self.dice DiceLoss(num_classesnum_classes) self.ce_weight ce_weight self.dice_weight dice_weight def forward(self, pred, target): ce_loss self.ce(pred, target) dice_loss self.dice(pred, target) return self.ce_weight * ce_loss self.dice_weight * dice_lossDiceLoss 的实现通常会把预测softmax之后概率图与独热编码的目标图逐类计算Dice系数。权重分配上我一般ce和dice各占一半如果少数类表现太差会把dice_weight调高到0.6到0.7。4.3 用 segmentation_models_pytorch 快速搭建模型与其自己徒手复现一遍U-Net不如直接基于 segmentation_models_pytorchSMP构建。它已经封装好了编码器、解码器、分割头的全套逻辑支持ResNet、EfficientNet等主流的backbone改动输入通道和类别数只需要改两个参数。import segmentation_models_pytorch as smp model smp.Unet( encoder_nameresnet34, encoder_weightsimagenet, in_channels3, classes6, activationNone, )这里in_channels3对应RGB输入如果你打算把近红外加进来改成in_channels4即可但注意 encoder 预训练权重是按3通道来的第一层卷积会做随机初始化需要一点额外的训练轮次去适应。如果不希望麻烦一般做法是继续用3通道。classes6对应Potsdam的类别数。activationNone表示输出的是logits损失函数内部配合交叉熵时不需要提前做SoftmaxDiceLoss内部再做概率化即可。4.4 训练循环优化器、学习率调度、评估指标一个都不能少训练部分的核心代码看起来不复杂但细节决定成败。我的习惯是优化器AdamW初始学习率5e-4学习率调度OneCycleLR 或 Poly 衰减前者适合快速收敛后者适合续训调优评估每个epoch在验证集上计算mIoU保存mIoU最高的权重早停patience设成10到15个epoch。一个带完整训练循环的最小示例import torch from torch.optim import AdamW from torch.optim.lr_scheduler import OneCycleLR from tqdm import tqdm def compute_miou(pred_mask, true_mask, num_classes6): ious [] pred_mask pred_mask.flatten() true_mask true_mask.flatten() for cls in range(num_classes): pred_cls pred_mask cls true_cls true_mask cls intersection (pred_cls true_cls).sum().item() union (pred_cls | true_cls).sum().item() if union 0: ious.append(float(nan)) else: ious.append(intersection / union) return np.nanmean(ious) optimizer AdamW(model.parameters(), lr5e-4) scheduler OneCycleLR( optimizer, max_lr5e-4, total_stepslen(train_loader) * epochs, pct_start0.1, ) best_iou 0.0 for epoch in range(epochs): model.train() train_loss 0.0 for images, masks in tqdm(train_loader): images images.cuda() masks masks.cuda() optimizer.zero_grad() logits model(images) loss criterion(logits, masks) loss.backward() optimizer.step() scheduler.step() train_loss loss.item() model.eval() val_ious [] with torch.no_grad(): for images, masks in val_loader: images images.cuda() masks masks.cuda() logits model(images) preds logits.argmax(dim1).cpu().numpy() val_ious.append(compute_miou(preds, masks.cpu().numpy())) miou np.mean(val_ious) if miou best_iou: best_iou miou torch.save(model.state_dict(), best_model.pth)几个容易踩的细节掩膜标签里有255这种边界忽略值时CrossEntropyLoss需要配ignore_index255否则在验证时也会把255当一类统计mIoU会异常偏低计算mIoU时最好也用np.nanmean因为某些类别在验证集中可能不出现union为0的情况不能直接当成0处理。5. 大图推理、拼接与后处理导出5.1 滑窗推理与概率融合如何做到无痕拼接模型训练完后面对原始的大幅遥感影像推理时不能整张塞进去这时候要用滑窗逐块预测再把结果拼回整幅图。最简单是“推完就拼”但拼出来常有明显的方块效应。更好的方案是做重叠滑窗同一点多次预测对概率取平均再argmax。下面是我常用的完整推理函数def inference_large_image(model, image, window_size512, stride256, num_classes6, devicecuda): model.eval() h, w image.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 - window_size 1, stride): for x in range(0, w - window_size 1, stride): crop image[y:y window_size, x:x window_size] crop_tensor torch.from_numpy(crop.transpose(2, 0, 1)).unsqueeze(0).float().to(device) crop_tensor normalize_tensor(crop_tensor) with torch.no_grad(): logits model(crop_tensor) prob torch.softmax(logits, dim1).squeeze(0).cpu().numpy() prob_map[:, y:y window_size, x:x window_size] prob count_map[y:y window_size, x:x window_size] 1.0 # 处理右下边界不满足步长整数的区域 if h % stride ! 0: y h - window_size for x in range(0, w - window_size 1, stride): crop image[y:y window_size, x:x window_size] crop_tensor torch.from_numpy(crop.transpose(2, 0, 1)).unsqueeze(0).float().to(device) crop_tensor normalize_tensor(crop_tensor) with torch.no_grad(): logits model(crop_tensor) prob torch.softmax(logits, dim1).squeeze(0).cpu().numpy() prob_map[:, y:y window_size, x:x window_size] prob count_map[y:y window_size, x:x window_size] 1.0 prob_map / np.maximum(count_map, 1.0) pred_mask np.argmax(prob_map, axis0).astype(np.uint8) return pred_mask这段代码的核心思想是每个窗口得到一个分类概率分布重叠区域多次累计然后平均。窗口重叠越多边缘痕迹越弱但推理时间也线性增加。一般stride取window_size的一半性价比最高。提示如果你要推理的影像非常大比如几十G的镶嵌影像建议分块读入而不是用imread一次性读进内存。可以用GDAL的ReadAsArray按块读取避免内存直接被打爆。5.2 结果可视化把类别编号转成彩色图预测结果是单通道的类别编号图像直接看是灰蒙蒙的不方便检查。我一般会把它映射成彩色图再和原图叠在一起肉眼评估。def mask_to_color(mask, palette): h, w mask.shape color_mask np.zeros((h, w, 3), dtypenp.uint8) for cls, color in enumerate(palette): color_mask[mask cls] color return color_mask # 以Potsdam类别为例 palette [ [255, 255, 255], # 背景 [0, 0, 255], # 不透水面 [0, 255, 255], # 建筑 [0, 255, 0], # 低矮植被 [255, 255, 0], # 树木 [255, 0, 0], # 汽车 ]把彩色结果保存成TIF或者PNG缩略图叠在原图上就能快速发现模型在哪些区域容易出错。这一步在项目汇报和调优阶段都很重要。5.3 业务化导出从栅格到矢量分割结果如果是栅格很多时候业务同事不一定能直接用。比如分析城市建筑变化更希望得到建筑轮廓矢量。这时候就需要把栅格转换成GeoJSON或Shapefile。最简单的做法是先通过GDAL将预测mask保存为带地理坐标的TIF然后用GDAL的Polygonize算成矢量from osgeo import gdal, ogr def polygonize_mask(mask_path, vector_path): src_ds gdal.Open(mask_path) src_band src_ds.GetRasterBand(1) drv ogr.GetDriverByName(GeoJSON) dst_ds drv.CreateDataSource(vector_path) dst_layer dst_ds.CreateLayer(mask, srssrc_ds.GetSpatialRef()) fld ogr.FieldDefn(class, ogr.OFTInteger) dst_layer.CreateField(fld) gdal.Polygonize(src_band, None, dst_layer, 0) dst_ds None src_ds None这里有个经验如果是城市建筑、道路这类边界清晰的类别矢量化之前先做一个形态学闭运算cv2.morphologyEx或者中值滤波去掉离散噪点矢量轮廓会干净很多。6. 遥感分割项目最容易翻车的五个环节踩坑记录6.1 类别严重不平衡小目标类别学了等于没学最早我用Potsdam跑U-Net时汽车类别的IoU只有0.1不到。原因很清楚汽车在整幅影像里占比极小交叉熵的梯度被背景和建筑淹没。排查链路先按类别统计验证集上的IoU发现IoU低的类别像素占比普遍低于2%。再观察训练集的batch抽样发现汽车样本平均每个batch只占0.3%根本没法形成有效梯度。解决方案是三层一起上损失函数换成CEDice组合让少数类对损失的贡献不再被淹没训练时用类别采样器WeightedRandomSampler让小类别样本被抽中的概率更高数据增强时对包含汽车的裁片做针对性复制增强过采样强制模型多看小目标。最终汽车IoU从0.1提到了0.65左右效果非常明显。6.2 预测图接缝明显滑窗拼接的“补丁感”训练和验证都正常但整图推理出来的结果有非常明显的网格接缝很多模块的颜色在窗口边界处突变。根因是窗口之间没有重叠或重叠太少。模型在窗口中心位置特征最充分窗口边缘的位置相当于“盲人摸象”上下文缺失导致边界预测不一致。解决方法是把滑窗改为重叠滑窗并在重叠区域用加权平均。如果重叠计算还无法完全消除可以在拼接后加一个5×5的中值滤波让边界过渡更平缓。6.3 显存不够一上来就吃爆显卡有次我在12GB的显卡上直接尝试全图推理不用说几秒就OOM了。即便训练时把patch调到512推理时如果用无损滑窗每一块预测都重新建图也会因为多个窗口并行而爆显存。解决思路推理时务必用torch.no_grad()能省下一大块显存不要搞“把整幅大图切完再一次性batch推理”而是边滑窗边预测边释放如果窗口数量太多可以每50个窗口清一次GPU缓存torch.cuda.empty_cache()释放碎片训练时实在显存不够就用梯度累积模拟更大的batch但训练步数和学习率要相应调整。6.4 标签文件和影像错位最隐蔽的坑有次训练曲线完全正常、验证mIoU却一直上不去最后才发现是数据本身的影像和标注错位了几个像素。排查方法很简单随机取出3到5个裁剪块把影像和标注按半透明方式叠加显示肉眼检查边界是否对齐。如果边缘轮廓有一致的偏移八成是裁片时坐标偏移或者读图时通道顺序搞错。另外一个常见原因是cv2.resize标签图时用了cv2.INTER_LINEAR而不是INTER_NEAREST导致标签边界被插值出额外的类别值。查到这个我就把标签resize统一改成最近邻。6.5 近红外波段不会用多光谱数据当普通RGB喂很多遥感数据集自带近红外波段但在实际训练中很多人都直接只用RGB三波段的裁片。一来是图省事二来是模型加载ImageNet预训练权重时输入通道固定是3。但近红外对植被、水体、土壤信息的区分度非常高。我的做法有两种把NIR替换掉RGB中的一个通道常见替代是“RGBN”也就是用R、G、NIR组成伪彩色通道用4通道输入但编码器预训练权重第一层需要特殊初始化可以把预训练卷积权重的3个通道复制一份给第4个通道加速收敛。4通道输入的修改其实不复杂import torch pretrained model.encoder.conv1.weight.data model.encoder.conv1 torch.nn.Conv2d(4, pretrained.shape[1], kernel_size7, stride2, padding3, biasFalse) with torch.no_grad(): model.encoder.conv1.weight[:, :3] pretrained model.encoder.conv1.weight[:, 3] pretrained.mean(dim1)实践结果是在植被和水体类别上RGBN融合比纯RGB的mIoU高出2到4个百分点。多光谱信息用起来之后提升非常值得。6.6 固定随机种子与日志让每个实验都可复现这个不算坑但我建议每个项目从第一天起就在训练脚本开头固定随机种子把每次实验的配置、训练曲线、mIoU都记录到CSV或者JSON文件里。否则一个模型调了几十次参数之后你根本说不清哪个版本精度最高、用了什么超参。分享一个小习惯训练日志文件命名带有实验日期和mIoU缩写比如exp_0411_iou_0.83.json跑完一个实验直接把最优权重和对应的折线图扔进同一个文件夹。这样过两周回来翻一眼就能找到最靠谱的那一版。遥感图像语义分割的全流程要说复杂每一步拆开其实都不算难要说简单数据、训练、推理、后处理每一个环节都有可能让整个项目反复返工。这套用Python实现的全流程方案核心思路就是让每一步都稳、可复现、好排查。如果你正打算跑通自己的第一个遥感分割项目建议先把Potsdam按这套流程完整走一遍再去挑战更大规模的数据集相信能少走不少弯路。