【SAMv1】 The “Segment Anything” Revolution in Computer Vision

文章目录

  • [一、为什么 SAM 会出现?](#一、为什么 SAM 会出现?)
    • [1.1 NLP 的"前辈们"已经探好了路](#1.1 NLP 的"前辈们"已经探好了路)
    • [1.2 分割任务本身需要一个新范式](#1.2 分割任务本身需要一个新范式)
    • [1.3 数据才是真正的护城河](#1.3 数据才是真正的护城河)
  • [二、Segment Anything Model 的 Pipeline 是什么](#二、Segment Anything Model 的 Pipeline 是什么)
    • [2.1 SAM v1 整体 Pipeline](#2.1 SAM v1 整体 Pipeline)
    • [2.2 SAM v1 三大核心模块](#2.2 SAM v1 三大核心模块)
      • [2.2.1 Image Encoder(图像编码器)](#2.2.1 Image Encoder(图像编码器))
      • [2.2.2 Prompt Encoder(提示编码器)](#2.2.2 Prompt Encoder(提示编码器))
      • [2.2.3 Mask Decoder(掩码解码器)](#2.2.3 Mask Decoder(掩码解码器))
  • [三、SAM 的 Interactive Pipeline(最核心)](#三、SAM 的 Interactive Pipeline(最核心))
  • [四、SAM 支持哪些交互功能](#四、SAM 支持哪些交互功能)
    • [4.1 Click Segmentation](#4.1 Click Segmentation)
    • [4.2 Multi-click Refinement](#4.2 Multi-click Refinement)
    • [4.3 Box-guided Segmentation](#4.3 Box-guided Segmentation)
    • [4.4 Everything Mode(分割一切)](#4.4 Everything Mode(分割一切))
    • [4.5 Automatic Mask Generation](#4.5 Automatic Mask Generation)
    • [4.6 Interactive Annotation](#4.6 Interactive Annotation)
  • [五、SAM Pipeline 为什么快](#五、SAM Pipeline 为什么快)
    • [5.1 重计算在 Encoder](#5.1 重计算在 Encoder)
    • [5.2 Decoder 很轻](#5.2 Decoder 很轻)
  • [六、Promptable Segmentation:到底"promptable"在什么?](#六、Promptable Segmentation:到底"promptable"在什么?)
    • [6.1 一句话理解](#6.1 一句话理解)
    • [6.2 zero-shot 的哲学](#6.2 zero-shot 的哲学)
    • [6.3 歧义感知:为什么一次返回 3 个 mask?](#6.3 歧义感知:为什么一次返回 3 个 mask?)
  • 七、三大模块拆解:不是论文翻译,是"它为什么这样设计"
    • [7.1 Image Encoder:为什么一定要 ViT?](#7.1 Image Encoder:为什么一定要 ViT?)
    • [7.2 Prompt Encoder:把"人类意图"翻译成"机器语言"](#7.2 Prompt Encoder:把"人类意图"翻译成"机器语言")
    • [7.3 Mask Decoder:SAM 最精妙的部分](#7.3 Mask Decoder:SAM 最精妙的部分)
  • [八、为什么 Foundation Model 思路能迁移到 Segmentation?](#八、为什么 Foundation Model 思路能迁移到 Segmentation?)
    • [8.1 什么是"分割的基础模型"?](#8.1 什么是"分割的基础模型"?)
    • [8.2 三个必要条件,SAM 都满足了](#8.2 三个必要条件,SAM 都满足了)
    • [8.3 SAM 学到的到底是什么?](#8.3 SAM 学到的到底是什么?)
  • 九、工程视角:从论文到生产线
    • [9.1 推理速度全景图](#9.1 推理速度全景图)
    • [9.2 Embedding Cache 的生产级设计](#9.2 Embedding Cache 的生产级设计)
    • [9.3 显存问题:模型很大,但可以分而治之](#9.3 显存问题:模型很大,但可以分而治之)
    • [9.4 移动端部署:这条路走得通吗?](#9.4 移动端部署:这条路走得通吗?)
    • [9.5 TensorRT / ONNX 部署实战](#9.5 TensorRT / ONNX 部署实战)
  • [十、SAM 的缺点:6 个真实场景下的翻车记录](#十、SAM 的缺点:6 个真实场景下的翻车记录)
    • [10.1 密集细长结构 ------ "纹理混乱"](#10.1 密集细长结构 —— "纹理混乱")
    • [10.2 低对比度目标 ------ "看不见就是看不见"](#10.2 低对比度目标 —— "看不见就是看不见")
    • [10.3 上下文依赖概念 ------ "它真的不懂语境"](#10.3 上下文依赖概念 —— "它真的不懂语境")
    • [10.4 对扰动的脆弱性](#10.4 对扰动的脆弱性)
    • [10.5 零样本不是万能 ------ 新域失败率高达 72.6%](#10.5 零样本不是万能 —— 新域失败率高达 72.6%)
    • [10.6 微调中的灾难性遗忘](#10.6 微调中的灾难性遗忘)
    • [10.7 补充](#10.7 补充)
  • [十一、SAM vs MobileSAM vs FastSAM:选型指南](#十一、SAM vs MobileSAM vs FastSAM:选型指南)
    • [11.1 架构差异一览](#11.1 架构差异一览)
    • [11.2 MobileSAM:比你想象的精巧](#11.2 MobileSAM:比你想象的精巧)
    • [11.3 FastSAM:完全不同的哲学](#11.3 FastSAM:完全不同的哲学)
    • [11.4 选型决策框架](#11.4 选型决策框架)
  • [十二、SAM v1 Python Demo:从入门到魔改](#十二、SAM v1 Python Demo:从入门到魔改)
    • [12.1 环境配置](#12.1 环境配置)
    • [12.2 最简 Demo:点一下,出 mask](#12.2 最简 Demo:点一下,出 mask)
    • [12.3 场景 1:交互式抠图(多点修正)](#12.3 场景 1:交互式抠图(多点修正))
    • [12.4 场景 2:全图自动分割 → 生成 instance masks](#12.4 场景 2:全图自动分割 → 生成 instance masks)
    • [12.5 场景 3:框选分割 ------ Bounding Box Prompt](#12.5 场景 3:框选分割 —— Bounding Box Prompt)
    • [12.6 场景 4:批量处理 ------ Embedding Cache 实战](#12.6 场景 4:批量处理 —— Embedding Cache 实战)
  • 十三、总结金句

为什么一个 632M 参数的模型能在 23 个没见过的数据集上零样本超越有监督方法?为什么 Meta 要花重金标 11 亿个掩码?这篇文章试图用工程师的语言回答这些问题。


一、为什么 SAM 会出现?

1.1 NLP 的"前辈们"已经探好了路

2023 年春天,如果你是一个 CV 研究员,你会陷入一种复杂的情绪。

一边是 NLPer 的狂欢 ------ GPT-4 刚发布两个月,few-shot 推理碾压一切榜单,CLIP 已经把视觉和语言打通。另一边是做检测、分割的工程师们,还在为每个新数据集重复写 pipeline:标数据 → 训模型 → 调参 → 上线 → 下一个数据集继续循环。

NLP 的剧本已经写好了:1 个大模型 + prompt,干翻所有下游 fine-tune。那 CV 呢?能不能有一个模型,你给什么它就分割什么?

这不是个技术问题,这是个对世界的表示方式的问题。

1.2 分割任务本身需要一个新范式

回顾 SAM 出现前的分割格局:

任务 典型方法 本质限制
语义分割 DeepLab / SegFormer 闭集,只能分固定类别
实例分割 Mask R-CNN / YOLACT 闭集,依赖检测框
交互式分割 RITM / f-BRS 不"理解"图像,只是像素分类

每一个任务都是一个孤岛。SAM 的野心是:用 prompt 的抽象,统一所有分割任务

1.3 数据才是真正的护城河

但如果你 2022 年就在 FAIR 工作,你知道比模型更稀缺的是什么 ------ 数据。互联网上有几十亿张图片,但每张图片上"所有物体"的精确掩码标注几乎不存在。

所以 SAM 的故事里,有两条并行的主线:模型 (SAM)+ 数据引擎(SA-1B)。两者滚动迭代,互相放大。

SAM 三大阶段数据引擎流程图------辅助标注→半自动→全自动,每轮参与的标注员数量变化、模型能力变化。


二、Segment Anything Model 的 Pipeline 是什么

SAM v1(Segment Anything Model)本质上是一个:

"可交互(interactive)的 Promptable Segmentation 系统"

它不是传统:

text 复制代码
输入图像 → 输出固定类别mask

而是:

text 复制代码
输入图像 + 用户提示(prompt)
→ 输出对应目标mask

核心思想:

"你告诉我想分割哪里,我负责给你 mask。"


2.1 SAM v1 整体 Pipeline

SAM v1 整体可以理解成:

text 复制代码
                 ┌────────────────┐
                 │   Input Image  │
                 └────────┬───────┘
                          │
                          ▼
              ┌────────────────────┐
              │   Image Encoder    │
              │      (ViT)         │
              └────────┬───────────┘
                       │
             Image Embedding
                       │
        ┌──────────────┴──────────────┐
        │                             │
        ▼                             ▼
┌────────────────┐        ┌─────────────────┐
│ Prompt Encoder │        │ Mask Decoder    │
│ point/box/mask │ ─────► │ cross-attention │
└────────────────┘        └────────┬────────┘
                                   │
                                   ▼
                           Segmentation Mask

可以把 SAM 想成:

text 复制代码
一个"视觉版 GPT"

Image Encoder:
负责理解世界

Prompt Encoder:
负责理解用户意图

Mask Decoder:
负责把意图映射到像素区域

这也是为什么:

SAM 被认为是 segmentation foundation model 的起点。

实际工程经常这样:

text 复制代码
Image
  ↓
Image Encoder (offline/cache)
  ↓
Embedding Cache
  ↓
User Prompt
  ↓
Lightweight Decoder
  ↓
Mask

2.2 SAM v1 三大核心模块

2.2.1 Image Encoder(图像编码器)

SAM v1 使用:

  • ViT-B
  • ViT-L
  • ViT-H

本质:

text 复制代码
Image → Dense Feature Embedding

作用:

把整张图编码成"通用视觉特征"。

类似:

text 复制代码
GPT 把文本编码成 token embedding

这里:

text 复制代码
SAM 把图像编码成 image embedding

特点

① 只计算一次

这是 SAM 非常关键的优化。

text 复制代码
图片不变
→ embedding 不变

所以:

  • 用户不断点击
  • 不需要重新跑 ViT

只需要:重新跑 decoder

这就是:

SAM 交互速度快的核心。


② embedding cache

工程里:

text 复制代码
先缓存 image embedding

之后:

text 复制代码
point prompt
bbox prompt
mask prompt

都复用 embedding。

这也是移动端部署重点。


2.2.2 Prompt Encoder(提示编码器)

这是 SAM 最革命的地方。

SAM 不再:

text 复制代码
固定类别

而是:

text 复制代码
Prompt-guided segmentation

SAM 支持哪些 Prompt

① Point Prompt(点击)

用户:

text 复制代码
点一下目标

例如:

  • 点猫
  • 点人
  • 点桌子

SAM:

text 复制代码
输出对应mask

② Positive / Negative Point

SAM 支持:

类型 含义
Positive Point "这是目标"
Negative Point "这不是目标"

例如:

text 复制代码
点人(正样本)
点背景(负样本)

mask 会不断修正。

这就是:

交互式分割。


③ Box Prompt(框提示)

输入:

text 复制代码
bbox

SAM:

text 复制代码
在框里做精细分割

非常像:

  • 检测 + 分割

工程里极其常见。


④ Mask Prompt

输入:

text 复制代码
已有mask

SAM:

text 复制代码
继续 refinement

用于:

  • 多轮编辑
  • 视频时序
  • 交互修正

2.2.3 Mask Decoder(掩码解码器)

核心:

text 复制代码
Image Embedding
+
Prompt Embedding
→ Mask

这里用了:

  • Transformer Decoder
  • Cross Attention

本质:

"根据 prompt,从 image embedding 中找到对应区域。"

类似:

text 复制代码
文本 prompt
→ LLM 找相关 token

SAM:

text 复制代码
point prompt
→ decoder 找对应像素区域

三、SAM 的 Interactive Pipeline(最核心)

SAM 真正厉害的是:

Human-in-the-loop

也就是:

text 复制代码
人不断点击
→ 模型实时修正mask

流程:

text 复制代码
用户点击
   ↓
Prompt Encoder
   ↓
Mask Decoder
   ↓
生成mask
   ↓
用户不满意继续点击
   ↓
再次更新mask

四、SAM 支持哪些交互功能

4.1 Click Segmentation

最经典。

text 复制代码
点哪里
分哪里

4.2 Multi-click Refinement

连续点击修正。

text 复制代码
正点
负点

逐步逼近真实边界。


4.3 Box-guided Segmentation

检测框引导分割。

常见于:

  • 自动标注
  • instance segmentation

4.4 Everything Mode(分割一切)

这是 SAM demo 最震撼的功能。

SAM 会:

text 复制代码
自动生成大量mask proposal

输出:

text 复制代码
整张图所有可分割区域

类似:

text 复制代码
class-agnostic segmentation

4.5 Automatic Mask Generation

自动扫描:

text 复制代码
grid points

然后:

text 复制代码
生成所有mask

工程里常用于:

  • 自动标注
  • 数据集生成

4.6 Interactive Annotation

这是工业界最重要应用。

人工:

text 复制代码
点一下

SAM:

text 复制代码
自动出mask

标注效率暴涨。


五、SAM Pipeline 为什么快

关键:

5.1 重计算在 Encoder

ViT 最重。

但:

text 复制代码
只算一次

5.2 Decoder 很轻

交互时:

text 复制代码
只更新 decoder

因此:

  • 可以实时点击
  • 可以连续交互


六、Promptable Segmentation:到底"promptable"在什么?

6.1 一句话理解

Promptable segmentation = 把分割变成一个"填空"任务,而不是一个"归类"任务。

传统语义分割的流程是:输入图像 → 模型 → 80 个类别的 mask 概率图。你只能得到模型见过的类别。

SAM 的流程是:输入图像 + "我想要这块" → 模型 → mask。

6.2 zero-shot 的哲学

这里有一个思维实验:

你给一个从未见过斑马的人看一张斑马照片,说"把左边这只指出来"。他能做到 ------ 哪怕他没见过斑马。为什么?因为 分割的本质是分组(grouping),不是分类(classification)

SAM 抓住了这个本质:把分割从"这个东西是什么"解耦为"这个东西在哪"

6.3 歧义感知:为什么一次返回 3 个 mask?

这是 SAM 最精巧的设计之一。考虑一个场景:你在一张人像上点了一下他的衣服。

  • mask 1:衣服(最细粒度)
  • mask 2:上衣(整体)
  • mask 3:整个人(最大粒度)

三个答案都对,只是歧义的层级不同。SAM 输出 3 个 mask 并按 IoU 排序,把"选择权"交还给用户。

三格图------同一个点击分别在衣服/上衣/整个人三个粒度上的 mask 输出,标注不同的 IoU 分数。


七、三大模块拆解:不是论文翻译,是"它为什么这样设计"

7.1 Image Encoder:为什么一定要 ViT?

核心作用 :将整张图像映射为一个稠密的特征图(256 × 64 × 64),每张图像只跑一次

设计上遵循两个关键原则:

原则 1:图像的表示应该是"一次性"的。

传统交互式分割(如 RITM)每次用户点击都要重新跑特征提取。SAM 的思路是:先一次性编码整张图的"语义字典",后续 prompt 只需在字典里查表,不需要重算。

类比一下就是:你有一本字典(image embedding),然后每次输入 prompt 就是在字典里翻到对应页,而不是每次重新编一本字典。

原则 2:用 ViT 而不是 CNN。

原因很直白 ------ ViT 的全局自注意力机制天然适合"理解整张图"。CNN 的卷积核感受野有限,即使深了也很难建立远距离关联。而 SAM 要做的是 对任何物体的泛化分割,这里面可能有头发丝那么细的物体,也可能有天空那么大的背景。

一个技术细节:SAM 用的是 MAE 预训练的 ViT-H(Masked Autoencoder)。MAE 预训练让 ViT 学会"从局部推断整体"的能力,这种能力天然适合分割 ------ 分割就是看局部像素推断整体 mask。

三个变体的工程权衡:

模型 参数 Encoder 特征维 单张图编码时间 适用场景
ViT-B 86M 768 → 256 ~0.15s (A100) 需要速度优先
ViT-L 307M 1024 → 256 ~0.38s (A100) 精度和速度平衡
ViT-H 632M 1280 → 256 ~0.73s (A100) 追求极致精度

ViT 把图像切成 16×16 patch 的示意图,最终输出 64×64×256 的 feature map。

7.2 Prompt Encoder:把"人类意图"翻译成"机器语言"

Prompt Encoder 是 SAM 的"翻译官"。它把各种形式的 prompt 统一编码为固定维度的向量,喂给 Mask Decoder。

稀疏 prompt(稀疏 = 少数几个位置信息):

  • (x, y) 坐标 + 前景/背景标记 → 位置编码 → 256 维向量。每个点就是一个 256 维向量,N 个点就是 N × 256。
  • :左上角 + 右下角 → 两个点的位置编码 → 2 × 256。
  • 文本:用 CLIP text encoder 编码。但原版 SAM 实际没有发布文本功能,训练时也没有暴露给文本 encoder。

稠密 prompt:

  • mask :输入一个粗 mask → 卷积下采样 4 倍 → 与 image embedding 逐元素相加

为什么是"相加"而不是"拼接"?因为 mask 本质上是在修正 image embedding ------ 告诉它"注意这里、忽略那里",而不是提供全新信息。

四种 prompt 类型(点/框/mask/文本)的编码示意图,突出每种编码方式是"稀疏"还是"稠密"。

7.3 Mask Decoder:SAM 最精妙的部分

如果 Image Encoder 是"眼睛",Prompt Encoder 是"耳朵",那 Mask Decoder 就是"大脑"。

它的任务:把 image embedding(全局特征)和 prompt embeddings(局部意图)融合,输出精确的 mask。

为什么只要 2 层 Transformer?

这是反直觉的设计 ------ SAM 的 encoder 用了 32 层 ViT,decoder 却只有 2 层。

原因在于:decoder 不需要重新理解图像,它只是在查表。Image Encoder 已经把图像压缩成 64×64 的"语义码本",decoder 只需快速扫描 + 匹配。这也是为什么 decoder 可以在 CPU 上 ~50ms 就跑完。

双层交叉注意力的真正含义:

复制代码
Layer × 2:
  1. Self-Attention (token 之间)       → 理解多个 prompt 之间的关系
  2. Cross-Attn: Token → Image         → "prompt 去图像里找对应特征"
  3. MLP + 残差                        → 非线性变换
  4. Cross-Attn: Image → Token         → "图像特征被 prompt 信息增强"

第 2 步和第 4 步是双向的 :不仅让 prompt 看到图像(先),还让图像"吸收"prompt 的信息(后)。这个双向设计是关键 ------ 它不是单向查询,而是对话

输出头的妙用:

  • 4 个可学习的 output tokens:3 个 mask token + 1 个 IoU token
  • 3 个 mask token 分别生成了 3 个不同粒度的 mask(歧义感知的核心机制)
  • IoU token 预测每个 mask 的质量分数

解码后,mask token 与 image embedding 做点积得到最终 mask。这意味着 mask 的本质是"token 与每个像素的相似度"。


八、为什么 Foundation Model 思路能迁移到 Segmentation?

8.1 什么是"分割的基础模型"?

NLP 的 GPT 之所以是 foundation model,因为它学到了一个通用的能力:给定前缀,预测下一个词。这个能力足够原子化,又足够通用,任何 NLP 任务都能被转化为这个形式。

SAM 的对应是:给定一张图 + 一个提示,预测对应的 mask

8.2 三个必要条件,SAM 都满足了

条件 NLP (GPT) Vision (SAM)
统一的接口 prompt → text prompt → mask
海量数据 WebText / CommonCrawl SA-1B (11 亿 mask)
足够大的模型容量 175B params 632M params

SA-1B 是第一个"分割版本的 CommonCrawl"------11M 张图片、1.1B 个 mask,是之前任何分割数据集的 400 倍。

8.3 SAM 学到的到底是什么?

这是一个哲学问题。SAM 学到的不是"猫的 mask 长什么样",而是一种通用的分组感知能力

  • 颜色一致性 → 这个区域应该连在一起
  • 纹理连续性 → 边缘在哪
  • 深度/遮挡 → 这里物体重叠了
  • 语义理解 → 这块是一个独立实体

这种"分组感知"就像婴儿学会的"物体恒存"概念 ------ 不需要知道物体名字,就知道它在哪、多大。


九、工程视角:从论文到生产线

9.1 推理速度全景图

SAM 的推理分为两个阶段,差异巨大:

阶段 耗时 (A100, bs=1) 耗时 (CPU) 是否可复用
Image Encoder (ViT-H) ~730ms ~10-20s ✅ 每图一次
Prompt Encoder + Mask Decoder ~5ms ~50ms ❌ 每次 prompt

这意味着:如果你要对同一张图做多次分割,只需编码一次

一个实际的交互式标注工作流:

  1. 加载图片 → 编码(730ms)← 一次性开销
  2. 用户点第一下 → 解码(5ms)
  3. 用户点第二下 → 解码(5ms)
  4. ......

9.2 Embedding Cache 的生产级设计

这是 SAM 应用中最重要也最容易被忽略的工程优化。

核心思想 :Image Encoder 的输出(256 × 64 × 64)只依赖图片不依赖 prompt,所以可以提前算好存起来

一个参考的设计模式:

python 复制代码
# 伪代码:embedding cache 模式
class SAMInferenceService:
    def __init__(self):
        self.image_encoder = load_sam_encoder("sam_vit_h.pth")  # GPU
        self.mask_decoder = load_sam_decoder("sam_vit_h.pth")   # CPU/GPU
        self.embedding_cache = LRUCache(max_size=100)  # 缓存最近100张图

    def segment(self, image_id: str, image: Image, prompt: Prompt) -> List[Mask]:
        # Step 1: 查缓存
        if image_id in self.embedding_cache:
            image_embedding = self.embedding_cache[image_id]
        else:
            # Step 2: 缓存未命中,编码
            image_embedding = self.image_encoder(image)  # GPU
            self.embedding_cache[image_id] = image_embedding

        # Step 3: 轻量解码(可以移到 CPU 甚至浏览器)
        masks, scores = self.mask_decoder(image_embedding, prompt)
        return masks

典型场景的收益:

场景 无缓存延迟 有缓存延迟 加速比
网页图片标注(100 次 prompt/图) 100 × 735ms ≈ 73s 730ms + 100 × 5ms ≈ 1.2s 60×
视频秒级分割(30fps × 1s) 30 × 735ms ≈ 22s 735ms + 30 × 5ms ≈ 0.9s 24×

存储成本:单张图的 embedding 大小 = 256 × 64 × 64 × 4 字节(float32)≈ 4MB。1000 张图缓存仅需 ~4GB。

9.3 显存问题:模型很大,但可以分而治之

组件 参数量 FP32 显存 FP16 显存
ViT-H Encoder 632M ~2.5 GB ~1.3 GB
Mask Decoder 4M ~16 MB ~8 MB
总计 636M ~2.5 GB ~1.3 GB

但关键在于:encoder 和 decoder 可以异构部署

  • Encoder 放在 GPU(只跑一次)
  • Decoder 放在 CPU(每次 prompt)

如果整条 pipeline 都在 CPU 上跑(纯 CPU 推理),单次分割耗时 ~10-20s ------ 对交互式应用来说太慢,但批量离线处理图片是完全可行的。

9.4 移动端部署:这条路走得通吗?

原版 SAM 无法直接部署到移动端。ViT-H 的 632M 参数对手机来说完全不可行。

但这催生了一系列轻量级工作:

模型 参数 速度 部署方式
SAM (ViT-H) 636M 730ms (A100) 服务器 GPU
SAM (ViT-B) 90M 150ms (A100) 服务器 GPU
MobileSAM ~10M ~10ms (GPU) ✅ 移动端
FastSAM 68M 40ms (GPU) ✅ 移动端
SAM-Lightening ~6M 7ms (A100) ✅ 边缘设备

9.5 TensorRT / ONNX 部署实战

ONNX 导出的可行性:

  • Image Encoder 和 Mask Decoder 需要分开导出,因为它们是独立运行的
  • ViT encoder 可以导出为静态 shape ONNX(1024×1024),推理时 resize
  • Decoder 的 prompt tokens 数量是动态的,需要处理 dynamic axis

TensorRT 优化效果(参考 SAM 家族模型的经验):

优化手段 加速比 显存节省
ONNX 导出 1.1× ~10%
TensorRT FP16 2.5× ~65%
TensorRT INT8 3.0× ~75%

一个参考的部署链路

  1. PyTorch → ONNX(torch.onnx.export)
  2. ONNX → TensorRT Engine(trtexec 或 Python API)
  3. 服务端:Triton Inference Server 加载 TensorRT engine
  4. 前端:发送图片 → 服务端编码 → 返回 embedding → 浏览器端解码

注意 :ViT 的注意力层在 TensorRT 中可能遇到算子不支持问题。常见做法是使用 torch2trt 或手写 plugin。


十、SAM 的缺点:6 个真实场景下的翻车记录

10.1 密集细长结构 ------ "纹理混乱"

树木枝干、血管、裂缝、头发丝 ------ SAM 在此类任务上表现糟糕。2024 年 WACV 一篇论文将其命名为 "纹理混淆(textural confusion)"SAM 把局部分叉模式误解为全局纹理。更关键的是,针对性微调也无法解决,说明这是架构层面的根本局限。

10.2 低对比度目标 ------ "看不见就是看不见"

在医学影像(CT、MRI)、红外监控等场景,SAM 对低对比度边界的判断能力大幅下降。一个 X 光片里的塑料瓶,对人眼来说很明显,对 SAM 来说几乎透明。

教训:SAM 学的是自然图像中的分组模式,跨域到医学/工业场景时,这种 pattern 不存在

10.3 上下文依赖概念 ------ "它真的不懂语境"

2024 年一项全面评测发现,SAM 对以下概念几乎无能为力:

  • 伪装物体(需要理解"这里应该藏了东西")
  • 产品缺陷(正常和异常只有细微差别)
  • 视觉显著性(人一眼看到的地方,SAM 不一定觉得重要)

这些都是需要判别能力的任务 ,而 SAM 本质上是个生成式分组模型,它不擅长"判断是不是"。

10.4 对扰动的脆弱性

噪声、模糊、JPEG 压缩 ------ 真实世界的图片不如实验室干净。2024 年 Pattern Recognition 期刊的研究显示 SAM 对常见图像降质敏感度差异很大,某些扰动下 mIoU 下降超过 20%。

10.5 零样本不是万能 ------ 新域失败率高达 72.6%

在全新领域(如特定工业场景),SAM 的 FR₃₀@90(达到 90% IoU 的失败率)高达 72.6%。这提醒我们:SAM 的"分割一切"是建立在自然图像分布上的"一切",不是真正的宇宙万物

10.6 微调中的灾难性遗忘

NeurIPS 2024 的一篇论文发现:对 SAM 做轻量微调后,模型在目标任务上变好了,但在其他任务上显著退化。这个现象在 NLP 领域早被讨论,但在 SAM 上表现得特别明显 ------ 因为它破坏了模型的"通用分组"能力。


10.7 补充

(1)Encoder 太大

ViT-H:

  • 非常吃显存
  • 移动端困难

(2)实时视频弱

因为:

text 复制代码
没有时序建模

所以:

  • 视频会闪烁
  • mask 不稳定

这也是后面 SAM 2 的方向。


十一、SAM vs MobileSAM vs FastSAM:选型指南

11.1 架构差异一览

维度 SAM MobileSAM FastSAM
作者 Meta FAIR 韩国KAIST 中科院自动化所
发布时间 2023.04 2023.06 2023.06
视觉编码器 ViT-H (Transformer) 蒸馏版 ViT-Tiny CNN (YOLOv8)
参数总量 636M ~10M 68M
Encoder 速度 730ms (A100) ~8ms (A100) ~38ms (A100)
Mask Decoder 2层 Transformer 2层 Transformer(同SAM) 无(两阶段)
速度 ~50ms/prompt ~5ms/prompt 恒定~40ms(不含prompt阶段)
训练策略 从头训 + MAE 知识蒸馏 YOLOv8 + 2% SA-1B

11.2 MobileSAM:比你想象的精巧

MobileSAM 的核心方法是解耦蒸馏(decoupled distillation):

  1. 先单独蒸馏 Image Encoder(ViT-Tiny ← ViT-H),使用特征层面的 MSE loss
  2. 然后直接复用 SAM 原版的 Mask Decoder(注意:decoder 是完全一样的

这意味着:MobileSAM 不是"重新训练一个轻量 SAM",而是"只蒸馏 encoder + 复用 decoder"。decoder 只有 4M 参数,对整体精度影响极小,却保留了 SAM 完整的交互式分割能力。

为什么 decoder 不用蒸馏? 因为 decoder 只有 2 层 Transformer 加几个 MLP,总共 4M 参数,推理只要 5ms。不值得花时间优化。

11.3 FastSAM:完全不同的哲学

FastSAM 不是一个 "SAM 的加速版",它是一个完全不同的架构。它的设计思路是:

用 YOLOv8 做类别无关的实例分割 → 用后处理匹配 prompt

FastSAM 的关键洞察:

  • 先分割图中所有"东西"(类别无关的实例 mask)
  • 然后根据 prompt 筛选出想要的 mask

这是两阶段方法的复兴:先提出候选 → 再匹配

优势

  • 速度不随点数量增加(因为"所有实例分割"是一次性完成的)
  • 完全 CNN 架构,成熟度极高,部署简单
  • 可以同时做目标检测(底层就是 YOLOv8)

劣势

  • 本质上没有"promptable"的灵活性 ------ 只能从"已有的实例"里选,不能精确到任意形状
  • 小物体边界容易出现伪影
  • 在复杂场景下,实例分割可能漏掉某些物体

11.4 选型决策框架

复制代码
你的场景是什么?

├── 追求极致精度,不计成本
│   └── SAM (ViT-H) + Embedding Cache
│
├── 交互式应用(用户频繁点),需要快速响应
│   └── SAM (ViT-H) + Cache(编码一次,解码快速)
│
├── 移动端 / 边缘设备
│   ├── 需要 SAM 风格的灵活交互 → MobileSAM
│   └── 只需要"选出所有物体" → FastSAM
│
├── 视频实时处理
│   └── FastSAM(CNN 架构更适合流式)
│
└── 批量离线处理(标注平台)
    ├── GPU 服务器 → SAM (ViT-H) + 多卡并行编码
    └── CPU 集群 → SAM (ViT-B) + ONNX Runtime

十二、SAM v1 Python Demo:从入门到魔改

12.1 环境配置

bash 复制代码
pip install git+https://github.com/facebookresearch/segment-anything.git
pip install opencv-python pycocotools matplotlib onnx onnxruntime

下载模型权重:

bash 复制代码
# ViT-H(最大最准)
wget https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth

# ViT-B(轻量快速)
wget https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth

# ViT-L(均衡选择)
wget https://dl.fbaipublicfiles.com/segment_anything/sam_vit_l_0b3195.pth

12.2 最简 Demo:点一下,出 mask

python 复制代码
import cv2
import numpy as np
import matplotlib
matplotlib.use('Agg')  # 无 GUI 后端,仅用于保存图片
import matplotlib.pyplot as plt
from segment_anything import sam_model_registry, SamPredictor

# 1. 加载模型
sam = sam_model_registry["vit_h"](checkpoint="./sam_vit_h_4b8939.pth")
sam.to("cuda")
predictor = SamPredictor(sam)

# 2. 加载并编码图像
image = cv2.imread("./demo.png")
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
predictor.set_image(image)  # ← Image Encoder 只在这里跑一次

# 3. 点一下 ------ 传入 image embedding + prompt
input_point = np.array([[280, 350]])   # (x, y) 坐标
input_label = np.array([1])            # 1=前景, 0=背景

masks, scores, logits = predictor.predict(
    point_coords=input_point,
    point_labels=input_label,
    multimask_output=True,  # 返回 3 个候选 mask
)

# 4. 可视化 ------ 取最高分的 mask
best_idx = np.argmax(scores)
plt.figure(figsize=(12, 8))
plt.imshow(image)
plt.imshow(masks[best_idx], alpha=0.5, cmap='jet')
plt.scatter(input_point[:, 0], input_point[:, 1], c='red', s=100)
plt.title(f"Mask Score: {scores[best_idx]:.3f}")
plt.axis('off')
plt.savefig("./demo_result.png", dpi=150, bbox_inches='tight')
# plt.show()
print("Result saved to ./demo_result.png")

原图

demo1

分割个包包

12.3 场景 1:交互式抠图(多点修正)

python 复制代码
import cv2
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from segment_anything import sam_model_registry, SamPredictor

# 1. 加载模型
sam = sam_model_registry["vit_h"](checkpoint="./sam_vit_h_4b8939.pth")
sam.to("cuda")
predictor = SamPredictor(sam)


def interactive_matting(image_path, output_path):
    """多点交互式抠图:前景点 + 背景点精修"""
    image = cv2.imread(image_path)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    predictor.set_image(image)

    # 初始点击:目标物体的中心
    masks, scores, logits = predictor.predict(
        point_coords=np.array([[280, 350]]),
        point_labels=np.array([1]),  # 前景
        multimask_output=True,
    )
    best_mask = masks[np.argmax(scores)]

    # 不满意?加一个背景点修正
    masks, scores, _ = predictor.predict(
        point_coords=np.array([[280, 350], [460, 165]]),  # 两个点
        point_labels=np.array([1, 0]),                     # 前景 + 背景
        mask_input=logits[np.argmax(scores):np.argmax(scores) + 1, :, :],
        multimask_output=False,
    )
    best_mask = masks[0]

    # 抠图:原图 × mask
    result = image.copy()
    result[~best_mask] = 0  # 背景变黑
    cv2.imwrite(output_path, cv2.cvtColor(result, cv2.COLOR_RGB2BGR))

    # 点的坐标和标签
    points = np.array([[280, 350], [460, 165]])
    labels = np.array([1, 0])  # 1=前景, 0=背景
    fg_points = points[labels == 1]
    bg_points = points[labels == 0]

    # 可视化并保存
    fig, axes = plt.subplots(1, 3, figsize=(18, 6))
    axes[0].imshow(image)
    if len(fg_points) > 0:
        axes[0].scatter(fg_points[:, 0], fg_points[:, 1], c='lime', s=80, edgecolors='k', label='Foreground')
    if len(bg_points) > 0:
        axes[0].scatter(bg_points[:, 0], bg_points[:, 1], c='red', s=80, edgecolors='k', label='Background')
    axes[0].set_title("Original")
    axes[0].axis('off')
    axes[0].legend()

    axes[1].imshow(best_mask, cmap='gray')
    axes[1].set_title("Mask")
    axes[1].axis('off')

    axes[2].imshow(result)
    if len(fg_points) > 0:
        axes[2].scatter(fg_points[:, 0], fg_points[:, 1], c='lime', s=80, edgecolors='k', label='Foreground')
    if len(bg_points) > 0:
        axes[2].scatter(bg_points[:, 0], bg_points[:, 1], c='red', s=80, edgecolors='k', label='Background')
    axes[2].set_title("Result (Background Removed)")
    axes[2].axis('off')
    axes[2].legend()

    viz_path = output_path.rsplit('.', 1)[0] + '_viz.png'
    plt.savefig(viz_path, dpi=150, bbox_inches='tight')
    plt.close()
    print(f"Mask result saved to {output_path}")
    print(f"Visualization saved to {viz_path}")
    return result


# 2. 执行抠图
interactive_matting("./demo.png", "./demo_matting_result.png")

12.4 场景 2:全图自动分割 → 生成 instance masks

python 复制代码
import cv2
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from segment_anything import sam_model_registry, SamAutomaticMaskGenerator

# 1. 加载模型
sam = sam_model_registry["vit_h"](checkpoint="./sam_vit_h_4b8939.pth")
sam.to("cuda")


def auto_segment_all(image_path, output_path="./demo_auto_result.png"):
    """无 prompt 全自动分割 ------ 生成图中所有可能的 mask"""
    image = cv2.imread(image_path)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

    mask_generator = SamAutomaticMaskGenerator(
        model=sam,
        points_per_side=32,           # 在 32×32 网格上采样点
        pred_iou_thresh=0.88,         # 过滤低质量 mask
        stability_score_thresh=0.95,  # 稳定性阈值
        min_mask_region_area=100,     # 最小 mask 面积
    )

    masks = mask_generator.generate(image)
    print(f"Found {len(masks)} regions")

    # 可视化:随机颜色覆盖所有 mask
    fig, axes = plt.subplots(1, 2, figsize=(20, 10))

    axes[0].imshow(image)
    axes[0].set_title("Original")
    axes[0].axis('off')

    axes[1].imshow(image)
    sorted_masks = sorted(masks, key=lambda x: x['area'], reverse=True)

    canvas = np.zeros((*image.shape[:2], 4))
    for mask_data in sorted_masks:
        color = np.random.random(3)
        canvas[mask_data['segmentation']] = np.append(color, 0.4)
    axes[1].imshow(canvas)
    axes[1].set_title(f"Auto Segmentation ({len(masks)} regions)")
    axes[1].axis('off')

    plt.savefig(output_path, dpi=150, bbox_inches='tight')
    plt.close()
    print(f"Result saved to {output_path}")
    return masks


# 2. 执行全自动分割
auto_segment_all("./demo.png", "./demo_auto_result.png")

12.5 场景 3:框选分割 ------ Bounding Box Prompt

python 复制代码
import cv2
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from segment_anything import sam_model_registry, SamPredictor

# 1. 加载模型
sam = sam_model_registry["vit_h"](checkpoint="./sam_vit_h_4b8939.pth")
sam.to("cuda")
predictor = SamPredictor(sam)


def segment_by_box(image_path, box_xyxy, output_path="./demo_box_result.png"):
    """用框来分割 ------ 自动找到框内最显著的目标"""
    image = cv2.imread(image_path)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    predictor.set_image(image)

    # box_xyxy: [x1, y1, x2, y2]
    input_box = np.array(box_xyxy)  # e.g. [100, 200, 500, 600]

    masks, scores, _ = predictor.predict(
        point_coords=None,
        point_labels=None,
        box=input_box[None, :],
        multimask_output=False,
    )

    # 可视化
    plt.figure(figsize=(12, 8))
    plt.imshow(image)
    plt.imshow(masks[0], alpha=0.5, cmap='jet')
    rect = plt.Rectangle(
        (box_xyxy[0], box_xyxy[1]),
        box_xyxy[2] - box_xyxy[0],
        box_xyxy[3] - box_xyxy[1],
        fill=False, edgecolor='red', linewidth=2
    )
    plt.gca().add_patch(rect)
    plt.title(f"Box Prompt | Score: {scores[0]:.3f}")
    plt.axis('off')
    plt.savefig(output_path, dpi=150, bbox_inches='tight')
    plt.close()
    print(f"Result saved to {output_path}")
    return masks[0]


# 2. 执行框分割
segment_by_box("./demo.png", [190, 210, 375, 415], "./demo_box_result.png")

12.6 场景 4:批量处理 ------ Embedding Cache 实战

python 复制代码
import time
import cv2
import numpy as np
import hashlib
import pickle
from pathlib import Path
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from segment_anything import sam_model_registry, SamPredictor


class CachedSamPredictor:
    """带 embedding 缓存的 SAM predictor"""

    def __init__(self, sam_model, cache_dir="./sam_cache"):
        self.predictor = SamPredictor(sam_model)
        self.cache_dir = Path(cache_dir)
        self.cache_dir.mkdir(exist_ok=True)

    def _image_hash(self, image: np.ndarray) -> str:
        """用图像哈希作为缓存 key"""
        return hashlib.md5(image.tobytes()).hexdigest()

    def _cache_path(self, image_hash: str) -> Path:
        return self.cache_dir / f"{image_hash}.pkl"

    def set_image(self, image: np.ndarray):
        img_hash = self._image_hash(image)
        cache_path = self._cache_path(img_hash)

        if cache_path.exists():
            t0 = time.perf_counter()
            with open(cache_path, 'rb') as f:
                cached = pickle.load(f)
            self.predictor.features = cached['features']
            self.predictor.original_size = cached['original_size']
            self.predictor.input_size = cached['input_size']
            self.predictor.is_image_set = True
            elapsed = (time.perf_counter() - t0) * 1000
            file_size = cache_path.stat().st_size / 1024 / 1024
            print(f"[Cache HIT]  hash={img_hash[:12]}...  load={elapsed:.1f}ms  file={file_size:.1f}MB  ✓")
            return

        print(f"[Cache MISS] hash={img_hash[:12]}... encoding...")
        t0 = time.perf_counter()
        self.predictor.set_image(image)
        elapsed = (time.perf_counter() - t0) * 1000

        with open(cache_path, 'wb') as f:
            pickle.dump({
                'features': self.predictor.features,
                'original_size': self.predictor.original_size,
                'input_size': self.predictor.input_size,
            }, f)
        file_size = cache_path.stat().st_size / 1024 / 1024
        print(f"[Cache MISS] hash={img_hash[:12]}... encode done in {elapsed:.0f}ms  saved={file_size:.1f}MB  → cache ready")

    def predict(self, **kwargs):
        return self.predictor.predict(**kwargs)


# 1. 加载模型
sam = sam_model_registry["vit_h"](checkpoint="./sam_vit_h_4b8939.pth")
sam.to("cuda")

# 2. 加载图像
image = cv2.imread("./demo.png")
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# 3. 使用缓存 predictor
print("=" * 60)
print("SAM Embedding Cache Benchmark")
print("=" * 60)

cached_predictor = CachedSamPredictor(sam)

# 第一次:缓存 MISS,需要编码
t0 = time.perf_counter()
cached_predictor.set_image(image)
t_miss = time.perf_counter() - t0

masks, scores, _ = cached_predictor.predict(
    point_coords=np.array([[280, 350]]),
    point_labels=np.array([1]),
    multimask_output=True,
)

# 第二次:缓存 HIT,秒加载
t0 = time.perf_counter()
cached_predictor.set_image(image)
t_hit = time.perf_counter() - t0

# 汇总
speedup = t_miss / t_hit if t_hit > 0 else float('inf')
print("=" * 60)
print(f"  Cache MISS : {t_miss * 1000:.0f} ms")
print(f"  Cache HIT  : {t_hit * 1000:.1f} ms")
print(f"  Speedup    : {speedup:.0f}x faster with cache")
print("=" * 60)

# 4. 可视化并保存
best_idx = np.argmax(scores)
plt.figure(figsize=(12, 8))
plt.imshow(image)
plt.imshow(masks[best_idx], alpha=0.5, cmap='jet')
plt.scatter(280, 350, c='red', s=100)
plt.title(f"Mask Score: {scores[best_idx]:.3f} | Cache: {speedup:.0f}x faster")
plt.axis('off')
plt.savefig("./demo_cache_result.png", dpi=150, bbox_inches='tight')
plt.close()
print("Result saved to ./demo_cache_result.png")

output

py 复制代码
============================================================
SAM Embedding Cache Benchmark
============================================================
[Cache MISS] hash=f540a683c951... encoding...
[Cache MISS] hash=f540a683c951... encode done in 736ms  saved=4.0MB  → cache ready
[Cache HIT]  hash=f540a683c951...  load=6.3ms  file=4.0MB  ✓
============================================================
  Cache MISS : 748 ms
  Cache HIT  : 9.2 ms
  Speedup    : 81x faster with cache
============================================================

十三、总结金句

  1. SAM 的本质:把分割从"这个东西叫什么"解耦为"这个东西在哪",用 prompt 统一了所有分割任务的接口。

  2. Image Encoder 是整个系统的"字典",只编一次,反复查;这个设计让交互式分割从"不可能"变成"丝滑"。

  3. Mask Decoder 的 2 层设计告诉我们:好的编码器做完了所有难的工作,解码器只需"查表"------这是经典的"重编码、轻解码"哲学。

  4. SA-1B 不是 SAM 附属品,是另一个更重的贡献------11 亿个 mask 才是让 foundation model 思路在 CV 落地的真正燃料。

  5. MobileSAM 证明了:不是所有层都需要压缩。只蒸馏 encoder、复用 decoder,四两拨千斤。

  6. FastSAM 的教训:YOLO + 后处理虽然快,但丢掉了 SAM 最核心的"promptable"灵活性------这是方案取舍的经典案例。

  7. Embedding Cache 是 SAM 落地的第一优化项,它把每次推理从"编码 + 解码"变成了"解码而已",在小批量交互场景下收益高达 60 倍。

  8. SAM 不是无敌的------低对比度、细长结构、跨域数据,它还有很多不会做的事情。但它的出现,让分割从"手工匠时代"进入了"工业时代"。


最后一句 :SAM 最重要的遗产不是那三个 mask,而是它证明了一件事 ------ 视觉也可以有 foundation model,关键在于把任务抽象成"填空"而不是"归类"


作者注:本文基于 Meta 官方论文《Segment Anything》(arXiv:2304.02643)、SA-1B 数据集、MobileSAM、FastSAM 原始论文,以及 2024-2025 年间社区在部署、优化、评测方面的最新进展撰写。

相关推荐
百度Geek说1 小时前
用数据说话:贴吧 AI CR(小码哥)落地 10 周,bug密度下降 66.87%
人工智能
码农小白AI1 小时前
电子原始记录进入“可审计时代”:AI 报告审核如何给出标准答案,IACheck重塑实验室数智化底层逻辑
人工智能
老鱼说AI1 小时前
统计学习方法第五章:从浅入深解析决策树
人工智能·深度学习·算法·决策树·机器学习·学习方法
zhangfeng11331 小时前
llamafactory 0.6.3 没有 llamafactory-cli
人工智能·机器学习
KaMeidebaby1 小时前
卡梅德生物技术快报|蛋白修饰调控 NETosis 分子机制及实验研究进展
前端·数据库·人工智能·算法·百度
十铭忘1 小时前
个人Agent实践方案
人工智能
Luminbox紫创测控1 小时前
太阳模拟器自动化测试系统:稳态、脉冲、闪光光源的控制与数据采集
人工智能·测试工具·测试标准
有个人神神叨叨1 小时前
Agent Memory 演进主线论文地图
人工智能
DisonTangor1 小时前
微软重磅开源 Lens: 重新思考基础文本到图像模型的训练效率
人工智能·microsoft·ai作画·开源·aigc