简介:这是一份关于遥感图像语义分割技术的入门与进阶教程,适合从事地理信息解译、城市规划、环境监测等方向的研究生、工程师和开发人员参考。资源以 docx 文档形式呈现,共 1 个文件,压缩包大小 16KB,内容紧凑但体系完整。内容系统梳理了 Swin Transformer + UNet、Samba(Mamba 编码器)等前沿方法与传统 CNN 的对比,并结合 LoveDA 数据集给出了实验表现;同时明确了遥感影像与自然场景分割在数据特征、技术挑战和评价指标(Kappa、F1-score)上的差异,以及基于 PyTorch-Unet 实现多类别分割的完整步骤。此外还综述了从边缘检测、区域分割到 FCN、SegNet、DeepLab 的传统与现代方法,覆盖数据预处理、模型选择、后处理及土地覆盖分类、灾害评估、农业监测、城市规划等典型应用。目前已有 224 人学习下载,可作为快速了解该领域研究脉络与实践要点的参考材料。
1. 遥感图像语义分割开发:一份教程文档背后的完整工程链路
拿到一份标题为“遥感图像语义分割开发教程.docx”的资料,多数人的第一反应是翻到网络结构章节,看模型代码怎么搭。但真实项目里,模型结构反而是最不值得卡住的地方——U-Net、DeepLabV3+、SegFormer哪个都能用,真正决定项目成败的是数据链路:原始遥感影像怎么标注、矢量标签怎么转成像素级mask、大图怎么切块进模型、分割结果怎么拼回去并带坐标输出。这篇文章不按文档目录复述原理,而是按实际开发顺序讲,覆盖选型、数据集制作、训练参数、高频踩坑和部署验证,适合手里有一批遥感影像、想独立完成语义分割开发的工程师,也适合刚入门就被数据格式和标注问题卡住的新手。
2. 开发前先把选型定下来:模型、框架与遥感数据格式的决策点
2.1 遥感图像分割和自然图像分割:边界在哪
遥感图像语义分割的目标是逐像素做类别预测,比如把建筑物、道路、水体、植被、裸地分开,和自然图像语义分割在任务定义上完全一致。但工程上两者的差别非常明显,如果按自然图像那套路子直接套,后面处处碰壁。
第一是尺度差异。自然图像分割数据集里,目标通常占图幅的5%到50%,而遥感影像是几千乘几千像素的大图,一栋房子可能只占几十个像素,一片农田却可能占上万像素。这种极端尺度跨度对模型的特征金字塔和感受野设计要求更高,常规分割模型不经调优很难同时照顾好大小目标。
第二是波段差异。自然图像几乎清一色是RGB三通道,遥感影像常见的是RGBN四个波段,也就是红绿蓝加近红外。多一个近红外波段,植被、水体、裸地的区分度会显著提升。高光谱数据更是几十上百个波段,数据读取和预处理逻辑完全不同。
第三是坐标系。自然图像语义分割输入输出都是普通图片,但遥感分割的成果通常要落成带地理参考的GeoTIFF,套进GIS系统做专题图、面积统计和变化检测。这意味着数据读取、推理输出都需要维护一套坐标信息,从训练到部署这条链路始终不能丢。
这三个差异直接决定了下面所有选型:模型输入通道数按波段数定,数据读取用rasterio而不是OpenCV,推理输出用GeoTIFF而不是PNG。选型的第一步不是挑模型,而是把数据源摸清楚。
2.2 语义分割模型怎么选:U-Net打底,还是直接上SegFormer
模型选型我给团队的建议很直接:第一次做遥感图像语义分割,数据量在几千张切片以内,用U-Net打底。U-Net结构简单、训练稳定、显存占用低,单张普通显卡就能跑,对中小规模数据集非常友好。用现成的segmentation_models_pytorch库(简称SMP)可以很快搭出来:
import segmentation_models_pytorch as smp model = smp.Unet( encoder_name="resnet34", encoder_weights="imagenet", in_channels=4, # RGBN四个波段 classes=5, # 背景 + 4类地物 )这段代码有三个参数值得说明。in_channels必须和你的影像波段数一致,读进来是四波段就把4填进去,填成3会在第一个forward就报错,而且这种错误往往在数据读取阶段才会暴露。encoder_weights用ImageNet预训练权重对RGB部分有帮助,近红外通道没有对应预训练权重,效果等于随机初始化,不影响收敛,只是前面几个epoch会慢一点。classes按任务类别数算,包括背景类。
当数据量上到一万张以上、类别超过七八类时,再考虑DeepLabV3+或SegFormer这类更强骨干的模型。DeepLabV3+用空洞卷积扩大感受野,对农田、水域这类大面积地物的边界更干净;SegFormer走Transformer路线,对道路、河流这类细长地物的连续性建模更好。
但我不建议一上来就追大模型。一个血泪经验:如果数据标注本身有问题,用复杂模型时你很难判断是模型不收敛还是数据错误,排查成本会成倍增加。先用U-Net把全流程跑通,确认数据没问题之后,再升级模型换精度,这个顺序能省下大量调试时间。
2.3 框架与数据格式:多波段GeoTIFF是一道绕不开的门槛
框架上,PyTorch是当前语义分割项目的事实标准,SMP和各个预训练模型库都在PyTorch这边,资料和排错经验最全。另外一个选择是PaddleSeg,遥感场景也有专门支持,但社区规模和第三方工具链还是PyTorch更成熟,遇到问题搜一圈基本都有答案。
遥感影像数据格式的坑在波段和坐标上。遥感影像一般是GeoTIFF,用OpenCV直接读会出问题:多波段TIFF读进来通道顺序可能不对,16bit的像素数据类型也可能被当成8bit处理。正确做法是用rasterio:
import rasterio import numpy as np with rasterio.open("scene.tif") as src: # 读取前四个波段,返回shape为(4, height, width) img = src.read([1, 2, 3, 4]) img_tensor = torch.from_numpy(img).unsqueeze(0).float() # 保存地理变换信息和坐标系,推理结束后写回结果要用 transform = src.transform crs = src.crs这里src.read([1, 2, 3, 4])的列表是波段编号,从1开始,不是从0开始,这是GDAL系API的共同习惯。src.transform保存的是仿射变换参数,src.crs保存坐标系,比如WGS84或UTM。这两样东西在推理结束、把预测结果写回带坐标的GeoTIFF时是必须的。很多项目训练时只关心像素,结果输出时发现成果在GIS软件里对不上原图,就是因为没有把transform和crs一路带过来。
另外要留意数据类型。很多公开遥感数据集把影像转成8bit RGB的PNG发布,方便直接喂给预训练模型。但原始卫星影像往往是16bit,如果你的业务数据是16bit,别急着归一化到0到255就完事,最好先查一下影像的统计值和直方图,按2%到98%分位数做拉伸,否则某些地物在输入图像里会变成一片死黑或死白,模型根本学不到特征。
3. 遥感图像数据集制作:从矢量标注到训练切片的完整流程
3.1 标注工具与标注规范:矢量标注比逐像素涂色靠谱得多
遥感图像标注是数据链路里最耗时也最容易返工的环节。直接打开影像用像素级画笔对着屏幕涂,效率极低,边界也画不准,几百张大图能让人崩溃。行业里通行的做法是矢量标注:先勾画地物多边形,再从矢量生成栅格标签。
工具选型要看数据有没有地理坐标。如果拿的是GeoTIFF,QGIS是首选,它原生支持遥感影像显示和矢量编辑,标注结果直接存成GeoJSON或Shapefile,坐标是准的,不用二次对齐。如果是快速原型验证、数据量不大且没有严格坐标要求,Labelme也够用,但它存的是像素坐标,和影像的映射关系需要手动维护。团队多人协作、数据量大的时候,可以搭建Label Studio做分块的协同标注,不过前期的服务部署和账号配置也要花时间。
标注规范要在动手前定死,否则返工成本极高。我一般会定三条硬性规矩:第一,每个类别一个独立图层,或者用统一的属性字段存类别编号,禁止在同一个图层里混着改;第二,类别编号从1开始,0固定留给背景或未标注区域,这样后面转mask时语义清晰;第三,每一类地物在图层属性里写上中文类别名和编号的对应表,单独导出一份CSV存着。这些看起来是小事,但项目做到中途要加类别或查数据质量时,一套清晰命名和属性规范能省下大把时间。
3.2 矢量转mask:gdal_rasterize和rasterio两条路的参数细节
标注完矢量后,要把矢量转成和原图尺寸完全一致的像素级mask。QGIS里可以手动导,但批量化处理必须走命令行或脚本。最常用的是GDAL的gdal_rasterize工具:
gdal_rasterize -l landcover -a class_id \ -ts 8192 8192 -ot Byte -burn 0 \ landcover.geojson label.tif四个关键参数拆开讲:-l指定图层名,要和GeoJSON内部的图层name字段完全一致,大小写敏感,配错了会报“layer not found”;-a指定用哪个属性字段作为输出像素值,比如GeoJSON每个feature里存了class_id字段,值就是0到N的类别编号;-ts指定输出栅格的宽高像素值,注意它先写入的是宽(列数)再是高(行数),和很多命令行工具的row col顺序相反,这里最容易搞反;-ot Byte把输出类型限制为uint8,mask用Byte足够,千万别用Float64,训练时读起来内存爆炸。
如果要在Python流程里动态控制对齐关系,用rasterio.features.rasterize更顺手,可以直接用原图的transform:
import rasterio from rasterio.features import rasterize from shapely.geometry import shape with rasterio.open("scene.tif") as src: height, width = src.height, src.width transform = src.transform geoms = [] values = [] for feature in geojson_data["features"]: geoms.append(shape(feature["geometry"])) values.append(feature["properties"]["class_id"]) mask = rasterize( zip(geoms, values), out_shape=(height, width), transform=transform, fill=0, dtype="uint8" )这段代码里out_shape的元组顺序是(height, width),也就是先行后列,和上面gdal_rasterize的-ts顺序正好相反,非常容易混。zip(geoms, values)把多边形的几何对象和类别值配对传给rasterize,fill=0表示没有矢量的区域填背景0。这样生成的mask和原图严格逐像素对齐,不需要二次变换。
3.3 滑窗切片:把大影像切成模型能吃的尺寸
遥感影像动辄几千像素,直接整图进模型既不现实也没必要。常规做法是滑窗切片,把大影像切成256×256或512×512的patch。两种策略:无重叠均匀切割适合地物分布均匀的场景,省计算量;带重叠切割适合地物尺度大或希望每个地物尽量完整落在窗口内的场景,重叠率一般取25%到50%。
批处理脚本常见写法是:
python split_remote_sensing.py \ --input scene.tif --label label.tif \ --patch-size 512 --stride 256 \ --output ./train_data这个组合表示窗口512×512、步长256,也就是50%重叠。步长和窗口尺寸的比例会影响样本冗余程度。步子太小会让相邻patch几乎一样,训练数据高度相关,模型容易过拟合到重复模式;步子太大又可能把关键地物切在窗口边缘造成目标不完整。我一般默认stride取patch_size的一半,再根据地物尺度微调。
切完的patch必须按影像名和坐标位置命名,比如scene_00012_col1024_row2048.tif。这样可以随时回溯某个patch在原图中的位置,排查问题时非常有用。很多项目切完图不命名,几百个patch混在一起,后面做精度验证或坐标恢复时还得重新对齐原图,纯属给自己挖坑。
4. 训练语义分割模型:损失函数、优化器与遥感数据的调参经验
4.1 损失函数怎么配:交叉熵和Dice的组合思路
遥感语义分割里损失函数选择直接影响收敛方向。单独用交叉熵,在类别不均衡时容易把占比小的类别全部预测成背景;单独用Dice损失,训练初期梯度波动大,loss曲线容易震荡。工程上最常见的做法是把两者加起来,既保留交叉熵的稳定收敛特性,又用Dice损失把优化目标拉到和IoU直接相关。
SMP库里直接提供了组合方式:
import torch.nn as nn import segmentation_models_pytorch as smp loss_fn = smp.losses.DiceLoss(mode="multiclass") ce_fn = nn.CrossEntropyLoss() def combined_loss(pred, target): return ce_fn(pred, target) + loss_fn(pred, target)DiceLoss的mode="multiclass"表示输入logits不做softmax,直接算多分类Dice;交叉熵也用logits版本,训练时模型最后不加softmax。两个损失默认权重各占一半,实际项目里类别不均衡严重时我会把Dice权重调到1.5到2倍,让模型更关注少数类。不需要在损失函数里再做one-hot转换,PyTorch的交叉熵自带处理,SMP的Dice也接收类索引形式的target。
4.2 学习率、batch size与优化器:遥感训练的不成文规矩
优化器首选Adam,初始学习率从1e-3到1e-4之间调。遥感分割任务有个特点:标注边界通常比自然图像粗糙,人工勾画的建筑物边缘和你想要的理想边界一定有偏差,损失函数里天然带着噪声。所以学习率宁可保守一点,我习惯用1e-4起手,配合余弦退火调度器。
batch size设定上,如果512×512的patch加RGBN四通道,batch为8时一张普通24G显存的卡基本能跑U-Net。显存不够时优先减batch size而不是降分辨率,因为降分辨率会改变地物的相对尺度,影响分割精度。这里有一个参数联动关系:learning rate和batch size是绑定的,batch size减半后学习率最好也相应调低,否则梯度估计的噪声变大,收敛会变慢甚至震荡。
优化器里还有一个容易被忽略的参数是weight decay,我一般设1e-4到1e-5。遥感数据量如果不大,weight decay能压一压过拟合,但设太大会让模型学不动,判断标准是训练集和验证集loss差距超过0.1时优先调这个参数。
4.3 数据增强与类别不均衡:让模型学地物特征而不是学采样偏差
遥感图像增强策略和自然图像不同。常规的随机翻转、90度旋转、颜色抖动对遥感分割都有效,因为地物识别的语义不依赖方向。但要注意三点:不要做任意角度的缩放,遥感地物有实际物理尺寸,过度缩放会破坏模型对尺度的判断;光照变化增强可以做但要节制,模拟的是不同时相的亮度差异,色相大改会让模型学到不真实的颜色分布;裁剪是遥感里最有效的增强,随机裁剪512像素窗口等于让模型在不同位置采样,天然带了位置多样性。
类别不均衡是遥感分割的常态,道路、管道、电线这类目标占比可能只有训练集的2%到3%。除了在损失函数里加权,我还建议做采样层面的处理:
from torch.utils.data import WeightedRandomSampler class_counts = [100000, 5000, 20000, 8000, 3000] # 每类像素数 weights = 1.0 / torch.tensor(class_counts, dtype=torch.float32) sample_weights = [weights[label].item() for label in all_labels] sampler = WeightedRandomSampler(sample_weights, num_samples=len(sample_weights))这里思路是对样本加权采样,让包含少数类的patch有更高概率被抽中。比单纯调损失权重更稳,因为损失权重只改变梯度比例,而采样层面直接把少数类样本的出场率提上来。两者结合使用,尤其当少数类的IoU卡在个位数百分比时,优先从采样层面解决,往往比换损失函数效果更直接。
5. 遥感图像语义分割避坑指南:五个高频翻车点与排查记录
5.1 多波段通道顺序错乱,loss照降但验证集IoU永远上不去
现象:训练loss正常下降,验证集loss也降,但验证IoU始终在低位徘徊,怎么调参都没用。
原因:波段顺序错了。比如原始影像波段顺序是BGRN,预处理时按RGBN读入,模型学到的颜色分布完全乱套。更隐蔽的是,同一个项目里换了一张影像,波段顺序不一致,训练集和验证集里同一类地物的特征表达不同。
解决:在数据读取脚本里写死波段顺序,并加一个可视化检查脚本,随机抽batch输出一张RGB预览图和对应mask覆盖图,人眼确认颜色和地物对应正确。
5.2 显存OOM不再怕,梯度累积和混合精度的组合方案
现象:512×512输入、batch_size设置为8时直接OOM,降到4还是偶尔爆,训练频繁中断。
原因:遥感影像多波段输入比三通道RGB在显存上多出约30%开销,加上模型中间特征图,显存消耗比自然图像项目大得多。
解决:先用混合精度训练,PyTorch的amp模块直接启用。如果还爆,用梯度累积,保持大batch的等效学习效果。
scaler = torch.cuda.amp.GradScaler() accum_steps = 2 # 梯度累积步数 for i, (img, label) in enumerate(dataloader): with torch.cuda.amp.autocast(): pred = model(img) loss = combined_loss(pred, label) / accum_steps scaler.scale(loss).backward() if (i + 1) % accum_steps == 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad()这里把loss除以accum_steps再backward,等效于把batch size翻倍,但显存占用不变。注意batch size变化后学习率需同步调整,梯度累积场景相当于扩大了batch,学习率可以略微调大一点。
5.3 推理拼图的接缝效应:重叠推理与加权平均的解法
现象:大图推理完拼回去,输出图上有明显的网格状接缝,地物边界在接缝处被硬生生截断。
原因:滑窗推理时窗口边缘信息不足,模型对边缘像素的预测置信度本来就低于中心区域,拼接时直接各取各的,边界自然出现裂缝。
解决:推理时也用带重叠的滑窗,对重叠区域做加权平均。靠近窗口中心权重大,靠近边缘权重小,接缝处的预测就会平滑过渡。
weight = np.ones((patch_size, patch_size), dtype=np.float32) # 中间权重高,边缘按线性衰减,生成一个对称权重模板 for i in range(patch_size): weight[i, :] = min(i, patch_size - i) / (patch_size // 2)5.4 输出结果和原图对不齐,地理参考信息必须一脉相承
现象:预测结果在GIS软件里打开,和原始影像叠加后整体偏移或无法套合。
原因:推理输出时没有保存原始transform和crs,直接拿numpy数组写成普通PNG或新TIFF,丢失了地理坐标信息。
解决:从读图到输出始终使用rasterio,把原始transform和crs传入输出文件。
with rasterio.open( "result.tif", "w", driver="GTiff", height=result.shape[0], width=result.shape[1], count=1, dtype="uint8", transform=transform, crs=crs ) as dst: dst.write(result, 1)这段代码的transform和crs正是训练前读原图时保存的那两个变量。整个流程只要坚持读图用rasterio、写图用rasterio,坐标信息就不会丢。最怕的是中间某一步把数组存成npy或PNG,那一步以后所有坐标信息就全断掉了。
5.5 训练loss出现NaN,多半是标签里有超出类别范围的像素
现象:训练到某个epoch突然loss变成NaN,重启后能跑几步又变NaN。
原因:标签mask里有超出类别范围的像素值。比如标注时背景0、类别1到4,但mask里存在像素值255(QGIS导出时未标注区域默认值),CrossEntropyLoss在计算时遇到类别索引超出输出通道数,梯度直接变NaN。
解决:在数据集加载时过滤或重映射标签,把255统一替换成0,并统计mask的取值集合。
unique_vals = np.unique(label) assert set(unique_vals).issubset({0, 1, 2, 3, 4}), f"发现非法类别: {unique_vals}" label[label == 255] = 0 # 将未知区域当背景处理这个检查在每次数据集加载时都做,可以把标注错误挡在训练之前。很多时候不会是255,也可能是231这种随机值,用assert提前炸出来,比让模型跑到一半做黑匣子发疯要省心得多。
6. 验证与部署:用IoU量化效果,把分割结果接进业务
6.1 IoU的正确计算方式,别被mIoU骗了
验证阶段最常用的是IoU,但要分清两个口径。逐类IoU只算某一类,mIoU是各类IoU的平均值。如果类别不均衡严重,mIoU容易被占比较高的类别主导,少数类单独看才真实。我一般在验证脚本里同时输出每类的IoU和mIoU,重点盯少数类的IoU有没有改善。
def compute_iou(pred, target, num_classes): ious = [] for cls in range(num_classes): pred_mask = (pred == cls) target_mask = (target == cls) intersection = (pred_mask & target_mask).sum() union = (pred_mask | target_mask).sum() iou = intersection / union if union > 0 else 0.0 ious.append(iou) return ious, sum(ious) / len(ious)这个实现只适合验证脚本里小规模用,大规模数据集上用混淆矩阵方式更高效。计算IoU时注意union可能为0的情况,尤其是某类在验证集里完全没出现,此时该类的IoU要特殊标记,不要直接计0,否则mIoU会被拉低得毫无意义。
6.2 推理工程化:把单张影像的处理流程固化下来
训练完成后,要做的是把推理流程固化成稳定脚本,而不是在notebook里一步步点。我一般会把推理做成三步管线:读图取transform和crs、滑窗推理并拼回整图、写GeoTIFF输出。整条管线都要在收到新影像时不需要人工干预就能跑完。
推理脚本里最容易出问题的是设备切换——假设机器上有GPU就放在cuda上,没有就回退到CPU。这看起来基础,但生产环境里换个机器跑挂掉的情况很多。
最后是模型导出。PyTorch模型在部署时一般转成ONNX,用ONNX Runtime推理。导出时固定输入尺寸能让转换更稳定,遥感分割项目输入尺寸固定为训练时的patch大小即可,整图推理交给滑窗逻辑处理。
做遥感图像语义分割,我自己的习惯是始终把数据链路放在模型前面。模型不收敛可以调,数据错了就是白训一个版本。刚开始做这个方向时,我也曾经花了三周在模型结构上换来换去,后来发现数据mask有问题才是IoU上不去的根本原因。现在每次拿到新数据,第一件事是用可视化脚本抽几张图,把影像和标签叠在一起人工过一遍,这个习惯帮我避开了绝大多数白费功夫的情况。希望这篇内容能帮你在遥感图像语义分割开发上少走几条弯路,把时间花在真正见效的地方。
本文还有配套的精品资源,点击获取