资讯动态

遥感图像语义分割开发实战:从GeoTIFF到地物分类矢量图

发布时间:2026/10/22 15:11:56 来源:尧图企业网站定制
简介遥感图像语义分割开发教程是一份面向地理空间信息处理、深度学习初学者及算法工程师的技术文档系统讲解遥感图像语义分割的核心概念与最新研究进展。文档聚焦Swin TransformerUNet、Samba等新型架构详细说明编码器设计、实验数据集及结果对比并梳理遥感影像与自然场景分割在数据特性、空间分辨率、多光谱通道及评价指标如Kappa系数、F1-score上的显著差异同时介绍基于Pytorch-Unet的多类别分割工程流程包括数据加载、模型训练与结果评估。资源为单个docx文档压缩包仅16KB内容凝练且逻辑完整涵盖传统边缘检测、区域分割方法以及FCN、SegNet、DeepLab等现代深度模型并展望多源数据融合与模型鲁棒性研究方向。文档已有225人学习适合需要快速建立遥感语义分割技术框架、对比主流方法并参考落地应用的研发与学习人员。1. 遥感图像语义分割开发教程从拿到影像到产出一张地物分类图中间的坑比想象多当甲方扔过来一张几个GB的航拍影像要求在三天内把“建筑、道路、水体、植被”逐像素分出来时语义分割模型就成了唯一靠谱的选择。和自然图像分割不同遥感影像的语义分割开发从数据准备开始就处处是暗坑一张大图放不进模型标注矢量模型不认辛辛苦苦训练完推理时又出现拼接缝和坐标错位。这份开发教程要讲的不是某个孤立的网络怎么搭而是完整的实战链路——先讲怎么把一张GeoTIFF切成模型能吃下的patch再讲网络和损失函数怎么选、训练参数怎么调最后教你如何把模型输出拼回带坐标的原图并导出矢量。适合有深度学习基础但没接触过遥感数据、正在接相关项目的开发者。下面内容全部来自一线操作经验直接给参数、代码和排错方法。2. 数据准备把一张GeoTIFF变成模型能吃的训练样本2.1 影像预处理统一坐标系和分辨率这是第一道坎遥感影像通常以GeoTIFF格式存储文件里带着地理参考信息包括投影坐标系、仿射变换参数和像素分辨率。但不同来源的数据差异很大有的是WGS84经纬度坐标系有的是UTM米制还有的是地方坐标系。模型本身只关心像素值但如果后续要切块、推理、拼图、转矢量坐标系不一致会造成根本无法容忍的几何偏移。我一般拿到数据后第一件事就是用GDAL把所有影像重投影到同一个投影坐标系比如UTM某个分区同时把分辨率重采样到训练目标分辨率。import rasterio from rasterio.warp import calculate_default_transform, reproject, Resampling src_path input.tif dst_path input_utm.tif dst_crs EPSG:32650 # UTM Zone 50N北半球中纬度带常用 with rasterio.open(src_path) as src: transform, width, height calculate_default_transform( src.crs, dst_crs, src.width, src.height, *src.bounds) profile src.profile.copy() profile.update(crsdst_crs, transformtransform, widthwidth, heightheight) with rasterio.open(dst_path, w, **profile) as dst: reproject( sourcerasterio.band(src, 1), destinationrasterio.band(dst, 1), src_transformsrc.transform, src_crssrc.crs, dst_transformtransform, dst_crsdst_crs, resamplingResampling.bilinear)这段代码把影像从原始坐标系转到EPSG:32650。这里用的是双线性重采样适合连续地表信息如果要重采样标签栅格必须用Resampling.nearest避免插值改变类别值。还有一个容易被忽略的点当影像和标签图叠加时两者的投影、分辨率和图幅范围必须完全一致否则后面切patch时会出现“图对不上标签”的玄学问题。实际项目中我一般直接要求数据提供方在交付前就统一坐标系和分辨率省得自己在预处理阶段花一整天调参数。但如果对方给不了你就得自己处理上面这段代码是保底方案。2.2 标注数据转换从shapefile到像素掩膜别拿多边形直接训练很多遥感项目的地物标注是用QGIS或者ArcGIS人工勾出来的shapefile或GeoJSON。但深度学习训练需要的是逐像素的标签图每个像素存一个整数类别ID而不是一个多边形。常见做法是用geopandas读取矢量再用rasterio的features.rasterize把几何图形“烧”到与影像同尺寸、同投影的栅格上。import geopandas as gpd import rasterio from rasterio.features import rasterize from PIL import Image shp_path labels.shp img_path input_utm.tif out_mask mask.png gdf gpd.read_file(shp_path) with rasterio.open(img_path) as src: shapes [(geom, class_id) for geom, class_id in zip(gdf.geometry, gdf[class_id])] mask rasterize(shapes, out_shape(src.height, src.width), transformsrc.transform, fill0, dtypeuint8) Image.fromarray(mask).save(out_mask)这里要求shapefile的属性表里有一个整数字段class_id背景类统一设为0。rasterize时fill0表示没有覆盖到的区域全部变成0。注意属性表字段名不要用class因为它是Python内置关键字通过geopandas读取时容易出诡异错误我见过有人在这里卡了一下午。转换完成后一定要在屏幕上把影像和mask叠加肉眼检查一遍。我习惯把mask乘以50显示成灰度图叠加到原图上确认建筑、道路、水体的边界和影像吻合。这一步不做后面训练出来的模型再准也白搭因为训练数据本身就是歪的。2.3 切片patch_size、stride与重叠率怎么设遥感影像动辄几万乘几万像素不可能整幅直接喂给模型。标准做法是切成固定大小的patch比如512×512或1024×1024。这里两个核心参数是patch_size和stride。patch_size决定了模型能看到的上下文范围也决定了显存占用stride决定相邻patch之间的重叠程度。一般重叠率建议在25%到50%之间也就是stride取patch尺寸的0.5到0.75。完全不重叠会带来严重的边界效应目标如果被切成两半模型只能看到半个目标识别准确率明显下降。import numpy as np import rasterio patch_size 512 stride 384 # 重叠率 (512-384)/512 25% count 0 with rasterio.open(img_path) as src: for y in range(0, src.height - patch_size 1, stride): for x in range(0, src.width - patch_size 1, stride): patch src.read(window((y, y patch_size), (x, x patch_size))) # patch shape: (bands, patch_size, patch_size) np.save(ftrain/images/img_{count:05d}.npy, patch) count 1这段代码里stride小于patch_size所以相邻窗口有重叠。如果内存充足可以同时把对应位置的mask也按同样窗口切出来保持文件名一致。训练时如果不想引入重复样本也可以用随机裁剪但推理时一定要用重叠滑窗后面第4章会专门讲。一个实用的参数经验训练时patch_size默认用512显存不够再降到256如果目标是建筑物这种小尺度的地物patch_size建议至少512否则单块影像可能只覆盖到目标的一小部分。分辨率0.5米时512×512对应的地面范围是256米×256米足够覆盖一个中等小区或一片连片农田。如果分辨率是0.1米同样512×512只有51.2米×51.2米上下文信息太少这时候patch_size要提到1024甚至更高。2.4 数据增强与数据集划分别让同一片区域同时出现在训练集和验证集遥感图像的数据增强和自然图像既有相同点也有不同点。相同的是随机翻转、旋转90度、亮度对比度扰动、高斯噪声不同的是遥感影像是俯视视角没有“上下左右”的语义方向区别所以旋转90度的增强对遥感非常有效而且不会造成“倒立的人”一类的语义错误。但在所有增强之上遥感项目有一个最容易忽视的坑划分训练集和验证集时绝不能让同一景影像切出来的patch同时进入两边。因为相邻patch高度相似甚至只有几米的重叠模型在验证集上看到的都是“熟脸”验证精度虚高真正换一块新区域时性能大幅跳水。我通常按景划分一景一个来源目录保证同一景影像的所有patch要么全部在训练集要么全部在验证集。import os import random random.seed(42) all_imgs [f for f in os.listdir(train/images) if f.endswith(.npy)] scene_a [f for f in all_imgs if f.startswith(sceneA_)] scene_b [f for f in all_imgs if f.startswith(sceneB_)] train_imgs scene_a scene_b[:-round(len(scene_b) * 0.2)] val_imgs scene_b[-round(len(scene_b) * 0.2):]这段代码示意按文件名前缀场景划分。实际操作中我建议把每景影像切成单独文件夹用文件列表管理避免内存中维护成百上千个路径。注意随机种子要固定否则每次实验结果无法对比。数据增强可以用现有库如albumentations但别为了刷指标把所有增强都开满遥感影像的亮度扰动幅度要温和因为多光谱数据对亮度变化更敏感。3. 模型选型与训练UNet、DeepLabV3还是SegFormer3.1 遥感语义分割的模型对比精度、显存、速度选型没有唯一答案但可以在项目初期定一个合理基线。遥感影像与自然图像的区别在于地物尺度跨度大道路细长、农田连片、类别不均衡普遍、边缘要求高。下面给出我常用的对比表模型特点建议输入尺寸显存占用512×512适用场景UNet编码器-解码器结构实现简单收敛快256~512低样本少、快速出基线UNet密集残差连接边界细节更好256~512中各类平衡的混合地物DeepLabV3ASPP多尺度空洞卷积感受野大512中高道路、建筑等几何目标SegFormerTransformer编码器全局语义能力强512~1024高大范围土地覆盖分类Swin-UnetTransformer与UNet结合512高精度优先且显存充足我一般建议新手先用UNet跑通整个流程。原因是UNet对训练参数不敏感显存占用小调试排错方便当你把数据、损失函数、评估流程都跑顺畅之后再切换到DeepLabV3或SegFormer来提精度。如果项目可以直接使用预训练权重那么ImageNet预训练可以加速收敛但遥感影像如果是多波段比如近红外、红边需要把首层卷积改造成与波段数匹配预训练权重就不能直接用了。这种情况我习惯直接从头训练配合更小的学习率。3.2 损失函数组合使用Dice和CrossEntropy处理类别不均遥感数据最常见的痛点是类别极度不均衡一景影像里植被占70%建筑只占5%。交叉熵在类别不平衡时会偏向多数类模型会学成“几乎全预测成背景”的偷懒模式。Dice损失直接优化区域交叠比对小目标更友好但单独使用训练过程不稳定。常见做法是把交叉熵和Dice损失加权相加权重比例可以按0.6比0.4或者参照验证集表现调整。import torch import torch.nn.functional as F def dice_loss(pred, target, smooth1.0): pred torch.softmax(pred, dim1) # (B, C, H, W) target_onehot F.one_hot(target, num_classespred.shape[1]).permute(0, 3, 1, 2).float() intersection (pred * target_onehot).sum(dim(2, 3)) union pred.sum(dim(2, 3)) target_onehot.sum(dim(2, 3)) dice (2 * intersection smooth) / (union smooth) return 1 - dice.mean()smooth参数一般设1.0作用就是防止分母为0。注意这里对target做了one-hot转换转换后维度顺序要变成和pred一致。实际训练中我把torch.nn.CrossEntropyLoss(weightclass_weight)和Dice损失按0.60.4加权相加。类别权重可以根据训练集每类像素占比反比设置比如某类只占5%权重就设为20左右。如果类别超过10个手动调权重的边际收益会下降这时候Dice损失的作用会更明显。3.3 训练关键参数batch size、学习率、epoch、优化器参数选择直接影响训练收敛和最终精度我给出一份经过多数项目验证的默认配置参数建议值说明batch size8512×512输入显存不足降到4或2初始学习率1e-3SGD/ 1e-4AdamW使用预训练权重时用1e-4学习率策略poly衰减或cosine退火不建议固定学习率epoch50~150以验证集最优为准优化器AdamW / SGD momentum 0.9AdamW省心但需配权重衰减输入分辨率512×512像素分辨率越高可适当增大一个训练循环的骨架optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) for epoch in range(epochs): model.train() for imgs, masks in train_loader: imgs, masks imgs.cuda(), masks.cuda() logits model(imgs) # (B, C, H, W) loss 0.6 * ce_loss(logits, masks) 0.4 * dice_loss(logits, masks) optimizer.zero_grad() loss.backward() optimizer.step() # 每个epoch结束后在验证集上计算mIoU val_miou evaluate(model, val_loader) if val_miou best_miou: torch.save(model.state_dict(), best_model.pth)学习率策略推荐poly衰减lr base_lr * (1 - iter / total_iters) ** 0.9。如果用了预训练权重初始学习率再除以10。epoch数不要死板看验证损失是否连续10个epoch不再下降提前停止。这里最容易犯的错误是学习率过大导致训练发散表现是loss在训练初期不降反升。我一般首训用1e-4保守起步多看几个batch的loss趋势再调整。3.4 训练监控mIoU、F1、Kappa以及保存最优模型训练监控指标中mIoU最常用但遥感项目里我建议大家额外看每个类别的IoU。整体mIoU好看不代表小类好比如水体占20%建筑占5%可能总体mIoU 0.78建筑IoU只有0.4。多分类分割时还会看macro F1和Kappa系数。F1是精确率和召回率的调和平均Kappa反映预测与真实标签的一致程度。但不管用哪个指标我们都应该保存验证集上表现最好的那个模型而不是最后一个epoch的模型。def calculate_iou(pred, mask, num_classes): ious [] for cls in range(1, num_classes): p (pred cls) t (mask cls) inter (p t).sum().float() union (p | t).sum().float() ious.append((inter / (union 1e-6)).item()) return np.mean(ious)这段代码计算每个类别的IoU然后取平均。注意这里从类别1开始把类别0当作背景。如果背景区域很大背景IoU几乎总是很高把背景含进平均会掩盖前景问题。我把每个类别的IoU记录到训练日志里形成表格每5个epoch打印一次这样可以清楚看到哪个类别一直在拖后腿。发现某个类别IoU偏低我就去检查它的样本量、标注一致性、以及是否被增强误伤。4. 推理与后处理把模型输出拼回整幅影像4.1 滑窗推理与重叠拼接消除拼接缝的两种方案训练完成后模型要处理的是几万像素的大影像。推理时同样使用滑窗但为了不出现明显的拼接缝必须采用重叠滑窗并做加权平均。常见做法是推理时stride取patch_size的一半或四分之一重叠区域被预测多次最后取平均概率。窗口边缘往往比中心预测置信度低因为边缘处目标可能被截断所以一些方案会对窗口加一个平滑权重例如中心权重高、边缘权重低。最简单的实现就是多次平均效果已经足够。def predict_full_image(model, img, patch_size512, stride256, num_classes5): H, W img.shape[1], img.shape[2] prob np.zeros((num_classes, H, W), dtypenp.float32) count np.zeros((H, W), dtypenp.float32) for y in range(0, H - patch_size 1, stride): for x in range(0, W - patch_size 1, stride): patch img[:, y:ypatch_size, x:xpatch_size] with torch.no_grad(): out torch.softmax( model(torch.tensor(patch).unsqueeze(0).cuda()), dim1 ).cpu().numpy()[0] prob[:, y:ypatch_size, x:xpatch_size] out count[y:ypatch_size, x:xpatch_size] 1 prob / np.maximum(count, 1e-6) return np.argmax(prob, axis0)count数组统计每个像素被预测的次数最后用它做平均。注意当影像宽高不是patch_size的整数倍时最右边和最下边会漏掉。处理方法是在边缘做镜像padding推理后再裁掉或者把窗口起点范围扩展到超出边界。我再强调一点推理时的patch大小最好和训练时保持一致如果用不同尺寸模型特征统计会变化精度可能下降。4.2 后处理用形态学去除碎斑必要时上CRF模型的输出常常带有椒盐噪声和边界毛刺。一个低成本的后处理是先用开运算去除小噪点再用连通域分析删除面积过小的碎块。对于建筑物和道路这类形状较规则的目标这样做能显著提升矢量结果的质量。import cv2 import numpy as np mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8)) n_labels, labels, stats, centroids cv2.connectedComponentsWithStats(mask, connectivity8) min_area 100 # 按像素数需根据分辨率换算 for label in range(1, n_labels): if stats[label, cv2.CC_STAT_AREA] min_area: mask[labels label] 0这段代码里min_area需要结合实际分辨率设定。0.5米分辨率下100像素对应25平方米适合去除小于25平方米的孤立噪声如果分辨率是2米100像素就对应400平方米那就要调整到20甚至更小。CRF能够进一步优化边界但对超大影像计算量太可怕我一般只在最终验收输出前对重点区域使用而且也只对前景类做局部细化。4.3 从栅格到矢量导出自带地理坐标的分类结果项目交付通常需要矢量多边形而不是一张PNG图。GDAL的Polygonize函数可以把栅格掩膜转成矢量并保留原影像的投影信息。输出格式可以用GeoJSON或ShapefileGIS软件都能打开。from osgeo import gdal, ogr src_ds gdal.Open(mask.tif) prj src_ds.GetProjection() dst_ds ogr.GetDriverByName(GeoJSON).CreateDataSource(result.geojson) layer dst_ds.CreateLayer(result, srsogr.osr.SpatialReference(wktprj)) gdal.Polygonize(src_ds.GetRasterBand(1), None, layer, 0, [], callbackNone)Polygonize会把每个互不连通的区域变成一个独立的多边形。如果原始mask有很多小碎斑转出来的矢量文件也会有成千上万个面所以必须在Polygonize之前做面积过滤。矢量化之后我通常还要用SimplifyPreserveTopology做一次简化去除锯齿边界。这样可以交付一个干净、符合制图习惯的图层。5. 遥感语义分割避坑指南五个高频问题的现象、原因与解法5.1 小目标漏得不忍直视现象验证时整体mIoU看着有0.7但单独看建筑物、独立树这些类别IoU只有0.3左右分割图上前景目标大量缺失。原因遥感数据里小目标在图像中占比太小交叉熵损失被大面积背景主导模型为了把整体损失压低倾向于把所有像素都预测成背景。解决给每个类别加权重按训练集类别像素占比反比设置class_weight在损失函数中把Dice和交叉熵以0.4比0.6组合。训练过程中按类别IoU监控一旦发现某个类别连续多个epoch不涨立刻调整权重或增加该类别的标注样本。5.2 推理拼接缝明显现象把模型预测结果拼回整幅影像后图上出现规律的网格线网格尺寸和patch大小一致看起来像“马赛克”。原因推理时stride等于patch_size没有任何重叠模型在每一个窗口边缘的预测不连续尤其是目标被切成两半时两边的预测往往不一致。解决推理时把stride设成patch_size的一半重叠区域多次预测并平均。如果还不能完全消除可以尝试在推理时对每个patch做小规模翻转预测再平均也就是测试时增强。这个方法代价是推理时间成倍增加。5.3 训练时CUDA OOM现象batch跑到第几步就报CUDA out of memory程序直接崩掉。原因多半是patch_size设得太大、batch size太大或者分割模型本身占用显存高。特别是SegFormer这类Transformer显存占用随输入尺寸非线性增长。解决先确认批量大小batch size降到2试试如果还不行再降patch_size从1024降到512。也可以用梯度累积把多步的梯度累加起来再更新参数等效batch size变大但不增加显存。另外开启混合精度训练AMP也能省一半显存代价是网络某些算子的精度轻微损失但绝大多数情况下不影响最终精度。5.4 输出结果和原始影像错位几百米现象把预测掩膜写成GeoTIFF丢进GIS软件里叠加到底图上发现分类结果浮在了一片空地和影像完全对不上。原因推理时处理的是像素数组但没有把原图的地理变换信息和投影信息写到输出文件里或者输入影像经过重投影后mask没有同步采用相同的transform和crs。解决保存mask时直接用原影像的transform和crs写入GeoTIFF。不要自己猜测参数更不要用默认的transform。代码里我习惯这样写profile src.profile.copy() profile.update(dtyperasterio.uint8, count1) with rasterio.open(mask.tif, w, **profile) as dst: dst.write(mask, 1)这样输出的mask和原影像严格像素对齐GIS里打开就是准确叠上的。5.5 标签和影像明明来自同一范围但对不齐现象训练时loss一直降得很低但可视化预测结果和原图错位几个像素边界歪歪扭扭甚至出现的“应该属于建筑的像素”跑到了路上。原因标注人员在勾画矢量的底图与切割模型的影像不是同一版本或者底图经过地理配准但影像没有导致两者存在几十厘米到几米不等的系统性偏移。解决在标注前就把影像和现有矢量数据叠加检查确认边界重合。如果只有整体平移在GIS里直接平移矢量如果有旋转或非线性形变就需要控制点配准。所有样本切片前用前一节的方法把mask和影像严格对齐再切切完后还要随机抽查几张叠加显示确认像素级重合。6. 进阶用IoU排序表和透明叠加图验收你的遥感分割模型项目交付前光看一个总体mIoU是远远不够的。我现在的习惯是训练结束时先跑一遍验证集输出每个类别的IoU、F1和Kappa做成一张排序表。哪一类排在最后就去看这一类的预测结果和真实标签叠加找出是漏检、误检还是边界太粗。代码可以用这一小段实现import numpy as np from sklearn.metrics import confusion_matrix def per_class_iou(pred_all, mask_all, num_classes): cm confusion_matrix(mask_all.flatten(), pred_all.flatten(), labelslist(range(num_classes))) ious {} for c in range(num_classes): inter cm[c, c] union cm[c, :].sum() cm[:, c].sum() - inter ious[c] inter / (union 1e-6) return ious注意这里mask_all和pred_all是拼接后的完整数组类别要包含背景0。列出排序表后我会再做一张透明叠加图预测结果用半透明色覆盖在原图上肉眼扫一眼建筑边缘是否贴合、道路有没有断开、水体有没有漏掉。这一步的成本很低但能发现所有数值指标都发现不了的细节问题。之前有一次项目验收整体mIoU达到0.82但甲方一眼看出阴影区域里的小块建筑被全部漏了。我回头才发现那类样本在训练集里占比不到1%损失函数对各种杂技参数都救不回来。现在每次训练结束我都会先看类别IoU排序再看可视化两头都不放过。这个习惯帮我挡掉了不少翻车现场。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑