跳转至

Lecture 8 | Depth Estimation & 3D Reconstruction

约 5007 个字 6 行代码 预计阅读时间 25 分钟

注意

本文尚未完全整理好!


引入

上一章 中,我们已经介绍了如何重建得到关键点的点云,但是此时我们得到的只是一个稀疏的点云,而本章需要解决的问题就是如何“稠密”地进行重建。

而在稠密重建过程中,我们不能单单着眼于关键点,而是要广泛地面向图像中的所有像素,而其中最重要的一个技术就是深度估计(Depth Estimation)


「深度」这个概念和它的字面意思几乎一样,而在「深度估计」中,我们需要做的是对于给定的图像,标记每一个像素的深度。而将其可视化,就是深度图。

只不过有时候深度指的是空间点到相机中间或者相机平面的距离,也有的时候指的是某个空间坐标轴向的深度,具体应当根据实际情况来。


按照深度感知的手段分类,有主动深度探测方法(Active depth sensing)被动深度探测方法(Passive depth sensing)两种,其中我们将要展开的视觉方法都是被动探测方法。

主动探测方法 / 非视觉方法

  • LiDAR
  • Structured light
  • Active stereo

被动探测方法 / 视觉方法

  • Stereo
  • Multi-view stereo
  • Monocular

逻辑链:

(双目视觉 -> 深度感知) => ((双视角匹配点 + 视差 -> 深度) + (对所有像素进行匹配)) => 极线上寻找匹配点

立体视觉

我们已经有了稀疏重建的方法,想要得到稠密重建的方法,一个最暴力的方法就是尝试着去匹配所有像素,当然这看起来时间开销非常大。但是毋庸置疑的是,在目前还没有介入人工智能技术的情况下,我们仍然需要像素的匹配关系。

以及还有一个需要解决的问题就是,如何得到匹配点的深度信息。

所以总结下来,有两个问题需要解决:

  • 如何高效地得到匹配关系;
  • 如何根据匹配点计算点深度;

由于匹配中会有部分优化内容涉及到深度计算的内容,所以我想先从深度计算开始讲,也就是从视差开始讲。


视差

首先,我们都知道人的眼赋予了人类视觉上深度感知的能力,也就是立体视觉(stereo vision)。一个比较家喻户晓的实验就是单眼穿针会变得十分困难。

在单目视觉下,体现远近最简单的原理描述就是「近大远小」,而在动态的情况下,这也体现在移动相同的距离,近处的物体移动更明显,而远处的更不明显。然而这个信息和单纯的“大小”信息是可以混淆的,或者说在“大小”变化不明显的情况下,这个信息是很难被识别出来的。

而在双目视觉下,我们有有一个更近一步的原理,也就是视差(disparity),通过视差,我们可以更精确地感受到“距离”(实际上也是对极几何那一套),接下来阐述其原理。

首先,我们绘制一个简单的模型:

在这个模型中,我们假设两个图像中的匹配点符合之后会在立体匹配一节里提到的最简情况(simplest case),但是可以不用管这个最简情况是什么,直接当作这是一个俯视视角下的简化,直接看下面的模型即可。

这图我画了至少半小时 T^T(强迫症の怨念)

(1) 中 OL 和 OR 分别是两相机中心,淡蓝色线条表示成像平面,XL 和 XR 分别表示实际点 X 在成像平面上的投影,橙色线条表示他们的横向坐标(假设他们都在同一水平线上),z 表示实际点 X 到基线的距离,这里我们暂且称之为“深度”(反正不管怎么定义深度,有 z 都能算)。

(2) 中我画图的时候忘记区分两个 XR 了,所以关于 (2) 图中的 XR 一律指左边那个 XR。

我们将 OR 的成像模型平移到左侧,即图 (2),发现橙色的三角形和红色的三角形相似:\(\Delta O X_L X_R \cong \Delta X O_L O_R\) (注意,实际情况不一定等腰!)

于是我们得到,在这种情况下:

\[ \frac{\overline{X_L X_R}}{f} = \frac{\overline{O_L O_R}}{z} \\ \text{ i.e. }\; disparity = \Delta{\mathbf{x}} = |\mathbf{x}_l - \mathbf{x}_r| = |x_l - x_r| = \frac{B \cdot f}{z} \\ \text{where }B\text{ is the length of baseline} \\ \text{ i.e. }\; z = \frac{B \cdot f}{|x_l-x_r|} \]

提示

虽然是个简化情况,但是在最简情况(simplest case)下是可以直接挪用结论的,稍后我会提醒回顾这一节来理解这一点,所以安心顺着读下去即可。

至此,我们在本节开头提到的两个问题,已经解决一个了。

  • 如何高效地得到匹配关系;
  • 如何根据匹配点计算点深度;

立体匹配

首先我们做一个假设,也就是在匹配过程中,我们的两个 view 的距离相差不大,也就是说局部特征仍然是相似的。


搜索

立体匹配(Stereo Matching)的原理就是,选定第一个图像内的某一点,然后去另外一张图像对应的对极线上搜索找到能够和它匹配的点。没错,我们只需要在一条直线上搜索找到匹配的点即可,这大大优化了复杂度。而它的根据恰好就是对极线的定义。

👉 回顾对极几何

没错,我们现在已知了 \(X_L\)(由我们选定的),那么 \(X_R\) 的轨迹便是对极线,于是我们只需要先找到我们选定的 \(X_L\) 在另外一张图像上对应的对极线,然后再在这条线上搜索特征与 \(X_L\) 最匹配的那个点即可。(这里我们也没有必要使用 SIFT,直接做差就够了)

但是话又说回来,计算对极线这件事也并非十分简单,如果对于每个点我们都需要去计算对极线,那么将带来不小的常数,而且由于这件事没法“整体地预处理”,所以我们还需要优化这件事。

最简情况

首先考虑最简情况(simplest case)这个特殊情况:

  1. 两个相机的焦距相同;
  2. 相机位置在同一水平面上,即高度相同;
  3. 两个成像平面和基线,三者相互平行,或者说两个成像平面共面(此时对极点不存在);

此时,对极线也水平,而且两条对极线也共线(可以思考一下为什么)。

换句话来说,在这种情况下,当我们选定左图中的某个点 \(X_L\),那么 \(X_R\) 的候选位置就在右图中同一高度的那条水平线上。

那么如何解决这种情况以外的情况呢?既然我都这么排了,那解决办法自然就是转化为最简情况来做了。

转化思想

又是一个突然的想法,很多时候我们或许可以不从整体去思考,而是先解决一个最简情况,再转化问题。

一般情况

这个问题被称为立体影像矫正,这里并不展开,可以参考这篇文章:

C. Loop and Z. Zhang. Computing Rectifying Homographies for Stereo Vision. IEEE Conf. Computer Vision and Pattern Recognition, 1999.

回顾

相信现在你对“最简情况”已经有更深刻的理解了,不妨回过头去理解一下为什么我们之前提到的那个简化的视差模型可以直接挪用结论?

👉 视差


匹配

所谓的寻找匹配,就是在充斥着候选点的对极线上搜索匹配最好的一个点,或者说差异最小的一个点。

常见的差异衡量方法有这么几种:

SSD: Sum of Squared Differences

\[ diff = \sum_{x,y} |W_1(x,y) - W_2(x,2)|^2 \]

SAD: Sum of Absolute Differences

\[ diff = \sum_{x,y} |W_1(x,y) - W_2(x,y)| \]

ZNCC: Zero-mean Normalized Cross Correlation

\[ diff = \frac{(W_1(x,y)-\overline{W_1})(W_2(x,y)-\overline{W_2})}{\sigma_{W_1}\sigma_{W_2}} \\ \text{where } \overline{W_i} = \frac{1}{n}\sum_{x,y} W_i \;\;\; \sigma_{W_i} = \sqrt{\frac{1}{n}\sum_{x,y}(W_i-\overline{W_i})^2} \]

其中,ZNCC 相比其他方法,优越性在于它对图像整体的光强度不敏感,因为我们可以发现,\(W_i\) 在参与计算前都减去了 \(\overline{W_i}\),所以消除了图像整体亮度带来的影响。

个人疑惑

这里所谓的“消除亮度影响”,是否是对于 multi-view 的情况来说的?因为如果只有两张图片,且亮度差距整体恒定的话,虽然计算出来的 diff 会偏大,但是整体上的大小关系好像也没有变化?我没有想清楚这一点。


匹配的改进

然而,匹配过程中还有一个额外的问题需要解决。

窗口搜索的弊端

窗口搜索的效果实际上常常不尽如人意,有一种基于图割的做法,效果相对稍好。

这里所说的窗口,指的是在匹配搜索过程中,用来比较相似度的窗口大小。

👉 Boykov et al., Fast Approximate Energy Minimization via Graph Cuts, International Conference on Computer Vision 1999.


还有另外一个思路,那就是转化为优化问题,如果说原来的目标函数是两点的匹配程度,那么现在的目标函数可以是匹配程度和平滑程度的加权和:

\[ \begin{aligned} E(d) = E_d(d) + \lambda E_s(s) \\ \text{match cost: } & E_d(d) = \sum_{(x,y)\in I} C(x,y,d(x,y)) \;\;\; \text{e.g., SSD, SAD, ZNCC} \\ \text{smoothness cost: } & E_s(d) = \sum_{(p,q) \in \varepsilon} V(d_p, d_q) \;\;\; \text{where } \varepsilon \text{ is the set of neighboring pixels } \end{aligned} \]

其中,\(V(d_p,d_q)\) 一般有两种选择:

\[ \begin{aligned} \begin{aligned} \text{L_1 Distance: } \\ V(d_p, d_q) &= |d_p - d_q| \end{aligned} & & \begin{aligned} \text{Potts Model: } \\ V(d_p, d_q) &= \left\{ \begin{array}{*} 0 & \text{ if } d_p = d_q \\ 1 & \text{ if } d_p \not = d_q \end{array} \right. \end{aligned} \end{aligned} \]


特别的,如果涉及的是一个线性的扫描,也就是当 \(\varepsilon\) 中只含有左右两侧的点,那么这个问题可以用 DP 解决。而这种方式优化出来的图片效果是这样的:

然而,如果不满足线性扫描这个条件的话,这个问题就变成了一个 NP-hard 问题,不过可以用图割求近似解。


综合

至此,我们在本节开头提到的两个问题,已经全部解决了。

  • 如何高效地得到匹配关系;
  • 如何根据匹配点计算点深度;

于是双目重建的大致流程如下——首先我们对相机进行标定,获得相机的内参;接下来通过立体影像矫正技术,将两张图片转化为最简情况;然后再对每一个像素进行匹配,只需要在同高度的对极线上搜索匹配局部特征最相似的另外一个点即可;接着再根据匹配点的关系进行深度估计即可。

graph LR;
    A --> B --> C --> D
    A["Calibrate Cameras"]
    B["Rectify Images"]
    C["Compute Disparity"]
    D["Estimate Depth"]
引起误差的可能因素

课程内这个列表其实是给大作业的提示,个人认为也有助于梳理整个流程。

  • Camera calibration errors
  • Poor image resolution
  • Occlusions
  • Violations of brightness constancy (specular reflections)
  • Large motions
  • Textureless regions

下面详细说明几个因素对结果的影响。

窗口大小的选择对结果的影响

当我们使用窗口搜索时,窗口大小的选择仍然是个难题——窗口越小,往往结果的细节会更多,相对的噪声也更多,图片看起来就很乱;而窗口越大,往往噪声更少、更平滑,但是细节也更少,也就是说,这里有一个平滑度和精确度的 trade off。

W 表示窗口大小。

基线大小的选择对结果的影响

  • 如果基线选择过大,那么两张图片之间的内容差别就会较大,在搜索过程中就更难找到能够匹配的点,此外,横向的误差也会增大,理由可以参考下面这张图的原理;
  • 而如果基线选择过小,则点的深度误差就会很大,具体原因可以参考下面这张图——单一像素对应的候选点区域更深,意味着当前点可能是这一菱形里的任何点;

谨记我又因为画图浪费半个小时。

上面的内容结束后,其实还有一个遗留下来问题,读者可以先思考一下目前这个流程有什么问题,再打开下面的提示框。

一个尚未被解决的问题

让我们回到第五章的关键点匹配——为什么我们只找关键点?

我个人的想法是,首先,关键点能够一定程度上表达一个物体的特征,或者说它是勾勒一个物体非常重要的一个部分;还有一个原因是,关键点特征明显,其表达具有一定的独特性(也就是关键点的独特性(uniqueness))。

而现在我们做匹配的内容是任意的、广泛的,例如我们现在在做匹配的很可能是一面白墙上的某一个点(就是上面提到过的 textureless regions),这样的点又要如何找到它的匹配点呢?

答案就是,我们可以用结构光来解决这个问题。

结构光

我们再梳理一下问题的要点,就以白墙为例子,它难以进行匹配是因为它缺少特征、缺少纹理,而我们的目标是对其结构进行重建——那么一个想法就是我们手动给他「上纹理」,当然并不是指画上去,而是使用结构光:

于是我们的图像就有了纹理,拥有了“特征”。

个人想法

我当时看到这个 idea 的时候,感觉很刺激,但是又觉得这破坏了景象本来的纹理,但是转念一想,是不是会拍两张,一张用来上色,一张用来建模?我暂时还不知道,线放个猜想在这里。


不过此时又有一个引申出来的点,就是在这种情况下,其实可以减少一个相机。

这是因为,假设我们把 camera 2 放在与 projector 一样的位置,此时 camera 2 收到的图像应当和 projector 射出去的是一样的,或者说是类似的。

如何解释?

说实话我还是不太能理解这一点。如果要我强行理解的话,我觉得大概是又这么两点,第一点是被结构光照射的东西对结构光的反射以漫反射为主,而且所谓的“一样”也只是累死,并不能包括阴影关系等,所以对这个结论的正确与否我表示怀疑。


多视角

多视角立体(Multi-view Stereo, MVS)重建,顾名思义就是利用更多的视角来进行重建,相比于前面提到的双目模型,它有更强的约束、能在多个候选中选择最合适的方案,以及能得到更完整的 3D

我们在双目匹配中,首先寻找能够匹配的点对再做接下来的事情,但是在多视角中,找到「匹配的一系列点」是比较困难且耗时的,因为 n 个点有 \(C_n^2\) 对匹配关系。所以我们要变换思路,我们需要以某一个视角为基础,并枚举这个点的深度,计算重投影误差,找到使重投影误差最优秀的那个深度作为这个点的深度(而不是在每一个对极线上找,可以思考为什么)。

个人疑问

话说这个重投影误差指的是什么啊,是需要先算好匹配点然后 L2 距离的误差还是相似度的误差?如果是相似度的误差,那怎么解决形变?

  • multi-view stereo MVS
    • 具体做法是
      • 可以使用 Plane-Sweep
        • 对于每一个其他视角,构造一个关于 x-y-z 的三维表,能够查询对于参考图片[x][y]处的像素,如果处于深度[z]时的情况
        • 利用两次投影,先在“A”字里相似到远处,然后再斜着投影到每个视角里
        • 每一层层扫过去
          • 在上图数组里找每一个[x][y]里表现最好的[z]作为那个像素的深度。
        • 然而这个做法还是不太高效,一个更高效的做法,是 PatchMatch。
          • 随即初始化,总有几个是接近最优解的
          • 对于这些比较优的解,它附近的像素答案在这个答案的附近寻找(传播),然后再在小范围内进一步优化(搜索)

3D reconstruction

具体来说,3D 重建包括这么三个步骤:

  1. 深度计算
  2. 深度 -> 3D网格表面
  3. 纹理填充

3D representations

补充图片

  • 点云 point cloud
  • 体素(3d像素)occupancy
  • signed distance function (SDF)
    • 大小距离表面的距离,内部为负,外部为正
    • Truncated Signed Distance Function 截断SDF,即仍和大于 1 都记为 1,小于 -1 的记为 -1
  • 网格 mesh(比较常用)

3D surface reconstruction

  1. 深度 -> 体素表示
  2. 体素表示 -> 三维网格

深度 -> 体素表示

常用方法:

Depth Fusion (depth maps to TSDF)

【对于图中】:

  • 可以计算每一个体素到相机中心的距离 \(d_p\)
  • 根据小孔成像规则,可以得到与当前体素所对应的像素,记对应的像素的深度为 \(D_k(u)\)
  • 则该体素的 TSDF 值截断前为 \(D_k(u) - d_p\)
  • 在多图情况下,每一张图的结果做加权计算

思路

之前也有类似的内容。一个总结性的想法是,当你需要把「分辨率」低的东西投影到「分辨率」高到地方去时,应当枚举「分辨率」高的那一侧的单元,然后利用「分辨率」低的这一侧去计算,而不是从低到高。

Poisson reconstruction (depth maps to occupancy volume)

柏松重建

  • 深度图 -> 带法向量的点云
    • Oriented points 那里的箭头是表面法向量(深度的梯度)
  • 用优化的方式求解 indicator function (大概就是最终结果的那个物体的表面发向量函数)
    • 这里有一个点就是,体素的曲面是闭合的,所以会有内外之分,内部实心外部空心,所以 indicator function 就是想同时表达 表面 和 内外 两件事。

体素表示 -> 三维网格

常用方法:

Marching cubes

  • 以 2D 为例,如果一个「边」的两边,黑白不一致(黑表示外部,白表示内部),则说明这里存在一个边界,于是标一个点,然后将所有的这样的点按照特定顺序连接起来,就得到的了边界。
  • 具体标在边界的哪个位置,如果是 SDF 的话可以计算知道,否则可能只能在中间了
  • 但是需要注意一个格子如果有多个点的时候怎么连的问题,有一个表可以查,撑死也就 2^4=16 种情况

  • 在 3D 情况下,就是在立方体里连接三个点形成三角面,然后也有一个表可以查,撑死也就 2^8=256 种情况。

Texture mapping

展开 get 纹理图(二维坐标能够对应一个网格面) using 参数化


最后更新: 2024年1月13日 19:00:24
创建日期: 2024年1月13日 19:00:24

评论