AB
AiBoss站
教程

Segment Anything(SAM)

教程

用 Segment Anything(SAM)在 Python 中做单点提示图像分割

面向需要快速上手 Segment Anything 的开发者,讲清 SAM 的提示式分割思路、Python 环境与依赖准备、模型权重与模型类型的选择、SamPredictor 的完整调用流程,以及单点提示分割从读图、设图、预测到掩码可视化的可运行示例。

Segment Anything(SAM)是 Meta AI 提出的图像分割模型,它的特点是把「分割什么」交给提示来决定:给一个点、一个框,或者若干正负样本点,模型就返回对应的物体掩码。它适合需要从图片里快速抠出任意物体的场景,比如做标注辅助、图像编辑前的选区生成、科学图像里的目标提取,也适合想在自己的 Python 流程里嵌入一个通用分割能力的开发者。这篇教程讲的是本地 Python 环境下的用法,重点是单点提示分割这条最短路径,同时把模型类型、权重文件、预测参数和返回值这些容易卡住的地方讲清楚。如果你更想先在线试一下效果,可以打开 Segment Anything 工具页面对照理解提示式分割的交互方式,再回到本地写代码。

准备工作

Python 与深度学习框架

运行 SAM 的代码要求 Python 3.8 及以上版本,并且需要 PyTorch 1.7 及以上、torchvision 0.8 及以上。如果机器上有 NVIDIA 显卡并装好了对应的驱动,建议安装支持 CUDA 的 PyTorch 与 torchvision,这样推理会明显快一些;只有 CPU 也能跑,但速度会慢很多,尤其是较大的模型。

PyTorch 的安装方式与 CUDA 版本强相关,不同系统、不同显卡驱动对应的安装命令不一样,具体命令请以 PyTorch 官网当前的安装指引为准。装好之后可以在 Python 里确认一下版本:

import torch, torchvision
print(torch.__version__)
print(torchvision.__version__)
print(torch.cuda.is_available())

最后一行返回 True 说明当前环境能用到 GPU,后面代码里的 device 就可以写 "cuda";返回 False 就写 "cpu"。

安装 segment-anything 包

SAM 的代码以 Python 包的形式发布,安装方式有两种。第一种是直接从代码仓库安装:

pip install git+https://github.com/facebookresearch/segment-anything.git

这种方式需要机器上已经装好 git,并且能访问代码托管站点。第二种是先把源码压缩包下载到本地,再在命令行里用 pip 安装这个 zip 文件,适合网络受限或者想固定某个版本的情况:

cd /d G:\downloads
pip install segment-anything-main.zip

路径按你实际存放压缩包的位置改。安装完成后,能在 Python 里成功 import 就说明装好了:

from segment_anything import sam_model_registry, SamPredictor
print("ok")

安装其余依赖

除了 torch 之外,这个项目还依赖 opencv-python、pycocotools、matplotlib、onnxruntime 等包。opencv 用来读写和缩放图片,matplotlib 用来把掩码画出来看,pycocotools 与 onnxruntime 在部分流程和导出场景中会用到。可以一次性装上:

pip install opencv-python pycocotools matplotlib onnxruntime onnx torch

如果 torch 已经按上一步装好了,这里重复写一次通常不会有问题,pip 会识别已安装的版本。

准备模型权重与模型类型

SAM 的权重文件需要单独下载,文件名与模型类型一一对应。代码里通过 model_type 指定用哪一个,通过 checkpoint 指定权重文件路径,两者必须匹配,否则加载时会报错。常见的对应关系是:

model_type权重文件名适用场景
vit_bsam_vit_b_01ec64.pth体积最小、速度最快,适合先跑通流程
vit_lsam_vit_l_0b3195.pth精度与速度折中
vit_hsam_vit_h_4b8939.pth精度最高,显存与耗时也最大

权重文件的具体下载地址、文件大小与许可证信息请以项目官方页面当前的信息为准。把下载好的 .pth 文件放在一个固定目录里,代码中用绝对路径引用最稳妥,可以避免工作目录变化导致的找不到文件。

准备一张测试图片

源码包的 notebooks/images 目录下自带示例图片,例如 truck.jpg,可以直接拿来测试。也可以换成自己的图片,但要注意后面输入点的坐标是相对于「送入模型的那张图」的像素坐标,如果对图片做了缩放,坐标也要跟着换算。

操作步骤

第一步:读入图片并转成 RGB

OpenCV 读进来的图片默认是 BGR 通道顺序,而 SAM 与 matplotlib 都按 RGB 处理,所以必须转换。如果图片太大,可以先缩放,减少显存占用和推理时间:

import cv2

image = cv2.imread(r"G:/data/images/truck.jpg")
image = cv2.resize(image, None, fx=0.5, fy=0.5)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
print(image.shape)

打印出的 shape 是 (高, 宽, 3)。记住这个高和宽,后面选提示点坐标时不能超出这个范围。

第二步:加载模型并搬到设备上

用 sam_model_registry 按 model_type 取出对应的模型构造函数,传入 checkpoint 完成权重加载,再用 .to(device) 把模型放到 GPU 或 CPU 上:

import torch
from segment_anything import sam_model_registry, SamPredictor

sam_checkpoint = "G:/models/sam_vit_b_01ec64.pth"
model_type = "vit_b"
device = "cuda" if torch.cuda.is_available() else "cpu"

sam = sam_model_registry[model_type](checkpoint=sam_checkpoint)
sam.to(device=device)
predictor = SamPredictor(sam)

这里把 device 写成自动判断,比硬编码 "cuda" 更省事:没有 GPU 的机器上不会因为找不到 CUDA 而直接崩掉。

第三步:把图片交给 predictor

SamPredictor 会先对整张图做一次图像编码,这一步是后续所有提示预测的公共开销。同一张图上做多次不同提示的预测时,只需要 set_image 一次:

predictor.set_image(image)

set_image 之后,predictor 内部保存了这张图的特征。换图时必须重新调用,否则预测结果会对应到上一张图。

第四步:构造提示点与标签

提示点用 numpy 数组表示,形状是 (N, 2),每个元素是 (x, y) 像素坐标。标签数组形状是 (N,),取值 1 表示这个点是前景(要分割的物体),0 表示背景(要排除的区域)。单点分割就是只给一个前景点:

import numpy as np

input_point = np.array([[250, 187]])
input_label = np.array([1])

坐标的选择很关键:点落在目标物体内部,返回的掩码就是这个物体;点落在背景上,返回的就是背景区域。如果第一次结果不理想,可以增加负样本点,把不想要的部分标成 0,模型会据此调整分割范围。

第五步:调用 predict 得到掩码

把提示点、标签传进 predict,multimask_output=True 表示让模型输出多个候选掩码,通常是一个整体、一个局部、一个更大范围,由调用方按分数挑选:

masks, scores, logits = predictor.predict(
    point_coords=input_point,
    point_labels=input_label,
    multimask_output=True,
)
print(masks.shape)
print(scores)

返回值的含义:

  • masks:形状为 (候选数, 高, 宽) 的布尔数组,每个候选掩码对应一个二维布尔图,True 表示该像素属于分割结果。
  • scores:每个候选掩码的质量分数,数值越高表示模型越确信。单点提示下通常取分数最高的那个。
  • logits:掩码的低分辨率对数几率输出,需要做进一步处理才能当掩码用,一般用于需要精细调整阈值的场景。

如果只需要一个结果,把 multimask_output 设为 False,此时 masks 的第一维就是 1,模型直接给出它认为最合适的单个掩码。

第六步:把掩码叠加到原图上查看

掩码本身是布尔数组,直接看没有意义,需要乘上一个带透明度的颜色再叠加到原图上。下面两个辅助函数分别负责画掩码和画提示点:

import matplotlib.pyplot as plt

def show_mask(mask, ax, random_color=False):
    if random_color:
        color = np.concatenate([np.random.random(3), np.array([0.6])], axis=0)
    else:
        color = np.array([30/255, 144/255, 255/255, 0.6])
    h, w = mask.shape[-2:]
    mask_image = mask.reshape(h, w, 1) * color.reshape(1, 1, -1)
    ax.imshow(mask_image)

def show_points(coords, labels, ax, marker_size=375):
    pos_points = coords[labels == 1]
    neg_points = coords[labels == 0]
    ax.scatter(pos_points[:, 0], pos_points[:, 1], color='green', marker='*',
               s=marker_size, edgecolor='white', linewidth=1.25)
    ax.scatter(neg_points[:, 0], neg_points[:, 1], color='red', marker='*',
               s=marker_size, edgecolor='white', linewidth=1.25)

然后逐个候选掩码画出来,标题里带上分数,方便对比:

for i, (mask, score) in enumerate(zip(masks, scores)):
    plt.figure(figsize=(10, 10))
    plt.imshow(image)
    show_mask(mask, plt.gca())
    show_points(input_point, input_label, plt.gca())
    plt.title(f"Mask {i+1}, Score: {score:.3f}", fontsize=18)
    plt.axis('off')
    plt.show()

绿色星号是前景提示点,红色星号是背景提示点。看图上掩码是否贴合目标边缘,就能判断这次提示选得好不好。

一个完整示例

下面把前面的步骤串成一个可以直接运行的脚本。只需要改三处:图片路径、权重路径、提示点坐标。其余部分不用动。

import sys
import numpy as np
import torch
import matplotlib.pyplot as plt
import cv2
from segment_anything import sam_model_registry, SamPredictor

def show_mask(mask, ax, random_color=False):
    if random_color:
        color = np.concatenate([np.random.random(3), np.array([0.6])], axis=0)
    else:
        color = np.array([30/255, 144/255, 255/255, 0.6])
    h, w = mask.shape[-2:]
    mask_image = mask.reshape(h, w, 1) * color.reshape(1, 1, -1)
    ax.imshow(mask_image)

def show_points(coords, labels, ax, marker_size=375):
    pos_points = coords[labels == 1]
    neg_points = coords[labels == 0]
    ax.scatter(pos_points[:, 0], pos_points[:, 1], color='green', marker='*',
               s=marker_size, edgecolor='white', linewidth=1.25)
    ax.scatter(neg_points[:, 0], neg_points[:, 1], color='red', marker='*',
               s=marker_size, edgecolor='white', linewidth=1.25)

# 1. 读图并转 RGB
image = cv2.imread(r"G:/data/images/truck.jpg")
image = cv2.resize(image, None, fx=0.5, fy=0.5)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# 2. 加载模型
sam_checkpoint = "G:/models/sam_vit_b_01ec64.pth"
model_type = "vit_b"
device = "cuda" if torch.cuda.is_available() else "cpu"
sam = sam_model_registry[model_type](checkpoint=sam_checkpoint)
sam.to(device=device)
predictor = SamPredictor(sam)

# 3. 图像编码
predictor.set_image(image)

# 4. 提示点
input_point = np.array([[250, 187]])
input_label = np.array([1])

# 5. 预测
masks, scores, logits = predictor.predict(
    point_coords=input_point,
    point_labels=input_label,
    multimask_output=True,
)
print(masks.shape)

# 6. 可视化
for i, (mask, score) in enumerate(zip(masks, scores)):
    plt.figure(figsize=(10, 10))
    plt.imshow(image)
    show_mask(mask, plt.gca())
    show_points(input_point, input_label, plt.gca())
    plt.title(f"Mask {i+1}, Score: {score:.3f}", fontsize=18)
    plt.axis('off')
    plt.show()

运行后应该看到三张叠加了掩码的图,每张标题里有一个分数。如果三张图里有一张准确圈住了目标,说明流程已经跑通;如果三张都偏了,先检查提示点坐标是不是落在目标上,再检查图片是否做过缩放导致坐标错位。

脚本里的 sys.path.append("..") 只在源码目录结构下运行 notebook 时才需要,用 pip 安装成包之后可以删掉。

注意事项

模型类型与权重必须匹配

model_type 写 vit_b 却加载 vit_h 的权重,会在加载阶段直接报错。三个权重文件不能混用,下载时看清楚文件名里的 vit_b、vit_l、vit_h 标记。

坐标基于送入模型的图片

提示点坐标是相对于 set_image 时传入的那张图的像素坐标。如果先 resize 再 set_image,坐标就要按缩放比例换算;如果直接拿原图上的坐标去点缩放后的图,位置会整体偏移。这是单点分割最常见的出错原因。

set_image 的开销与复用

set_image 会对整张图做一次编码,是整条流程里比较重的一步。同一张图上试多个提示点时,不要每次都重新 set_image,把 predictor 复用起来即可。反过来,换了图片就必须重新调用,否则预测结果对应的是旧图。

显存与设备

vit_h 模型对显存的要求明显高于 vit_b。显存不足时会出现 CUDA out of memory,可以改用更小的模型类型、缩小输入图片尺寸,或者退回 CPU 运行(速度会慢很多)。代码里用 torch.cuda.is_available() 判断设备,可以避免在没有 GPU 的机器上直接报错。

单点提示的局限

一个点只能表达「我要这个位置的东西」,无法表达边界。当目标与背景颜色接近、或者目标由多个不连通部分组成时,单点结果往往不完整。这时可以增加负样本点排除干扰区域,或者改用框提示限定范围,再在框内补点微调。

notebook 文件需要 Jupyter 打开

源码包的 notebooks 目录下是 .ipynb 文件,用 PyCharm 这类编辑器直接打开看不到可执行内容,需要 Jupyter Notebook 或 JupyterLab 环境。如果只是想在本地跑通分割,直接用上面的 .py 脚本更省事。

版本与许可信息以官方为准

模型权重的下载方式、文件大小、可商用范围,以及依赖包之间的版本兼容关系都可能变化,使用前请以项目官方页面当前公布的信息为准。安装依赖时如果遇到版本冲突,优先保证 torch 与 torchvision 的版本匹配,再装其余包。