我们直接聊点实在的。去年我就一直想上手 SAM(Segment Anything Model),但网上教程要么停在“介绍理念”层面,要么就直接甩一张结构图让人自己猜。官方发布的权重和 Demo 仓库看着很全,真到自己跑代码的时候,安装环境、下载模型、调用接口,处处都是坑。这篇是我自己把 SAM 从零安装到实际应用的完整记录,不是把官方 Readme 复述一遍,而是把我踩过的、绕过的、实打实验证过的东西讲给你听。
这个系列做到第 04 篇,前面如果讨论过分割能力、zero-shot 泛化边界,那这篇就是纯粹的工程实践:怎么把 SAM 跑起来,怎么用自己的数据出结果,怎么把它接进你自己的项目里。目标读者是已经知道 SAM 是什么、准备开始动手的人。如果你只是听熟了 “万物分割” 这个概念但还没真正跑过一行代码,那我建议跟着这篇文章从头走一遍;如果你已经跑通了基础 Demo,后面关于输入提示的细节和 batch 推理的坑,应该也能让你少走点弯路。
1. SAM 项目的整体拆解与核心设计思路
在动手敲命令之前,我觉得有必要先把 SAM 的代码结构在脑子里过一遍。很多新手推不下去,不是环境问题,而是根本没搞懂仓库里每个文件、每个 API 该怎么配合使用。
1.1 SAM 到底由哪几个核心模块组成
从官方仓库的角度拆,SAM 的代码体系大致分两层。第一层是模型定义,也就是segment_anything这个核心目录里的东西,它对应 SAM 的三段式结构:一个图像编码器(Image Encoder)、一个提示编码器(Prompt Encoder)、一个掩码解码器(Mask Decoder)。第二层是辅助工具,包括predictor.py里的高封装预测器、automatic_mask_generator.py里的全图自动分割生成器,以及官方示例的notebooks里的调用 demo。
这三段式结构是 SAM 的精髓,理解它对你后续调参非常有帮助。图像编码器把一张任意尺寸的输入图转换成高维向量特征,这个编码器是最耗显存的部分;提示编码器负责把你喂进来的点、框、掩码转换成模型能理解的提示向量;掩码解码器则结合图像特征和提示向量,输出最终的分割结果和置信度。
在代码层面,建议你第一次接触时不要直接开始改模型结构,而是先跑通predictor.py的调用链路。我的经验是,它是一个很好的“最小可运行范式”:先加载权重,再设置图像,然后指定提示,最后拿到 masks。整个链路只有四步,但每一步都对应着模型内部一个完整的处理阶段,顺序和参数你都得心里有数。
1.2 官方权重选择与效率考量
官方仓库里提供了几个不同规格的权重文件,分别是vit_h、vit_l和vit_b。这三个对照的模型尺寸依次减小,对显存和内存的开销也依次递减。很多人一上来就无脑选vit_h,觉得参数越大、效果越好,这其实是个误解,而且是个容易把自己机器拖垮的误解。
从实际效果看,对于常规自然图像,vit_b和vit_h在肉眼观测层面的分割质量差距并没有想象中那么大,但在推理速度上,vit_b能快出三到四倍。我自己的显卡只有 8GB 显存,如果用vit_h跑一张分辨率为 1024 的图,显存基本就快满了,换到vit_b之后,不仅各种边缘情况都跑得动,还能保持不错的实时交互体验。
这里给大家一个落地参考:如果是本地单张图调试、验证算法思路,直接选vit_b,绝对够用;如果是做精细标注、对边缘平滑度要求极高的任务,再考虑vit_h+ 高分辨率输入。后面我会专门讲在不同场景下怎么选权重,免得上来就被显存卡死。
2. 从零搭建运行环境:安装细节与依赖避坑
先说结论:整个安装过程并不复杂,核心就三条——搞对 Python 版本、装对 PyTorch、用对安装方式。但恰恰是这三条,最容易把一个下午耗掉。
2.1 Python 环境与依赖管理
SAM 官方要求 Python 3.8 以上,我实测 3.9、3.10 都能跑得很好,3.12 我没试过,谨慎起见不推荐直接用太新的版本,因为某些依赖库可能还没跟上。
无论你多熟悉 Conda、Poetry 还是 venv,我都强烈建议你为 SAM 单独建一个虚拟环境,不要直接装进 base 环境。原因很现实:SAM 依赖的 PyTorch 版本、opencv-python版本,和你日常做别的事所用的环境很可能产生冲突,如果装在一起,轻则依赖管理器崩溃,重则某天导入torch直接报错,然后你开始怀疑人生。
我自己的做法是:
conda create -n sam python=3.9 conda activate sam pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118注意,我没有先安装segment-anything,而是先装 PyTorch。这是因为官方仓库在安装时要求你已有可用的 PyTorch 环境,如果你先装包,包管理器有时候会动到你现有的 GPU 环境,再到后面就乱套了。
对于没有独立显卡、只有 CPU 环境的朋友,也不是完全不能用。把上面的安装命令换成 CPU 版,然后老老实实用vit_b,一张普通图推理大约十来秒,作为学习 demo 完全可以接受。但不要想着用 CPU 做实时交互或批量处理,那会等到怀疑人生。
2.2 安装 segment-anything 与验证
接着装核心包和依赖,两条路任选:
pip install git+https://github.com/facebookresearch/segment-anything.git或者先把仓库克隆到本地再装:
git clone https://github.com/facebookresearch/segment-anything.git cd segment-anything pip install -e .我推荐第二种方式。原因有两个:第一,官方仓库里的notebooks目录包含了非常有价值的交互式 demo,虽然现在网页上有人做了各种封装,但原汁原味的 notebook 对理解代码内部调用流程是最好的教材;第二,-e是开发者模式,你以后如果自己改动了源码,不需要重新安装,改动即刻生效。
装完之后,强烈建议先做一个最小验证:
from segment_anything import SamPredictor, sam_model_registry sam = sam_model_registry["vit_b"](checkpoint="路径/你的权重文件.pth") print("SAM loaded successfully")如果这一行都跑不过,先别急着往下走,因为后面所有的错误都会从这里开始叠加。我遇到的最多情况是torch的 CUDA 版本和驱动不匹配,报错信息五花八门,甚至有些完全不提示 CUDA 错误,比如ImportError: cannot import name 'xxx' from 'torch',这种就是典型的环境错乱,重建环境最省心,不用硬着头皮排查半天。
3. 模型加载与核心 API 调用:把每个参数抠明白
很多人学会加载模型之后,就开始在predict这个函数里摸索。这个阶段你对参数的理解越透彻,后面处理复杂图像时就越从容。SAM 的核心调用分布在两个高层接口上:一个是SamPredictor,一个是SamAutomaticMaskGenerator。我分开来讲。
3.1 SamPredictor 逐参数拆解
SamPredictor是官方封装的“提示式分割”接口,核心方法就是predict。在调用它之前,有一个前提操作不能跳过:set_image。这一步是把输入图像送进图像编码器,得到图像特征并缓存,方便后续多次提示复用。
from segment_anything import SamPredictor, sam_model_registry predictor = SamPredictor(sam) predictor.set_image(image_rgb) # image_rgb 是 RGB 模式 ndarray这里有一个非常毁新手的细节:官方文档和示例里明确写着predictor.set_image接收的是 RGB 图像,但很多人用cv2.imread读进来,得到的是 BGR 顺序,不转换就直接喂进去,导致分割结果出现诡异的颜色偏差和区域错乱。CV 老手可能早期就吃过这个亏,我在这儿帮你固定记忆:cv2.imread读的就是 BGR,要么用cv2.cvtColor转换,要么用PIL.Image.open直接得到 RGB。
然后调用predict:
masks, scores, logits = predictor.predict( point_coords=point_coords, point_labels=point_labels, box=box, multimask_output=True, )参数列表看着不多,但每个都值得掰开揉碎讲。
point_coords和point_labels:这两个必须成对出现,一共喂几个点,point_coords就是 Nx2 的数组,每个坐标是[x, y]格式;point_labels是长度为 N 的数组,1 表示这个点属于目标区域,0 表示这个点不属于目标区域(也就是背景点)。这种正负样本的提示方式让 SAM 能精确区分前景和背景,也是它真正的亮点。box:一个长度为 4 的数组,格式是[x1, y1, x2, y2],是一个框住目标的矩形框。box和point_coords可以单独用,也可以组合馈入,组合时分割精度往往更高。multimask_output:这个参数我一开始也忽略了。当它为True时,模型会一次返回 3 个候选掩码,对应的scores是模型对每个候选的置信度。这是 SAM 的设计特性——在提示信息不明确的时候,给出多个可能的分割方案,由你来选。为False时则只输出一个掩码,最自信的那一个。
实际业务里,对同一个目标点,我经常拿到两个分数接近但形状不同的掩码,这时你应该把multimask_output=True的结果先展示给用户,让用户做最终选择,这比硬取 score 最大的那个更像真实应用。
3.2 SamAutomaticMaskGenerator 与全图分割
如果说SamPredictor是让用户主动给提示,那SamAutomaticMaskGenerator就是让模型主动干活。它的核心价值在于输入一张图,直接返回图中所有对象的掩码,不需要任何人工标注。适合批量预标注、内容分析等场景。
一个最基础的用法是:
from segment_anything import sam_model_registry, SamAutomaticMaskGenerator sam = sam_model_registry["vit_b"](checkpoint="权重文件.pth") mask_generator = SamAutomaticMaskGenerator(sam) masks = mask_generator.generate(image_rgb)generate方法输出的masks是一个列表,每个元素是一个字典,包含segmentation、area、bbox、predicted_iou、stability_score等字段。其中segmentation是一个二维布尔数组,predicted_iou是模型对该掩码质量的内部评估,stability_score是经过图像扰动后的稳定性评分。
这个接口的默认参数对小型物体、低对比度场景处理得不算好,你需要调pred_iou_thresh、stability_score_thresh、crop_n_layers等超参。后面我在实战环节会详细演示调节参数带来的直接差别。
4. 实战演示:从单目标分割到批量处理完整流程
这一节才是重点。我会用一个模拟场景贯穿整个流程:假设你有一张包含多个桌子、杯子和书本的场景照片,你想把画面中的某个咖啡杯从背景中完整抠出来。这个场景足够简单,但足够说明从文件读取到结果导出的全链路。
4.1 单目标点提示分割:最简流程
首先加载模型和图像,然后选一个落在咖啡杯内部区域的目标点,最有效的位置是咖啡杯中心,不要选在杯缘或阴影区域。
import cv2 import numpy as np from segment_anything import sam_model_registry, SamPredictor sam = sam_model_registry["vit_b"](checkpoint="./checkpoints/sam_vit_b_01ec64.pth") predictor = SamPredictor(sam) image = cv2.imread("coffee_cup.jpg") image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) predictor.set_image(image_rgb) point_coords = np.array([[350, 240]]) # 假设咖啡杯中心坐标 point_labels = np.array([1]) masks, scores, logits = predictor.predict( point_coords=point_coords, point_labels=point_labels, multimask_output=True, )拿到masks后,你要处理的是masks[0]、masks[1]、masks[2]三个二值掩码数组。我第一次打印这些变量时,满屏幕的True、False,差点以为是空结果。这里给你一个直接的视觉效果检查方法:
import matplotlib.pyplot as plt for i, mask in enumerate(masks): plt.imshow(image_rgb) plt.imshow(mask, alpha=0.5, cmap="jet") plt.title(f"Mask {i} score={scores[i]:.3f}") plt.show()如果你画的点位置精准,三个候选掩码大概率形状接近,只是个别边缘细节不同。你直接选择scores最高的那个就行。
然后把它叠加到原图上导出来:
mask = masks[0] colored_mask = np.zeros_like(image_rgb) colored_mask[mask] = [255, 0, 0] # 高亮目标区域 result = cv2.addWeighted(image_rgb, 0.7, colored_mask, 0.3, 0) cv2.imwrite("cup_with_mask.jpg", cv2.cvtColor(result, cv2.COLOR_RGB2BGR))这一步不要跳。输出是 BGR 的话你直接用cv2.imwrite没问题,但如果先用 PIL 保存,记得转回 RGB,不然颜色不对。
4.2 配框加坐标双提示,效果更稳
我实际测试中发现,即使是免费的vit_b,单独只给一个点,在复杂背景下偶尔也会把目标周围相似颜色的一起割进去,比如咖啡杯旁边的桌垫、咖啡表层的拉花等。解决方式是同时提供包围框和点,让提示信息更强。
你可以通过目标检测模型先得到一个目标框,或者人工标注一个框:
box = np.array([210, 90, 480, 390]) # 咖啡杯的外包围框 [x1, y1, x2, y2] masks, scores, logits = predictor.predict( point_coords=point_coords, point_labels=point_labels, box=box, multimask_output=False, )注意,这里我把multimask_output设为False,因为有了明确的框和点之后,模型基本能锁定唯一答案,这时候只返回一个掩码更符合预期。如果你想看到多个候选,就设True。
这个模式在实际项目中很常见——先用检测器找出目标候选框,再用 SAM 提取精确像素级掩码。这个组合拳比传统 Box 分割方法优雅得多,也是很多工业级工具的底层逻辑。
4.3 批量分割与结果筛选
当你有几百张图需要处理,或者一张高分辨率图中目标太多时,手写循环调predict就麻烦了。这时SamAutomaticMaskGenerator就派上用场。但是默认参数直接跑,你会发现生成了一大堆碎片掩码,比如背景纹理也被拆成好几块。
想压住这些小碎片,要把pred_iou_thresh调高一点,比如 0.88,把stability_score_thresh调到 0.95。完整代码如下:
from segment_anything import sam_model_registry, SamAutomaticMaskGenerator mask_generator = SamAutomaticMaskGenerator( sam, pred_iou_thresh=0.88, stability_score_thresh=0.95, crop_n_layers=1, crop_n_points_downscale_factor=2, ) masks = mask_generator.generate(image_rgb) masks_sorted = sorted(masks, key=lambda x: x["area"], reverse=True)对masks按面积排序后,再看predicted_iou和stability_score,这两个指标能帮你筛掉很多明显没意义的掩码。比如stability_score过低,说明轻微改动输入后掩码形状变化很大,这种结果本质上是不可信的,直接丢弃。
我跑过一张 800x600 的室内图,默认参数生成 200 多个掩码,其中一半以上是噪声;调到上面的参数后,剩 40 个左右,基本都是实体对象,效果天差地别。所以自动模式不是装上就能用,参数不调好你就在和垃圾堆搏斗。
5. 我在实际部署中踩过的坑:问题排查与经验教训
很多问题不是官方文档能覆盖的,全靠实测和踩坑。这一节我把自己遇到的几个高频问题整理出来,按出现频率排序,你可以直接照着排查。
5.1 显存不足与图像尺寸的平衡
如果用vit_h跑大图,显存不够是个必然事件,报错多半是CUDA out of memory。我踩过的坑是:图像分辨率过大时,即使vit_b也会在set_image阶段溢出。
set_image内部实现中,会先把图像按最长边缩放到 1024 左右,再喂给编码器。这个过程叫Transform。如果你的原图是 4000x3000 的超大图,即使缩放了,内存传输过程依然不轻松,但显存压力更多来自后续predict时的特征尺寸和候选掩码。
我的实践经验:把输入图先降采样到 1200 以下,再传入 SAM,分割精度基本不受影响。如果你需要高分辨率掩码,可以在 SAM 输出粗掩码之后再做一个上采样后处理,用传统 CV 做边缘精修,效果比直接让 SAM 吃大图性价比高得多。
还有一个隐形坑:你的图片是 RGBA 四通道图,没有转成三通道,set_image时 numpy 维度不一致的报错会出现,不够报错提示还不明显。记住所有输入predictor的图像都必须是三通道。
5.2 点提示坐标错位问题
如果你用matplotlib显示图片后,直接用鼠标定位点,然后再把坐标传给 SAM,这里有个坐标系陷阱。matplotlib的坐标原点通常在图左下角,而 numpy 数组的原点在左上角,两者纵向相反。我发现很多初学者把画布上的 y 坐标原封不动传给 SAM,结果分割区域永远在画面下方偏移,还以为模型出了问题。
解决方式很简单:你要确保传给 SAM 的[x, y]坐标是以图像左上角为原点的行列索引,也就是 numpy 数组在第 y 行、第 x 列的位置。如果你是从matplotlib取点,记得转换:y_sam = img_height - y_mpl。
5.3 输入输出图片的通道顺序问题
这个问题在前面反复被提及,但值得单独列一条,因为覆盖面太广。opencv的默认编码是BGR,PIL是RGB,SAM 官方示例默认用的是 RGB。我见过有人在加载一张医学影像(灰度图)时直接当彩色图喂进去,结果因为单通道形状问题报错。
处理灰度图的方式是:
gray = cv2.imread("slice.png", cv2.IMREAD_GRAYSCALE) image_rgb = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB)如果你不转,直接把一个二维数组传进去,set_image会报维度错误。别问我怎么这么清楚的。
5.4 网络环境与权重文件加载失败
checkpoint路径和权重文件名要保持一致,代码里写的是sam_vit_b_01ec64.pth,那就别只给个不带文件名后缀的路径,这很低级但很常见。另外,官方权重托管在特定地址,如果你那边网络状态波动,下载经常中断,很容易得到坏文件,运行时你会看到奇怪的尺寸匹配错误。
我的习惯是把下载好的权重存到一个单独的checkpoints文件夹,然后在代码里用绝对路径引。永远不要用 hardcode 截断路径,更不要在代码里依赖相对路径。
还有一个高级小技巧:官方权重是在特定分辨率下训练的,如果你希望 SAM 在原图比 1024 更大时仍保持细分能力,可以在SamPredictor里覆盖transform的target_length,手动改成 1333 附近。但不建议新手这么做,因为模型原本在 1024 范围内表现最稳,硬改超分往往得不偿失。
5.5 依赖冲突与过时 API
最后一个是环境类问题。因为 SAM 二次开发了很多工具库,所以每次我升级 torch 之后,再跑原来的脚本时常遇到torch.functional相关 API 变动导致的报错。这类问题没有一劳永逸的解法,我的建议是:
- 如果项目周期短,环境能不动就不动。
- 如果需要升级,最稳的路径是重开一个虚拟环境,重新安装,而不是在原环境里做易失性升级。
torch的大版本升级往往会牵动许多隐式依赖,今天报错一个符号找不到,明天报错一个算子不匹配,十天都耗在环境修复上,这种投入产出比太低了。直接用新环境,让新的搭配自己去玩。
6. 灵活性扩展:把 SAM 接进自己的项目里
如果你只是想跑通官方 demo,前面这些已经够用。但大部分读者肯定不止于此,你大概率有自己的数据、自己的业务,需要把 SAM 作为一个模块接进更大的系统里。这部分我说几个我自己常用的扩展思路。
6.1 将 SAM 封装成独立推理服务
我通常会把模型加载、图像预处理、推断和后处理,统一封装成一个类,对外只暴露一个segment()接口。这个接口接收一个图像路径和一个提示参数(点或框),返回一个二值掩码数组。这样不管后来你是接 Flask、FastAPI 还是内部离线任务调度,都非常干净。
一个典型的封装结构如下:
class SAMClient: def __init__(self, model_type="vit_b", checkpoint_path=""): self.sam = sam_model_registry[model_type](checkpoint=checkpoint_path) self.predictor = SamPredictor(self.sam) def segment_by_points(self, image, coords, labels): rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) self.predictor.set_image(rgb) masks, scores, _ = self.predictor.predict( point_coords=coords, point_labels=labels, multimask_output=True ) return masks[np.argmax(scores)]这个方式兼容性很强。你以后想在别的项目里复用同一个 SAM 服务,直接from sam_client import SAMClient,不需要再重写一遍模型加载流程。
6.2 提示自动生成:把用户点击转为点击坐标
很多标注工具都会实现“点击即分割”的交互逻辑。原理非常简单:监听鼠标点击事件,把屏幕坐标映射到原图坐标,然后调用点提示分割,返回掩码后叠加显示在界面上。这背后并不需要复杂的算法,但体验效果极佳,用户只需点几下就能完成精细标注,比手工描边快了不知道多少倍。
但要注意,处理交互场景时,不能用SamAutomaticMaskGenerator,每次鼠标事件里调用set_image重新编码图像也非常奢侈,正确做法是在第一次显示图像时就把特征缓存住(set_image会自动缓存),每次鼠标点击只走提示编码和掩码解码,这部分推理开销相对小得多,能够支撑流畅交互。
6.3 从单图走向视频流
很多人问 SAM 能不能直接做视频分割,或者做关键帧插值。严格来说,SAM 本身并没有跨帧时序模型,直接逐帧调用推理性能吃不消,应对方案通常是:对首帧做精细分割,后续帧利用光流或跟踪算法修正掩码。这种组合思路也被很多项目实践过,比如跟踪加分割结合。如果你有类似需求,建议别一头扎进训练时序模型,而是先用 SAM 对稀疏关键帧做标注,再用传统跟踪器做帧间传播,效果已经很稳定了。
7. 运行性能优化与权重选型方案
最后一个部分聊聊性能。很多人忽视了模型的加载速度和推理速度对实际项目的影响,动手之前就已经决定用vit_h,跑起来之后才发现时间根本承受不起。
7.1 各权重规格的实际表现对照
我测试过在同一张图上三个规格的表现:
| 权重 | 参数量 | 推理耗时(GPU) | 显存占用 | 效果评价 |
|---|---|---|---|---|
| vit_b | 约 91M | 0.2~0.4 秒/次 | 约 3GB | 足够日常分割,复杂边缘稍弱 |
| vit_l | 约 308M | 0.8~1.2 秒/次 | 约 6GB | 边缘更细腻,适合高质量标注 |
| vit_h | 约 637M | 1.5~2.5 秒/次 | 约 11GB | 最强分割力,但需要大显存硬件 |
这里的时间是在 RTX 级别显卡上的实测范围,CPU 请直接忽略这个表。如果你的目标是做实时交互式标注,vit_b是唯一合理选择;如果离线批量处理、不介意时间,vit_h才能发挥最强实力。
7.2 转换到半精度或量化进行加速
如果你已经用了vit_b还觉得慢,可以考虑把模型转成半精度推理。做法如下:
import torch sam = sam_model_registry["vit_b"](checkpoint="...") sam = sam.half().cuda()转换后,显存占用会再下降近一半,速度提升 20%~30%。唯一要注意的是,你的图像输入特征也要注意转成float16,否则会有一个 dtype mismatch 的报错。predictor.set_image内部处理的部分地方没有显式转类型,需要自己在调用时注意。做推理测试没问题,但你要是在做需要严格数值稳定的任务,谨慎使用半精度。
7.3 批处理你的图像:定时批量推理
批量场景下,与其写一个for循环反复调用predict,不如把不同图像放到一个 batch 里,一次把图像编码都做完,但提示编码和掩码解码还是要逐图进行的。这个道理和所有深度学习推理一致,最耗时的编码阶段是共享的。实际操作中,我发现大部分任务瓶颈就在于图像编码,而不是具体推断,所以预先批量编码能明显提高吞吐量。
如果你做的是离线标注,可以把图像按批分组,编码完成后保存特征到内存或者缓存文件,后续多次提示复用时就无需重新编码了,这种工程化设计能少掉大量等待时间。
经过这一整套折腾,我个人对 SAM 的使用建议可以浓缩成一句话:先用vit_b把链路跑通,再根据效果决定要不要升级权重,不要反过来。很多时候你觉得效果不好,不是模型不够强,而是提示方式、阈值参数和图像预处理没有配合好。先从最简单的单点提示开始,观察输出,然后再逐步把框、负点、多候选这些手段叠加进去,不同的任务需要的提示组合方式差异很大,没有哪个组合是万能解。这个规律在我处理不同数据集时反复验证,省下的 Git 提交记录和崩溃日志,比任何读文档的时间都有价值。