【小白深度教程 1.1】手把手教你双目深度估计(立体匹配),原理讲解和代码实战,使用 OpenCV(包含 Python、C++ 代码)

1. 引言

不知道你有没有曾经想过机器人是如何自主导航,抓取不同的物体,或者在移动时避免碰撞的?

其中,使用基于立体视觉的深度估计,是用于此类应用的常用方法。

在这篇文章中,我们将讨论 立体匹配双目深度估计 的经典方法,并分享 Python 和 C++ 的代码来进行代码实战。

对于诸如在移动中抓取物体和避免碰撞等任务,机器人需要在3D空间中感知世界。下面是一个避障机器人的演示,它使用基于立体视觉的双目深度估计来感知三维世界:

在这里插入图片描述

2. 前置知识

假设我们已经了解了估计给定场景深度(3D 结构)的两个基本要求:点对应和相机的相对位置。

快速回顾一下,对应点是不同图像的像素,它们是同一 3D 点的投影。在一个立体图像对的两个图像中,找到所有捕获的三维点的点对应关系,得到密集对应关系,可以用来找到密集的深度图和感知三维世界。

我们还可以观察到,密集对应点通过水平位移相关联,我们这种位移叫做视差。

在这篇文章中,我们将学习如何创建一个自定义的低成本立体相机,通过估计密集的视差图来计算深度,并使用 OpenCV 捕捉 3D 视频。

3. 什么是立体视觉

我们都看过 3D 电影和视频,其中需要红青色 3D 眼镜,如图所示,才能体验 3D 效果。

在这里插入图片描述

你有没有想过,为什么当你戴着特殊的3D眼镜看电影时,你能体验到美妙的3D效果?或者为什么闭着一只眼睛的时候,很难抓住物体(很难估计深度)?

这一切都与立体视觉有关,也就是我们用两只眼睛感知深度的能力。

在这里插入图片描述
上图展示了立体视觉使得计算机能够判断物体的距离。

3.1 我们需要多张图像来计算深度吗?

当我们在图像中捕获(投影)一个3D物体时,我们将它从 3D 空间投影到 2D(平面)投影空间。这被称为平面投影。问题是,由于这种平面投影,我们失去了深度信息

那么我们如何恢复深度呢?我们能用一张图像计算出场景的深度吗?我们来举一个简单的例子。

在这里插入图片描述

在上图中, C 1 C1 C1 X X X 是三维空间中的点,单位向量 L 1 L1 L1 给出了从 C 1 C1 C1 X X X 的射线的方向。

现在,如果我们知道点 C 1 C1 C1 和方向向量 L 1 L1 L1 的值,我们能找到 X X X 吗?

数学上,它的意思就是解出方程中的 X X X

在这里插入图片描述
很显然, k k k 的值是未知的,我们也不能确认一个特定的值。

在这里插入图片描述

在上图中,我们有一个额外的点 C 2 C2 C2 L 2 L2 L2 是从 C 2 C2 C2 X X X 的射线的方向向量,如果我们知道 C 2 C2 C2 L 2 L2 L2,我们能找到 X X X 的唯一值吗?

显然可以

因为从 C 1 C1 C1 C 2 C2 C2 发出的射线相交于一个唯一的点 X X X

这叫做三角测量。

我们对点 X X X 进行三角剖分。

在这里插入图片描述
上图显示了当在两个不同的视图(图像)中捕获(投影)时,如何使用三角测量来计算点( X X X)的深度。

图中 C 1 C1 C1 C 2 C2 C2 分别为已知的左右摄像头的三维位置。 x 1 x1 x1 为左侧摄像机拍摄的三维点 X X X 的图像, x 2 x2 x2 为右侧摄像机拍摄的三维点X的图像。

x 1 x1 x1 x 2 x2 x2 被称为对应点,因为它们是同一个三维点的投影。

我们用 x 1 x1 x1 C 1 C1 C1 L 1 L1 L1,然后用 x 2 x2 x2 C 2 C2 C2 L 2 L2 L2

因此,我们可以使用三角法来求 X X X 的具体位置。

从上面的例子中,我们了解到,要使用从不同角度捕获的两张图像对一个 3D 点进行三角测量,关键要求是:

  • 摄像机的位置:也就是 C 1 C1 C1 C 2 C2 C2
  • 对应点:也就是 x 1 x1 x1 x 2 x2 x2

那现在就很清楚了,我们需要不止一张图像来寻找深度。

但这只是我们试图计算的一个三维点。我们如何通过从两个不同的视角捕捉真实场景来计算它的3D结构?

显而易见的答案是对两个视图中捕获的所有 3D 点重复上述过程

3.2 双目几何的实践和理论理解

在这里插入图片描述
上图显示了从不同视点捕捉真实场景的两幅图像。为了计算三维结构,我们试图找到前面提到的两个关键要求:

  • 摄像机在现实世界坐标系中的 相机位置 C 1 C1 C1 C 2 C2 C2)。我们通过假设其中一个相机位置作为原点来计算 3D 点,从而简化了这个问题。我们可以通过使用已知的校准模式校准双目系统来找到它。这个过程被称为 立体校准
  • 计算场景中每个3D点( X X X)的 点对应 x 1 x1 x1 x 2 x2 x2)。我们将讨论计算点对应的各种改进。

注意,只有当图像是由一对相机严格固定进行拍摄时,立体相机校准才有用。

在这里插入图片描述
上图显示了手工标记的不同匹配点。我们很容易识别相应的点,但关键的问题是,我们如何让计算机做到这一点呢?

3.3 特征匹配来寻找点对应

在计算机视觉领域,人们经常使用的一种方法被称为 特征匹配(Feature Matching)

下面的图显示了使用 ORB 特着急描述符的左右图像之间匹配的特性。这是查找点对应(匹配)的一种常用方法。

在这里插入图片描述
然而,我们观察到,具有已知点对应的像素数,与总像素数相比还是太少。

这意味着我们将有一个非常稀疏重建的 3D 场景。对于密集重建,我们需要获得尽可能多的像素点对应。

在这里插入图片描述
寻找点对应的一种简化方法是寻找具有相似相邻像素信息的像素。

在图中,我们观察到,使用这种具有相似相邻信息的像素匹配方法,会导致一幅图像中的单个像素在另一幅图像中具有多个匹配。

那么有没有办法减少我们的搜索空间?我们可以用一些定理来消除所有额外的导致不准确对应的错误匹配?我们在这里使用 对极几何(epipolar geometry)

所有这些解释和积累都是为了引入对极几何的概念。

现在我们将了解对极几何在减少点对应的搜索空间中的重要性。

3.4 对极几何及其在点对应中的应用

在这里插入图片描述
在上图中,我们假设一个 3D 点 X X X 分别在相机 C 1 C1 C1 C 2 C2 C2 处被捕获为 x 1 x1 x1 x 2 x2 x2

由于 x 1 x1 x1 X X X 的投影,如果我们尝试从 C 1 C1 C1 延伸出一条通过 x 1 x1 x1 的射线 R 1 R1 R1,它也应通过 X X X。该射线 R 1 R1 R1 被捕获为 L 2 L2 L2,并且 X X X 在图像 i 2 i2 i2 中被捕获为 x 2 x2 x2

由于 X X X 位于 R 1 R1 R1 上,因此 x 2 x2 x2 应该位于 L 2 L2 L2 上。这样, x 2 x2 x2 的位置被约束为一条线,因此我们可以说图像 i 2 i2 i2 中与像素 x 1 x1 x1 对应的搜索空间被减少为单线 L 2 L2 L2

我们使用对极几何找到 L 2 L2 L2

现在让我们来定义一些技术术语。

除了 X X X,我们还可以将相机中心投影到相应的相反图像中。 e 2 e2 e2 是相机中心 C 1 C1 C1 在图像 i 2 i2 i2 中的投影,而 e 1 e1 e1 是相机中心 C 2 C2 C2 在图像 i 1 i1 i1 中的投影。

术语 e 1 e1 e1 e 2 e2 e2 是极点。因此,在两视图几何设置中,极点是一个视图中相机中心在另一个视图中的图像。

连接两个相机中心的线称为基线。因此,极点也可以定义为基线与图像平面的交点。

上图显示,使用 R 1 R1 R1 和基线,我们可以定义一个平面 P P P。该平面包含 X X X, C 1 C1 C1, x 1 x1 x1, x 2 x2 x2, 和 C 2 C2 C2。我们称这个平面为对极平面。

此外,从对极平面和图像平面的交点得到的线被称为对极线。例如, L 2 L2 L2 是一条对极线。对于不同的 X X X 值,我们会有不同的对极平面,从而产生不同的对极线。

然而,所有对极平面在基线相交,所有对极线在极点相交。这一切构成了对极几何。

现在回顾图重新回顾这张图:

在这里插入图片描述

现在,我们有一个由基线 B B B 和射线 R 1 R1 R1 创建的对极平面 P P P e 1 e1 e1 e 2 e2 e2 是极点, L 2 L2 L2 是对极线。基于给定图的对极几何,与像素 x 1 x1 x1 对应的图像 i 2 i2 i2 中像素的搜索空间被约束为一条二维线,即对极线 l 2 l2 l2。这被称为对极约束。

是否有一种方法可以用单个矩阵表示整个对极几何?而且,能否仅通过两个捕获的图像来计算这个矩阵?

好消息是,确实有这样的矩阵,它被称为基本矩阵。

在接下来的两个部分中,我们首先理解什么是射影几何和齐次表示,然后尝试推导基本矩阵的表达式。最后,我们使用基本矩阵计算对极线并表示对极约束。

3.5 理解射影几何和齐次表示

如何表示二维平面中的一条线?

二维平面中一条线的方程是 a x + b y + c = 0 ax + by + c = 0 ax+by+c=0。通过改变 a , b , c a, b, c a,b,c 的值,我们可以得到平面中的不同直线。因此,向量 ( a , b , c ) (a, b, c) (a,b,c) 可以用来表示一条线。

假设我们有直线 l n 1 : 2 x + 3 y + 7 = 0 ln1: 2x + 3y + 7 = 0 ln1:2x+3y+7=0 l n 2 : 4 x + 6 y + 14 = 0 ln2: 4x + 6y + 14 = 0 ln2:4x+6y+14=0。根据前面的讨论, l 1 l1 l1 可以用向量 ( 2 , 3 , 7 ) (2, 3, 7) (2,3,7) 表示, l 2 l2 l2 用向量 ( 4 , 6 , 14 ) (4, 6, 14) (4,6,14) 表示。

显然, l 1 l1 l1 l 2 l2 l2 实际上代表同一条线,因为 ( 4 , 6 , 14 ) (4, 6, 14) (4,6,14) ( 2 , 3 , 7 ) (2, 3, 7) (2,3,7) 的缩放版本,缩放因子为 2。

因此,任何两个向量 ( a , b , c ) (a, b, c) (a,b,c) k ( a , b , c ) k(a, b, c) k(a,b,c) k k k 为非零缩放常数)表示同一条线。

这些仅由缩放常数相关的等效向量构成一个同类的齐次向量类。向量 ( a , b , c ) (a, b, c) (a,b,c) 是其对应等价向量类的齐次表示。

所有等价类的集合,由 ( a , b , c ) (a, b, c) (a,b,c) 表示,对于所有可能的实数 a , b , c a, b, c a,b,c(除 a = b = c = 0 a=b=c=0 a=b=c=0 外),构成了射影空间。

我们使用齐次坐标的齐次表示来定义射影空间中的点、线、面等元素,并利用射影几何的规则对这些元素进行变换。

3.6 基本矩阵求导

我们重新回顾这张图:
在这里插入图片描述

再上图中,假设我们知道两个相机的投影矩阵,即位于 C 1 C1 C1 的相机的 P 1 P1 P1 和位于 C 2 C2 C2 的相机的 P 2 P2 P2

什么是投影矩阵?

相机的投影矩阵定义了三维世界坐标和相机捕获时的像素坐标之间的关系。关于相机投影矩阵的更多信息,可以参考有关相机校准的文章。

就像 P 1 P1 P1 将三维世界坐标投影到图像坐标一样,我们定义 P 1 i n v P1_{inv} P1inv,即 P 1 P1 P1 的伪逆,这样我们可以定义从 C 1 C1 C1 经过 x 1 x1 x1 X X X 的射线 R 1 R1 R1
在这里插入图片描述
其中, k k k 是一个缩放参数,因为我们不知道 X X X C 1 C1 C1 的实际距离。我们需要找到对极线 L n 2 Ln2 Ln2,以减少图像 i 2 i2 i2 中与 i 1 i1 i1 中像素 x 1 x1 x1 对应的像素的搜索空间,因为我们知道 L n 2 Ln2 Ln2 是射线 R 1 R1 R1 i 2 i2 i2 中的图像。因此,为了计算 L n 2 Ln2 Ln2,我们首先找到射线 R 1 R1 R1 上的两个点,用 P 2 P2 P2 将它们投影到图像 i 2 i2 i2 中,并使用这两个点的投影图像来确定 L n 2 Ln2 Ln2

我们可以考虑的第一个点是射线的起点 C 1 C1 C1。第二个点可以通过设定 k = 0 k=0 k=0 来计算。因此,我们得到的点为 C 1 C1 C1 ( P 1 i n v ) ( x 1 ) (P1_{inv})(x1) (P1inv)(x1)

使用投影矩阵 P 2 P2 P2,我们得到这些点在图像 i 2 i2 i2 中的图像坐标为 P 2 ⋅ C 1 P2 \cdot C1 P2C1 P 2 ⋅ P 1 i n v ⋅ x 1 P2 \cdot P1_{inv} \cdot x1 P2P1invx1。我们还注意到, P 2 ⋅ C 1 P2 \cdot C1 P2C1 实际上是图像 i 2 i2 i2 中的极点 e 2 e2 e2

在射影几何中,可以通过找到两个点 p 1 p1 p1 p 2 p2 p2 的叉积 p 1 × p 2 p1 \times p2 p1×p2 来定义一条线。因此:

L n 2 = P 2 ⋅ C 1 × P 2 ⋅ P 1 i n v ⋅ x 1 Ln2 = P2 \cdot C1 \times P2 \cdot P1_{inv} \cdot x1 Ln2=P2C1×P2P1invx1

由于 e 2 = P 2 ⋅ C 1 e2 = P2 \cdot C1 e2=P2C1,所以:

L n 2 = e 2 × P 2 ⋅ P 1 i n v ⋅ x 1 Ln2 = e2 \times P2 \cdot P1_{inv} \cdot x1 Ln2=e2×P2P1invx1

F = e 2 × P 2 ⋅ P 1 i n v    ( 基本矩阵 ) F = e2 \times P2 \cdot P1_{inv} \; (基本矩阵) F=e2×P2P1inv(基本矩阵)

∴ L n 2 = F ⋅ x 1 \therefore Ln2 = F \cdot x1 Ln2=Fx1

在射影几何中,如果一个点 x x x 位于一条线 L L L 上,可以写为:

x T × L = 0 x^T \times L = 0 xT×L=0

因此, x 2 x2 x2 位于对极线 L n 2 Ln2 Ln2 上,可以表示为:

x 2 T × L n 2 = 0 x2^T \times Ln2 = 0 x2T×Ln2=0

L n 2 Ln2 Ln2 的值代入上式,我们得到:

x 2 T × F × x 1 = 0 x2^T \times F \times x1 = 0 x2T×F×x1=0

这是两个点 x 1 x1 x1 x 2 x2 x2 为对应点的必要条件,也是对极约束的一种形式。因此, F F F 表示了整个两视图系统的对极几何。

那么,思考这条公式还有什么特别之处?它可以用来找到对极线!

3.7 用基本矩阵求极线

由于 x 1 x1 x1 x 2 x2 x2 是对应点,如果我们能找到一些点的对应关系,可以使用 ORB 或 SIFT 等特征匹配方法来求解上面的方程,从而计算出基本矩阵 F F F

OpenCV 的 findFundamentalMat() 方法提供了多种算法的实现,如 7-点算法、8-点算法、RANSAC 算法和 LMedS 算法,用于通过匹配的特征点计算基本矩阵。

一旦知道 F F F,我们可以用公式计算对极线 L n 2 Ln2 Ln2

L n 2 = F ⋅ x 1 Ln2 = F \cdot x1 Ln2=Fx1

如果我们知道 L n 2 Ln2 Ln2,我们可以利用对极约束限制像素 x 1 x1 x1 在图像 i 2 i2 i2 中对应的像素 x 2 x2 x2 的搜索范围。

4. 双目视觉的一种特殊情况——平行成像平面

我们一直在尝试解决对应问题。我们从特征匹配开始,但发现它导致了稀疏的 3D 结构,因为我们只知道一小部分像素的点对应关系。

随后,我们探讨了如何使用模板匹配来寻找像素对应关系,并了解了如何利用对极几何将点对应的搜索空间减少到一条对极线。

那么,我们能否进一步简化找到密集点对应的过程呢?

在这里插入图片描述
在这里插入图片描述

上面两张图展示了两组不同图像对的特征匹配结果和对极线约束。两图在特征匹配和对极线方面的最大区别是什么?

没错!第一张图中匹配的特征点具有相同的垂直坐标。所有对应点的垂直坐标相等。因此,所有对极线必须平行,并且与左图中的相应点具有相同的垂直坐标。

那么这有什么特别之处呢?

正是如此!与第一张图不同,第二张图 无需逐条计算对极线。如果左图中的像素坐标为 ( x 1 , y 1 ) (x1, y1) (x1,y1),则右图中相应的对极线方程为 y = y 1 y = y1 y=y1

我们在右图的同一行中搜索每个左图像素的对应像素。

这是一个特殊的双视图几何情况,其中成像平面是平行的,因此极点(一个相机在另一个相机中捕获的图像)位于无穷远处。基于对极几何的理解,对极线相交于极点,因此在这种情况下,由于极点在无穷远处,我们的对极线是平行的。

这极大简化了密集点对应的问题。然而,我们仍需对每个点执行三角测量。

我们能否简化这个问题呢?其实平行成像平面的特殊情况,使我们能够应用立体视差。

它类似于立体视觉或立体视力,这是帮助人类感知深度的方法。让我们详细了解这一点。

5. 进一步理解双目视差

下面的 gif 是使用 Middlebury 立体数据集中的图像生成的。它演示了相机的纯平移运动,使成像平面平行。

在这里插入图片描述
我们可以清楚地看到,底部的玩具牛比最上面一排的玩具更靠近摄像机。

我们如何使用它来避免计算深度的点三角化?

我们计算每个像素的视差(两个图像中像素的位移),并应用比例映射来找到给定视差值的深度。

视差公式为:

Disparity = x − x ′ = B f Z \text{Disparity} = x - x' = \frac{Bf}{Z} Disparity=xx=ZBf

其中, B B B 是基线(即两相机之间的距离), f f f 是焦距。

6. 代码实战

我们将使用 OpenCV 的 StereoSGBM 方法来编写计算给定图像对的视差图代码。

C++ 代码:

// Reading the left and right images.
 
cv::Mat imgL,imgR;
imgL = cv::imread("../im0.png"); // path to left image is "../im0.png"
imgR = cv::imread("../im1.png"); // path to left image is "../im1.png"
 
// Setting parameters for StereoSGBM algorithm
int minDisparity = 0;
int numDisparities = 64;
int blockSize = 8;
int disp12MaxDiff = 1;
int uniquenessRatio = 10;
int speckleWindowSize = 10;
int speckleRange = 8;
 
// Creating an object of StereoSGBM algorithm
cv::Ptr<cv::StereoSGBM> stereo = cv::StereoSGBM::create(minDisparity,numDisparities,blockSize,
disp12MaxDiff,uniquenessRatio,speckleWindowSize,speckleRange);
 
// Calculating disparith using the StereoSGBM algorithm
cv::Mat disp;
stereo->compute(imgL,imgR,disp);
 
// Normalizing the disparity map for better visualisation 
cv::normalize(disp, disp, 0, 255, cv::NORM_MINMAX, CV_8UC1);
 
// Displaying the disparity map
cv::imshow("disparity",disp);
cv::waitKey(0);

Python:

# Reading the left and right images.
 
imgL = cv2.imread("../im0.png",0)
imgR = cv2.imread("../im1.png",0)
 
# Setting parameters for StereoSGBM algorithm
minDisparity = 0;
numDisparities = 64;
blockSize = 8;
disp12MaxDiff = 1;
uniquenessRatio = 10;
speckleWindowSize = 10;
speckleRange = 8;
 
# Creating an object of StereoSGBM algorithm
stereo = cv2.StereoSGBM_create(minDisparity = minDisparity,
        numDisparities = numDisparities,
        blockSize = blockSize,
        disp12MaxDiff = disp12MaxDiff,
        uniquenessRatio = uniquenessRatio,
        speckleWindowSize = speckleWindowSize,
        speckleRange = speckleRange
    )
 
# Calculating disparith using the StereoSGBM algorithm
disp = stereo.compute(imgL, imgR).astype(np.float32)
disp = cv2.normalize(disp,0,255,cv2.NORM_MINMAX)
 
# Displaying the disparity map
cv2.imshow("disparity",disp)
cv2.waitKey(0)

在这里插入图片描述
在下一篇文章中,我们将学习如何创建自己的立体相机,同时学习如何将视差图转换为深度图。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小寒学姐学AI

有用的话可以请我喝一杯咖啡~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值