双目视觉(三)立体匹配算法

1,986 阅读5分钟

视差与深度

视差

上篇文章中我们对双目相机的成像结果进行了校正,如下图所示,对于同名点对 (x0,y)(x1,y)(x_0, y)、(x_1, y),它们的纵坐标是相同的,它们的横坐标之差 d=x0x1d = x_0 - x_1 称为视差。

image.png

深度

如封面所示,将手指放在视野中间,交替闭上左右眼,观察手指相对于视野前方某个物体的位置变动,这就是视差,并且越远的物体视差越大,越近的物体视差越小。

对于左视图中的每个点,去右视图中寻找同名点,并计算视差,我们可以得到一个灰度图,称为视差图或深度图,如下图所示。

image.png

图中越亮的地方视差越小,也就是越近;越暗的地方视差越大,也就是越远。并且深度图中左侧会有一部分黑色,这表示左视图中这部分的点无法在右视图中找到同名点,因为左右相机拍摄的视野范围不同。

通过校正后的双目图像得到深度图的过程就叫做立体匹配,上面的深度图是通过 SGM 立体匹配算法得到的,下面展开来讲。

立体匹配

想获取视差图需要先找到同名点对。设 p=(x,y)p = (x, y) 为左视图中的像素点,qi=(xi,y)q_i = (x_i, y) 为右视图中纵坐标为 yy 的像素点。因为图像是校正过的,所以 pp 的同名点 qq 只需要遍历 qiq_i 去寻找,如下图所示。

image.png

代价空间

那么在遍历过程中,如何确定 qiq_ipp 是否是同名点呢?这里引入代价(Cost)的概念,pqp、q 的代价为 C(p,q)C(p, q),代价越小,pqp、q 为同名点的可能性越高。

C(p,q)=Il(p)Ir(qi)C(p, q) = |I_l(p) - I_r(q_i)|

代价函数 CC 的计算方式不唯一,这里函数 II 这里表示像素点对应的颜色值,是最简单的一种代价计算。

又因为 xi=xdx_i = x - ddd 表示视差,因此上面的等式还可以这样写。

C(x,y,d)=Il(x,y)Ir(xd,y)C(x, y, d) = |I_l(x, y) - I_r(x - d, y)|

这里不用太纠结是 xi=xdx_i = x - d 还是 xi=x+dx_i = x + d,这里以本文第一张图片为例,使用 xi=xdx_i = x - d 表示。

image.png

函数 C(x,y,d)C(x, y, d) 在空间坐标系中表示一个立方体,称该立方体为代价空间,如上图所示,每个单元存储一对像素点的代价。

对于左视图中的一个像素点 (x,y)(x, y)d[0,dmax]d ∈ [0, d_{max}],当 d=did = d_iC(x,y,d)C(x, y, d) 最小,则 (x,y)(x, y) 的同名点为 (xdi,y)(x - d_i, y)。如此确定同名点的方式称为 Winner Takes All (WTA)C(x,y,di)C(x, y, d_i) 为最优代价。

代价函数

代价计算函数有很多种,上面提过的 C(x,y,d)=Il(x,y)Ir(xd,y)C(x, y, d) = |I_l(x, y) - I_r(x - d, y)| 称为 Absolute Difference (AD),这里使用 CADC_{AD} 来表示。

CADC_{AD} 的基础上还有 CSADC_{SAD} (Sum of Absolute Difference),用指定的像素点及其周围的像素点组成一个正方形的窗口,相比于 CADC_{AD} 效果会更好一些。

CSAD(x,y,d)=i=22Il(x+i,y+i)Ir(x+id,y+i)C_{SAD}(x, y, d) = \sum_{i=-2}^2|I_l(x+i, y+i) - I_r(x+i-d, y+i)|

上面公式中 i=22\sum_{i=-2}^2 表示一个 5×55\times5 大小的窗口。

除了 CADCSADC_{AD}、C_{SAD} 还有其他种类的代价计算函数,不过比较复杂这里就不多说了,这里把代价计算函数当作一个黑盒就行。

代价聚合

到了这里我们通过代价计算函数得到了代价空间,有 WTA 我们可以获得所有的同名点对并计算得到深度图,但是这样子得到的深度图的效果还是不好的。

我们需要对得到的代价空间进行优化,这个优化的过程称为代价聚合,要解释代价聚合,首先明确视差平面的概念。

image.png

如上图所示,代价空间由若干个视差平面组成,代价聚合就是在视差平面上进行滤波操作,比较简单的一种是使用均值滤波,滤波可以理解为对视差平面的平滑处理。

image.png

如上图所示,遍历视差平面,对每个 (x,y)(x, y) 取周围 3×33\times3 窗口内均值为 (x,y)(x, y) 处新的代价,这个过程就是均值滤波。

OpenCV 中可以使用 blur 函数进行均值滤波,滤波前后结果如下图所示。

image.png

之所以要动手跑一下主要是好奇在遍历取均值的过程中,后面的会不会使用前面计算好的值,得出的结果是不会。

上面说的是局部优化的思路,相对应的是全局优化的思路,即为每个像素寻找到一个视差值,使得整体能量 EE 最小。

E=(C(p,dp)+P1T[dpdq=1]+P2T[dpdq>1])E = \sum(C(p, d_p) + \sum P_1T[|d_p - d_q| = 1] + \sum P_2T[|d_p - d_q| > 1])

能量 EE 由两部分组成:

  1. 所有像素点的代价之和。
  2. 对于每个像素点的领域点,如果视差连续则加上 P1P_1,如果视差不连续则加上 P2P_2P1<P2P_1 < P_2

image.png

对于上图中标注的窗口来说,局部优化视窗口内所有像素的视差(深度)相同,而全局优化则是将视差(深度)也考量在内。至于如何寻找每个像素的视差值使得 EE 最小则是一个困难的问题。

视差优化

通过上面说的一系列步骤,我们能得到若干个 (x,y,d)(x, y, d),将这些拼凑成图片,像素 (x,y)(x, y) 处的值为 dd,就是一张视差图。

这里获取的视差图还需要进一步优化,例如对视差图进行一次滤波操作等等。

End

最后总结一下立体匹配的流程:

  1. 代价计算
  2. 代价聚合
    • 局部法 + WTA
    • 局部法(可选) + 全局法
  3. 视差优化

立体匹配算法有很多种,但大体都是这个流程,只不过不同算法对于每一步都有不同的实现。