搞遥感分割的朋友应该都体会过这个痛:标注一张高分影像的成本,基本可以劝退大多数个人开发者和中小团队。建筑物轮廓、农田地块、水体边界,这些都得有人拿着标注工具一点一点抠,一个几千像素的目标,抠下来半小时起步。正因如此,我一直很关注“不用标注怎么做分割”这个方向。
前阵子折腾完 SegEarth-OV3 和 SAM 3 这套组合之后,我最大的感受是:零标注做遥感分割这件事,真的已经从实验室概念变成了可以上手的操作流程。SAM 3 是分割基础模型的新版本,官方在语义分割和实例分割上表现又往前推了一截;SegEarth-OV3 则是一套专门针对遥感场景的开放性评估协议,把“能不能不打标签直接分割”这件事拆成了可量化的步骤。
这篇文章我不打算讲太多理论,就把我这两天完整跑通的流程整理成保姆级教程,从环境配置到数据准备,从提示词采样到后处理导出,每个环节的坑我都踩了一遍,顺手记录下来了。不管你是刚开始接触遥感分割,还是已经用过 SAM 系列想迁移到遥感场景,这篇应该都能帮你省下不少弯路。
1. 整体思路拆解:为什么零标注遥感分割能成立
先把这个事儿的底层逻辑说清楚,不然你照着步骤跑完也不知道自己在调什么。
1.1 传统遥感分割为何“贵”
以前做遥感影像分割,路径基本是:收集影像、人工标注、训练模型、推理预测。问题出在前两步,影像可以批量下载,标注却完全依赖人力。
遥感图像的标注和自然图像不一样,自然图里一个“人”就是一个框或一条边,遥感图里一栋房子、一块田地,边缘往往模糊,有阴影遮挡,有光谱混淆。标注一个类别,东一块西一块,质量还要经过二次审核。五个类别的标注数据集,投入两三个人力,做一个月是常有的事。这份成本摆在这里,大量细分场景根本凑不出像样的训练集。
这也是为什么我一直留意零样本分割方向。如果模型本身已经具备“什么东西都能分”的能力,我们为什么还要为每个新场景重新造一套标注数据?
1.2 SAM 3 做了什么升级
SAM 系列的核心思路是用海量掩膜数据预训练一个通用分割模型。输入一张图,再输入一些提示(点、框或者文本),模型输出对应目标的掩膜。SAM 3 在同一条路线上继续升级,模型的架构做了优化,提示的理解能力更强,对边界模糊的目标分割结果更干净。
放在遥感场景里,这意味着很多以前需要针对性训练的类别——建筑物、道路、裸地、林地——现在可以直接被 SAM 3 “一次到位”地区分出来。你不需要为它准备任何训练样本,它本身就已经是“训练好了的”通用分割器。
1.3 SegEarth-OV3 补上最后一块拼图
但通用分割模型有一个实际问题:它会分,但它不知道你想分什么。一张影像丢进去,模型能输出成千上万个潜在目标的掩膜,到底哪个是你要的房子,哪个是背景里的空地?如果靠人工在图上点来点去提示,在大范围遥感影像上效率还是太低。
SegEarth-OV3 解决的正是这个问题。它提供了一套在遥感场景下的提示采样、推理编排和结果评估流程。简单来说,就是让 SAM 3 在大幅遥感影像上自动生成候选分割掩膜,再用类别信息或者规则过滤,最终把“模型能力”转成“实际可用结果”。这套协议是从遥感影像和地物特性出发设计的,对大幅面的拼接处理、小目标密集地物的提示密度都做了适配。
所以这套组合的逻辑闭环是:SAM 3 负责通用分割能力,SegEarth-OV3 负责遥感场景编排,两边一接,标注成本归零。
2. 环境准备:硬件要求与依赖清单
先把环境搞定,跑模型最烦的就是环境问题。
2.1 显存和算力的实测参考
先泼一盆冷水:零标注不等于零算力。SAM 3 这类基础模型参数量在那里摆着,不可能像以前训练个小 Unet 一样随便找块显卡都能跑。
我这边的实测环境是一张 RTX 4090(24GB 显存),在 1024×1024 的影像块上推理,单块耗时约 1.5 到 2.5 秒,具体取决于目标数量。如果你手上是 16GB 显存的显卡,比如 4080、V100 16G,也能跑,但建议把影像切成 512×512 的块,防止显存溢出。8GB 显存也不是完全没戏,可以用 CPU 推理做兜底,但速度会让人比较着急,体验差很多。
如果你只是想在缺少显卡的机器上快速验证效果,可以先下载官方提供的小权重版本,占用的显存大约能降到 6GB 左右,适合跑通流程,正式处理再用完整权重。
2.2 依赖库安装与权重下载
我是在 Linux 服务器上跑的,Windows 下流程基本一致,只是路径写法注意区分。核心依赖如下:
# Python 3.10 以上版本 conda create -n sam3 python=3.10 -y conda activate sam3 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install opencv-python pillow numpy rasterio geopandas fiona shapelyrasterio 和 geopandas 这两个是为了读遥感影像和导出矢量结果,实际处理矢量导出时还要用到 fiona。如果安装 geopandas 遇到依赖冲突,建议用 conda 统一安装:
conda install -c conda-forge geopandas rasterioSAM 3 的代码和权重需要从官方仓库获取。不同版本的名称略有差异,你只需要在仓库的 Release 页面里找到对应版本的权重文件下载即可。以我这次用的版本为例,模型权重文件名大致是 sam3_vit_h.pth,体积在 2.5GB 左右。下载后放在项目目录下的 weights 文件夹里,后边调用直接指定路径。
提示:下载权重时一定注意检查文件大小是否和页面标注一致。我之前遇到过下载到一半中断但系统没报错的情况,结果加载权重时报错“unexpected key in state_dict”,排查了很久才发现是文件不完整。
环境准备好之后,建议先跑一个最小测试:加载模型 + 推理一张普通照片,确认整个链路没问题,再进遥感数据。这一步能帮你区分后续报错是环境问题还是遥感数据处理的问题。
3. 保姆级实操:从影像到矢量结果的完整流程
这个环节我按完整的处理链路来说,每一步都附上代码和思路解释。整体链路是:读影像 → 切块 → 生成提示 → SAM 3 推理 → 掩膜清洗 → 拼接导出。
3.1 读取大幅遥感影像
遥感影像和普通照片的区别在于:尺寸大、通道多、带地理坐标。普通的 OpenCV 直接读,会丢掉地理信息,后边导出的时候坐标就对不上了。
我习惯用 rasterio 读影像,它能把像素坐标和地理坐标绑定在一起:
import rasterio from rasterio.windows import from_bounds src = rasterio.open("sample_area.tif") print(src.crs) # 坐标系信息,比如 EPSG:32650 print(src.transform) # 仿射变换参数 print(src.width, src.height) # 影像宽高 img = src.read([1, 2, 3]) # 取 RGB 三个波段 # 转成 HWC 顺序,并归一化到 0-255 img = img.transpose(1, 2, 0) img = img[:, :, ::-1] # BGR 转 RGB,根据你的源数据情况决定是否需要如果你的影像是多光谱的,不要直接把所有波段喂给模型。SAM 3 预训练时用的是三通道自然图像,多出来的波段对模型来说反而是干扰。建议通过主成分分析或者直接选 R、G、B 三个波段合成真彩色图。
3.2 影像切块与重叠处理
SAM 3 虽然能处理大图,但遥感影像动辄上万像素宽,直接整张塞进去,显存必然爆掉。而且大图上的目标太小,模型容易漏检。我的做法是切块处理,同时设置重叠区域。
def split_image_into_tiles(img, tile_size=1024, overlap=128): h, w = img.shape[:2] tiles = [] step = tile_size - overlap for y in range(0, h, step): for x in range(0, w, step): y_end = min(y + tile_size, h) x_end = min(x + tile_size, w) tile = img[y:y_end, x:x_end] # 边缘块可能不足尺寸,用 padding 补足 pad_y = tile_size - tile.shape[0] pad_x = tile_size - tile.shape[1] if pad_y > 0 or pad_x > 0: tile = cv2.copyMakeBorder( tile, 0, pad_y, 0, pad_x, cv2.BORDER_CONSTANT, value=(0, 0, 0) ) tiles.append((x, y, tile)) return tiles切块的时候重叠区域设置多大,我建议是 128 到 256 像素。重叠的意义在于:如果目标恰好跨在切块边界上,模型可能在两个块里各分割出一半,后边拼接就要做去重合并。没有重叠的话,跨边界的目标可能直接在接缝处断开,后期很难补。
3.3 自动提示采样:让模型知道该分谁
这是零标注流程里最核心的一步。提示采样的质量直接决定了最后分割结果的完整性。
最基本的思路是网格采样。在影像上均匀撒点,每个点都作为一个提示送入模型,SAM 3 会为每个点返回最有可能包含该点的目标掩膜。网格密度越高,小目标越不容易漏,但推理耗时会线性增加。我这边的经验值:
- 中高分辨率影像(0.3m-1m),建筑物为主,网格间距 32-48 像素,效果比较稳。
- 大范围低分辨率影像(10m 以上),地物以大块为主,网格间距可以放到 80-120 像素。
- 目标密集的城区,建议 24-32 像素,宁多勿漏。
def build_grid_prompt(img_shape, step=32): h, w = img_shape[:2] points = [] # 从 step//2 开始,避免所有点都落在边缘 for y in range(step // 2, h, step): for x in range(step // 2, w, step): points.append([x, y]) labels = [1] * len(points) # 1 表示前景点,0 表示背景点 return points, labels这里有一个我自己踩过的坑:网格点不能从 0 开始,而是从 step//2 开始。如果从 0 开始,每行每列的第一个点都贴在图块边缘,SAM 3 对边缘点的处理不太稳定,有时候会返回空的预测结果。用半偏移采样可以让点均匀分布在网格中心,采样覆盖更合理。
还有一种增强方式是把点都映射到梯度较大的位置,也就是影像变化剧烈的区域。用 OpenCV 的 Canny 边缘检测或 Sobel 梯度幅度做引导,选择在边缘附近的点作为提示。这样做的好处是提示集中在地物边界区域,掩膜边界质量更高。不过这种采样方式对参数敏感,实际操作时我建议先用均匀网格跑一版,发现问题再考虑梯度引导。
3.4 推理:批量调用模型
SAM 3 的推理接口接收图像、提示点和标签,输出目标掩膜及置信度。以我实现的流程为例:
from sam3 import Sam3 model = Sam3("weights/sam3_vit_h.pth") model.to("cuda") def predict_tile(model, tile, points, labels): masks, scores = model.predict( image=tile, point_coords=points, point_labels=labels, multimask_output=True ) return masks, scoresmultimask_output 这个参数值得展开说一下。开启后,模型为每个提示点返回多个候选掩膜,比如一个完整的目标、一个只包含核心区域的目标,还有一个包含周边扩展区域的目标。三个候选里哪个是对的,模型会给出置信度分数。SegEarth-OV3 的默认策略是选择置信度最高的那个掩膜,如果后续做类别过滤,可以把三个都保留,让后续筛选自己选。
处理大幅面时,建议每跑完一类地物的提示后,立即把掩膜存成临时文件,不要把所有结果都堆在内存里。我处理一个 2 万乘 2 万的区域,光掩膜文件就占了好几 GB,不落地迟早把内存打满。
3.5 掩膜清洗与拼接
模型输出的原始掩膜是“像素级布尔数组”,是单块范围的。在拼接前,必须先做几步清洗:
- 去掉面积过小的连通域,这些大多是噪声。面积阈值根据地物类型设定,建筑物一般小于 20 个像素的可以直接干掉。
- 去掉置信度过低的掩膜。SegEarth-OV3 里一般把置信度阈值设在 0.8 左右,低于这个值的掩膜可靠性差。
- 对同一目标产生多个掩膜的情况,用 IoU 去重。两个掩膜的 IoU 大于 0.7,就认为它们指向同一个目标,保留置信度高的那个。
清洗完的掩膜,按切块时的坐标位置拼回整幅影像范围。重叠区域的掩膜通过取并集或者取置信度较高的结果,这里我建议用置信度加权融合,过渡更自然。
def merge_masks_tile(masks, scores, x_offset, y_offset, full_size, min_iou=0.7): # 先做连通域分析,提取每个掩膜中的独立目标 # 再按坐标偏移写入全图掩膜数组 pass这一步的函数实现很简单,我就不贴完整代码了,说下几个容易出错的位置:坐标偏移算错、边界同步不一致、Mask 的尺寸和原图没对上。每次拼接完后,务必可视化叠加检查一遍。
3.6 导出矢量结果并配准坐标
最后一步是把掩膜转成矢量,并写入地理坐标系,这样 GIS 软件里才能直接用。
import geopandas as gpd from shapely.geometry import shape from rasterio.features import shapes as rio_shapes import numpy as np def mask_to_vector(mask, transform, crs): results = [] for geom, value in rio_shapes(mask.astype(np.uint8), mask=mask, transform=transform): if value == 1: results.append(shape(geom)) gdf = gpd.GeoDataFrame({"geometry": results}, crs=crs) return gdf gdf = mask_to_vector(binary_mask, src.transform, src.crs) gdf.to_file("output_water.shp")这里必须强调 transform 和 crs 一定要用 rasterio 打开原始影像时获取的那份,不能自己推断。我见过不少人在这步直接给了默认坐标系,导出的 Shapefile 在 ArcGIS 里叠到卫星影像上全部错位,浪费了大半天排查。
4. 提示词设计与精度调优技巧
零标注分割看起来是“点一下就跑”,实际要得到好的效果,需要在提示和参数层面反复打磨。下面这几个点是影响遥感分割精度最直接的因素。
4.1 提示点密度:不是越多越好
提示点密度和分割精度的关系,并不完全是线性的。点太密,一是耗时成倍增长,二是相邻点会让模型产生大量重叠掩膜,去重阶段处理复杂度飙升。
我在农田地块分割场景测试过一组对比实验:在同一个测试区域使用 16 像素、32 像素、64 像素三种网格密度。16 像素网格耗时最高,但和多类别场景里的小地块分割相比,效果不一定优于 32 像素;64 像素网格虽然快,但小块田之间的窄边界经常被跳过,导致地块合并。最后我采取的是分块变密度策略:用边缘检测生成梯度图,梯度强的区域用 24 像素密度,梯度弱的区域用 64 像素密度。效果和均匀 24 像素基本接近,但推理时间缩短了约 40%。
这套思路很实用,你在自己的数据上也可以试一试。
4.2 置信度阈值调整策略
掩膜的置信度阈值直接影响输出结果的干净程度。阈值设高了,漏掉一部分目标;阈值设低了,会混入大量背景碎片。
SegEarth-OV3 里默认建议是 0.8,但实际应用中要根据地物类型灵活调整:
- 建筑物轮廓清晰,与背景差异大,置信度通常很高,阈值 0.85 以上没压力。
- 水体边缘过渡缓慢,模型给出的置信度偏低,可以把阈值降到 0.75。
- 林地与草地边界模糊,阈值建议 0.7,宁可多保留,在后续矢量化阶段再人工筛选。
这里我建议先跑一个小候选区域,用不同阈值分别导出,在 GIS 里目视比较一遍,确定最佳值再全量跑,避免浪费算力。
4.3 提示框与文本提示的互补用法
虽然 SegEarth-OV3 主打的是点提示自动采样,但在实际使用中,有些场景确实需要手工干预。
比如一个区域内只想要某一类目标,自动网格采样会分出所有目标,这时候可以画一个框限定范围,把框坐标交给模型,只对框内做密集采样。又比如某些非常特殊的场景,比如识别某个特定形状的目标,可以用文本提示试试,SAM 3 的文本理解能力比前代更强,虽然不保证每个遥感类别都认识,但值得一试。
我用下来觉得最合理的策略是:先用网格点提示跑全图,得到所有候选掩膜;再用一个简单的分类器(或者人工抽样确认)筛选类别;最后对不确定的区域人工撒点补漏。有选择地干预,效率最高。
4.4 大幅面场景的内存调度
大幅面影像处理时,最怕的不是分割效果差,而是跑到一半内存溢出,进程被杀,前功尽弃。建议按以下方式控制资源:
- 切块尺寸不要超过 1024×1024。
- 每个批次处理 1-2 个块,不要一次性把所有块载入。
- 用 numpy 数组保存临时结果,不要用 Python 列表存图片数据,内存占用差距巨大。
- 每处理完几个块,主动调用 gc.collect() 释放内存。
5. 常见问题与排查技巧实录
这一节把我遇到的高频问题按“现象 - 原因 - 解法”整理成表格,你如果正好撞上了,直接对照着查。
5.1 问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 模型加载报“unexpected key” | 权重文件下载不完整或版本不匹配 | 重新下载权重并核对文件大小 |
| 推理时显存溢出(CUDA out of memory) | 切块尺寸过大或批处理太多 | 缩小切块到 512;一次只处理一个块 |
| 输出掩膜全是空的 | 提示点全部落在无目标区域或采样步长过大 | 检查采样点的分布情况,缩小 step |
| 掩膜边缘锯齿严重 | 后处理没有进行平滑 | 在矢量化前用 scipy 的 gaussian_filter 或形态学闭运算做平滑 |
| 拼接处出现裂缝 | 切块重叠区域设置过小 | 增大 overlap 到 256,并对重叠区做置信度融合 |
| 导出的 Shapefile 与影像错位 | crs 或 transform 没有取对 | 确保使用 rasterio 读取原始影像时的参数 |
| 大图跑几天没结果 | 没有合理切块或内存泄漏 | 检查脚本是否在循环中不断累积变量 |
5.2 踩坑实录:几个典型的排查故事
第一个坑是有一次大范围水体提取,跑出来的结果在水面中央出现大量孤立的小碎片。我一开始以为阈值设低了,调高后碎片更多,排查了很久才发现问题出在提示采样:水体区域的光谱特征非常均匀,梯度很低,模型对均匀区域容易生成破碎的候选掩膜。解决办法是把置信度阈值提高,同时在后处理里加了一个最小面积过滤,把小于 100 像素的连通域全部去掉,结果才干净。
第二个坑是拼接边界错位。我处理一个 2 万像素宽的影像时,顶部几行和底部几行的掩膜对不齐,中间出现明显的水平错位线。排查后发现是 padding 补边时没有对齐坐标,边缘块的实际内容偏移了,导致拼接时位置错了一半。这个问题的教训是:padding 的信息一定要跟着切块记录走,不能只用坐标算回原图位置,还得考虑 padding 带来的偏移。
第三个坑是显存优化。最开始我一次处理 4 个 1024 的块,结果经常跑到一半就 OOM。后来改成一次 1 个块,并用 torch.no_grad() 包裹推理过程,显存占用直接降了三分之二,速度反而因为不用频繁清理内存变得更快了。
5.3 效率优化经验
处理超大幅面时,有几个优化策略可以组合使用:
- 按地物类型分类处理。先提取所有候选掩膜,再根据掩膜的平均光谱值做快速聚类,同类目标一次性后处理,避免反复读全图。
- 用多进程并行处理不同切块。SAM 3 推理是典型的 CPU 预处理 + GPU 推理模式,多进程能有效利用多核 CPU 做数据读取和预处理。推荐进程数设为 CPU 核数一半左右,太高反而会有调度开销。
- 推理阶段关闭所有日志输出。一句话:控制台打印 I/O 在高频推理循环里会显著拖慢速度。
6. 个人经验与后续扩展建议
这次实测最直观的体会是:零标注遥感分割真正落地的时间节点已经到了。以前遇到一个新场景,先想能不能找到公开数据集,找不到就只能发动大家手工标注。现在用这套组合,第一天拿到影像,当天就能出第一版结果,对项目预研和方案验证的价值非常大。
当然也要说句公道话,零标注不代表在所有场景下都能直接超过有监督模型的效果。在类别高度雷同、边界极不清晰的场景里,有监督训练的算法仍然可能更优。但零标注流程可以作为一个快速预筛工具,把大量精力从标注中解放出来,集中处理模型解决不了的那部分难题。
后续值得折腾的方向,我列几个自己正在看的:一是把分割掩膜接一个小分类器,做成完整的“分割 + 分类”自动解译链路;二是针对自己的地物类别积累一套提示采样参数模板,覆盖不同分辨率和地物密度;三是把分割结果接进变化检测流程,用不同时期的影像对比,判断地物变化情况。
整个流程的核心就是提示设计,建议你拿到自己的数据后,先切几个代表性区域实验,把提示密度、阈值、后处理顺序跑通,再全量处理,这个习惯能帮你避免很多浪费。希望这篇教程能帮你跳过我已经踩过的坑,少一点头秃时刻。