双目相机立体匹配-stereo matching
预备知识
最近在学习 CV 相关的双目相机立体匹配 Stereo Matching,本文是关于 GwcNet 的一些学习和思考。开始之前,先需要理解几个概念:RGB 像素、特征向量、张量、视差、立体匹配、三维重建、代价体、视差求解的范式、卷积正则化。
RGB 像素
图像由像素组成,一张 1920x1080 分辨率的屏幕就代表有 1920x1080 个像素。RGB 代表光的三原色。R = Red,G = Green,B = Blue。这三种颜色通过不同的亮度组合可以形成各种颜色。计算机给每种颜色的亮度划分了 0 ~ 255 共 256 个等级。
可以计算出,三种原色,256 种亮度,可以组合出 $256^3≈1677万$ 中颜色。
每一个彩色图像的像素点都由红、绿、蓝三个发光的子像素(Sub-pixels,可以理解为三个微型 LED 灯)紧挨着组成的,可以看成:(R, G, B),比如 (255, 0, 0) 代表纯红。这是 原始像素。
特征向量
像素的高维向量。一个图像有 1024 个像素,理论上特征向量也有 1024 个,一一对应,相当于给每一个像素发一张“高维身份证”。但在工程上,使用 CNN 提取高维特征向量十分消耗显存,如果像素和特征向量 1:1 很容易爆显存。
所以要 降采样 : 使用“步长卷积(Stride Convolution)”或“池化(Pooling)”将图像 缩小(降采样)。比如在比如 GwcNet 中,特征图的宽和高都被缩小到了原来的 $1/4$。
假设原本像素为 $32×32$,降采样 1/4 后得到 $8*8=64$ 个特征向量。此时这 64 个特征向量里的每一个,代表的不再是单个像素,而是一小块区域(感受野)的特征汇总。
张量
向量-Vector,张量-tensor。数学上,向量就是一阶张量。但是,在物理几何体系和计算机工程(深度学习)体系下,它们代表了两种完全不同的“世界观”。
深度学习视角
在这个世界里(也就是 PyTorch / TensorFlow 的世界),张量纯粹就是一个 装载数字的收纳盒(多维数组)。可以把张量理解为高维数组。
- 核心概念: 张量是 数据结构,用来高效存取成千上万个数字。
- 什么是“维度”: 指的是 嵌套的层级(轴的数量 / Rank)。
- 1 维张量 (1D Tensor):一排格子(只有行)。比如
[3, 4],或者你的 320 维特征[0.5, 0.1, ..., 0.9]。 - 2 维张量 (2D Tensor):一个表格(有行、有列)。比如一张灰度图片。
[[1,2,3], [4,5,6], [7,8,9]]是一个形状(Shape)为(3, 3)的二维张量。 - 3 维张量 (3D Tensor):一个魔方(有行、列、深度)。比如彩色图片,或者你刚学的代价体。
- 1 维张量 (1D Tensor):一排格子(只有行)。比如
- 什么是“长度/大小”: 某个轴上有多少个格子(元素个数)。
- 关注点: 程序员和 CV 研究员关心的是,我的显存能不能装下这个多维数组?它的 Shape(形状)能不能和下一个网络层对齐(Broadcasting)?
| 视角 | 这个东西是什么? | “维度 (Dimension)” 的含义 | “长度 (Length/Size)” 的含义 |
|---|---|---|---|
| 几何/数学 | 这是一个存在于极高维抽象空间中的 320 维几何向量。 | 它需要 320 个坐标轴来定位空间位置。 | 根据多维勾股定理算出来的极其抽象的空间模长(距离)。 |
| 深度学习/代码 | 这是一个存放了 320 个特征属性的 1 维张量 (1D Tensor)。 | 它的套娃层级只有 1 层(它只有一条轴 / 一个方向)。 | 这个数组里装了 320 个具体的数字元素。 |
特征向量
为什么在深度学习里我们还要管它叫“特征向量”?
既然它是张量,为什么论文(包括这篇 GwcNet)里还是满篇都在写“Feature Vector(特征向量)”呢?
因为当我们对这个 1D 张量进行计算时,我们借用了数学世界的工具。 比如在 GwcNet 中,我们把左图的 8 个数字和右图的 8 个数字拿来做 内积(Dot Product),借此来衡量这两个特征在抽象空间里的“夹角”(相似度)。
视差
定义
有一个理想的双目相机系统,两个相机的成像平面是共面和行对齐的,左图中某个点,与对应右图中的匹配点在同一个水平线上。
如果空间中有一个三维点 $P$,它在左图的像素坐标是 $(u_L, v)$,在右图的对应坐标是 $(u_R, v)$ (注意纵坐标 $v$ 是一样的),那么这个点的视差 $d$ 就是它们在水平方向上的坐标差:
这就是视差。它是一个 标量,单位是 像素(Pixel)。
应用
视差可以用来求一个像素点的深度(Depth),就是像素点对应的物体实际离相机的距离。具体公式为:
Z 为深度,f 为相机焦距,B 为基线(左右两个相机光心之间的物理距离),d 为视差

学术
当前有一些立体匹配网络,基于代价体构建的 GwcNet、IGEV,基于循环迭代的 RAFT-Stereo。这些网络本质都是求视差 d,即为左图的一个像素点,在右图如何寻找最准确的对应点求出视差 $d$。
简而言之,视差是连接 2D 图像特征和 3D 几何物理世界的桥梁。
立体匹配 和 三维重建
双目相机匹配
核心任务是:在左右两张图像中,把显示中同一个物理点的像素点找出来。(寻找同名点)
这里举一个实例
使用双目相机拍摄一个苹果。苹果的果梗在左相机 $(u_L,v)$,在右相机的 $(u_R,v)$。使用人眼很容易根据经验找出两幅图像的相同像素点。但是计算机需要一种算法,当它拿到左图果梗的像素时,能去右图里 精确地“匹配”到 对应的那个像素。
事先要对相机做 极线校正,使双目向机成像水平,这样找一点在另一图像中像素点,只需要沿着水平方向滑动搜索。
立体匹配
Stereo Matching
核心任务是:寻找同名点,即像素的同一个物理点,根据视差计算深度。
三维重建
3D Reconstruction
核心任务是:在计算机世界复刻现实物体或场景的3D几何结构和外观。立体匹配是三维重建的前置重要任务。
graph LR
%% ==========================================
%% 定义样式
%% ==========================================
classDef dataNode fill:#f9f9f9,stroke:#333,stroke-width:2px;
classDef algoNode fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px;
classDef task1Node fill:#fff3e0,stroke:#ff9800,stroke-width:3px;
classDef task2Node fill:#e8f5e9,stroke:#4caf50,stroke-width:3px;
%% ==========================================
%% 节点定义
%% ==========================================
IMG[/"2D 图像对\n(Left & Right)"/]
MATCH("立体匹配算法\n(Stereo Matching)")
DISP[/"2.5D 深度/视差图\n(Depth Map)"/]
PROJ("空间反投影与融合\n(Projection & Fusion)")
PC[/"3D 点云\n(Point Cloud)"/]
RECON("表面重建与网格化\n(Surface Reconstruction)")
MODEL[/"纯正 3D 模型\n(3D Mesh / Model)"/]
%% ==========================================
%% 连线
%% ==========================================
IMG --> MATCH
MATCH -->|"输出"| DISP
DISP --> PROJ
PROJ -->|"生成"| PC
PC --> RECON
RECON -->|"输出"| MODEL
%% ==========================================
%% 框选区域 (使用安全双引号)
%% ==========================================
subgraph stereo ["属于立体匹配领域"]
MATCH
end
subgraph recon ["属于三维重建领域"]
PROJ
PC
RECON
MODEL
end
%% ==========================================
%% 统一分配样式
%% ==========================================
class IMG,DISP,PC,MODEL dataNode;
class PROJ algoNode;
class MATCH task1Node;
class RECON task2Node;
代价体 Cost Volume
介绍
一种为了求解视差定值的数据结构,他是三维张量。
在刚才的苹果匹配例子中,实际采用的是 穷举法打分:对于左图的一个像素点,在右图中把所有可能的视差偏移量(比如 0~192 像素)穷举一遍,每次都计算出左右两个 像素块 的相似度或者差异度(代价)。当这个代价最小,即两个像素块差异最小时,可以认为找到了匹配像素点。
构建代价体的传统方法:
- 传统方法:计算两个像素快的 绝对误差和 SAD
- 深度学习:提取左右图的高维特征矩阵,计算内积,相似度越高越好
把左图所有的像素 $(H \times W)$,针对所有可能的视差搜索范围 $(D)$,全部算出一个匹配得分后,把这些得分堆叠在一起,就形成了一个三维的张量(Tensor)—— 这就是代价体(Cost Volume)。
它的维度通常是:最大视差数 $(D) \times$ 图像高度 $(H) \times$ 图像宽度 $(W)$。

关于这个坐标 D ($D$ / Disparity),被称为通道维度或深度维度,它代表了所有的 候选视差值。
关于坐标 $(x, y, d)$ 的含义,每一个坐标存放一个数值(打分),这个数值代表了:左图的坐标 $(x, y)$ 这个像素,如果去和右图的坐标 $(x-d, y)$ 这个像素匹配,它们的“差异度(Cost)”或“相似度(Correlation)”有多大。
作用
有了这个存满打分值的三维张量,即可以求视差。
如果这个数值 $f(x_0, y_0, d_0)$ 是相似度,那么这个数值的最大值 $f_m(x_0, y_0, d_0)$,对应的 $(x_0, y_0, d_0)$ 点,这个 $d_0$ 即为像素 $(x_0, y_0)$ 实际视差。
为什么要把它做成一个完整的三维张量? 如果只看单个像素,很容易受噪声干扰(比如一块纯白色的墙,右图哪个位置看起来都很像)。把所有像素的匹配得分整合到一个宏大的 3D 张量中后,现代深度学习模型(比如 3D CNN 或 3D 堆叠沙漏网络)就可以在这个大张量里进行 空间维度的平滑和上下文信息聚合。它可以参考周围像素的信息,把原本不明显的极值变得更加突出和准确。
3D 卷积正则化
Gwc 这篇论文代表的是 3D 匹配的卷积正则化的范式。在立体匹配和三维重建的语境下,正则化=代价聚合=空间平滑。严格意义上,代价聚合是目的,卷积正则化是手段。
概述
在立体匹配的语境下,卷积正则化本质是为了 去噪 和 上下文投票。当我们构建好代价体后,他是一个堆评分
Gwc 这篇论文使用 3D 堆叠沙漏网络 作为 3D 卷积正则化的工具。
求视差的基本流程
一共分为五步,输入像素、提取特征向量、构建代价体、卷积正则化、视差回归。
graph TD
%% 定义样式
classDef inputNode fill:#f9f9f9,stroke:#333,stroke-width:2px;
classDef processNode fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px;
classDef tensorNode fill:#fff3e0,stroke:#ff9800,stroke-width:2px,stroke-dasharray: 5 5;
classDef outputNode fill:#e8f5e9,stroke:#4caf50,stroke-width:2px;
classDef highlightNode fill:#ffebee,stroke:#f44336,stroke-width:3px;
subgraph step1 ["Step 1: 输入 (Input)"]
IL[/"左图 (Left Image)\nTensor: (3, H, W)"/]
IR[/"右图 (Right Image)\nTensor: (3, H, W)"/]
end
subgraph step2 ["Step 2: 特征提取 (Feature Extraction)"]
CNN("共享权重 2D CNN\n(如 ResNet 提特征)")
FL[/"左图特征 (Left Features)\nTensor: (C, H, W)"/]
FR[/"右图特征 (Right Features)\nTensor: (C, H, W)"/]
IL --> CNN
IR --> CNN
CNN --> FL
CNN --> FR
end
subgraph step3 ["Step 3: 代价体构建 (Cost Volume Construction)"]
GWC{"匹配度计算\n(核心:分组相关)"}
CV1[/"初始代价体 (Initial Cost Volume)\nTensor: (Groups, D, H, W)"/]
FL -->|滑窗搜索| GWC
FR -->|同源比对| GWC
GWC -->|"打包匹配得分"| CV1
end
subgraph step4 ["Step 4: 代价聚合 (Cost Aggregation)"]
AGG("3D 卷积神经网络\n(如 3D Stacked Hourglass)")
CV2[/"精炼代价体 (Refined Cost Volume)\nTensor: (1, D, H, W)"/]
CV1 -->|"上下文过滤去噪"| AGG
AGG -->|"输出所有视差概率分布"| CV2
end
subgraph step5 ["Step 5: 视差回归 (Disparity Regression)"]
SARG("Soft-argmin 操作\n(概率加权求和)")
OUT[/"终极输出:视差图 (Disparity Map)\nTensor: (1, H, W)"/]
CV2 -->|"将概率转为连续数值"| SARG
SARG --> OUT
end
%% ==========================================
%% 统一分配样式 (兼容 Typora 10.9+ 的最安全写法)
%% ==========================================
class IL,IR inputNode;
class CNN,AGG,SARG processNode;
class FL,FR,CV1,CV2 tensorNode;
class OUT outputNode;
class GWC highlightNode;
关于这个流程
这个流程本质上是计算机在做信号搜索与解码。
Step1 RGB像素输入
系统接收到左右图,为许多 RGB 像素点。但不能直接使用 RGB 原始像素去匹配,因为左右图相同颜色的点可能不止一对,引发歧义。
Step2 特征提取
为解决歧义,使用 2D CNN 卷积神经网络,将原本单薄的 RGB 像素被编码成了一个高维的 特征向量(比如 64 维度),, 然后 Pooling 进行降采样,这个特征向量包含了一个像素点高级征信息(如是否处于边缘、材质),是他的“指纹”。
Step3 构建代价体
根据特征向量,在右图的同一行寻找左图像素的对应点,假设最大视差范围是 192 个像素。系统会把左图的特征,与右图平移 0 到 192 个位置的特征分别进行比对(GwcNet 采用了 分组相关,按特征子空间分别计算相似度)。
计算完毕构成了巨大的张亮 — 代价体。
像素提取特征向量,经历了降采样的步骤,这时特征向量数量小于 RGB 像素数量,这时是怎么根据特征向量来构建代价体的?能够根据它直接对比吗?
代价体是由“降采样”后的少量特征向量构成的
数学上比较高维向量的手段非常优雅
在 CV 领域,比较两个高维特征向量(比如左右图都是 320 维的向量)相似度,向量内积。如果两个向量相似度高,方向指向相似,那么内积值会很大。如果积出来是复数或者是 0,那么关系很微弱。
Step4 代价聚合
刚建好的代价体是非常嘈杂的,使用 3D 卷积神经网络 CNN 把代价体进行精炼,使其变成干净的概率分布体。
Step5 视差回归
针对左图的某一个像素,我们沿着代价体的视差维度看过去,会得到一条平滑的概率曲线。理论上直接找最高点,即视差。但实际上要构建视差图:不直接取最高点(Argmax),而是使用 Soft-argmin 操作。它会对这条概率曲线进行加权求和(算期望值)。这样不仅能输出连续的、平滑的视差图,还能让整个网络保持“可微”状态,从而支持端到端的反向传播训练。
GwcNet
这篇论文的核心贡献在 Step3,提取出图像特征后构建代价体。
GwcNet
Group-wise Correlation Stereo Network (GwcNet)
这篇论文属于双目立体匹配领域 Stereo Matching,是 2019CVPR 的会议论文。它提出了一种构建 代价体 的高效方法(GwcNet),方便求解视差。
背景 - 构建代价体的两难
极端一:全相关
极端二:直接拼接
折中解法:分组相关
核心贡献
混合代价体(Combined Volume)的互补设计
该方法将左右图提取的两组特征向量,沿着维度通道 channel dimension 划分成多个组。然后每一组的对应特征之间独立计算相关性。

该图左侧(蓝色部分)是特征提取部分,使用 CNN 提取 类似 ResNet 分别对左图 ($I_L$) 和右图 ($I_R$) 提取特征。
该图中间(橘色部分)为核心,分两路构建两个代价体,最后再合并 Combined Volume 。上面的分支 Concat Volume 把提取的特征压缩后,直接进行拼接;下路 Group-Corr Volume 为 分组相关代价体,下面这一路专门用来提供高精度的匹配相似度特征。
需要注意的是并非左图只用 直接拼接 , 右图只用 分组相关 ,而是把左图和右图的代价体分成两半,一半用直接拼接,一半用分组相关,分别构建两个代价体,再合并起来,这在论文中被称为 Combined Volume(混合代价体)。
这样做可以实现优势互补:
- 分支 A(直接拼接代价体):左右图各自压缩后直接拼接,他保留了绝对完整的上下文语义信息。能为后续的 3D 网络提供丰富的全局感知。
- 分支 B(分组相关代价体):左右图特征进行分组内积,计算精准的相似度得分,减轻 3D 网络推断相似度的负担。
该图右侧,合并出混合代价体(粉色部分),最后通过 Soft-argmin 输出黑白的视差图。
分组相关 Group Wise Correlation
那么分组相关的具体细节是怎么实现的呢?这个主要在论文 3.2 节 (Group-wise correlation volume) 中,有严格的数学定义。
graph TD
%% ==========================================
%% 定义全局美观样式 (兼容性极强版)
%% ==========================================
classDef inputNode fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px,color:#1a237e;
classDef splitNode fill:#fff9c4,stroke:#fbc02d,stroke-width:2px,color:#f57f17,stroke-dasharray: 4 4;
classDef mathNode fill:#ffebee,stroke:#d32f2f,stroke-width:3px,color:#b71c1c;
classDef loopNode fill:#e0f7fa,stroke:#0097a7,stroke-width:2px,color:#006064;
classDef outputNode fill:#e8f5e9,stroke:#388e3c,stroke-width:2px,color:#1b5e20;
%% ==========================================
%% 阶段 1:提取的单像素高维特征输入
%% ==========================================
subgraph Stage1 ["阶段 1: 提取单像素高维特征"]
P_L[/"左图像素的特征向量\n形状: 长度为 320 的 1D 张量"/]
P_R[/"右图候选像素的特征向量\n形状: 长度为 320 的 1D 张量"/]
end
%% ==========================================
%% 阶段 2:通道维度的切分 (核心操作)
%% ==========================================
subgraph Stage2 ["阶段 2: 通道分组拆解"]
SPLIT{"按通道均匀切分\n(总通道 320 / 组数 40)"}
G_L[/"左图 40 个小组特征\n每组包含 8 个维度的子特征"/]
G_R[/"右图 40 个小组特征\n每组包含 8 个维度的子特征"/]
end
%% ==========================================
%% 阶段 3:组内相似度计算 (数学原点)
%% ==========================================
subgraph Stage3 ["阶段 3: 组内数学计算 (Dot Product)"]
MATH("内积运算\n将 8 维向量对应元素相乘再相加")
SCORE[/"标量得分\n提取出 1 个维度的'组相似度'"/]
end
%% ==========================================
%% 阶段 4:遍历收集与视差穷举
%% ==========================================
subgraph Stage4 ["阶段 4: 遍历收集与空间滑动"]
LOOP_G("循环 1: 遍历 40 个通道组\n(生成 40 个独立的相似度打分)")
VEC_40[/"单点匹配度向量\n形状: 长度为 40 的 1D 张量"/]
LOOP_D("循环 2: 沿着视差层级 d 滑动\n(遍历所有候选匹配位置)")
MAT_D[/"该像素的所有视差匹配图\n形状: D/4 x 40 的 2D 张量"/]
end
%% ==========================================
%% 阶段 5:打包生成最终代价体
%% ==========================================
subgraph Stage5 ["阶段 5: 打包生成全局代价体"]
PACK("将全图所有像素的匹配图进行空间打包")
FINAL_CV[/"终极产出:分组相关代价体\nShape: (40, D/4, H/4, W/4)\n包含 40 种维度的全空间打分表"/]
end
%% ==========================================
%% 连接流程线
%% ==========================================
P_L --> SPLIT
P_R --> SPLIT
SPLIT -->|"切分左特征"| G_L
SPLIT -->|"切分右特征"| G_R
G_L -->|"取出第 g 组 (8维)"| MATH
G_R -->|"取出第 g 组 (8维)"| MATH
MATH -->|"坍缩降维"| SCORE
SCORE --> LOOP_G
LOOP_G -->|"聚合成一根向量"| VEC_40
VEC_40 --> LOOP_D
LOOP_D -->|"拼接所有候选视差"| MAT_D
MAT_D --> PACK
PACK --> FINAL_CV
%% ==========================================
%% 统一分配 CSS 样式
%% ==========================================
class P_L,P_R inputNode;
class SPLIT,G_L,G_R splitNode;
class MATH,SCORE mathNode;
class LOOP_G,VEC_40,LOOP_D,MAT_D loopNode;
class PACK,FINAL_CV outputNode;
可以用张量流转的思路理解:
改进的 3D 堆叠沙漏聚合网络

这是将上一图中 粉色方块(3D aggregation network) 的内部结构。主要作痛是代价聚合、空间正则化。