前言

  • 本文是对传统点云处理方法的一次扩展,灵感来源于深夜刷到的一段视频。本系列后续内容也将持续更新,第九期我们将介绍经典的 π 0 \pi_0 π0​ 算法,敬请期待!

  • 我们曾经在【玩转VLA具身智能机械臂】(四):点云分割与 OBB 夹取——从 PCA 失效到主动扫描 介绍过一种基于 RANSAC(Random Sample Consensus,随机采样一致性)平面拟合的点云滤波方法。当时,我们利用该方法分离桌面与桌面上的物体,随后对目标点云进行 OBB(Oriented Bounding Box,有向包围盒)估计,并进一步计算抓取位姿。请添加图片描述

  • 但是这种方法主要依据几何特征对点云进行分离,无法直接根据物体的语义类别进行筛选。例如,当桌面上摆放着多个物体,而机械臂只需要抓取其中的杯子时,仅依靠平面拟合就难以准确地找到指定目标。

  • 区别于传统方法需要针对特定目标提前训练 YOLO(You Only Look Once,你只看一次)等检测或分割模型,本文介绍一种结合 Grounding DINO + SAM(Segment Anything Model,分割一切模型) 的目标分割方案。利用官方预训练权重,无需针对当前任务额外训练或微调模型,即可通过文本提示定位目标物体并生成像素级分割掩码,再结合 RGB-D(Red Green Blue Depth,彩色加深度)深度信息提取目标点云,为后续的 OBB 估计与机械臂抓取提供基础。

  • 最终我们可以输入一张图像和一句描述目标物体的文本,无需针对当前任务额外训练就能完成目标定位与像素级分割,并进一步提取对应的三维点云。效果如下:请添加图片描述


文章目录


1 Grounding DINO

1-1 介绍

请添加图片描述

  • Grounding DINO 是 Liu 等人于 2023 年提出的一种开放集目标检测模型,其论文 Grounding DINO: Marrying DINO with Grounded Pre-Training for Open-Set Object Detection 发表于 ECCV(European Conference on Computer Vision,欧洲计算机视觉会议)2024。
    • 该模型将基于 Transformer 的目标检测器 DINO(DETR with Improved deNoising anchOr boxes,改进去噪锚框的 DETR)与视觉语言对齐的预训练方法相结合,使模型能够根据输入的类别名称或自然语言描述,在图像中定位对应目标,而不局限于传统检测器预先定义的固定类别集合。
  • 与固定类别检测器不同,Grounding DINO 的类别集合由文本提示动态给出,因此同一个模型可以处理训练时未见的类别。
  • 官方 Swin-T 权重在大规模定位数据上预训练,推理时只需要图像与文本提示,不需要重新训练分类头。
1-2 结构
  • 整体结构如下图所示:

输入图像
RGB Image

图像骨干网络
Image Backbone

输入文本
Text Prompt

文本骨干网络
Text Backbone

图像特征
Image Features

文本特征
Text Features

特征增强器
Feature Enhancer

语言引导查询选择
Language-Guided Query Selection

图像与文本特征

跨模态解码器
Cross-Modality Decoder

目标边界框
Bounding Boxes

文本匹配分数
Text Matching Scores

  • 在网络架构上,Grounding DINO 由图像骨干网络、文本骨干网络、特征增强器、语言引导查询选择模块和跨模态解码器组成。

  • 论文 Fig. 3 将整个框架拆成三个部分:block 1 给出整体数据流,block 2 展开一层特征增强器,block 3 展开一层解码器。请添加图片描述

  • 从 block 1 可以看到,文本与图像先分别经过骨干网络得到 Vanilla 特征(经过基础、标准流程提取的特征),再进入特征增强器完成跨模态对齐;增强后的特征一方面用于语言引导查询选择,另一方面作为键值输入跨模态解码器。

  • 从 block 2 可以看到,一层特征增强器内部同时包含文本自注意力、图像可变形自注意力以及两个方向的图文交叉注意力。

  • 从 block 3 可以看到,一层解码器依次执行自注意力、图像交叉注意力、文本交叉注意力与前馈网络,输出更新后的跨模态查询。

  • 整体数据流可以写成从图像与文本到检测集合的映射: B ^ , S ^ = f θ ( I , T ) \hat{\mathcal{B}}, \hat{\mathcal{S}} = f_{\theta}(I, T) B^,S^=fθ​(I,T)其中 :

    • I I I :输入图像
    • T T T :文本提示
    • B ^ \hat{\mathcal{B}} B^ :预测边界框集合
    • S ^ \hat{\mathcal{S}} S^ :每个框与文本的匹配分数集合。
  • 从编码方式看,图像与文本分别由独立编码器处理,之后在特征增强器中交互,因此该结构属于双编码器、单解码器(Dual-Encoder Single-Decoder)架构。

    • 编码器负责把两种模态映射到同一特征空间
    • 解码器负责在语言条件下逐步细化目标查询
  • 输入图像先经过尺度变换与归一化: I ~ = I / 255 − μ σ \tilde{I} = \frac{I/255 - \mu}{\sigma} I~=σI/255−μ​其中 :

    • μ \mu μ 与 σ \sigma σ 为 ImageNet 统计量,短边缩放到 800 像素,长边不超过 1333 像素。
  • 文本提示先转为小写并去除首尾空白,若末尾没有句号则补一个句号,使 Tokenizer 得到稳定的句子边界。

说人话:图像编码器回答"图里有什么形状",文本编码器回答"要找的是什么词",特征增强器把两边的信息对齐,解码器再逐个框出候选目标。

1-3 图像骨干网络
1-3-1 Swin Transformer

请添加图片描述

  • 图像骨干网络负责从输入图像中提取视觉特征,官方常用配置采用 Swin Transformer(Shifted Window Transformer,移位窗口 Transformer)。
  • Swin Transformer(Shifted Window Transformer,移位窗口 Transformer) 是一种基于 Transformer 的视觉骨干网络,由 Liu 等人于 2021 年提出,论文 Swin Transformer: Hierarchical Vision Transformer using Shifted Windows 发表于 ICCV 2021。
    • 与标准 ViT 通常在固定 Patch 尺度上进行特征提取不同,Swin Transformer 采用分层结构和移位窗口注意力机制,逐步构建不同空间分辨率的特征图。这种类似 CNN 的层次化特征表示能够为目标检测提供多尺度视觉信息,也便于后续的多尺度可变形注意力进行特征融合。
  • 其主要特点包括:
    • 局部窗口注意力: 将自注意力计算限制在局部窗口内,降低高分辨率图像下的计算开销。
    • 移位窗口机制: 交替使用常规窗口和移位窗口,使原本属于不同窗口的图像块能够建立联系。
    • 层次化特征表示: 通过逐阶段合并图像块,构建从高分辨率、细粒度信息到低分辨率、高语义信息的多尺度特征。
    • 良好的视觉任务适应性: 层次化结构和多尺度输出使其适合目标检测、语义分割等视觉任务,也可以作为其他视觉模型的图像骨干网络。

说人话:区别传统ViT直接将整张图像转换为单一特征向量不同,Swin Transformer 通过分层特征提取与窗口注意力机制,生成不同空间分辨率的多尺度特征图,为后续目标定位提供信息。

  • 在代码中,提取出的多尺度特征还会经过输入投影层(input_proj),统一通道维度,并结合位置编码,供后续 Transformer 模块使用。

输入图像
3 × H × W

Patch Embedding
4倍下采样
96通道

Stage 1
2个 Swin Block
H/4 × W/4 × 96

Patch Merging
2倍下采样

Stage 2
2个 Swin Block
H/8 × W/8 × 192

Patch Merging
2倍下采样

Stage 3
6个 Swin Block
H/16 × W/16 × 384

Patch Merging
2倍下采样

Stage 4
2个 Swin Block
H/32 × W/32 × 768

多尺度特征输出
Stage 2、3、4

1-3-2 多尺度特征与输入投影
  • 官方 Swin-T 配置使用 return_interm_indices = [1, 2, 3],主干输出三个尺度的特征图。
    • 为了得到第四个尺度,模型在最后一个尺度上再执行一次步长为 2 的卷积,因此 num_feature_levels = 4。
  • 每个尺度的特征先经过 1 × 1 1 \times 1 1×1 卷积与 GN(Group Normalization,组归一化),把通道数统一到 d = 256 d = 256 d=256: F l = G N ( C o n v 1 × 1 ( P l ) ) , l = 1 , 2 , 3 , 4 F_l = \mathrm{GN}\left(\mathrm{Conv}_{1 \times 1}(P_l)\right), \quad l= 1, 2, 3, 4 Fl​=GN(Conv1×1​(Pl​)),l=1,2,3,4其中 :
    • P l P_l Pl​ :第 l l l 个尺度的主干特征图
    • F l F_l Fl​ :投影后的特征图。
  • 对应的实现如下,srcs 保存四个尺度的特征,masks 保存对应的填充掩码:
srcs, masks = [], []
for level, feat in enumerate(self.features):
    src, mask = feat.decompose()
    srcs.append(self.input_proj[level](src))
    masks.append(mask)
  • 第四个尺度由最后一个特征图下采样得到,位置编码由主干的位置编码分支单独计算:
for level in range(len(srcs), self.num_feature_levels):
    if level == len(srcs):
        src = self.input_proj[level](self.features[-1].tensors)
    else:
        src = self.input_proj[level](srcs[-1])
    mask = F.interpolate(
        samples.mask[None].float(), size=src.shape[-2:]
    ).to(torch.bool)[0]
    pos_l = self.backbone[1](NestedTensor(src, mask)).to(src.dtype)
    srcs.append(src)
    masks.append(mask)
    self.poss.append(pos_l)
  • 这样四个尺度的空间分辨率依次降低,而通道维度保持一致,后续可变形注意力可以在不同尺度上采样参考点。
1-3-3 二维位置编码
  • 图像特征在进入 Transformer 前需要加入位置编码,官方配置使用正弦位置编码,温度系数为 20: P E ( p o s , 2 i ) = sin ⁡ ( p o s τ 2 i / d ) , P E ( p o s , 2 i + 1 ) = cos ⁡ ( p o s τ 2 i / d ) PE_{(pos, 2i)} = \sin\left(\frac{pos}{\tau^{2i/d}}\right), \quad PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{\tau^{2i/d}}\right) PE(pos,2i)​=sin(τ2i/dpos​),PE(pos,2i+1)​=cos(τ2i/dpos​)其中 :
    • P E ( p o s , 2 i ) PE_{(pos, 2i)} PE(pos,2i)​、 P E ( p o s , 2 i + 1 ) PE_{(pos, 2i+1)} PE(pos,2i+1)​ :偶通道与奇通道的位置编码
    • p o s pos pos :特征点在特征图中的二维坐标,计算时横轴与纵轴分别代入
    • d d d :特征维度
    • τ \tau τ :温度系数,官方 Swin-T 配置取 20。
  • 对二维特征图,分别对横坐标与纵坐标计算正弦、余弦编码,两个方向各占一半通道,再沿通道维拼接成该空间位置的位置向量。
  • 多尺度特征还会加上一个可学习的尺度嵌入 level_embed,使模型能够区分不同分辨率的特征: F ~ l = F l + E level ( l ) \tilde{F}_l = F_l + E_{\text{level}}^{(l)} F~l​=Fl​+Elevel(l)​其中 :
    • F ~ l \tilde{F}_l F~l​ :第 l l l 层加上尺度嵌入后的特征
    • F l F_l Fl​ :第 l l l 层的原始特征
    • E level ( l ) E_{\text{level}}^{(l)} Elevel(l)​ :第 l l l 层可学习的尺度嵌入。
  • 位置编码与尺度嵌入都在特征展平之前完成,展平后得到形状为 ∑ l H l W l × d \sum_l H_l W_l \times d ∑l​Hl​Wl​×d 的序列。

说人话:主干给出几张由大到小的特征图,投影层把它们都压到同样多的通道,位置编码再告诉 Transformer 每个特征点原来在图的什么位置。

1-3-4 最终输出
  • 图像骨干网络的输出并不是一张特征图,而是四个尺度上的特征图、对应的填充掩码与位置编码,三者打包后交给 Transformer 的编码器,也就是 1-5 节的特征增强器。
  • 四个尺度的相对分辨率依次为原图的 1 / 8 1/8 1/8、 1 / 16 1/16 1/16、 1 / 32 1/32 1/32、 1 / 64 1/64 1/64,通道数先由 1 × 1 1 \times 1 1×1 卷积投影到同一个维度 d = 256 d = 256 d=256:
层级相对分辨率特征形状
第 1 层1/8256 × H/8 × W/8
第 2 层1/16256 × H/16 × W/16
第 3 层1/32256 × H/32 × W/32
第 4 层1/64256 × H/64 × W/64
  • 以演示图为例,原图 960 × 720 960 \times 720 960×720 按短边缩放到 800 后为 1066 × 800 1066 \times 800 1066×800,四个尺度的实际形状如下:
层级主干通道数相对分辨率特征形状
第 1 层1921/8256 × 100 × 134
第 2 层3841/16256 × 50 × 67
第 3 层7681/32256 × 25 × 34
第 4 层7681/64256 × 13 × 17
  • 前三个尺度的通道数由 Swin 各 Stage 的输出决定,第四个尺度由第三尺度的特征图经过一次步长为 2 的 3 × 3 3 \times 3 3×3 卷积得到,因此主干通道数仍是 768。

  • 四个尺度展平后共有 13400 + 3350 + 850 + 221 = 17821 13400 + 3350 + 850 + 221 = 17821 13400+3350+850+221=17821 个 Token。

  • 把每个尺度的 256 维特征按主成分投影到 RGB,四个尺度的特征图如下:
    请添加图片描述

  • 从左到右分辨率依次降低,保留的信息也随之变化:

    • 1 / 8 1/8 1/8 的图上细节最丰富,叶丛的碎块与挂轴上的笔画都还能分辨
    • 1 / 16 1/16 1/16 与 1 / 32 1/32 1/32 上细节被聚合成块,杯身、瓶身与桌面各自成为独立的区域
    • 1 / 64 1/64 1/64 只剩大范围的颜色分区,物体边界已经模糊
  • 这与 Swin 的分层设计一致:浅层保留纹理与边缘,深层聚合出语义区域,检测同时需要这两种信息。

  • 需要注意,PCA 图只保留三个主成分,颜色本身没有语义,可比较的只是"哪些区域在特征空间里彼此相似"。

  • 每层的特征图在进入 Transformer 之前都要展平成序列,把两个空间维度并入序列长度: X l ∈ R d × H l × W l ⇒ X l ⊤ ∈ R H l W l × d X_l \in \mathbb{R}^{d \times H_l \times W_l} \Rightarrow X_l^{\top} \in \mathbb{R}^{H_l W_l \times d} Xl​∈Rd×Hl​×Wl​⇒Xl⊤​∈RHl​Wl​×d其中 :

    • X l X_l Xl​ :第 l l l 层的特征图,三个维度依次为通道、高、宽
    • H l H_l Hl​、 W l W_l Wl​ :第 l l l 层的高与宽
    • X l ⊤ X_l^{\top} Xl⊤​ :展平后的序列,长度为 H l W l H_l W_l Hl​Wl​。
  • 四个尺度的序列按由细到粗的顺序拼接成一条长序列: X = [ X 1 ; X 2 ; X 3 ; X 4 ] ∈ R N × d , N = ∑ l = 1 4 H l W l X = \left[X_1; X_2; X_3; X_4\right] \in \mathbb{R}^{N \times d}, \quad N = \sum_{l=1}^{4} H_l W_l X=[X1​;X2​;X3​;X4​]∈RN×d,N=l=1∑4​Hl​Wl​其中 :

    • X X X :四个尺度拼接后的长序列
    • N = ∑ l = 1 4 H l W l N = \sum_{l=1}^{4} H_l W_l N=∑l=14​Hl​Wl​ :拼接后的序列长度。
  • 拼接之后,仅靠位置编码已经无法区分某个 Token 属于第几层,因此在位置编码上再加一个可学习的层级嵌入: E ~ l = E l + e level ( l ) \tilde{E}_l = E_l + e_{\text{level}}^{(l)} E~l​=El​+elevel(l)​其中 :

    • E ~ l \tilde{E}_l E~l​ :加上层级嵌入后的位置编码
    • E l E_l El​ :第 l l l 层原有的位置编码
    • e level ( l ) e_{\text{level}}^{(l)} elevel(l)​ :第 l l l 层可学习的层级嵌入。
  • 由于序列是分段拼接的,后续的可变形注意力还需要知道每一段的起点与形状,实现上用两个量描述: b l = ∑ j < l H j W j , S l = ( H l , W l ) b_l = \sum_{j<l} H_j W_j, \quad S_l = \left(H_l, W_l\right) bl​=j<l∑​Hj​Wj​,Sl​=(Hl​,Wl​)其中 :

    • b l b_l bl​ :第 l l l 层在拼接序列中的起始下标
    • S l S_l Sl​ :第 l l l 层的空间形状。
  • 还需要记录每层的有效区域比例。输入图像被缩放到固定尺寸并补零后,右侧与下侧存在填充区域,采样参考点时必须按有效比例缩放: v l = ( W l valid W l ,   H l valid H l ) v_l = \left(\frac{W_l^{\text{valid}}}{W_l},\ \frac{H_l^{\text{valid}}}{H_l}\right) vl​=(Wl​Wlvalid​​, Hl​Hlvalid​​)其中 :

    • W l valid W_l^{\text{valid}} Wlvalid​、 H l valid H_l^{\text{valid}} Hlvalid​ :第 l l l 层有效区域的宽与高
    • W l W_l Wl​、 H l H_l Hl​ :补零后特征图的宽与高
    • v l v_l vl​ :该层的有效区域比例。
  • 这里的分量顺序为宽在前、高在后,与实现中 valid_ratios 的排列一致。

  • 对应的实现如下:

src_flatten, mask_flatten, lvl_pos_embed_flatten = [], [], []
spatial_shapes = []
for lvl, (src, mask, pos_embed) in enumerate(zip(srcs, masks, pos_embeds)):
    bs, c, h, w = src.shape
    spatial_shapes.append((h, w))

    src = src.flatten(2).transpose(1, 2)          # bs, hw, c
    mask = mask.flatten(1)                        # bs, hw
    pos_embed = pos_embed.flatten(2).transpose(1, 2)
    lvl_pos_embed = pos_embed + self.level_embed[lvl].view(1, 1, -1)
    lvl_pos_embed_flatten.append(lvl_pos_embed)
    src_flatten.append(src)
    mask_flatten.append(mask)

src_flatten = torch.cat(src_flatten, 1)               # bs, sum(hw), c
mask_flatten = torch.cat(mask_flatten, 1)             # bs, sum(hw)
lvl_pos_embed_flatten = torch.cat(lvl_pos_embed_flatten, 1)
spatial_shapes = torch.as_tensor(spatial_shapes, dtype=torch.long)
level_start_index = torch.cat(
    (spatial_shapes.new_zeros((1,)), spatial_shapes.prod(1).cumsum(0)[:-1])
)
valid_ratios = torch.stack([self.get_valid_ratio(m) for m in masks], 1)
  • 最终交给特征增强器的张量形状如下表,表中 B B B 为批大小, N N N 为四个尺度展平后的 Token 总数:
张量形状含义
src_flattenB × N × 256拼接后的多尺度视觉特征
mask_flattenB × N填充掩码,True 表示补零位置
lvl_pos_embed_flattenB × N × 256位置编码与层级嵌入之和
spatial_shapes4 × 2四个尺度的空间形状
level_start_index4四个尺度在序列中的起点
valid_ratiosB × 4 × 2四个尺度的有效区域比例
  • 这六个张量正好是 1-5 节中可变形自注意力所需的全部输入,因此 1-3 的输出与 1-5 的输入一一对应。
  • 四个尺度被压成一条序列,但层级信息并没有丢失:层级嵌入回答“属于哪一层”,空间形状回答“这一层有多宽多高”,起始下标回答“这一段从哪开始”,有效比例回答“哪些位置是真的”。

说人话:主干最后交出来的是一叠从细到粗的特征图,外加一张“哪里是补零”的掩码和一套位置说明;后面的注意力模块靠这些说明才知道每个 Token 原来在图的什么位置、属于哪一层。

1-4 文本骨干网络
1-4-1 BERT

请添加图片描述

  • 文本骨干网络负责将输入的文本提示转换为模型能够处理的语义特征,官方实现使用 BERT(Bidirectional Encoder Representations from Transformers,双向编码器表示)。
  • BERT 是 Google 研究团队于 2018 年提出的预训练语言模型,相关论文 BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding 发表于 NAACL 2019。其核心是基于 Transformer Encoder,通过双向上下文建模学习文本的语义表示。
  • BERT 的主要特点包括:
    • 双向上下文建模: 通过自注意力机制,让每个 Token 在编码时利用文本中其他相关 Token 的信息。
    • 上下文相关的特征表示: 同一个词语在不同语境下可以得到不同的特征表示。
    • 预训练机制: 原始 BERT 采用掩码语言建模(Masked Language Modeling,MLM)等任务进行预训练,学习通用的语言表示能力。
    • Token 级特征输出: 模型能够为输入序列中的每个 Token 输出对应的上下文特征,而不只是生成一个代表整段文本的向量。

说人话:BERT 的双向上下文建模使每个 Token 的语义表示能够同时融合前后文信息,从而更充分地理解词语在具体语境中的含义,并为 Grounding DINO 后续的图文特征融合提供文本语义特征。

  • 首先,文本经过 Tokenizer 分词,转换为 Token 序列;随后,BERT 根据 Token 的上下文关系生成对应的文本特征。
  • 在代码中,文本特征还会配合注意力掩码(Attention Mask)等信息使用,以区分有效文本 Token 与填充位置,并在后续模块中参与视觉与语言的融合。
1-4-2 文本分词与子句注意力掩码
  • 官方实现把 max_text_len 设为 256,分词结果包含 [CLS]、[SEP]、句号等特殊 Token:
tokenized = self.tokenizer(
    captions, padding="longest", return_tensors="pt"
).to(samples.device)
  • 设 Token 序列为 T = ( t 1 , … , t L ) T = (t_1, \dots, t_L) T=(t1​,…,tL​),注意力掩码为 M t ∈ { 0 , 1 } L M_t \in \{0,1\}^L Mt​∈{0,1}L,其中 1 表示有效 Token,0 表示填充位置。
  • 为了不让不同子句的 Token 在自注意力中相互干扰,代码调用 generate_masks_with_special_tokens_and_transfer_map 构造子句级掩码:
    M i j self = { 1 , same clause or special token 0 , otherwise M^{\text{self}}_{ij} = \begin{cases} 1, & \text{same clause or special token} \\ 0, & \text{otherwise} \end{cases} Mijself​={1,0,​same clause or special tokenotherwise​
    其中 :
    • M i j self M^{\text{self}}_{ij} Mijself​ :第 i i i 个 Token 对第 j j j 个 Token 是否可见
    • i i i、 j j j :Token 下标。
  • 这样 BERT 的每个 Token 主要关注同一子句内的上下文,而 [CLS]、[SEP] 等特殊 Token 仍然可以汇总整句信息。
  • 对应实现如下:
text_self_attention_masks, position_ids, _ = \
    generate_masks_with_special_tokens_and_transfer_map(
        tokenized, self.specical_tokens, self.tokenizer
    )
  • position_ids 记录每个 Token 在子句中的相对位置,用于后续文本位置编码。
1-4-3 BERT 上下文编码与特征映射
  • BERT 的输出维度为 768,而 Grounding DINO 的隐藏维度为 256,因此需要一个线性映射: H t = B E R T ( T ) , E t = W f H t + b f H_t = \mathrm{BERT}(T), \quad E_t = W_f H_t + b_f Ht​=BERT(T),Et​=Wf​Ht​+bf​其中 :
    • H t ∈ R L × 768 H_t \in \mathbb{R}^{L \times 768} Ht​∈RL×768 :BERT 最后一层隐藏状态
    • T T T :分词后的 Token 序列
    • W f ∈ R 256 × 768 W_f \in \mathbb{R}^{256 \times 768} Wf​∈R256×768、 b f b_f bf​ :线性映射的权重与偏置
    • E t ∈ R L × 256 E_t \in \mathbb{R}^{L \times 256} Et​∈RL×256 :映射后的文本特征。
  • 对应实现如下:
bert_output = self.bert(**tokenized_for_encoder)
encoded_text = self.feat_map(bert_output["last_hidden_state"])
text_token_mask = tokenized.attention_mask.bool()
  • encoded_text 与 text_token_mask 一起组成 text_dict,供特征增强器与解码器使用。
  • 文本特征中的填充位置在后续匹配计算中会被掩掉,不参与相似度比较。

说人话:Tokenizer 先把句子切成词片,BERT 再结合上下文把每个词片变成一个向量,最后一层线性映射把 768 维压到和图像特征一致的 256 维。

1-5 特征增强器

请添加图片描述

  • 特征增强器是 Grounding DINO 实现视觉与语言融合的关键模块,负责将图像特征与文本特征进行交互,得到融合了跨模态信息的特征表示。
  • 论文 Fig. 3 block 2 画的是概念上的四类操作,官方实现在每一层里的执行顺序则是固定的三步:先做双向跨模态融合,再做文本自注意力,最后做图像可变形自注意力。
    • 双向跨模态融合(BiAttention):同时计算图像到文本与文本到图像两个方向的注意力,让两种特征在同一层内交换信息。
    • 文本自注意力:由 TransformerEncoderLayer 完成,建模不同文本 Token 之间的上下文关系。
    • 图像可变形自注意力:由 DeformableTransformerEncoderLayer 完成,对多尺度图像特征做稀疏采样,避免全局注意力的平方开销。
  • 双向跨模态融合通过归一化、层缩放与残差连接更新两侧特征,这一步本身不含前馈网络。
  • 随后的文本自注意力与图像可变形自注意力各自带有残差连接与前馈网络;三种操作在同一层内依次执行,而不是相互覆盖。
  • 上述三步在 enc_layers = 6 层中重复执行:

多尺度图像特征
Image Features

文本 Token 特征
Text Features

双向跨模态融合
BiAttention
图像到文本 + 文本到图像

文本自注意力
Text Self-Attention

图像可变形自注意力
Deformable Self-Attention

增强后的文本特征

增强后的图像特征

  • 官方实现通过六层这样的结构反复交互,逐步得到增强后的图像特征与文本特征。
1-5-1 双向跨模态融合(核心)
  • 每一层的第一步是 BiAttentionBlock,它同时执行图像到文本与文本到图像两个方向的注意力,让两种特征在进入各自的内部建模之前先交换一次信息。
  • 设图像特征为 V ∈ R B × N × 256 V \in \mathbb{R}^{B \times N \times 256} V∈RB×N×256,文本特征为 L ∈ R B × T × 256 L \in \mathbb{R}^{B \times T \times 256} L∈RB×T×256,其中 T T T 为文本 Token 数。两个方向并不各自计算一套相似度,而是共用同一个分数矩阵: S = ( V W q ) ( L W k ) ⊤ d S = \frac{\left(V W_q\right)\left(L W_k\right)^{\top}}{\sqrt{d}} S=d ​(VWq​)(LWk​)⊤​其中 :
    • S ∈ R N × T S \in \mathbb{R}^{N \times T} S∈RN×T :图像位置与文本 Token 的分数矩阵
    • V V V、 L L L :图像特征与文本特征
    • W q W_q Wq​、 W k W_k Wk​ :查询与键的线性投影
    • d d d :每个注意力头的维度。
  • 图像到文本方向对 S S S 的每一行做 softmax,文本到图像方向对 S ⊤ S^{\top} S⊤ 的每一行做 softmax: A v = s o f t m a x ( S ) , A l = s o f t m a x ( S ⊤ ) A_v = \mathrm{softmax}\left(S\right), \quad A_l = \mathrm{softmax}\left(S^{\top}\right) Av​=softmax(S),Al​=softmax(S⊤)其中 :
    • A v ∈ R N × T A_v \in \mathbb{R}^{N \times T} Av​∈RN×T :图像到文本方向的注意力权重
    • A l ∈ R T × N A_l \in \mathbb{R}^{T \times N} Al​∈RT×N :文本到图像方向的注意力权重
    • S ⊤ S^{\top} S⊤ :分数矩阵的转置。
  • 两个方向分别用对方的值做加权: Δ V = A v ( L W v l ) , Δ L = A l ( V W v v ) \Delta V = A_v \left(L W_v^{l}\right), \quad \Delta L = A_l \left(V W_v^{v}\right) ΔV=Av​(LWvl​),ΔL=Al​(VWvv​)其中 :
    • W v l W_v^{l} Wvl​、 W v v W_v^{v} Wvv​ :两个方向的值投影
    • Δ V \Delta V ΔV、 Δ L \Delta L ΔL :两个方向的注意力增量。
  • 两个方向共用同一个分数矩阵,因此图像位置与文本 Token 的相关性只需要算一次,这也是该模块计算量较小的原因。
  • 两个方向的结果分别经过可学习的层缩放系数 γ v \gamma_v γv​ 与 γ l \gamma_l γl​,再与原特征相加: V ′ = V + γ v Δ V , L ′ = L + γ l Δ L V' = V + \gamma_v \Delta V, \quad L' = L + \gamma_l \Delta L V′=V+γv​ΔV,L′=L+γl​ΔL其中 :
    • γ v \gamma_v γv​、 γ l \gamma_l γl​ :两个方向的可学习层缩放系数
    • V ′ V' V′、 L ′ L' L′ :融合后的图像特征与文本特征。
  • 层缩放系数初始化为 10 − 4 10^{-4} 10−4,使训练初期跨模态分支对主干的扰动较小。
  • 融合内部的工作维度不是 256,而是 dim_feedforward // 2 = 1024,注意力头数为 nheads // 2 = 4;输入与输出仍然是 256 维,维度在投影层里完成升降。
  • 实现中还会用图像侧的填充掩码屏蔽文本对补零位置的注意力,用文本侧的注意力掩码屏蔽图像对填充 Token 的注意力。
  • 对应实现如下:
v = self.layer_norm_v(v)
l = self.layer_norm_l(l)
delta_v, delta_l = self.attn(
    v, l, attention_mask_v=attention_mask_v, attention_mask_l=attention_mask_l
)
v = v + self.drop_path(self.gamma_v * delta_v)
l = l + self.drop_path(self.gamma_l * delta_l)

说人话:这个模块让图像特征和文本特征相互参考、交换信息,使图像特征能够融入文本语义,文本特征也能结合图像内容。经过融合后,两种特征建立了语义联系,便于后续计算图像位置与文本 Token 之间的匹配分数,为语言引导查询选择提供依据。

1-5-2 文本自注意力
  • 融合之后,文本特征进入 TransformerEncoderLayer 做自注意力,建模不同文本 Token 之间的上下文关系。
  • 它使用的是标准缩放点积注意力: A t t n ( Q , K , V ) = s o f t m a x ( Q K ⊤ d k ) V \mathrm{Attn}(Q, K, V) = \mathrm{softmax}\left(\frac{QK^{\top}}{\sqrt{d_k}}\right)V Attn(Q,K,V)=softmax(dk​ ​QK⊤​)V其中 :
    • Q Q Q、 K K K、 V V V :查询、键、值矩阵
    • d k d_k dk​ :每个注意力头的维度。
  • 与普通自注意力不同的是,这里使用子句级掩码,把不属于同一子句的 Token 屏蔽掉。设屏蔽后的掩码为:
    M ~ i j self = { 0 , allowed − ∞ , masked \tilde{M}^{\text{self}}_{ij} = \begin{cases} 0, & \text{allowed} \\ -\infty, & \text{masked} \end{cases} M~ijself​={0,−∞,​allowedmasked​
    其中 :
    • M ~ i j self \tilde{M}^{\text{self}}_{ij} M~ijself​ :加在注意力分数上的掩码值
    • 0 0 0 :允许关注
    • − ∞ -\infty −∞ :屏蔽,softmax 后权重为 0。
  • 则文本自注意力可以写成: A t t n ( Q , K , V ) = s o f t m a x ( Q K ⊤ d k + M ~ self ) V \mathrm{Attn}(Q, K, V) = \mathrm{softmax}\left(\frac{QK^{\top}}{\sqrt{d_k}} + \tilde{M}^{\text{self}}\right)V Attn(Q,K,V)=softmax(dk​ ​QK⊤​+M~self)V其中 :
    • M ~ self \tilde{M}^{\text{self}} M~self :子句级掩码矩阵
    • d k d_k dk​ :每个注意力头的维度。
  • 被屏蔽位置的权重经过 softmax 后为 0,不会参与上下文汇总。
  • 文本层的注意力头数为 nheads // 2 = 4,前馈维度为 dim_feedforward // 2 = 1024,同样保持 256 维输入输出。
  • 对应实现如下:
memory_text = self.text_layers[layer_id](
    src=memory_text.transpose(0, 1),
    src_mask=~text_self_attention_masks,
    src_key_padding_mask=text_attention_mask,
    pos=pos_text.transpose(0, 1),
).transpose(0, 1)
1-5-3 图像可变形自注意力
  • 每层的第三步是图像侧的自注意力。它不逐对计算图像位置之间的注意力,而是使用多尺度可变形注意力,避免计算量随特征图面积平方增长。
  • 对查询 q q q,可变形注意力在 L L L 个尺度上各取 K K K 个采样点: M S D e f o r m A t t n ( q ) = ∑ l = 1 L ∑ k = 1 K A l q k W l V l ( p l q + Δ p l q k ) \mathrm{MSDeformAttn}(q) = \sum_{l=1}^{L} \sum_{k=1}^{K} A_{lqk} W_l V_l\left(p_{lq} + \Delta p_{lqk}\right) MSDeformAttn(q)=l=1∑L​k=1∑K​Alqk​Wl​Vl​(plq​+Δplqk​)其中 :
    • L L L :尺度数,官方配置为 4
    • K K K :每个尺度的采样点数,官方配置为 4
    • p l q p_{lq} plq​ :查询 q q q 在第 l l l 个尺度上的参考点
    • Δ p l q k \Delta p_{lqk} Δplqk​ :第 k k k 个采样点的偏移量
    • A l q k A_{lqk} Alqk​ :该采样点的注意力权重
    • W l W_l Wl​ :第 l l l 个尺度的投影矩阵。
  • 参考点由 get_reference_points 按 1-3-4 中的有效区域比例生成,采样点偏移量则由查询特征线性预测得到。
  • 官方配置中 enc_n_points = 4,即每个尺度取 4 个采样点,四个尺度共 16 个采样点。
  • 自注意力之后接前馈网络,两步都带残差与层归一化,因此这一层只改变特征内容,不改变特征形状。
  • 对应实现如下:
src2 = self.self_attn(
    query=self.with_pos_embed(src, pos),
    reference_points=reference_points,
    value=src,
    spatial_shapes=spatial_shapes,
    level_start_index=level_start_index,
    key_padding_mask=key_padding_mask,
)
src = src + self.dropout1(src2)
src = self.norm1(src)
src = self.forward_ffn(src)
  • 这样每个查询只需要访问少量采样点,就能聚合多尺度上下文。

说人话:每一层先让图像和文本互相看一眼,再让文本自己理清句子内部的关系,最后让图像在少量关键位置上做精细采样;这三步重复六层,位置编码不参与特征的存储,只在每一步提供空间提示。

1-5-4 输出
  • 特征增强器的作用是改写特征内容,不改变特征的形状:输入与输出的通道维度都是 256,Token 数量也保持不变。
  • 图像一侧的输入是展平后的多尺度特征 X ∈ R B × N × 256 X \in \mathbb{R}^{B \times N \times 256} X∈RB×N×256,文本一侧的输入是 Token 特征 E t ∈ R B × L × 256 E_t \in \mathbb{R}^{B \times L \times 256} Et​∈RB×L×256: ( m e m o r y ,   m e m o r y _ t e x t ) = f enh ( X , E t ) \left(\mathrm{memory},\ \mathrm{memory\_text}\right) = f_{\text{enh}}\left(X, E_t\right) (memory, memory_text)=fenh​(X,Et​)其中 :
    • X X X :展平后的多尺度图像特征
    • E t E_t Et​ :文本 Token 特征
    • f enh f_{\text{enh}} fenh​ :六层特征增强结构
    • N = ∑ l = 1 4 H l W l N = \sum_{l=1}^{4} H_l W_l N=∑l=14​Hl​Wl​ :四个尺度的 Token 总数
    • L L L :文本 Token 数。
  • 输出是两组同形的特征: m e m o r y ∈ R B × N × 256 , m e m o r y _ t e x t ∈ R B × L × 256 \mathrm{memory} \in \mathbb{R}^{B \times N \times 256}, \quad \mathrm{memory\_text} \in \mathbb{R}^{B \times L \times 256} memory∈RB×N×256,memory_text∈RB×L×256其中 :
    • m e m o r y \mathrm{memory} memory :增强后的图像特征
    • m e m o r y _ t e x t \mathrm{memory\_text} memory_text :增强后的文本特征。
  • 这里的 256 是通道维度,来自官方 Swin-T 配置中的 hidden_dim = 256。图像侧由 1 × 1 1 \times 1 1×1 卷积投影到 256,文本侧由 feat_map 把 BERT 的 768 维输出线性映射到 256,因此两侧在进入增强器之前已经对齐到同一维度。
  • 需要区分的是,文本的 256 指通道数,不是序列长度: L L L 由词片数量决定,其上限由 max_text_len = 256 截断。两个 256 恰好同值,但含义不同。
  • 六层结构中每一层都只做注意力与前馈的残差更新,不做升维或降维,因此六层之后形状仍是 B × N × 256 B \times N \times 256 B×N×256 与 B × L × 256 B \times L \times 256 B×L×256。
  • 输出本身不携带位置编码:位置编码在每一层内部单独传入,增强后的特征只表示内容。
  • 两组特征的用途如下表:
输出形状去向
memoryB × N × 2561-6 的匹配分数与查询选择,1-7 的图像交叉注意力
memory_textB × L × 2561-6 的匹配分数,1-7 的文本交叉注意力
  • 对应实现如下:
memory, memory_text = self.encoder(
    src_flatten,
    pos=lvl_pos_embed_flatten,
    level_start_index=level_start_index,
    spatial_shapes=spatial_shapes,
    valid_ratios=valid_ratios,
    key_padding_mask=mask_flatten,
    memory_text=text_dict["encoded_text"],
    text_attention_mask=~text_dict["text_token_mask"],
    position_ids=text_dict["position_ids"],
    text_self_attention_masks=text_dict["text_self_attention_masks"],
)
text_dict["encoded_text"] = memory_text   # decoder now uses the enhanced text features

# return value of TransformerEncoder.forward
return output, memory_text
  • 由于输出仍然保持原 Token 顺序与分段结构,spatial_shapes 与 level_start_index 在后续模块中依然有效,不需要重新计算。

  • 把 memory 按 spatial_shapes 拆回四个尺度,与增强之前的同一层特征并排对比,结果如下。两排各自做 PCA,因此颜色只能在行内比较,不能跨行比较:
    请添加图片描述

  • 上排是主干的输出,下排是六层增强之后的结果,两排形状完全相同,内容差别很大:

    • 下排的纹理细节明显减少,同一物体内部变得更均匀,物体所在区域表现为成片的色块
    • 逐 Token 计算增强前后的余弦相似度,四个尺度的均值依次为 0.03 0.03 0.03、 0.01 0.01 0.01、 − 0.01 -0.01 −0.01、 − 0.04 -0.04 −0.04,接近正交
  • 这说明增强器改写的是特征方向,不只是幅度:每一层都包含跨模态融合、文本自注意力与图像可变形自注意力,六层累积后每个 Token 都换了一个方向,但 Token 的数量与顺序不变,后续模块可以直接使用。

  • 换句话说,"形状不变"保证了接口稳定,"内容大变"才是增强器存在的意义。

说人话:进去是两串 256 维向量,一串对应图像位置,一串对应词;出来还是同样长度的两串 256 维向量,只是每个向量都被另一边的信息改写过,位置信息仍然靠外部单独提供。

1-6 语言引导查询选择

请添加图片描述

  • 通过双向跨模态注意力,特征增强器让图像特征与文本特征相互参考、交换信息,使两者建立语义联系,最终输出增强后的图像特征和文本特征,为后续的匹配分数计算提供基础。
  • 在 DETR 系列检测器中,模型通常使用 Object Queries 作为寻找目标的初始查询。Grounding DINO 则进一步引入文本语义,利用语言引导查询选择机制初始化目标查询。大致的流程如下:
    1. 计算匹配分数:评估每个图像位置与文本描述的相关程度。
    2. Top-k 筛选:选出匹配分数最高的 900 个图像位置。
    3. 初始化参考框:取出这些位置对应的候选框,并转换为解码器使用的参考框表示。
    4. 初始化查询内容:tgt 来自可学习的 tgt_embed,并不是直接取出被选中位置的图像特征。
  • 在两阶段检测配置中,选出的查询还会与对应的位置编码、参考框等信息结合,供解码器进一步处理。候选数量由模型配置决定。

说人话:这一步模型根据图像与文本的匹配分数,筛选出 900 个候选位置,并为每个位置初始化查询内容和参考框,交给后续解码器进一步处理。

1-6-1 文本与图像的匹配分数
  • 设增强后的图像特征为 E I ∈ R N × d E_I \in \mathbb{R}^{N \times d} EI​∈RN×d,文本特征为 E t ∈ R L × d E_t \in \mathbb{R}^{L \times d} Et​∈RL×d,其中 N = ∑ l H l W l N = \sum_l H_l W_l N=∑l​Hl​Wl​ 为多尺度特征展平后的位置数。
  • 图像位置 i i i 与文本 Token j j j 的匹配分数定义为二者的内积: S i j = ⟨ E I ( i ) , E t ( j ) ⟩ S_{ij} = \langle E_I^{(i)}, E_t^{(j)} \rangle Sij​=⟨EI(i)​,Et(j)​⟩其中 :
    • E I ( i ) E_I^{(i)} EI(i)​ :第 i i i 个图像位置的特征
    • E t ( j ) E_t^{(j)} Et(j)​ :第 j j j 个文本 Token 的特征
    • S i j S_{ij} Sij​ :二者的内积。
  • 对每个图像位置取所有文本 Token 上的最大分数,得到该位置与整句文本的匹配程度: s i = max ⁡ 1 ≤ j ≤ L S i j s_i = \max_{1 \le j \le L} S_{ij} si​=1≤j≤Lmax​Sij​其中 :
    • s i s_i si​ :第 i i i 个位置与整句文本的匹配分数
    • L L L :文本 Token 数。
  • 填充位置的分数被设为 − ∞ -\infty −∞,不会进入 Top-k 选择。
  • 对应实现如下:
res = x @ y.transpose(-1, -2)
res.masked_fill_(~text_token_mask[:, None, :], float("-inf"))
enc_outputs_class_unselected = self.enc_out_class_embed(output_memory, text_dict)
topk_logits = enc_outputs_class_unselected.max(-1)[0]

也就是说,模型通过图像特征与文本特征之间的内积,计算每个图像位置与各个文本 Token 的匹配分数,再取其中的最大值,作为该图像位置与文本描述的匹配程度。

1-6-2 Top-k 查询与参考框初始化
  • 模型在全部 N N N 个图像位置中选出匹配分数最高的 k k k 个位置,官方配置中 k = 900 k = 900 k=900: I = T o p K ( { s i } i = 1 N , k ) \mathcal{I} = \mathrm{TopK}\left(\{s_i\}_{i=1}^{N}, k\right) I=TopK({si​}i=1N​,k)其中 :
    • I \mathcal{I} I :选中的位置下标集合
    • s i s_i si​ :位置 i i i 的匹配分数
    • k k k :查询数,官方配置为 900。
  • 在官方配置 embed_init_tgt = True 下,初始查询内容来自可学习的 tgt_embed,而不是直接从选中的图像特征中提取,因此查询内容本身不随当前图像动态生成;图像与文本信息主要通过 Top-k 位置筛选、参考框初始化与后续交叉注意力进入解码过程。
  • 设第 i i i 个位置的中心与宽高为 b i = ( c x , c y , w , h ) b_i = (c_x, c_y, w, h) bi​=(cx​,cy​,w,h),参考框的初始值为: r i = σ ( M L P ( E I ( i ) ) + l o g i t ( b i ) ) r_i = \sigma\left(\mathrm{MLP}(E_I^{(i)}) + \mathrm{logit}(b_i)\right) ri​=σ(MLP(EI(i)​)+logit(bi​))其中 :
    • M L P \mathrm{MLP} MLP :多层感知机,负责回归框偏移
    • b i b_i bi​ :该位置的网格建议框
    • σ \sigma σ :sigmoid 函数
    • l o g i t \mathrm{logit} logit :sigmoid 的反函数。
  • 对应实现如下:
bs = srcs[0].shape[0]
topk_proposals = torch.topk(topk_logits, self.num_queries, dim=1)[1]
refpoint_embed_ = torch.gather(
    enc_outputs_coord_unselected, 1,
    topk_proposals.unsqueeze(-1).repeat(1, 1, 4),
).detach()
tgt_ = self.tgt_embed.weight[:, None, :].repeat(1, bs, 1).transpose(0, 1)
  • 查询内容特征使用可学习的 tgt_embed,参考框使用选中的位置回归结果,两者共同作为解码器的初始输入。

说人话:模型先在整张图的所有位置里,按"这个位置像不像提示词描述的东西"打分,挑出分数最高的 900 个位置,再把这些位置对应的框交给解码器,查询本身仍然使用固定的可学习向量。

1-6-3 输出
  • 查询选择结束时,模块向解码器交出查询内容、参考框与初始框提议三组张量,此外还有此前的图像特征与文本特征继续参与后续计算。
  • 设批大小为 B B B,查询数为 k = 900 k = 900 k=900,特征维度为 d = 256 d = 256 d=256,查询内容为: Q ∈ R B × k × d Q \in \mathbb{R}^{B \times k \times d} Q∈RB×k×d其中 :
    • Q Q Q :查询内容
    • B B B、 k k k、 d d d :批大小、查询数与特征维度。
  • 官方配置中 embed_init_tgt = True,因此 Q Q Q 的取值全部来自可学习参数 tgt_embed,与图像内容无关。每个查询有自己的一条可学习向量,但这条向量在训练完成后对所有输入固定。
    • 这意味着语言引导只发生在"选哪些位置"这一步:文本特征决定 Top-k 的顺序,选中位置则通过参考框把空间信息带进解码器。
  • 参考框保持 sigmoid 之前的对数几率形式: R i = M L P ( E I ( i ) ) + l o g i t ( b i ) , i ∈ I R_i = \mathrm{MLP}\left(E_I^{(i)}\right) + \mathrm{logit}\left(b_i\right), \quad i \in \mathcal{I} Ri​=MLP(EI(i)​)+logit(bi​),i∈I其中 :
    • R i R_i Ri​ :第 i i i 个查询的参考框,保持对数几率形式
    • I \mathcal{I} I :选中的位置下标集合
    • b i b_i bi​ :该位置的网格建议框。
  • 全部选中的位置排列起来即为 R ∈ R B × k × 4 R \in \mathbb{R}^{B \times k \times 4} R∈RB×k×4。
  • 这一形式会直接作为可变形注意力的采样参照,所以不能提前取 sigmoid;模块另外保存一份经过 sigmoid 的候选框提议,它是选中位置自身的建议框,没有经过回归网络的修正。
  • 输出汇总如下:
名称形状含义去向
tgtB × 900 × 256查询内容,来自 tgt_embed解码器自注意力的初始输入
refpoint_embedB × 900 × 4参考框,对数几率形式解码器图像交叉注意力的采样参照
init_box_proposalB × 900 × 4候选位置的原始建议框,经过 sigmoid仅作记录,不参与解码
memoryB × N × 256增强后的图像特征解码器图像交叉注意力
memory_textB × L × 256增强后的文本特征解码器文本交叉注意力
  • 对应实现如下:
refpoint_embed_ = refpoint_embed_undetach.detach()  # unsigmoid,交给解码器
init_box_proposal = torch.gather(
    output_proposals, 1, topk_proposals.unsqueeze(-1).repeat(1, 1, 4)
).sigmoid()
tgt_ = self.tgt_embed.weight[:, None, :].repeat(1, bs, 1).transpose(0, 1)
refpoint_embed, tgt = refpoint_embed_, tgt_
  • 传入解码器前,tgt 会转置为首维为查询数的布局,与 DETR 系列解码器的实现保持一致。
  • 与查询内容不同,参考框携带了图像位置信息,因此解码器每一层的可变形注意力都从这些参考框出发,预测采样偏移量并读回对应区域的特征。
1-6-4 为什么不直接使用最大分数输出
  • 尽管匹配分数能够衡量图像位置与文本描述的相关程度,但它并不能直接保证候选框的定位精度。因此,Grounding DINO 并不直接将最高分候选框作为最终结果,而是选取 Top-k 候选位置及其对应的参考框,交由跨模态解码器进一步融合图像与文本信息、细化目标定位,最终得到检测结果。
  • 论文把这个模块定位为查询初始化,把解码器定位为框细化:最终的边界框与短语都取自最后一层解码器的输出,分数只负责决定选哪些位置。
    • 直接取最高分还有三个实现上的障碍:
      • 分数只描述位置与文本的相似度,不含几何信息;
      • Top-k 内部高度冗余,同一物体附近会有多个相邻位置同时入选且分数接近;
      • 查询之间的关系只能由解码器建模,编码器阶段没有这一环。
  • 消融也支持这一分工:把语言引导选择换成静态查询后,LVIS 零样本 AP 从 16.1 降到 13.6,说明选择这一步本身有效,但它的作用是给解码器挑起点,而不是替代解码器。
  • 论文正文中直接用到最大分数的地方是 REC(Referring Expression Comprehension 代表达理解),取的仍是解码器输出,不是 1-6-1 的编码器分数。

说人话:1-6 选出来的是"值得细看的地方",不是"最终答案"。分数只回答像不像,不回答框得准不准,所以还要交给解码器把框修一遍、把重复的框压下去。

1-7 跨模态解码器

请添加图片描述

  • 图中模块自下而上依次为查询自注意力、图像交叉注意力、文本交叉注意力与前馈网络,官方实现的执行顺序则是查询自注意力、文本交叉注意力、图像交叉注意力、前馈网络。
  • 有了语言引导查询选择,我们已经得到了 900 个候选查询的查询内容和参考框。接下来,跨模态解码器以这些查询为基础,进一步与增强后的图像特征和文本特征进行交互,逐层更新查询内容并细化参考框,最终输出目标的检测框和对应的文本匹配分数。
    • 跨模态解码器负责进一步处理初始目标查询,并结合图像特征和文本特征,逐步优化目标定位结果。
  • 根据论文的设计,解码器层主要包含以下操作:
    • 查询自注意力:建模不同目标查询之间的关系。
    • 文本交叉注意力:让目标查询获取与文本描述相关的语义信息。
    • 图像交叉注意力:让目标查询从多尺度图像特征中获取视觉信息,官方实现采用可变形注意力机制。
    • 前馈网络(FFN,Feed-Forward Network):对融合后的特征进行进一步变换。
    • 边界框迭代优化:根据当前查询特征逐层调整预测框的位置和尺寸。
  • 经过多层解码后,模型输出预测边界框以及与文本 Token 对应的匹配分数。推理阶段再根据匹配分数阈值等设置筛选检测结果。
  • 需要注意:尽管编码器和解码器都使用了图像与文本的交叉注意力,但二者的作用不同。
    • 编码器主要负责增强图像与文本特征之间的语义联系
    • 解码器则以筛选出的候选查询为中心,通过交叉注意力从图像和文本特征中提取与当前目标相关的信息,逐层更新查询表示并细化目标边界框。

说人话:编码器的交叉注意力是为了让图像和文本特征相互理解、建立联系;解码器的交叉注意力则是让候选查询结合图像内容和文本描述,进一步确定目标的位置并细化边界框。

1-7-1 解码器层的计算顺序
  • 设第 l l l 层输入的查询特征为 q l q^l ql,文本特征为 E t E_t Et​,多尺度图像特征为 F F F。
  • 查询自注意力先建模不同目标之间的关系: q ~ l = q l + M H A ( q l , q l , q l ) \tilde{q}^l = q^l + \mathrm{MHA}(q^l, q^l, q^l) q~​l=ql+MHA(ql,ql,ql)其中 :
    • q l q^l ql :第 l l l 层输入的查询特征
    • q ~ l \tilde{q}^l q~​l :查询自注意力后的特征
    • M H A \mathrm{MHA} MHA :多头注意力。
  • 文本交叉注意力让查询读取文本语义: q ˇ l = q ~ l + M H A ( q ~ l , E t , E t ) \check{q}^l = \tilde{q}^l + \mathrm{MHA}(\tilde{q}^l, E_t, E_t) qˇ​l=q~​l+MHA(q~​l,Et​,Et​)其中 :
    • q ˇ l \check{q}^l qˇ​l :文本交叉注意力后的特征
    • E t E_t Et​ :增强后的文本特征。
  • 图像交叉注意力使用可变形注意力从多尺度特征中采样: q ^ l = q ˇ l + M S D e f o r m A t t n ( q ˇ l , F ) \hat{q}^l = \check{q}^l + \mathrm{MSDeformAttn}(\check{q}^l, F) q^​l=qˇ​l+MSDeformAttn(qˇ​l,F)其中 :
    • q ^ l \hat{q}^l q^​l :图像交叉注意力后的特征
    • F F F :多尺度图像特征。
  • 最后经过前馈网络: q l + 1 = q ^ l + F F N ( q ^ l ) q^{l+1} = \hat{q}^l + \mathrm{FFN}(\hat{q}^l) ql+1=q^​l+FFN(q^​l)其中 :
    • q l + 1 q^{l+1} ql+1 :第 l l l 层输出的查询特征
    • F F N \mathrm{FFN} FFN :前馈网络。
  • 官方实现把上述操作放在 DeformableTransformerDecoderLayer 中,对应代码如下:
# 查询自注意力
q = k = self.with_pos_embed(tgt, tgt_query_pos)
tgt2 = self.self_attn(q, k, tgt, attn_mask=self_attn_mask)[0]
tgt = self.norm2(tgt + self.dropout2(tgt2))
# 文本交叉注意力
tgt2 = self.ca_text(
    self.with_pos_embed(tgt, tgt_query_pos),
    memory_text.transpose(0, 1),
    memory_text.transpose(0, 1),
    key_padding_mask=text_attention_mask,
)[0]
tgt = self.catext_norm(tgt + self.catext_dropout(tgt2))
# 图像交叉注意力
tgt2 = self.cross_attn(
    query=self.with_pos_embed(tgt, tgt_query_pos).transpose(0, 1),
    reference_points=tgt_reference_points.transpose(0, 1).contiguous(),
    value=memory.transpose(0, 1),
    spatial_shapes=memory_spatial_shapes,
    level_start_index=memory_level_start_index,
    key_padding_mask=memory_key_padding_mask,
).transpose(0, 1)
tgt = self.norm1(tgt + self.dropout1(tgt2))
# 前馈网络
tgt = self.forward_ffn(tgt)
1-7-2 边界框的逐层迭代
  • 每一层解码器不仅更新查询特征,还更新参考框。
  • 设第 l l l 层的参考框为 b l b^l bl,查询特征为 q l q^l ql,边界框预测头为 M L P box \mathrm{MLP}_{\text{box}} MLPbox​,则下一层参考框为: b l + 1 = σ ( M L P box ( q l ) + l o g i t ( b l ) ) b^{l+1} = \sigma\left(\mathrm{MLP}_{\text{box}}(q^l) + \mathrm{logit}(b^l)\right) bl+1=σ(MLPbox​(ql)+logit(bl))其中 :
    • b l b^l bl :第 l l l 层输入的参考框
    • M L P box \mathrm{MLP}_{\text{box}} MLPbox​ :边界框预测头
    • σ \sigma σ、 l o g i t \mathrm{logit} logit :sigmoid 与其反函数。
  • 这里预测的是相对于上一层参考框的偏移量,而不是绝对坐标,因此每一层只需要学习一个较小的修正量。
  • 对应实现如下:
reference_before_sigmoid = inverse_sigmoid(reference_points)
delta_unsig = self.bbox_embed[layer_id](output)
outputs_unsig = delta_unsig + reference_before_sigmoid
new_reference_points = outputs_unsig.sigmoid()
reference_points = new_reference_points.detach()
  • 解码器最后一层的查询特征同时送入分类头与边界框头,分类头计算查询特征与每个文本 Token 的内积,边界框头输出归一化的中心与宽高。
  • 对应实现如下:
layer_delta_unsig = layer_bbox_embed(layer_hs)
layer_outputs_unsig = layer_delta_unsig + inverse_sigmoid(layer_ref_sig)
layer_outputs_unsig = layer_outputs_unsig.sigmoid()
outputs_class = torch.stack([
    layer_cls_embed(layer_hs, text_dict)
    for layer_cls_embed, layer_hs in zip(self.class_embed, hs)
])
  • 其中 layer_cls_embed 就是 1-6-1 节中的 ContrastiveEmbed,因此分类分数仍然是查询特征与文本特征的内积。

说人话:每一层解码器先让查询互相交流,再读文本,再看图像,最后根据当前特征把框往正确位置挪一点,多层叠加后框就越来越准。

1-7-3 输出
  • 解码器每一层都返回一份查询特征和一份参考框,因此输出是两组带层维度的张量,但最终预测只取最后一层。
  • 设批大小为 B B B,查询数为 k = 900 k = 900 k=900,特征维度为 d = 256 d = 256 d=256,层数为 L = 6 L = 6 L=6: h s ∈ R L × B × k × d , r e f s ∈ R ( L + 1 ) × B × k × 4 \mathrm{hs} \in \mathbb{R}^{L \times B \times k \times d}, \quad \mathrm{refs} \in \mathbb{R}^{(L+1) \times B \times k \times 4} hs∈RL×B×k×d,refs∈R(L+1)×B×k×4其中 :
    • h s \mathrm{hs} hs :各层查询特征
    • r e f s \mathrm{refs} refs :初始参考框与各层更新结果
    • L L L :解码器层数,官方配置为 6。
  • 参考框比查询特征多一项:首项是查询选择给出的初始参考框,其后每一项都是前一层的输出,两个序列长度的差异正对应 1-7-2 中的迭代关系。
  • 最后一层的查询特征同时送入两个预测头。分类头复现 1-6-1 的内积,得到每个查询与每个文本 Token 的匹配分数: l o g i t s = q L E t ⊤ ∈ R B × k × 256 \mathrm{logits} = q^{L} E_t^{\top} \in \mathbb{R}^{B \times k \times 256} logits=qLEt⊤​∈RB×k×256其中 :
    • l o g i t s \mathrm{logits} logits :查询与各文本 Token 的匹配分数
    • q L q^{L} qL :最后一层的查询特征。
  • 这里的第三维是 max_text_len = 256,超出实际 Token 数的位置被填成 − ∞ -\infty −∞,与填充位置的分数处理保持一致。
  • 边界框头输出的是相对参考框的偏移,与最后一层输入的参考框相加后再取 sigmoid,得到归一化的绝对坐标: b o x e s = σ ( M L P box ( q L ) + l o g i t ( b L − 1 ) ) ∈ R B × k × 4 \mathrm{boxes} = \sigma\left(\mathrm{MLP}_{\text{box}}\left(q^{L}\right) + \mathrm{logit}\left(b^{L-1}\right)\right) \in \mathbb{R}^{B \times k \times 4} boxes=σ(MLPbox​(qL)+logit(bL−1))∈RB×k×4其中 :
    • b o x e s \mathrm{boxes} boxes :归一化的绝对坐标
    • b L − 1 b^{L-1} bL−1 :最后一层的输入参考框。
  • 因此这一步与 1-7-2 的迭代公式完全相同,只是不再把结果传给下一层。
  • 输出汇总如下:
名称形状含义去向
hs6 × B × 900 × 256各层查询特征只有最后一层进入最终预测
reference7 × B × 900 × 4初始参考框与各层更新结果,经过 sigmoid最后一层的输入作为框预测的基准
pred_logitsB × 900 × 256查询与各文本 Token 的匹配分数,未过 sigmoid1-8 的阈值筛选与短语提取
pred_boxesB × 900 × 4归一化的中心与宽高1-8 的坐标还原
  • 对应实现如下:
hs, reference, hs_enc, ref_enc, init_box_proposal = self.transformer(...)
outputs_coord_list = []
for dec_lid, (layer_ref_sig, layer_bbox_embed, layer_hs) in enumerate(
    zip(reference[:-1], self.bbox_embed, hs)
):
    layer_outputs_unsig = layer_bbox_embed(layer_hs) + inverse_sigmoid(layer_ref_sig)
    outputs_coord_list.append(layer_outputs_unsig.sigmoid())
outputs_class = torch.stack(
    [
        layer_cls_embed(layer_hs, text_dict)
        for layer_cls_embed, layer_hs in zip(self.class_embed, hs)
    ]
)
out = {"pred_logits": outputs_class[-1], "pred_boxes": outputs_coord_list[-1]}
  • 各层结果被完整堆叠,但只取最后一层放入输出字典;中间层原本用于辅助监督,本实现中这部分损失没有启用。
  • pred_logits 保留未过 sigmoid 的原始分数,推理阶段再统一取 sigmoid 并做阈值筛选。

说人话:解码器交出来的是 900 个框和 900 组词分数,框已经归一化到 0 到 1,分数还没有变成概率。取哪个框、认哪个词,要等到推理阶段按阈值挑。

1-8 完整流程

请添加图片描述

  • 把前述模块串起来,Grounding DINO 的推理过程可以分成输入预处理、模型前向、阈值筛选、短语提取与坐标还原五个阶段。
1-8-1 推理输入与模型前向
  • 推理时先把图像变换为张量,再把文本提示传入模型:
caption = preprocess_caption(caption)
image = image.to(device)
with torch.no_grad():
    outputs = model(image[None], captions=[caption])
prediction_logits = outputs["pred_logits"].cpu().sigmoid()[0]
prediction_boxes = outputs["pred_boxes"].cpu()[0]
  • 其中 pred_logits 的形状为 N q × L N_q \times L Nq​×L, N q = 900 N_q = 900 Nq​=900 为查询数, L L L 为文本 Token 数;pred_boxes 的形状为 N q × 4 N_q \times 4 Nq​×4。
  • 对每个查询的每个文本 Token 做 sigmoid,得到匹配概率: p i j = σ ( ℓ i j ) p_{ij} = \sigma\left(\ell_{ij}\right) pij​=σ(ℓij​)其中 :
    • p i j p_{ij} pij​ :查询 i i i 与文本 Token j j j 的匹配概率
    • ℓ i j \ell_{ij} ℓij​ :对应的原始分数。
  • 边界框的输出为归一化的中心与宽高: b ^ i = ( c x , c y , w , h ) i ∈ [ 0 , 1 ] 4 \hat{b}_i = \left(c_x, c_y, w, h\right)_i \in [0,1]^4 b^i​=(cx​,cy​,w,h)i​∈[0,1]4其中 :
    • b ^ i \hat{b}_i b^i​ :第 i i i 个查询的归一化框
    • c x c_x cx​、 c y c_y cy​、 w w w、 h h h :中心坐标与宽高。
  • 这里的归一化坐标相对于输入图像尺寸,后续需要乘回原图宽高。
1-8-2 阈值筛选与短语提取
  • 对每个查询取所有文本 Token 上的最大概率,若超过 box_threshold 则保留该查询: K = { i ∣ max ⁡ 1 ≤ j ≤ L p i j > τ box } \mathcal{K} = \left\{i \mid \max_{1 \le j \le L} p_{ij} > \tau_{\text{box}}\right\} K={i∣1≤j≤Lmax​pij​>τbox​}其中 :
    • K \mathcal{K} K :保留下来的查询集合
    • τ box \tau_{\text{box}} τbox​ :框阈值,演示中取 0.20。
  • 对保留下来的查询,再用 text_threshold 选出真正参与短语的 Token: T i = { j ∣ p i j > τ text } \mathcal{T}_i = \left\{j \mid p_{ij} > \tau_{\text{text}}\right\} Ti​={j∣pij​>τtext​}其中 :
    • T i \mathcal{T}_i Ti​ :第 i i i 个查询的短语 Token 集合
    • τ text \tau_{\text{text}} τtext​ :文本阈值,演示中取 0.15。
  • 把 T i \mathcal{T}_i Ti​ 中的 Token 解码回字符串,就得到该框对应的短语:
mask = prediction_logits.max(dim=1)[0] > box_threshold
logits = prediction_logits[mask]
boxes = prediction_boxes[mask]
phrases = [
    get_phrases_from_posmap(
        logit > text_threshold, tokenized, tokenizer
    ).replace(".", "")
    for logit in logits
]
  • 在演示中,box_threshold 取 0.20,text_threshold 取 0.15;阈值越低,召回越多,但误检也会增加。
1-8-3 坐标还原与非极大值抑制
  • 设原图宽高为 W W W 与 H H H,归一化框 ( c x , c y , w , h ) (c_x, c_y, w, h) (cx​,cy​,w,h) 还原为像素坐标: x 0 = ( c x − w 2 ) W , y 0 = ( c y − h 2 ) H , x 1 = ( c x + w 2 ) W , y 1 = ( c y + h 2 ) H x_0 = \left(c_x - \frac{w}{2}\right)W, \quad y_0 = \left(c_y - \frac{h}{2}\right)H, \quad x_1 = \left(c_x + \frac{w}{2}\right)W, \quad y_1 = \left(c_y + \frac{h}{2}\right)H x0​=(cx​−2w​)W,y0​=(cy​−2h​)H,x1​=(cx​+2w​)W,y1​=(cy​+2h​)H其中 :
    • W W W、 H H H :原图的宽与高
    • ( c x , c y , w , h ) (c_x, c_y, w, h) (cx​,cy​,w,h) :归一化框
    • ( x 0 , y 0 ) (x_0, y_0) (x0​,y0​)、 ( x 1 , y 1 ) (x_1, y_1) (x1​,y1​) :左上角与右下角的像素坐标。
  • 多个查询可能框住同一个目标,因此用 NMS(Non-Maximum Suppression,非极大值抑制)去重: I o U ( b i , b j ) = ∣ b i ∩ b j ∣ ∣ b i ∪ b j ∣ \mathrm{IoU}(b_i, b_j) = \frac{|b_i \cap b_j|}{|b_i \cup b_j|} IoU(bi​,bj​)=∣bi​∪bj​∣∣bi​∩bj​∣​其中 :
    • b i b_i bi​、 b j b_j bj​ :两个候选框
    • I o U \mathrm{IoU} IoU :交并比。
  • 若两个框的 IoU 超过阈值,则保留分数较高的框,抑制分数较低的框。
  • 对应实现如下:
scaled = boxes * torch.tensor([width, height, width, height], dtype=boxes.dtype)
xyxy = box_convert(boxes=scaled, in_fmt="cxcywh", out_fmt="xyxy")
xyxy[:, 0::2] = xyxy[:, 0::2].clamp(0, width)
xyxy[:, 1::2] = xyxy[:, 1::2].clamp(0, height)
keep = nms(xyxy, logits, iou_threshold=0.5).tolist()
chosen = max(keep, key=lambda i: float(logits[i]))
  • 演示场景只保留分数最高的一个检测框,作为 SAM 的框提示。
  • 如果需要保留多个目标,可以把 chosen 换成一个列表,对每个框分别调用 SAM。

说人话:模型对 900 个候选查询各给一套"框加词概率",先按框分数筛掉不靠谱的,再把词概率够高的 Token 拼成短语,最后把归一化框乘回原图尺寸并去掉重复框。


2 SAM

2-1 介绍

请添加图片描述

  • SAM 是 Meta AI 于 2023 年提出的通用图像分割基础模型,相关论文 Segment Anything 发表于 ICCV(International Conference on Computer Vision,国际计算机视觉会议)2023。
    • 其核心目标是构建一种具有良好泛化能力的可提示分割模型,使用户能够通过点、框或掩码等提示信息,指定图像中需要分割的目标,而不必针对每一种目标类别单独训练分割模型。
  • 传统语义分割或实例分割模型通常需要利用特定任务的数据进行监督训练,模型的检测与分割能力也往往受到训练类别和数据分布的限制,SAM 则采用 Promptable Segmentation(可提示分割) 的设计思路,将分割任务转化为根据提示信息生成目标掩码的过程。模型经过大规模数据预训练后,可以通过提示适应不同的分割需求,并具备较强的零样本迁移能力。
  • 形式化地,SAM 学习一个提示条件映射: M = f θ ( I , P ) , P ∈ { point , box , mask } M = f_{\theta}(I, P), \quad P \in \{\text{point}, \text{box}, \text{mask}\} M=fθ​(I,P),P∈{point,box,mask}其中 :
    • I I I :图像
    • P P P :提示
    • M M M :分割掩码。
  • 当提示存在歧义时,模型输出多个候选掩码,并用预测 IoU 排序。
2-2 架构
  • SAM 的整体数据流如下图所示:

输入图像
Input Image

图像编码器
Image Encoder
ViT

图像嵌入特征
Image Embedding

输入提示
Points / Boxes / Masks

提示编码器
Prompt Encoder

稀疏 / 稠密提示特征
Sparse / Dense Embeddings

轻量级掩码解码器
Mask Decoder

分割掩码
Segmentation Masks

预测质量分数
Predicted IoU

  • 论文 Figure 4 的图注给出了 SAM 结构的三个要点:

    • 一个重型图像编码器输出图像嵌入。
    • 该嵌入可以被多种输入提示高效查询,从而以摊销后的实时速度生成目标掩码。
    • 对于对应多个目标的歧义提示,SAM 可以输出多个有效掩码及其置信度分数。
  • 这三句话对应了 SAM 的三个设计选择:图像编码器只运行一次,提示编码器把不同类型的提示映射为统一表示,掩码解码器在提示条件下生成候选掩码并给出质量分数。

  • 论文 Figure 4 原图如下:请添加图片描述

  • 图中左侧是重型图像编码器,中间是提示编码器与掩码解码器,右侧是同一提示下输出的多个候选掩码及各自的分数。

  • 图中的 conv 模块用于把低分辨率掩码提示降到与图像嵌入相同的空间尺度,因而与点、框、文本等稀疏提示走不同的编码路径。

  • SAM 主要由三个模块组成:

    • 图像编码器(Image Encoder):提取输入图像的视觉特征。该模块通常采用基于 Vision Transformer 的图像编码器,计算得到图像嵌入特征。
    • 提示编码器(Prompt Encoder):将用户提供的点、边界框或已有掩码等提示转换为特征表示。其中,点和框等稀疏提示与掩码等稠密提示采用不同的编码方式。
    • 掩码解码器(Mask Decoder):融合图像特征与提示特征,预测目标的分割掩码,并输出用于评估预测质量的置信度信息。
  • 在推理过程中,图像编码器可以先计算图像嵌入;当用户调整点或框等提示时,模型能够复用已有图像特征,通过轻量级解码器快速生成新的分割结果。

需要注意的是,尽管论文中的 Prompt Encoder 示意图包含文本提示(Text Prompt),但原始 SAM 的开源实现并未提供这一功能。Meta 官方 FAQ 也明确说明,文本提示功能仅在论文中进行了探索,并未随原始模型一同发布。

  • 这也是本文选择 Grounding DINO + SAM 组合的原因:通过 Grounding DINO 实现文本提示的目标定位,再将检测框传递给 SAM 完成像素级分割,从而实现从自然语言描述到目标分割的完整流程。
2-2-1 三模块的分工
  • 设输入图像为 I I I,提示为 P P P,分割结果为 M M M,则 SAM 的前向过程可以写成: E I = f image ( I ) , E P = f prompt ( P ) , M = f mask ( E I , E P ) E_I = f_{\text{image}}(I), \quad E_P = f_{\text{prompt}}(P), \quad M = f_{\text{mask}}(E_I, E_P) EI​=fimage​(I),EP​=fprompt​(P),M=fmask​(EI​,EP​)其中 :
    • I I I :输入图像
    • P P P :提示
    • M M M :输出掩码
    • E I E_I EI​ :图像嵌入
    • E P E_P EP​ :提示嵌入
    • f mask f_{\text{mask}} fmask​ :掩码解码器。
  • 图像编码器只依赖图像,提示编码器只依赖提示,掩码解码器负责把两者结合,这种分工使同一张图像可以对应任意多个提示。
2-2-2 摊销后的实时查询
  • 图注中的 “amortized real-time speed” 指的是把重型图像编码器的计算摊销到多次提示查询上。
  • 设图像编码器的计算量为 C image C_{\text{image}} Cimage​,单次提示解码的计算量为 C prompt C_{\text{prompt}} Cprompt​,同一张图像上查询 n n n 次的总计算量为: C total ( n ) = C image + n C prompt C_{\text{total}}(n) = C_{\text{image}} + n C_{\text{prompt}} Ctotal​(n)=Cimage​+nCprompt​其中 :
    • C image C_{\text{image}} Cimage​ :图像编码器的一次性计算量
    • C prompt C_{\text{prompt}} Cprompt​ :单次提示解码的计算量
    • n n n :同一张图上的查询次数。
  • 由于 C image C_{\text{image}} Cimage​ 只计算一次,当 n n n 增大时,平均到每次查询的计算量下降: C ˉ ( n ) = C image n + C prompt \bar{C}(n) = \frac{C_{\text{image}}}{n} + C_{\text{prompt}} Cˉ(n)=nCimage​​+Cprompt​其中 :
    • C ˉ ( n ) \bar{C}(n) Cˉ(n) : n n n 次查询的平均计算量。
  • 这就是 “摊销” 的含义:第一次查询需要承担图像编码的成本,后续查询只承担轻量级解码的成本。
  • 对应实现上,SamPredictor 把图像编码与提示解码拆成 set_image 与 predict 两个阶段。
2-2-3 歧义提示与多掩码输出
  • 图注中的第二层含义是:当提示对应多个目标时,SAM 不强行给出单一答案,而是输出多个候选掩码与置信度。
  • 设候选掩码为 M 1 , … , M K M_1, \dots, M_K M1​,…,MK​,对应的置信度为 q 1 , … , q K q_1, \dots, q_K q1​,…,qK​,则推理时可以按置信度排序: k ∗ = arg ⁡ max ⁡ 1 ≤ k ≤ K q k k^{*} = \arg\max_{1 \le k \le K} q_k k∗=arg1≤k≤Kmax​qk​其中 :
    • q k q_k qk​ :第 k k k 个候选掩码的预测置信度
    • K K K :候选掩码数
    • k ∗ k^{*} k∗ :被选中的候选下标。
  • 官方实现在多掩码模式下输出 3 个候选掩码,每个候选对应一个预测 IoU 分数。
  • 这种设计与传统分割模型不同:传统模型通常只输出一个类别相关的掩码,而 SAM 把歧义显式地保留在候选集合中。

说人话:SAM 的重型图像编码器只跑一次,之后换点、换框都只跑轻量解码器;如果一个提示可能对应多个物体,它会把几个候选掩码和置信度一起给出,而不是赌一个。

2-3 图像编码器

请添加图片描述

  • 图像编码器负责提取输入图像的视觉特征,是 SAM 中计算量较大的部分。
  • SAM 原版采用基于 ViT(Vision Transformer,视觉 Transformer)的图像编码器。输入图像经过预处理后,被转换为图像特征表示(Image Embedding),供后续的掩码预测使用。
    • 与每次分割都重新处理整张图像不同,SAM 可以缓存图像编码结果。
    • 当需要分割同一张图像中的不同目标时,只需更换提示信息,就可以复用已有的图像特征,从而减少重复计算。
2-3-1 ViT 分块嵌入与窗口注意力
  • ViT-H 版本把 1024 × 1024 1024 \times 1024 1024×1024 的输入图像切成 16 × 16 16 \times 16 16×16 的图像块,得到 64 × 64 = 4096 64 \times 64 = 4096 64×64=4096 个 Token。
  • 每个图像块先经过线性投影,再加上绝对位置编码: z 0 = [ x p 1 E ; x p 2 E ; …   ; x p N E ] + E pos z_0 = \left[x_p^1 E; x_p^2 E; \dots; x_p^N E\right] + E_{\text{pos}} z0​=[xp1​E;xp2​E;…;xpN​E]+Epos​其中 :
    • x p n x_p^n xpn​ :第 n n n 个图像块展平后的向量
    • E E E :投影矩阵
    • E pos E_{\text{pos}} Epos​ :位置编码
    • N = 4096 N = 4096 N=4096。
  • ViT-H 的嵌入维度为 1280,深度为 32,注意力头数为 16。
    • 大部分 Transformer 块使用 14 × 14 14 \times 14 14×14 的窗口注意力,只在第 7、15、23、31 块使用全局注意力,以兼顾计算效率与全局建模能力。
  • 每个注意力块的计算可以写成: A t t n ( Q , K , V ) = s o f t m a x ( Q K ⊤ d k + B ) V \mathrm{Attn}(Q, K, V) = \mathrm{softmax}\left(\frac{QK^{\top}}{\sqrt{d_k}} + B\right)V Attn(Q,K,V)=softmax(dk​ ​QK⊤​+B)V其中 :
    • Q Q Q、 K K K、 V V V :查询、键、值矩阵
    • B B B :相对位置偏置
    • d k d_k dk​ :每个注意力头的维度。
  • 对应实现如下:
def forward(self, x):
    x = self.patch_embed(x)
    if self.pos_embed is not None:
        x = x + self.pos_embed
    for blk in self.blocks:
        x = blk(x)
    x = self.neck(x.permute(0, 3, 1, 2))
    return x
  • 最后的 neck 由两个卷积层与 LayerNorm 组成,把通道数统一到 256,输出形状为 256 × 64 × 64 256 \times 64 \times 64 256×64×64 的图像嵌入。
2-3-2 特征缓存与复用
  • SamPredictor.set_image 负责完成图像预处理与编码,并把结果保存在 self.features 中:
input_image = self.transform.apply_image(image)
input_image_torch = torch.as_tensor(input_image, device=self.device)
input_image_torch = input_image_torch.permute(2, 0, 1).contiguous()[None, :, :, :]
self.set_torch_image(input_image_torch, image.shape[:2])
  • set_torch_image 内部调用 self.model.preprocess 与 self.model.image_encoder:
input_image = self.model.preprocess(transformed_image)
self.features = self.model.image_encoder(input_image)
self.is_image_set = True
  • 因此后续每次 predict 都直接使用 self.features,不再重复运行 ViT。

说人话:图像编码器把整张图压成一张 64 × 64 64 \times 64 64×64 的特征图,这张特征图算一次就能反复用,换框、换点都不需要重新跑 ViT。

2-3-3 效果展示
  • 以演示图为例,图像编码器依次输出两张特征图:ViT-H 主干最后一层的输出为 1280 × 64 × 64 1280 \times 64 \times 64 1280×64×64,经过 neck 之后得到 256 × 64 × 64 256 \times 64 \times 64 256×64×64 的图像嵌入。

  • 原图尺寸为 960 × 720 960 \times 720 960×720,长边缩放到 1024 后得到 1024 × 768 1024 \times 768 1024×768,再在右侧与下方补零到 1024 × 1024 1024 \times 1024 1024×1024,因此按 16 × 16 16 \times 16 16×16 分块共得到 64 × 64 = 4096 64 \times 64 = 4096 64×64=4096 个 Token,其中只有 64 × 48 = 3072 64 \times 48 = 3072 64×48=3072 个对应真实像素。

  • 特征维度远高于三通道,无法直接显示。常用的做法是对每个空间位置的特征向量做主成分分析,取前三个主成分作为 RGB 三个通道。设特征图展平为 X X X,则: C 3 = X V 3 , V 3 ∈ R C × 3 C_3 = X V_3, \quad V_3 \in \mathbb{R}^{C \times 3} C3​=XV3​,V3​∈RC×3其中 :

    • X X X :把 C × H × W C \times H \times W C×H×W 的特征图按空间位置展平得到的 N × C N \times C N×C 矩阵, N = H W N = HW N=HW
    • V 3 V_3 V3​ :特征协方差矩阵前三个特征向量组成的投影矩阵
    • C 3 C_3 C3​ : N × 3 N \times 3 N×3 的主成分坐标,还原成 H × W × 3 H \times W \times 3 H×W×3 后把每一维归一化到 [ 0 , 1 ] [0,1] [0,1] 用于显示。
  • 展示如下:
    请添加图片描述

  • 从左到右依次为原图、ViT-H 主干输出与 neck 之后的图像嵌入。

    • 主干输出中,黄色杯子与蓝色瓶子各自成为一块独立的颜色,与周围叶丛和红色桌面明显分开,说明 1280 维特征已经把颜色与材质相近的区域聚在一起。
    • neck 之后空间分辨率不变,仍是 64 × 64 64 \times 64 64×64,通道数由 1280 降到 256,杯子与瓶子的轮廓依然清晰,说明这一步主要压缩的是冗余方向。
  • 需要注意,PCA 图只保留三个方向,颜色本身没有语义,能读出的是"哪些区域在特征空间里彼此相似"这一类关系。

  • 补零区域在两张图中都呈现与其他区域不同的颜色,因为它们没有对应的真实像素,取值来自位置编码与填充。

说人话:ViT 把补齐到 1024×1024 的图切成 64×64 个小块,每块变成一个 1280 维向量;neck 再把每个向量压到 256 维。把高维向量压到三个主成分画出来,杯子、瓶子与桌面各自成块的效果就能直接看到。

2-4 提示编码器
  • 提示编码器负责将用户提供的提示信息转换为模型能够处理的特征表示。SAM 原版支持的提示主要包括点、边界框和已有掩码。
    • 对于点和边界框等提示,模型会将其位置信息编码为稀疏特征(Sparse Embeddings);对于已有掩码,则通过相应的卷积结构将其编码为稠密特征(Dense Embeddings)。
  • 编码后的提示特征会传递给掩码解码器,与图像特征结合,从而引导模型分割指定目标。
2-4-1 点提示与框提示的稀疏编码
  • 点提示包含坐标与标签,标签 1 表示前景点,0 表示背景点。
  • 每个点先经过随机傅里叶位置编码,再加上对应标签的可学习嵌入: e p = P E ( p ) + e label e_p = \mathrm{PE}(p) + e_{\text{label}} ep​=PE(p)+elabel​其中 :
    • p p p :点坐标
    • P E ( p ) \mathrm{PE}(p) PE(p) :点坐标的位置编码
    • e label e_{\text{label}} elabel​ :前景或背景的可学习嵌入。
  • 边界框被表示为一对角点,两个角点分别加上不同的可学习嵌入: e b = [ P E ( p 0 ) + e 2 ; P E ( p 1 ) + e 3 ] e_b = \left[\mathrm{PE}(p_0) + e_2; \mathrm{PE}(p_1) + e_3\right] eb​=[PE(p0​)+e2​;PE(p1​)+e3​]其中 :
    • p 0 p_0 p0​、 p 1 p_1 p1​ :框的两个角点
    • e 2 e_2 e2​、 e 3 e_3 e3​ :两个角点对应的可学习嵌入。
  • 这样框提示在形式上与两个点提示一致,掩码解码器不需要为框单独设计分支。
  • 对应实现如下:
def _embed_points(self, points, labels, pad):
    points = points + 0.5
    point_embedding = self.pe_layer.forward_with_coords(points, self.input_image_size)
    point_embedding[labels == 0] += self.point_embeddings[0].weight
    point_embedding[labels == 1] += self.point_embeddings[1].weight
    return point_embedding

def _embed_boxes(self, boxes):
    boxes = boxes + 0.5
    coords = boxes.reshape(-1, 2, 2)
    corner_embedding = self.pe_layer.forward_with_coords(coords, self.input_image_size)
    corner_embedding[:, 0, :] += self.point_embeddings[2].weight
    corner_embedding[:, 1, :] += self.point_embeddings[3].weight
    return corner_embedding
  • 位置编码使用随机高斯频率矩阵,把二维坐标映射到高维正弦与余弦特征: P E ( p ) = [ sin ⁡ ( 2 π p ~ G ) ; cos ⁡ ( 2 π p ~ G ) ] \mathrm{PE}(p) = \left[\sin(2\pi \tilde{p} G); \cos(2\pi \tilde{p} G)\right] PE(p)=[sin(2πp~​G);cos(2πp~​G)]其中 :
    • p ~ \tilde{p} p~​ :归一化到 [ − 1 , 1 ] [-1,1] [−1,1] 的二维坐标
    • G G G :随机高斯频率矩阵。
2-4-2 掩码提示的稠密编码
  • 若用户提供已有掩码,编码器用三层卷积把它下采样到与图像嵌入相同的空间尺寸: E m = C o n v 1 × 1 ( C o n v 2 × 2 s = 2 ( C o n v 2 × 2 s = 2 ( M ) ) ) E_m = \mathrm{Conv}_{1 \times 1}\left(\mathrm{Conv}_{2 \times 2}^{s=2}\left(\mathrm{Conv}_{2 \times 2}^{s=2}(M)\right)\right) Em​=Conv1×1​(Conv2×2s=2​(Conv2×2s=2​(M)))其中 :
    • M M M :输入的掩码提示
    • C o n v 2 × 2 s = 2 \mathrm{Conv}_{2 \times 2}^{s=2} Conv2×2s=2​ :步长为 2 的 2 × 2 2 \times 2 2×2 卷积
    • E m E_m Em​ :稠密掩码提示。
  • 前两层卷积的步长为 2,把掩码分辨率降低 4 倍,最后一层 1 × 1 1 \times 1 1×1 卷积把通道数映射到 256。
  • 若没有掩码提示,则使用一个可学习的 no_mask_embed 填充稠密特征。
  • 对应实现如下:
self.mask_downscaling = nn.Sequential(
    nn.Conv2d(1, mask_in_chans // 4, kernel_size=2, stride=2),
    LayerNorm2d(mask_in_chans // 4),
    nn.GELU(),
    nn.Conv2d(mask_in_chans // 4, mask_in_chans, kernel_size=2, stride=2),
    LayerNorm2d(mask_in_chans),
    nn.GELU(),
    nn.Conv2d(mask_in_chans, embed_dim, kernel_size=1),
)
  • 掩码解码器最终接收的稠密提示特征形状为 256 × 64 × 64 256 \times 64 \times 64 256×64×64,与图像嵌入逐元素相加。

说人话:点和框被编码成少量"提示 Token",掩码被编码成一张和图像特征同样大小的"提示图",两者一起告诉解码器该往哪里分割。

2-4-3 效果展示
  • 框提示来自 Grounding DINO 的检测结果,提示编码器对它只输出两个 Token,形状为 2 × 256 2 \times 256 2×256:

    • 第一个 Token 对应框的左上角,取值为位置编码加上角点嵌入 e 2 e_2 e2​
    • 第二个 Token 对应框的右下角,取值为位置编码加上角点嵌入 e 3 e_3 e3​。
  • 与图像嵌入的 256 × 64 × 64 256 \times 64 \times 64 256×64×64 相比,稀疏提示少了三个数量级的元素,它不携带空间布局,只携带"往哪里看"的信息。

  • 没有掩码输入时,稠密提示是同一个常数向量广播得到的 256 × 64 × 64 256 \times 64 \times 64 256×64×64 张量,因此在这个例子里,提示一侧真正携带信息的只有这两个 Token。

  • 稀疏提示与图像嵌入并不在同一个子空间,直接计算两者的余弦相似度并不具解释性;要观察提示的作用,需要看解码器的 Token 到图像交叉注意力。

  • 展示如下,左侧为框提示,中间为两个角点 Token 在 256 个通道上的取值,右侧为解码器第二层 Token 到图像的交叉注意力(对 8 个注意力头与 4 个掩码 Token 取平均):
    请添加图片描述

  • 注意力几乎全部落在框内的杯身上,桌面、瓶子与背景叶丛只分到极小的权重。

    • 两个角点 Token 在通道上的取值看起来像随机噪声,这是因为位置编码使用随机高斯频率矩阵,相邻通道之间近似独立,单个通道没有明确语义。
    • 第二层的注意力比第一层集中,第一层还有大量权重落在补零区域,说明提示与图像信息的对齐是在两层双向注意力中逐步完成的(注意力的计算本身属于 2-5 的解码器,这里借用它说明提示的作用)。

说人话:框在提示编码器里只变成两个 256 维的向量,单看数值看不出形状;这两个向量进入解码器后,才变成"只看框里那块"的注意力权重,把杯身从桌面上挑出来。

2-5 掩码解码器

请添加图片描述

  • 掩码解码器负责融合图像特征与提示特征,并根据提示信息预测目标的分割掩码。
    • 传统模型通常通过固定的输出层生成掩码,其参数在训练阶段学习并固定,不会针对不同的候选掩码动态生成。
  • SAM 则借鉴了超网络(Hypernetwork)的思想,根据不同 Mask Token 的特征动态生成掩码预测所需的参数,使不同 Token 能够产生不同的候选分割结果。
2-5-1 超网络(Hypernetwork)
  • 超网络(Hypernetwork)是一种神经网络架构,其核心思想是利用一个神经网络生成另一个神经网络所需的权重或参数。前者称为超网络,后者称为目标网络(Target Network)。
    • 与使用固定参数进行预测的传统网络不同,超网络能够根据输入特征或任务信息动态生成参数,从而增强模型对不同输入和任务的适应能力。
  • 在 SAM 中,每个 Mask Token 都经过独立的三层 MLP,生成用于预测对应掩码的动态权重。随后,这些权重与上采样后的图像特征逐位置计算内积,得到相应的候选掩码。这里的 MLP 充当超网络,生成的动态权重用于完成掩码预测,而非直接输出最终掩码。

Mask Tokens
mask_tokens_out
B × 4 × 256

4 个独立的三层 MLP
MLP(256, 256, 32)

生成动态权重
hyper_in:B × 4 × 32

上采样图像特征
upscaled_embedding
B × 32 × 256 × 256

展平特征图
B × 32 × 65536

矩阵乘法
动态权重 × 图像特征

重塑输出
B × 4 × 256 × 256

4 张候选掩码
Mask Logits

  • 下面两段代码分别是超网络的构造与使用。代码里的 mask_tokens_out 是 2-5-2 末尾解码器输出的四个掩码 Token,upscaled_embedding 是 2-5-3 中上采样得到的特征图。
  • 构造部分为每个掩码 Token 配一个三层感知机,输入是 256 维的 Token 特征,中间层 256 维,输出 32 维:
self.output_hypernetworks_mlps = nn.ModuleList(
    [
        MLP(transformer_dim, transformer_dim, transformer_dim // 8, 3)
        for i in range(self.num_mask_tokens)
    ]
)
  • transformer_dim = 256,transformer_dim // 8 = 32,因此实际结构是 MLP(256, 256, 32, 3),最后一层输出 32 维。
  • 感知机的输出就是目标网络在该候选掩码上的权重: w k = M L P k ( h mask k ) , w k ∈ R 32 w_k = \mathrm{MLP}_k\left(h_{\text{mask}}^{k}\right), \quad w_k \in \mathbb{R}^{32} wk​=MLPk​(hmaskk​),wk​∈R32其中 :
    • h mask k h_{\text{mask}}^{k} hmaskk​ :第 k k k 个掩码 Token 经过解码器后的 256 维特征
    • M L P k \mathrm{MLP}_k MLPk​ :第 k k k 个掩码 Token 专属的三层感知机
    • w k w_k wk​ :第 k k k 个候选掩码的动态权重。
  • 目标网络只有一层逐位置内积:权重由 Token 现场生成,与上采样特征图在每个空间位置做点积,就得到该位置属于该候选掩码的对数: m k ( u , v ) = ⟨ w k ,   U : , u , v ⟩ m_k(u, v) = \left\langle w_k,\ U_{:,u,v} \right\rangle mk​(u,v)=⟨wk​, U:,u,v​⟩其中 :
    • U : , u , v U_{:,u,v} U:,u,v​ :上采样特征图在 ( u , v ) (u, v) (u,v) 处的 32 维向量
    • m k ( u , v ) m_k(u, v) mk​(u,v) :第 k k k 个候选掩码在该位置的掩码 logit。
  • 使用部分先把四个权重拼成 4 × 32 4 \times 32 4×32 的矩阵,再用一次矩阵乘法同时得到四张低分辨率掩码:
hyper_in_list = []
for i in range(self.num_mask_tokens):
    hyper_in_list.append(self.output_hypernetworks_mlps[i](mask_tokens_out[:, i, :]))
hyper_in = torch.stack(hyper_in_list, dim=1)                  # B x 4 x 32

b, c, h, w = upscaled_embedding.shape
masks = (hyper_in @ upscaled_embedding.view(b, c, h * w)).view(b, -1, h, w)
  • 与固定输出层相比,超网络把"学习参数"换成了"学习生成参数":四个候选共享同一张上采样特征图,却各自拥有一把由 Token 决定的权重,因此不必为每个候选单独训练一整套输出层参数。
  • 代价是目标网络无法写成一个固定的卷积层:权重随输入变化,每次推理都要由 Token 重新算一遍,也不能像普通卷积那样把权重与特征提前融合。

说人话:普通模型是训练好一把固定的刀,所有图都用它切;超网络是每个提示现场磨一把刀,刀的形状由 Token 决定,磨完往同一张特征图上一划就是一张候选掩码。

2-5-2 输出 Token 与双向注意力
  • 解码器的输入由三部分组成:可学习的输出 Token、提示编码器给出的稀疏提示特征、以及图像嵌入与稠密提示特征逐元素相加得到的图像 Token。
  • 输出 Token 包含一个 IoU Token 与四个掩码 Token,其中第一个专用于单掩码输出,后三个用于多掩码输出: H ( 0 ) = [ t iou ; t mask 1 ; t mask 2 ; t mask 3 ; t mask 4 ; E P sparse ] H^{(0)} = \left[t_{\text{iou}}; t_{\text{mask}}^{1}; t_{\text{mask}}^{2}; t_{\text{mask}}^{3}; t_{\text{mask}}^{4}; E_P^{\text{sparse}}\right] H(0)=[tiou​;tmask1​;tmask2​;tmask3​;tmask4​;EPsparse​]其中 :
    • t iou t_{\text{iou}} tiou​ :IoU Token
    • t mask 1 t_{\text{mask}}^{1} tmask1​ 到 t mask 4 t_{\text{mask}}^{4} tmask4​ :四个掩码 Token
    • E P sparse E_P^{\text{sparse}} EPsparse​ :稀疏提示特征。
  • 图像一侧的 Token 为: Z ( 0 ) = E I + E P dense Z^{(0)} = E_I + E_P^{\text{dense}} Z(0)=EI​+EPdense​其中 :
    • E I E_I EI​ :图像嵌入
    • E P dense E_P^{\text{dense}} EPdense​ :稠密提示特征
    • Z ( 0 ) Z^{(0)} Z(0) :图像一侧的初始 Token。
  • 双向 Transformer 共两层,每层依次执行五个子模块:Token 自注意力、Token 到图像的交叉注意力、Token 的前馈网络、图像到 Token 的交叉注意力、图像的前馈网络。
  • 注意力计算统一写成缩放点积形式: A t t n ( Q , K , V ) = s o f t m a x ( Q K ⊤ d ) V \mathrm{Attn}(Q, K, V) = \mathrm{softmax}\left(\frac{QK^{\top}}{\sqrt{d}}\right)V Attn(Q,K,V)=softmax(d ​QK⊤​)V其中 :
    • Q Q Q、 K K K、 V V V :查询、键、值矩阵
    • d d d :每个注意力头的维度。
  • 以 Token 到图像的交叉注意力为例,查询来自 Token,键与值来自图像 Token: H ′ = A t t n ( H W Q ,   Z W K ,   Z W V ) H' = \mathrm{Attn}(H W_Q,\ Z W_K,\ Z W_V) H′=Attn(HWQ​, ZWK​, ZWV​)其中 :
    • H H H :Token 一侧的特征
    • Z Z Z :图像一侧的特征
    • W Q W_Q WQ​、 W K W_K WK​、 W V W_V WV​ :三组投影矩阵
    • H ′ H' H′ :交叉注意力后的 Token 特征。
  • 图像到 Token 的方向对称,查询来自图像 Token,键与值来自 Token。两个方向叠加,使提示信息能够读取图像内容,同时图像特征也能被提示调制。
  • Token 自注意力与两个前馈网络都采用残差连接,图像一侧的查询还会加上图像位置编码,使注意力具备空间定位能力。
  • 对应实现如下:
output_tokens = torch.cat([self.iou_token.weight, self.mask_tokens.weight], dim=0)
output_tokens = output_tokens.unsqueeze(0).expand(sparse_prompt_embeddings.size(0), -1, -1)
tokens = torch.cat((output_tokens, sparse_prompt_embeddings), dim=1)

src = torch.repeat_interleave(image_embeddings, tokens.shape[0], dim=0)
src = src + dense_prompt_embeddings
pos_src = torch.repeat_interleave(image_pe, tokens.shape[0], dim=0)

hs, src = self.transformer(src, pos_src, tokens)
iou_token_out = hs[:, 0, :]
mask_tokens_out = hs[:, 1 : (1 + self.num_mask_tokens), :]
  • 设计上的取舍在于,解码器只保留两层,参数量远小于图像编码器,因此可以在已有图像嵌入上反复调用。
  • 解码器的输出分成两路:mask_tokens_out 交给 2-5-1 的超网络生成四张候选掩码的权重,iou_token_out 交给 IoU 头打分,图像一侧的 src 则交给 2-5-3 上采样成特征图。
2-5-3 掩码生成与 IoU 分数
  • Transformer 输出的图像 Token 先做两次转置卷积上采样,通道数由 256 降到 64 再降到 32,空间尺寸由 64 × 64 64 \times 64 64×64 升到 256 × 256 256 \times 256 256×256: U = D e c o n v 2 × 2 s = 2 ( D e c o n v 2 × 2 s = 2 ( Z ( L ) ) ) U = \mathrm{Deconv}_{2 \times 2}^{s=2}\left(\mathrm{Deconv}_{2 \times 2}^{s=2}(Z^{(L)})\right) U=Deconv2×2s=2​(Deconv2×2s=2​(Z(L)))其中 :
    • Z ( L ) Z^{(L)} Z(L) :解码器输出的图像 Token
    • U U U :上采样后的特征图。
  • 上采样后的特征图 U U U 与 2-5-1 中由掩码 Token 生成的权重逐位置做内积,四张候选掩码一次得到,形状为 4 × 256 × 256 4 \times 256 \times 256 4×256×256。公式与推导已在 2-5-1 给出,这里只强调顺序:先上采样出 U U U,再把每个 Token 的权重作用在 U U U 上。
  • IoU Token 经过另一个感知机,一次性输出四个候选掩码的质量分数: q ^ = M L P iou ( h iou ) ∈ R 4 \hat{\mathbf{q}} = \mathrm{MLP}_{\text{iou}}\left(h_{\text{iou}}\right) \in \mathbb{R}^{4} q^​=MLPiou​(hiou​)∈R4其中 :
    • h iou h_{\text{iou}} hiou​ :IoU Token 的输出
    • q ^ \hat{\mathbf{q}} q^​ :四个候选掩码的预测质量分数。
  • 与掩码头不同,IoU 头是普通的前馈网络:参数在训练阶段学好后固定,既不读取图像特征,也不生成任何权重,只把 IoU Token 的特征映射成四个标量。
  • 这一分支的最后一层是线性层,输出没有经过 sigmoid,因此预测值不被限制在 [ 0 , 1 ] [0,1] [0,1] 内,略大于 1 属于正常现象;官方推理直接使用这些原始分数对候选排序。
  • 推理时若开启多掩码输出,实现上取第 1 到第 3 个候选,丢弃第 0 个专用于单掩码的 Token:
if multimask_output:
    mask_slice = slice(1, None)
else:
    mask_slice = slice(0, 1)
masks = masks[:, mask_slice, :, :]
iou_pred = iou_pred[:, mask_slice]
  • 解码器在内部按切片规则给出三张 256 × 256 256 \times 256 256×256 的低分辨率掩码 logit 与三个预测分数,掩码经过后处理再恢复到原图尺寸。
  • SamPredictor.predict() 一次返回三项:原图尺寸的掩码(默认已按阈值 0 二值化)、预测分数、 256 × 256 256 \times 256 256×256 的低分辨率 logit;它本身不挑选最终结果,选哪一张由调用方根据预测分数决定。

说人话:解码器拿一小串可学习的"提问 Token",在图像特征上问两次,问完每个 Token 各自生成一把小卷积核,卷出一张候选掩码;IoU Token 则负责给这些候选打分。

2-5-4 展示
  • 用演示图中黄色杯子的框提示跑一次掩码解码器,把四个掩码 Token 的候选结果、动态权重与最终选择画在同一张图上。
    请添加图片描述

  • 第一排是四个掩码 Token 各自的候选掩码,颜色为 sigmoid 之后的概率,画面裁剪到框附近;绿框标出最终选中的 Token 2。

    • 按 2-5-3 的切片规则,多掩码输出只从 Token 1 到 3 中挑选,Token 0 留给单掩码;图中把四个都画出来,便于比较。
    • 四个候选二值化后两两 IoU 为 0.997 到 0.999,只有 4 个像素的符号不同,说明在框已经很明确的目标上,四个 Token 的分歧很小。
    • IoU 预测头的原始输出为 0.982、1.002、1.002、0.994,排序几乎打平。
    • 该分支直接回归掩码质量,输出没有经过 sigmoid,所以略大于 1 属于正常现象;端到端演示里报告的掩码分数 1.002 就是其中一个原始输出,选掩码时直接比较这些分数,不再做变换。
  • 第二排左一是上采样特征图 U U U 的主成分投影,通道数为 32,空间尺寸为 256 × 256 256 \times 256 256×256;杯子的位置在投影中与桌面、背景明显分开。

  • 第二排左二是四个 Token 生成的动态权重,形状为 4 × 32 4 \times 32 4×32:

    • 四行权重两两相关系数为 0.976 到 0.999,平均绝对差 15.6,约为平均绝对值 96.2 的 16%。超网络确实为每个 Token 生成了不同的权重,只是这些权重在 U U U 上的响应几乎一致。
    • 主成分投影前先做 2 × 2 2 \times 2 2×2 平均,否则转置卷积带来的棋盘格纹理会盖过目标本身。
  • 第二排右二是选中的掩码叠加到原图,最右侧是按大于 0 二值化的结果,面积为 12,567 像素,占全图的 1.82%,与端到端演示的统计一致。

  • 这张图也说明多掩码输出的意义:框里只有一个明确目标时,四个候选几乎重合,选哪个都不影响结果;遇到边界模糊或框内含多个物体时,不同 Token 才会给出不同的假设,再由 IoU 分数挑选。

说人话:四个 Token 像四个人各自磨一把刀去切同一张特征图,这次四刀切出来的形状几乎一样,打出来的分也只差千分之几;真正让四个候选分开的,是框里有歧义、一个框罩住两个物体这类情况。

2-6 完整流程
  • 官方实现把一次分割拆成两个阶段:set_image 完成图像侧的编码并缓存,predict 只处理提示与解码。
2-6-1 图像嵌入与提示准备
  • set_image 先把图像的长边缩放到 1024 像素,短边按同比例缩放,再在右下角补零到 1024 × 1024 1024 \times 1024 1024×1024: W ′ = r o u n d ( 1024 W max ⁡ ( H , W ) ) , H ′ = r o u n d ( 1024 H max ⁡ ( H , W ) ) W' = \mathrm{round}\left(\frac{1024 W}{\max(H, W)}\right), \quad H' = \mathrm{round}\left(\frac{1024 H}{\max(H, W)}\right) W′=round(max(H,W)1024W​),H′=round(max(H,W)1024H​)其中 :
    • W W W、 H H H :原图的宽与高
    • W ′ W' W′、 H ′ H' H′ :缩放后的宽与高。
  • 缩放后的图像按通道做归一化,再送入图像编码器,得到 256 × 64 × 64 256 \times 64 \times 64 256×64×64 的图像嵌入并缓存: E I = f image ( N o r m ( P a d ( R e s i z e ( I ) ) ) ) E_I = f_{\text{image}}\left(\mathrm{Norm}\left(\mathrm{Pad}\left(\mathrm{Resize}(I)\right)\right)\right) EI​=fimage​(Norm(Pad(Resize(I))))其中 :
    • I I I :输入图像
    • E I E_I EI​ :缓存下来的图像嵌入。
  • predict 接收的框提示是原图坐标系下的 xyxy 像素值,必须先做同样的缩放变换,才能与图像嵌入对齐: x ′ = W ′ W x , y ′ = H ′ H y x' = \frac{W'}{W} x, \quad y' = \frac{H'}{H} y x′=WW′​x,y′=HH′​y其中 :
    • x x x、 y y y :原图坐标
    • x ′ x' x′、 y ′ y' y′ :缩放后的坐标。
  • 点提示与框提示都经过随机傅里叶位置编码后拼接为稀疏提示,掩码提示则经过三层卷积得到稠密提示。
2-6-2 框提示分割与后处理
  • 提示编码器输出的稀疏特征与稠密特征送入掩码解码器,得到 256 × 256 256 \times 256 256×256 的低分辨率掩码: M low , q ^ = f mask ( E I , E P ) M_{\text{low}}, \hat{q} = f_{\text{mask}}\left(E_I, E_P\right) Mlow​,q^​=fmask​(EI​,EP​)其中 :
    • E I E_I EI​ :图像嵌入
    • E P E_P EP​ :提示特征
    • M low M_{\text{low}} Mlow​ :低分辨率掩码
    • q ^ \hat{q} q^​ :四个候选掩码的预测质量分数。
  • 后处理分三步:先把低分辨率掩码双线性插值到 1024 × 1024 1024 \times 1024 1024×1024,再裁掉补零区域恢复到缩放后的尺寸,最后插值回原图尺寸: M ~ = R e s i z e H × W ( C r o p H ′ × W ′ ( I n t e r p 1024 ( M low ) ) ) \tilde{M} = \mathrm{Resize}_{H \times W}\left(\mathrm{Crop}_{H' \times W'}\left(\mathrm{Interp}_{1024}\left(M_{\text{low}}\right)\right)\right) M~=ResizeH×W​(CropH′×W′​(Interp1024​(Mlow​)))其中 :
    • M low M_{\text{low}} Mlow​ :低分辨率掩码
    • M ~ \tilde{M} M~ :恢复到原图尺寸的掩码。
  • 与训练时的 mask 损失一致,二值化阈值取 0,而不是 0.5: M bin ( x , y ) = 1 [ M ~ ( x , y ) > 0 ] M_{\text{bin}}(x, y) = \mathbb{1}\left[\tilde{M}(x, y) > 0\right] Mbin​(x,y)=1[M~(x,y)>0]其中 :
    • M ~ ( x , y ) \tilde{M}(x,y) M~(x,y) :该像素的掩码 logit
    • 1 [ ⋅ ] \mathbb{1}[\cdot] 1[⋅] :指示函数,条件成立取 1。
  • 对应实现如下:
low_res_masks, iou_predictions = self.model.mask_decoder(
    image_embeddings=self.features,
    image_pe=self.model.prompt_encoder.get_dense_pe(),
    sparse_prompt_embeddings=sparse_embeddings,
    dense_prompt_embeddings=dense_embeddings,
    multimask_output=multimask_output,
)
masks = self.model.postprocess_masks(low_res_masks, self.input_size, self.original_size)
if not return_logits:
    masks = masks > self.model.mask_threshold
  • 由于图像嵌入被缓存,同一张图上更换框提示时只需要重新执行提示编码与解码,这就是前文所说的摊销式实时查询。

说人话:图像先缩放补零成 1024 见方编码一次,框坐标也要按同样比例缩放;解码器在 256 见方上出掩码,再插值、裁边、缩回原图,最后按大于 0 取二值。


3 实验

3-1 本地部署
3-1-1 运行环境与依赖
  • 环境:Python 3.10、PyTorch 2.3.1(CUDA 12.1)、8 GB 显存 GPU。
  • Grounding DINO 需编译安装自定义 CUDA 算子,SAM 可直接源码导入;编译时需注意 CUDA 版本与显卡架构兼容性。
3-1-2 显存管理与权重加载
  • Grounding DINO(Swin-T)与 SAM(ViT-H)的权重分别约为 662 MB 和 2.4 GB,采用顺序加载,检测完成后释放模型,再加载 SAM,峰值显存约 5.6 GiB。
  • Grounding DINO 检查点已包含完整的 BERT 权重,因此可复用检查点参数,无需额外下载 BERT 权重。
3-2 Grounding DINO + SAM 结合
3-2-1 检测框到分割提示的坐标传递
  • Grounding DINO 输出的框是相对图像宽高归一化的 cxcywh 格式,而 SAM 的框提示要求原图坐标系下的 xyxy 像素格式。
  • 转换分两步:先按图像宽高还原到像素坐标,再做格式转换: ( x 0 , y 0 , x 1 , y 1 ) = ( c x − w 2 ,   c y − h 2 ,   c x + w 2 ,   c y + h 2 ) \left(x_0, y_0, x_1, y_1\right) = \left(c_x - \frac{w}{2},\ c_y - \frac{h}{2},\ c_x + \frac{w}{2},\ c_y + \frac{h}{2}\right) (x0​,y0​,x1​,y1​)=(cx​−2w​, cy​−2h​, cx​+2w​, cy​+2h​)其中 :
    • ( c x , c y , w , h ) (c_x, c_y, w, h) (cx​,cy​,w,h) :归一化框
    • ( x 0 , y 0 ) (x_0, y_0) (x0​,y0​)、 ( x 1 , y 1 ) (x_1, y_1) (x1​,y1​) :像素坐标系下的左上角与右下角。
  • 还原后的坐标需要裁剪到图像范围内,因为归一化框在图像边界处可能略微越界。
  • 对应实现直接复用 torchvision 的格式转换与裁剪操作:
scaled = boxes * torch.tensor([width, height, width, height], dtype=boxes.dtype)
xyxy = box_convert(boxes=scaled, in_fmt="cxcywh", out_fmt="xyxy")
xyxy[:, 0::2] = xyxy[:, 0::2].clamp(0, width)
xyxy[:, 1::2] = xyxy[:, 1::2].clamp(0, height)
  • 注意缩放因子的顺序为宽、高、宽、高,与 cxcywh 的分量顺序一一对应;顺序写错会让框在横纵方向上被错误拉伸。
  • SAM 内部还会再做一次缩放,把原图坐标映射到 1024 见方的输入坐标系,因此传给 SAM 的必须是原图像素坐标,而不是已经归一化的值。
3-2-2 掩码后处理
  • SAM 输出三张候选掩码,按预测 IoU 取最大者作为最终掩码。
  • 原始掩码中可能出现与目标不相连的小连通域,例如桌面反光或背景中的相似色块。这些像素面积很小,但会污染后续的三维反投影。
  • 处理方式是连通域分析:统计各连通域面积,只保留面积不小于最大连通域 5% 的区域: Ω keep = ⋃ i { Ω i ∣ A ( Ω i ) ≥ ρ max ⁡ j A ( Ω j ) } , ρ = 0.05 \Omega_{\text{keep}} = \bigcup_{i} \left\{\Omega_i \mid A(\Omega_i) \ge \rho \max_j A(\Omega_j)\right\}, \quad \rho = 0.05 Ωkeep​=i⋃​{Ωi​∣A(Ωi​)≥ρjmax​A(Ωj​)},ρ=0.05其中 :
    • Ω i \Omega_i Ωi​ :第 i i i 个连通域
    • A ( ⋅ ) A(\cdot) A(⋅) :连通域面积
    • ρ \rho ρ :保留比例,取 0.05。
  • 该阈值的作用是保留杯子把手这类与主体分离但实际属于目标的部件,同时去掉孤立的小块。
  • 对应实现如下:
num, labels, stats, _ = cv2.connectedComponentsWithStats(
    mask.astype(np.uint8), connectivity=8
)
areas = stats[1:, cv2.CC_STAT_AREA]
largest = int(areas.max())
kept = np.zeros_like(mask, dtype=bool)
for label, area in enumerate(areas, start=1):
    if area >= largest * min_ratio:
        kept |= labels == label
  • 在本实验图像上,清理前后掩码面积均为 12567 像素,说明 SAM 的输出本身没有产生孤立碎块,该步骤在此场景下没有改变结果,但在反光更强的场景中仍然是必要的保险。
  • 二值掩码之外,还生成了半透明叠加图与透明背景抠图:前者用于检查边界是否贴合,后者用于检查掩码之外是否残留背景像素。
3-2-3 端到端推理流程
  • 整体流程分为检测与分割两段:检测段得到候选框与短语,分割段把选中框作为提示得到掩码。
    • 检测段内部依次执行:图像预处理与前向、按框阈值筛选、按文本阈值提取短语、NMS 去重、按分数选出目标框。
    • 由于本实验的提示只对应一个目标,流程最终只保留分数最高的框;若提示对应多个目标,则需要把分割段改成按框循环。
  • 推理结果同时保存为结构化字段,包括检测分数、框坐标、掩码面积与预测 IoU,便于后续与三维模块对接。
3-2-4 提示词设计
  • Grounding DINO 的文本提示直接影响检测结果,提示应尽量贴近图像中目标的直接描述。
  • “颜色 + 物体名”是最稳定的一类提示,例如 yellow cup 与 blue bottle,因为颜色与类别都是检测器在预训练阶段见过的强视觉线索。
  • 因此本实验直接使用 yellow cup,不引入任何空间词。

说人话:提示词就说"什么颜色、什么东西";检测框要换成像素坐标再交给 SAM。

3-3 效果展示
  • 本节给出上述流程在真实桌面场景上的运行结果。
3-3-1 实验内容
  • 实验的目标是验证“文本提示、开放词表检测、框提示分割”这条链路在真实场景下的可用性。
    • 选用一张包含黄色杯子、蓝色瓶子、植物、书法挂轴与贝壳的桌面照片,分辨率为 960 × 720 960 \times 720 960×720。
    • 提示词取 yellow cup,期望模型在没有任何类别表的情况下,把黄色杯子单独分割出来。
  • 评价关注三点:检测是否唯一命中目标、掩码是否贴合物体边界、掩码是否可以直接用于后续三维处理。
3-3-2 实验实现
  • 两个模型均使用官方预训练权重,不做任何微调,推理在单张 GPU 上完成。
    • 检测阶段设置框阈值 0.20、文本阈值 0.15。若用户把阈值调高导致没有检测结果,实现会自动回退到这两个默认值重试一次。
    • 分割阶段开启多掩码输出,以预测 IoU 选出最优候选,再做一次连通域清理。
    • 结果分别保存为检测框图、二值掩码、半透明叠加图与透明背景抠图四种形式,便于从不同角度检查。
3-3-3 结果与分析
  • 检测阶段只输出 1 个框,短语为 yellow cup,分数为 0.893,说明提示与目标匹配明确,没有同类物体干扰。
  • 检测框结果如下:

请添加图片描述

  • 红色粗框为最终选中的检测,选中框的像素坐标为 [ 315.0 , 445.4 , 421.6 , 593.7 ] [315.0, 445.4, 421.6, 593.7] [315.0,445.4,421.6,593.7]。
  • 分割阶段选中的候选掩码预测 IoU 为 1.002。该分数由回归头直接预测,没有做上限裁剪,因此略大于 1 属于正常现象。
  • 掩码面积为 12567 像素,占整图的 1.82%,与画面中杯子的视觉占比一致。
  • 半透明叠加图如下:

请添加图片描述

  • 从叠加图看,掩码完整覆盖杯身与把手,边界落在杯沿上,没有明显溢到桌面。
  • 二值掩码如下:

请添加图片描述

  • 二值掩码中把手与杯身保持连通,形状连续,没有出现断裂或空洞。
  • 白色背景抠图如下:

请添加图片描述

  • 抠图中背景干净,没有残留桌面纹理或相邻物体的像素,说明框提示已经把目标范围限定得足够紧。
  • 结果汇总如下表:
项目数值
输入图像尺寸960 × 720
文本提示yellow cup
检测数量1
检测分数0.893
选中框坐标315.0 / 445.4 / 421.6 / 593.7
掩码面积12567 像素
掩码面积占比1.82%
预测 IoU1.002
峰值显存5.60 GiB
  • 实验表明,在该场景下检测与分割两级串联可以只靠一条文本提示得到像素级目标掩码,不需要为每个类别训练检测器或分割器。
  • 这一结论的前提是场景中目标的外观与提示词一致。若提示词与目标外观不符,检测阶段就会失败,分割阶段无法补救。
  • 当前流程只处理单个目标:提示命中多个同类物体时,最终只保留分数最高的一个,多目标场景需要在外层增加循环。

说人话:一句 “yellow cup” 就框出了杯子,SAM 再把框里的杯子和把手抠得干干净净,面积和目测基本一致。

3-4 后续扩展
  • 本节的流程只到二维掩码为止,要接入机械臂还需要补上从像素到三维、从三维到抓取的两步。
3-4-1 从掩码到目标点云
  • 对 RGB-D 相机,把掩码内像素与深度图对齐,即可得到目标点云: P = { ( X ( u , v ) , Y ( u , v ) , Z ( u , v ) ) ∣ ( u , v ) ∈ M bin } \mathcal{P} = \left\{ \left(X(u,v), Y(u,v), Z(u,v)\right) \mid (u,v) \in M_{\text{bin}} \right\} P={(X(u,v),Y(u,v),Z(u,v))∣(u,v)∈Mbin​}其中 :
    • P \mathcal{P} P :目标点云
    • ( u , v ) (u,v) (u,v) :像素坐标
    • M bin M_{\text{bin}} Mbin​ :二值掩码
  • Z ( u , v ) Z(u,v) Z(u,v) :该像素的深度值; X ( u , v ) X(u,v) X(u,v)、 Y ( u , v ) Y(u,v) Y(u,v) 由相机内参反投影得到: X = ( u − c x ) Z f x , Y = ( v − c y ) Z f y X = \frac{(u - c_x) Z}{f_x}, \quad Y = \frac{(v - c_y) Z}{f_y} X=fx​(u−cx​)Z​,Y=fy​(v−cy​)Z​其中 :
    • f x f_x fx​、 f y f_y fy​ :相机在 x x x、 y y y 方向的焦距
    • ( c x , c y ) (c_x, c_y) (cx​,cy​) :主点坐标
    • Z Z Z :该像素的深度值。
  • 掩码在这里的作用是滤除背景点云,只保留目标表面,从而大幅降低后续配准与位姿估计的输入规模。
  • 由于掩码边界处的深度常常缺失或有噪声,反投影后通常还需要一次离群点滤波与体素下采样。
3-4-2 从点云到抓取位姿
  • 得到目标点云后,可以送入抓取检测网络估计六自由度抓取位姿。
    • 抓取位姿通常记为 ( R , t ) \left(\mathbf{R}, \mathbf{t}\right) (R,t),其中 R \mathbf{R} R 为夹爪姿态, t \mathbf{t} t 为夹爪中心位置。
    • 相比对整幅场景直接做抓取检测,先分割再检测的好处是候选抓取只落在目标表面,避免抓住背景物体或桌面的情况。
  • 具体可以参考对接GraspNet:【玩转VLA具身智能机械臂】(五):GraspNet 6-DoF 抓取——从网络提议到筛选执行

总结

  • 本文围绕“自然语言驱动目标分割”这一任务,分析了 Grounding DINO 与 SAM 的结构与计算过程,并把两者串成一条可运行的无训练流水线。
  • Grounding DINO 用双编码器加跨模态解码器实现开放词表检测,语言引导查询选择把文本与图像的匹配分数转换为目标查询的初始化。
  • SAM 用重型图像编码器加轻量提示解码器实现提示驱动的分割,图像嵌入只计算一次,因而可以在同一张图上反复查询。
  • 两者串联后,文本提示先被转成边界框,再被转成框提示,最终得到像素级掩码,全程不需要训练。
  • 如有错误,欢迎指出!感谢观看!
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐