遥感影像分割这块,我折腾了快六年,从最早手写 NDVI 阈值、到后来拿 U-Net 一帧一帧标样本训练,再到这两年被 Segment Geospatial 这种把「分割任意模型」包进 Python 工作流的工具改写流程,感受挺复杂。传统路子不是不行,而是太贵——贵在标注、贵在调参、贵在换个区域就崩。而分割任意模型(Segment Anything Model 这一类零样本分割模型)给了一个新选项:不用训练,给点提示就能出掩膜。问题是,把这个模型直接怼到地理空间数据上,八成第一张图就翻车:16 位深影像被压成一片黑、上万像素的整景被缩到 1024 边长导致小目标全丢、输出掩膜没有坐标系飘到隔壁测区、多边形面积算出来是「平方度」。Segment Geospatial 这个 Python 库干的活,本质就是把这些坑一个个填平——读栅格、切瓦片、反算地理坐标、矢量化导出、批量跑。这篇写给三类人:做遥感解译和地类提取的、做 GIS 数据生产的、以及想用零样本分割模型快速验证想法的开发者。基础弱一点也能照着跑通,我会把每个参数为什么这么设讲清楚。
1. 遥感影像为什么不能直接把分割任意模型怼上去
1.1 传统流程的三个成本瓶颈,逼着人找新路子
先说清楚痛点,不然没法理解为什么值得换工具。传统遥感分割流程走的是「选样—训练—推理—后处理」四段式,第一段就劝退很多人:一个县级行政区的地物提取,要覆盖耕地、林地、建设用地、水域、裸地几大类,每个类目在不同光照、不同季相、不同地形下表现都不一样,合格的训练样本轻松上千个图斑,还得人工勾绘边界、保证纯净度。这笔人力成本在小项目里根本摊不平。
第二个瓶颈是模型本身。U-Net、DeepLab 这类语义分割网络,超参敏感度极高,学习率、批大小、损失函数权重、数据增强策略,任何一项调不好,结果就是「训练集 IoU 0.92、验证集 0.61」的经典过拟合。而且模型和区域强绑定,南方水田训出来的权重拿到西北旱区,精度直接掉一大截,等于每个新项目都要重来一遍。
第三个瓶颈是交付节奏。很多业务场景要的是「今天给我这块地的图斑」,不是「三个月后给你一个模型」。零样本分割模型最大的价值就在这:不需要训练集,给几个提示点或者一个框,几秒钟出掩膜。它当然不可能一步到位替代精调模型,但作为快速摸底、样本预标注、变化初筛的工具,性价比高得离谱。我现在的习惯是先用它跑一遍出初稿,再人工修,整体工时能压到原来的三成左右。
1.2 模型的三个「水土不服」,是踩坑的根源
分割任意模型是为自然图像设计的,它的假设和遥感影像的实际状况有几处硬冲突,不理解这几点,后面所有参数都设不明白。
第一处是输入尺寸。这类模型内部会把输入图像的长边缩放到固定边长(常见是 1024),短边补零。你把一张 12000×12000 像素的正射影像整张喂进去,缩放比例是 0.085,原本 0.5 米分辨率的影像等效分辨率变成接近 6 米。一辆车、一条田埂、一栋独立小楼,在缩放后只剩几个像素,模型的编码器根本提取不到有效特征,输出要么是空的,要么是一大片糊在一起的掩膜。所以整景必须切瓦片,这是硬性要求,不是「内存够就能整张跑」的问题。
第二处是波段与位深。模型吃的是 8 位三通道 RGB。遥感影像常见的是 16 位、四波段(含近红外)甚至多光谱几十个波段。你要是直接把 DN 值 clip 到 0–255,得到的图几乎全黑——因为很多传感器的有效 DN 值集中在几百到一两千这个区间,映射完就剩一片深灰。正确做法是按百分比做线性拉伸,比如取 2%–98% 分位数拉伸到 0–255,让地物对比度出来。波段组合也得挑,真彩色(红、绿、蓝)对建筑物、道路友好,假彩色(近红外、红、绿)对植被和耕地边界更敏感,这个选择直接决定后面分割的质量上限。
第三处是坐标系。模型输出的是像素索引的布尔掩膜,它不知道地球是圆的。从掩膜到可交付的矢量图斑,中间必须走一次仿射变换,把像素行列号换算成地图坐标。这一步用错 transform、或者栅格和矢量用了不同 EPSG,结果就是你打开 GIS 软件一看,图斑整体偏移几百米甚至跑出测区。我见过最离谱的一次是同事把行列号顺序写反了,结果整个成果镜像翻转,还自以为精度不错。
1.3 Segment Geospatial 到底补了哪几块板
这个库的定位很清楚:它不是新模型,是工程封装层。核心能力大概分四块。
一是栅格读写与坐标管理,底层靠 rasterio 完成,读进来的影像带着 CRS 和 transform,切瓦片的时候同步记录每个瓦片在原图中的偏移量,这样掩膜拼回去的时候坐标不会错。二是瓦片化推理调度,把大图按设定的尺寸和重叠度切成块,逐块送进模型,再把结果融合回去,重叠区用投票或者取并集的方式消解。三是掩膜后处理与矢量化,包括按面积过滤碎斑、形态学开闭运算、多边形简化,最后输出 GeoJSON 或者 GeoPackage。四是批量与提示工程,支持网格点自动生成掩膜,也支持用已有的矢量、手画的框、甚至文本提示来引导分割。
我特别喜欢它的一点是接口设计得很「GIS 味」,generate()出全图掩膜、segment()用提示出单个目标、raster_to_vector()转矢量,函数命名一看就知道干什么,不用翻半天文档猜。对做数据生产的人来说,这种直白比什么花哨特性都值钱。
2. 环境搭建与数据准备:八成的问题出在这一步
2.1 权重选型与显存预算,先算清楚再动手
模型权重一般有三档规模,参数量从几千万到六亿多不等。选哪档不是看哪个精度高就选哪个,得看你的显存和交付时限。我给个实测参考:最小的那档,单张 1024 瓦片推理显存占用大概 2–3 GB,速度最快,适合先跑通流程和做大范围初筛;中间档 4–5 GB,精度明显好一截,是我日常的默认选择;最大那档要 7–8 GB 起步,对细小地物边界更细腻,但速度慢一倍以上,只在成果质量要求高的项目里用。
装依赖这块,建议单独建虚拟环境,主体是栅格处理、矢量处理、深度学习框架和模型本体几类库。有个坑要提前说:深度学习框架的版本和模型库的版本对不上,经常报一些莫名其妙的算子错误或者权重加载失败。我的做法是先按模型库的官方要求锁框架版本,再装其他库,别反过来。
首次运行会自动下载权重文件,几百兆到两三个 G,网络不稳的话容易下到一半断掉,留下一个损坏的缓存文件,之后每次加载都报格式错误。遇到这种情况别怀疑代码,直接去缓存目录把权重文件删掉重下。我在一个网络受限的环境里被这个问题卡了整整一个下午。
2.2 位深拉伸与波段组合:让影像「看起来正常」
这一步最容易被跳过,但也最容易导致结果惨不忍睹。核心操作是把 16 位或者高位深影像按分位数拉伸到 8 位。
具体做法:对每个参与组合的波段,统计其像素值的 2% 和 98% 分位数,记为 lo 和 hi,然后按(value - lo) / (hi - lo) * 255线性映射,超出范围的值截断到 0 或 255。为什么用 2%/98% 而不是最小最大值?因为遥感影像里总有几个极端亮斑(云、裸白建筑屋顶)和极端暗值(水体阴影、云影),用最大最小值拉伸会把整个有效区间压扁,地物对比度全丢。2%/98% 相当于砍掉两端各 2% 的异常值,剩下的动态范围更贴近真实地物分布。
波段组合我给几个常用配置,直接照抄:
| 目标地物 | 推荐波段组合 | 说明 |
|---|---|---|
| 建筑物、道路、硬化地表 | 红、绿、蓝(真彩色) | 与自然图像分布最接近,模型泛化最好 |
| 耕地、林地、园地边界 | 近红外、红、绿(标准假彩色) | 植被在近红外波段反射强,边界对比明显 |
| 水体提取 | 绿、近红外、红 | 水体在近红外强吸收,与陆地反差大 |
| 裸土、沙地 | 短波红外、近红外、红 | 对含水量和矿物成分敏感 |
如果影像本身只有 RGB 三波段,那就省事了,但拉伸这步还是不能省。另外注意,有些影像带 alpha 通道或者第 0 波段是填充值,读取时要显式指定参与组合的波段索引,别想当然按 1、2、3 取。
还有一个细节:如果原影像的 NoData 值(比如 -9999)没处理,拉伸时它会拉低分位数,导致整图偏亮。读进来先做一次掩膜,把 NoData 置为无效值再统计分位数。
2.3 瓦片尺寸与重叠度:把参数算出来,别凭感觉
瓦片怎么切,直接决定结果的完整性和接缝质量。参数就三个:瓦片边长、重叠像素数、缩放策略。
瓦片边长,主流是 1024。原因是模型内部就是缩放到这个尺度,你切 1024 送进去是 1:1,不损失细节;切 512 送进去会被放大到 1024,反而引入了插值伪影;切 2048 送进去会被缩小,细节又丢了。所以 1024 是最优解,没必要折腾。
重叠度,我一般设 128 像素,也就是瓦片边长的 1/8。为什么需要重叠?因为切在边界上的地物会被切成两半,两块瓦片各自只看到一个残片,模型可能都判成背景,或者各判一半产生错位。有了重叠区,同一个地物在至少一块瓦片里是完整的,融合时取并集就能补回来。重叠太小(比如 32)补不回来,重叠太大(比如 512)计算量翻倍还容易在融合区产生重复图斑。128 这个值是我试过比较平衡的。
瓦片数量可以提前算出来,公式是n = ceil((W - tile) / stride) + 1,其中stride = tile - overlap。举个例子,一张 10000×10000 的影像,瓦片 1024、重叠 128、步长 896:横向瓦片数 = ceil((10000 - 1024) / 896) + 1 = ceil(10.02) + 1 = 12,纵向同样是 12,总共 144 块。
这个数字很关键,它决定了你的时间预算。按中档权重、单块推理加后处理 3–5 秒估算,144 块大概 8–12 分钟,可以接受。但如果你的影像边长是 40000 像素,瓦片数直接飙到 1900 多块,跑两个小时以上,中间还可能因为显存碎片累积崩掉,这时候就得考虑降采样、分块调度或者分批落盘。
3. 从影像到矢量成果:完整实操链路
3.1 提示策略怎么选:网格点、框、还是矢量引导
分割任意模型的推理必须带提示,提示方式不同,产出形态完全不同。
网格点自动模式是最省事的,在瓦片内均匀撒 N×N 个点,每个点让模型吐出若干个候选掩膜,再用非极大值抑制去重。参数就是points_per_side,设 32 意味着每边 32 个点、共 1024 个提示点,理论候选掩膜上千个。设小了(比如 8)会漏掉大片地物,设大了(64)计算量翻四倍但收益递减。我一般从 32 起步,小目标密集的场景提到 48。
这个模式的问题是「碎」。遥感地物是大块连通的,而网格点会在一块完整农田里撒十几个点,每个点独立出一个掩膜,去重后虽然合并了,但边界往往是不规则的锯齿状。解决办法是配合crop_n_layers参数做多尺度裁剪,让模型在不同尺度上各跑一遍,再把结果融合,边界会平滑很多。
框提示模式适合已知目标大概位置的场景,比如你已经有一份粗精度的建筑物普查数据,拿图斑外接矩形当提示,让模型精修边界。这个用法在数据更新项目里非常好用,把老图斑喂进去,模型输出新边界,人工只需要判断「变没变」,工作量降一个量级。
点提示模式是最灵活的,正样本点标在目标内部,负样本点标在紧邻的背景上。负样本这个技巧很多人忽略,但它对抑制「过度生长」特别有效——模型有时候会把一栋楼和它旁边的硬化地面连成一片,加一两个负样本点就能切开。
矢量引导是把已有的面要素转成提示,适合大批量处理。我做过一个园地提取的活,先用粗略的缓冲区生成候选区域,再逐块送进去分割,最后按面积和形状过滤,整体效果比纯网格点好,而且可控。
3.2 掩膜后处理:三个参数决定成果能不能用
模型出来的原始掩膜是不能直接交付的,必须过一遍后处理。这里有三个关键参数,我按重要性排。
第一个是掩膜质量阈值。模型对每个候选掩膜会给一个预测质量分和一个稳定性分,通常设 0.88 和 0.92 作为下限,低于这个值的直接丢。设太松(0.7)会混进大量噪声掩膜,设太紧(0.95)会把一些边界模糊但实际正确的目标也丢掉。这两个值的含义不一样:质量分反映「这个掩膜像不像一个真实物体」,稳定性分反映「把掩膜二值化阈值上下浮动一点,结果变不变」。后者对遥感尤其重要,因为地物边界常常是渐变的。
第二个是最小区域面积。这个参数单位是像素,作用是把零碎的小掩膜清掉。换算成地面面积要考虑分辨率:0.5 米分辨率的影像,100 像素大约 25 平方米。我一般按目标地物的最小合理尺寸反推,比如提取建筑物,最小独栋按 20 平米算,那就是 80 像素,取 100 比较稳妥。这个值设太大,细碎但真实的地物(比如小水池、田埂边的机井房)会被误删。
第三个是多边形简化容差。矢量化后的多边形顶点数可能成千上万,一个图斑几百 KB,几万个图斑的文件根本打不开。简化容差同样按地面距离换算成像素,我一般取 1–2 个像素,对应 0.5–1 米的地面精度。这个尺度上的简化肉眼看不出差别,但顶点数能砍掉 70% 以上。
除了这三个参数,还有两步值得加:形态学开运算(先腐蚀后膨胀)用来断开细小的粘连,闭运算(先膨胀后腐蚀)用来填补内部小孔洞。掩膜里那种「甜甜圈」形状的孔洞特别常见,多来自屋顶上的设备或者树冠遮挡,用闭运算填一下,图斑会干净很多。但注意核大小别超过 3×3,否则会把真实的凹陷边界也抹平。
3.3 矢量化与属性回写:坐标一致性是生死线
从掩膜到矢量这一步,核心是仿射变换。栅格的 transform 对象记录了像素坐标系到地图坐标系的映射关系,公式是x = a * col + b * row + c,y = d * col + e * row + f。绝大多数情况下 b 和 d 是 0(没有旋转),所以其实就两个参数:左上角原点坐标和像素分辨率。
用栅格转矢量的标准接口,把掩膜数组、transform、CRS 一起传进去,输出的 GeoJSON 就自带正确坐标系了。这里有个隐蔽的坑:如果你是自己切瓦片、自己拼掩膜,拼回去的时候忘了把瓦片的偏移量加回去,结果就是所有图斑集中在原点附近。我建议的做法是,切瓦片时同步生成一个「瓦片索引表」,记录每块的 row_off、col_off,拼掩膜时按索引表贴回去,比手工算偏移可靠得多。
矢量化之后还要检查两件事。一是几何有效性,自动分割出来的多边形经常有自相交、重复顶点、零面积环这些毛病,用矢量库的修复接口过一遍,否则后续做空间分析、入库都会报错。二是面积计算,如果你的数据是地理坐标系(经纬度),直接算面积得到的单位是「平方度」,这是一个毫无物理意义的数值。必须先把矢量投影到等积投影坐标系再算面积,或者用测地线面积算法。
我给个具体做法:先判断 CRS 是不是地理坐标系,如果是,按数据覆盖范围选一个合适的投影带(我国常用高斯-克吕格分带或者 Albers 等积投影),投影后再算面积,算完把面积字段写回属性表。这一步做得不对,后面的统计报表全是错的,而且错得很隐蔽——数字看着挺正常,就是单位不对。
最后补充一点实践经验:大批量矢量化时,不要一次性把所有掩膜转完再统一写文件,内存扛不住。我的做法是每处理 50–100 块瓦片就落一次盘,写成 GeoPackage 的追加模式,中途崩了也不至于全部重来。
4. 精度调优:参数怎么试,效果怎么验
4.1 提示点密度与组合策略的对照实验
我做过一组对照实验,同一张 2000×2000 的城区影像,固定其他参数,只改提示策略,结果差异很明显。
| 提示策略 | 参数配置 | 图斑数量 | 目视效果 | 耗时 |
|---|---|---|---|---|
| 网格点稀疏 | points_per_side=16 | 约 180 | 大建筑完整,小建筑成片粘连 | 40 秒 |
| 网格点中等 | points_per_side=32 | 约 420 | 大部分建筑独立,边界略锯齿 | 95 秒 |
| 网格点密集 | points_per_side=48 | 约 610 | 小建筑也能出,但碎斑增多 | 210 秒 |
| 多尺度裁剪 | points_per_side=32, crop_n_layers=1 | 约 450 | 边界明显平滑,粘连减少 | 160 秒 |
| 框提示精修 | 输入粗图斑外接框 | 等于输入数量 | 边界贴合度最高 | 30 秒 |
结论很清楚:单纯堆点密度不划算,收益递减还带来碎斑;crop_n_layers=1这个多尺度策略性价比最高,用 1.7 倍的时间换来明显更好的边界质量。如果手上已经有粗精度的矢量,框提示精修是效率之王。
还有一个组合技巧值得单独说:框加点混合。给一个目标的外接框,再在框内中心给一个正样本点、在框角给一个负样本点,模型会以框为约束、以点为引导,输出精度通常比单用框还好。这个用法在提取规则形状地物(矩形厂房、标准农田)时特别有效。
4.2 后处理参数的取值逻辑与一张速查表
参数不是调出来的,是算出来的。我习惯先确定「最小可接受目标的地面尺寸」,再反推所有像素单位参数。
假设分辨率 0.5 米,我要提取的最小地物是 30 平米的独栋小楼。那么最小面积 = 30 / (0.5 × 0.5) = 120 像素。多边形简化容差取半个像素到一像素,即 0.25–0.5 米地面误差,够用。形态学核 3×3 对应 1.5 米,刚好能断开宽度小于 1.5 米的粘连。
| 参数 | 作用 | 起始建议值 | 调整方向 |
|---|---|---|---|
| 预测质量分下限 | 过滤低置信掩膜 | 0.88 | 碎斑多则调高,漏提多则调低 |
| 稳定性分下限 | 过滤边界不稳的掩膜 | 0.92 | 同上,但比质量分更敏感 |
| 最小区域面积(像素) | 清除碎斑 | 按最小地物反推 | 误删小目标则调低 |
| 简化容差(像素) | 压缩顶点数 | 1.0 | 文件过大则调高,边界失真则调低 |
| 瓦片重叠(像素) | 消解接缝 | 128 | 接缝处断裂则调高 |
| 多尺度裁剪层数 | 改善边界平滑度 | 1 | 大目标边界粗糙则加到 2 |
提醒一句,这些参数是联动的,别一次改三个。我的习惯是固定其他项,只动一个,跑一小块样本区看效果,满意了再上全图。全图跑一次十几分钟,盲目调参纯属浪费。
4.3 怎么验证:不能只看图好看
自动化流程最危险的地方在于「看起来对」。一张叠加图放大看,图斑和建筑对得挺齐,很容易就判合格了。但真正的错误往往藏在中尺度——整体偏移、系统性漏提某类地物、边界普遍外扩半米。
我一般走三层验证。第一层是目视叠加,把矢量以半透明描边叠加到影像上,缩放到 1:2000 看局部,重点看四类地方:阴影区、高层建筑侧面、树遮挡区域、地物交界处。这四处是失误高发区。
第二层是量化抽查。随机抽 50–100 个图斑,人工勾一遍参考边界,算交并比。建筑物这类边界清晰的目标,零样本模型在好影像上通常能到 0.7–0.85,边界模糊的植被、裸地会低不少。低于 0.6 的就要分析原因了,是提示不足还是后处理过头。
第三层是总量校验,把提取结果按类别统计面积,和已有的统计数据或者高精度参考数据对比。如果某类地物的总面积偏差超过 15%,基本可以判定参数有问题,不用逐个图斑去查。这个方法粗糙但高效,能在几分钟内发现系统性错误。
5. 踩过的坑与排查速查
5.1 显存溢出与进程卡死
最典型的报错是显存不足。但要注意,很多时候不是单块瓦片太大,而是循环里没有及时释放中间变量,显存碎片一点点累积,跑到一两百块的时候突然崩掉。解决办法是在每块瓦片处理完后显式清理缓存、把不再需要的张量置空,并且用分批落盘的方式控制内存增长。
另一个隐蔽问题是某些瓦片特别慢,跑到某一块卡住不动。八成是那块瓦片刚好覆盖了大片水域或者纯黑区域,模型在自动生成掩膜时产生了海量候选,NMS 阶段耗时暴涨。可以在送模型前加一道判断,统计瓦片的标准差,低于阈值的(基本是纯色)直接跳过,标记为无效区。
5.2 接缝错位与重复图斑
如果成果里出现「一条笔直的断线把整块农田切成两半」,那就是接缝没消解好。先查重叠度够不够,再查融合逻辑是取并集还是取交集。取交集会在边界处产生细缝,取并集会产生重叠带。我推荐的做法是:重叠区按掩膜中心到瓦片中心的距离加权,靠近哪块瓦片就信哪块,这样过渡自然。
还有一种情况是融合后出现「双份图斑」,同一个建筑被提取了两次,位置略有偏移。这是去重没做好,需要在融合后加一轮基于交并比的去重,交并比超过 0.6 的两个图斑合并成一个。
5.3 坐标系与面积单位的连环坑
前面说过面积单位的问题,这里补充两个衍生问题。一个是矢量与影像对不上,在 GIS 里打开发现整体偏移。先查两边的 CRS 声明是否一致,再查投影变换时有没有做基准面转换。有些数据源声明的是某个地理坐标系,但实际坐标值已经被加密偏移过,这种情况只能靠控制点做配准,没有别的办法。
另一个是导出格式的精度损失。GeoJSON 默认用经纬度存储,如果你的成果是投影坐标,导出时会被强制转换,来回折腾几次精度就损了。大批量成果建议直接用 GeoPackage 或者 Shapefile 存投影坐标,需要发布时再转。
5.4 结果全空或者全是噪声
全空的情况,先检查影像拉伸做没做。我遇到过好几次,同事直接拿 16 位原始数据送模型,输出一片空白,还以为是模型坏了。把拉伸加上,结果立刻正常。
全是噪声的情况,通常是提示点撒到了无效区。比如影像四周有大片黑边(旋转校正留下的),网格点全落在黑边上,模型当然只能输出垃圾。解决办法是先算一个有效数据范围的掩膜,只在有效区内撒点。另外,NoData 值如果没设对,被当成真实的 0 值参与统计,拉伸参数就会算偏,也会导致结果异常。
还有一种特别隐蔽的情况:模型确实在正常工作,但你的后处理把结果全过滤掉了。比如最小面积设成了 5000 像素,而实际目标平均只有 300 像素,那出来的东西就全被清空了。排查时把中间掩膜先存一张出来看看,比盯着最终成果猜要快得多。
我个人在实际操作中的体会是,这套流程真正的门槛不在模型本身,而在数据准备和结果校验这两头。中间那段推理是最省心的,反而是位深拉伸的参数、瓦片的重叠度、面积单位这些「不性感」的细节,决定了成果能不能交付。还有个小习惯分享一下:每次跑新数据前,先裁一块 2000×2000 的样本区,用三组参数各跑一遍,五分钟内就能摸清这片影像的脾气,比直接上全图省太多时间。至于零样本模型和精调模型的关系,我的看法是它把「从零到能用」的门槛拉低了,但「从能用 to 好用」那段路,还是得靠人工和领域知识去补。