一.三角化

【1】三角化得到空间点的三维信息(深度值)

(1)三角化的提出

三角化最早由高斯提出,并应用于测量学中。简单来讲就是:在不同的位置观测同一个三维点P(x, y, z),已知在不同位置处观察到的三维点的二维投影点X1(x1, y1), X2(x2, y2)利用三角关系,恢复出三维点的深度信息z。

(2)三角化公式

按照对极几何中的定义,设x1, x2为两个特征点的归一化坐标,则它们满足:

s1x1 = s2Rx2 + t                                                                公式(1)

=> s1x1 - s2Rx= t                                                            公式(2)

对公式(2)左右两侧分别乘以x1T,得:

s1x1Tx1 - s2x1TRx= x1T t                                                  公式(3)

对公式(2)左右两侧分别乘以(Rx2)T,得:

s1(Rx2)Tx1 - s2(Rx2)TRx= (Rx2)T t                                     公式(4)

由公式(3)和公式(4)可以联立得到一个一元二次线性方程组,然后可以利用Cramer's法则(参见线性代数书)进行求解。

如下是对应的代码(如果大家感觉不易读懂,可以先跳过这段代码,等看完理论部分再返回来看不迟)

 1     // 方程
2 // d_ref * f_ref = d_cur * ( R_RC * f_cur ) + t_RC
3 // => [ f_ref^T f_ref, -f_ref^T f_cur ] [d_ref] = [f_ref^T t]
4 // [ f_cur^T f_ref, -f_cur^T f_cur ] [d_cur] = [f_cur^T t]
5 // 二阶方程用克莱默法则求解并解之
6 Vector3d t = T_R_C.translation();
7 Vector3d f2 = T_R_C.rotation_matrix() * f_curr;
8 Vector2d b = Vector2d ( t.dot ( f_ref ), t.dot ( f2 ) );
9 double A[4];
10 A[0] = f_ref.dot ( f_ref );
11 A[2] = f_ref.dot ( f2 );
12 A[1] = -A[2];
13 A[3] = - f2.dot ( f2 );
14 double d = A[0]*A[3]-A[1]*A[2];
15 Vector2d lambdavec =
16 Vector2d ( A[3] * b ( 0,0 ) - A[1] * b ( 1,0 ),
17 -A[2] * b ( 0,0 ) + A[0] * b ( 1,0 )) /d;
18 Vector3d xm = lambdavec ( 0,0 ) * f_ref;
19 Vector3d xn = t + lambdavec ( 1,0 ) * f2;
20 Vector3d d_esti = ( xm+xn ) / 2.0; // 三角化算得的深度向量
21 double depth_estimation = d_esti.norm(); // 深度值

(3)求解深度的另外两种方法

a.利用叉乘进行消元进行求解

s1x1 = s2Rx2 + t                                                                公式(1)

左右两边同时乘以x1的反对称矩阵,可得:

s1x1^x1 = 0 = s2x1^Rx2 + x1^t                                           公式(2)

由上式可解得s2,将s2代入公式(1),可求得s1

b.利用Mid Point Method进行求解(Hartley大名鼎鼎的《Multiple View Geometry》中有讲解)

从此图中我们可以知道,理想情况下O1P和O2P会相交于空间中的一点,但是由于图像分辨率以及噪声的存在,实际的情况更可能是上图所描述的那样:O1P和O2P在空间中没有交点,这时我们需要找到一个O1P与O2P之间的公垂线,然后取其上的中点作为我们重建出的三维点,此即为Mid Point Method,具体的推导及公式请参看Hartley的《Multiple View Geometry》。【需要将上面的图想象的立体一些】

附:

1.Cramer's 法则:

如果A的行列式不为0, Ax=b可以通过如下行列式进行求解:

矩阵BjA的第j列被b替换后得到的新的矩阵。

【2】三角化得到的三维信息中深度的误差

(1)三角化中误差分析

如上图所示:

P为空间中的一个三维点,p1和p2分别为在两个位置处,摄像机观察到的投影的二维点坐标。

l2为p1在第二幅图中所对应的极线(极线的概念请参考立体视觉中的对极几何,这里不再赘述)。

现在,我们要探讨的是:

如果我们在l2进行极线搜索时,所找到的p2'点与真实的p2点有一个像素的误差,那么会给三角化后的三维点P的深度z带来多大的误差?

首先,根据上图,我们可以得到向量之间的关系,以及三角化中的两个夹角的定义:

a = p - t                公式(1)

α = arccos<p, t>    公式(2)

β = arccos<a, -t>   公式(3)

其中,a, p, t均为向量,α和β为图中所示的两个夹角。

如果此时,我们求取的p2'点与p2点有一个像素的偏差,同时,这一个像素的偏差又会给β带来δβ的角度变化,我们利用β'来表示对β进行δβ扰动后的新的角度。

设相机的焦距为f,则:

公式(4):

公式(5):

公式(6):

至此,加入扰动后的所有新的角度我们都求出来了。

由正弦定理(a/sinA=b/sinB=c/sinC),我们可以得到:

公式(7):

则由第二个位置上的二维点的一个像素的误差,可能导致的三角化后深度的误差为:

δp = ||p|| - ||p'||

这里的δp其实也正是深度的一个均方差(不确定度σobs),这个不确定度是我们后面要介绍的深度滤波器的一个很重要的概念,深度滤波器的目的也正是要不断减小这个不确定度,使得深度的不确定度最后能够收敛到一个能够接受的值。

(2)三角化中误差的来源

上面分析了第二幅图中的特征点p2的误差是如何影响三角化后的深度值的。

下面,我们来指出三角化的误差来源有哪几方面:

a.图像的分辨率:图像的分辨率越高,一个像素所带来的δβ就越小。

b.特征点求取时的精度:是否做到亚像素,在亚像素的基础上,误差有多大?

c.p1点的误差:会引起极线l2的误差,从而间接地影响p2点的精度。

d.相机两次位置的平移向量t的大小:t的模的大小也代表了对极几何中的基线长度,由公式(7)可以看出基线长度越大,三角化的误差越小。

所以,这也体现出来了三角化的矛盾:若想提高三角化的精度,其一提高特征点的提取精度,即提高图像的分辨率,但这会导致图像的增大,增加计算成本;其二,使平移量增大,但这会导致图像外观的明显变化,外观变化会使得特征提取与匹配变得困难。总而言之,平移太大,会导致匹配失效;平移太小,三角化精度不够。

(3)如何减小三角化所带来的误差

根据【(2)三角化中误差的来源分析】中所分析的一些因素可知,要想减小三角化过程中引入的误差,可以有如下几个方法:

a.选取尽可能高分辨率的相机。

b.进行亚像素的优化(比如在极线搜索时对像素点坐标进行双线性插值)

// 双线性灰度插值
inline double getBilinearInterpolatedValue( const Mat& img, const Vector2d& pt )
{
uchar* d = & img.data[ int(pt(1,0))*img.step+int(pt(0,0)) ];
double xx = pt(0,0) - floor(pt(0,0));
double yy = pt(1,0) - floor(pt(1,0));
return (( 1-xx ) * ( 1-yy ) * double(d[0]) +
xx* ( 1-yy ) * double(d[1]) +
( 1-xx ) *yy* double(d[img.step]) +
xx*yy*double(d[img.step+1]))/255.0;
}

(关于双线性插值,这篇文章做了比较清晰的讲解:http://blog.163.com/guohuanhuan_cool@126/blog/static/167614238201161525538402/)

c.同样使用亚像素级的图像处理算法来处理p1点。

d.在不丢失特征点的情况下,让平移量t尽量大

由上面的公式推导我们可以看出,三角化中,必须要有平移量t,否则无法构成三角形,进行三角化。所以在有些单目的SLAM,AR/VR的场景中,有经验的人都会有意识地将设备或者相机进行一定量的平移,而不会在原地进行纯旋转。

 

二.单目稠密重建

【1】立体视觉

稠密重建中,我们需要知道每个像素点(或大部分像素点)的距离,对此大致有一下解决方案:

1.使用单目相机,通过移动相机之后进行三角化测量像素的距离。

2.使用双目相机,利用左右目的视差计算像素的距离。

3.使用RGB-D相机直接获得像素距离。

前两种称为立体视觉,相比于RGB-D直接测量的深度,单目和双目对深度的获取往往费力不讨好,但是RGB-D有应用范围和光照的限制,目前RGB-D还无法很好的应用于室外、大场景场合中,仍需通过立体视觉估计深度信息。

下面来介绍单目的稠密估计:

从最简单的说起,我们给定的相机轨迹的基础上,如何来估计某幅图像的深度?

(回顾:对于特征点部分我们完成此过程的描述:我们对图像提取特征,根据描述子计算了特征之间的匹配,即通过特征对某一空间点进行跟踪,知道它在图像的各个位置;由于一幅图像无法确定特征点的空间位置,所以需要不同的视角下的观测估计它的深度,即三角测量。)

而在稠密深度图估计中,我们无法把每个像素都当作特征点计算描述子,所以匹配就显得尤为重要,这里用到了极线搜索块匹配技术。但我们知道了某个像素在各个图中的位置,我们就能像特征点那样,用三角测量确定它们的深度。这里不同的是,我们需要很多次三角测量让深度估计收敛到一个稳定值,这就是深度滤波器技术。

【2】极限搜索与块匹配

左边的相机观测到了某个像素,由于是个单目相机,我们无法知道其深度,所以假设深度可能在某个区域内,不妨说是某个最小值到无穷,即该像素对应的空间点对应在本图中的射线d。在右图中,这条线段的投影也形成图像平面上的一条线,也就是我们称的极线。问题:极线上的哪一点才是我们对应的点呢?

在特征点中,我们通过特征匹配找到了的位置,然而现在我们没有描述子,所以在极线上搜索与长的相似的点,我们可能沿着右图中的极线从一头走到另一头,逐个比较每个像素与的相似程度。从直接比较像素的角度来看,和直接法是异曲同工的。但在直接法中,我们发现比较单个像素点的亮度并不是稳定可靠的,万一极线上有很多与相似的点,我们如何确定哪一个是真实的呢?所以,既然单个像素的亮度没有区分性,我们来比较像素块,在周围取一个w*w的像小块,然后在极线上也取很多同样大小的小块进行比较,在一定程度上提高区分性,这就是块匹配

我们把周围的小块记成 ,在极线上的n个小块 。小块与小块之间的差异,用NNC(归一化互相关)来计算,计算A与每一个的相关性:

相关性0,表示图像不相似;相关性1,表示图像相似;

我们将得到一个沿极线的NCC分布,这个分布的形状取决于图像本身的样子,如下图所示。

图13-3   匹配得分沿距离的分布

在搜索距离较长的情况下,我们通常会得到一个非凸函数:这个分布存在着很多峰值,然而真实的对应点必定一个只有。在这种情况下,我们倾向于使用概率分布描述深度值,而非用某一单一的数值描述深度。我们的问题转为了在不断对不同的图像进行极限搜索时,我们估计的深度分布会有怎样的变化---这就是所谓的深度滤波器

【3】深度滤波器的原理及实现

介绍的是比较简单的高斯分布假设下的深度滤波器。

高斯分布是自然界中最常见的一种分布形式,并且也符合绝大部分的自然情况。简单起见,我们先假设三角化后恢复的深度值符合高斯分布

对于像素点的深度值d,满足:P(d) = N(μ,σ2)

每当新的数据过来,我们就要利用新的观测数据更新原有的深度d的分布。

这里的数据融合的方式与经典的Kalman滤波方式大同小异。这里,我们利用观测方程进行信息融合。

假设新计算出来的深度数据的分布为:P(dobs) = N(μobs,σobs2)

我们将新计算出来的深度数据乘在原来的分布上,进行信息融合,得到融合后的高斯分布:P(dfuse) = N(μfuse, σfuse2)

(两个高斯分布的乘积还是高斯分布)。其中,

这里的μobs,σobs2该如何才能得到呢?

这里的μobs实际上就是每次我们新三角化出来的深度值,而对于σobs2,就是上面提到的 δp不确定度σobs)。

那么原始的分布μ,σ2该如何得到呢?

这个很简单,第一次三角化出来的μ,σ2就可以作为初始值,然后每次新三角化出一个三维点,就去更新深度值的分布。

至此,我们似乎得到了一个不错的结果:既简单又优美的公式。

实际上还会存在什么问题呢?

(1)实际的深度值分布是否真的符合高斯分布?

(2)如果我们中间过程有一次三角化的过程求错了,并且还进行了信息融合,会有什么后果?

(3)我们如何避免第二个问题中所提出的情况?

流程图

主函数:                                                                              update:

                                 

 
 1 bool update(const Mat& ref, const Mat& curr, const SE3& T_C_R, Mat& depth, Mat& depth_cov )
2 {
3 #pragma omp parallel for
4 for ( int x=boarder; x<width-boarder; x++ )
5 #pragma omp parallel for
6 for ( int y=boarder; y<height-boarder; y++ )
7 {
8 // 遍历每个像素
9 if ( depth_cov.ptr<double>(y)[x] < min_cov || depth_cov.ptr<double>(y)[x] > max_cov ) // 深度已收敛或发散
10 continue;
11 // 在极线上搜索 (x,y) 的匹配
12 Vector2d pt_curr;
13 bool ret = epipolarSearch (
14 ref,
15 curr,
16 T_C_R,
17 Vector2d(x,y),
18 depth.ptr<double>(y)[x],
19 sqrt(depth_cov.ptr<double>(y)[x]),
20 pt_curr
21 );
22
23 if ( ret == false ) // 匹配失败
24 continue;
25
26 // 取消该注释以显示匹配
27 // showEpipolarMatch( ref, curr, Vector2d(x,y), pt_curr );
28
29 // 匹配成功,更新深度图
30 updateDepthFilter( Vector2d(x,y), pt_curr, T_C_R, depth, depth_cov );
31 }
32 }
 

极线搜索:

 1 // 极线搜索
2 // 方法见书 13.2 13.3 两节
3 bool epipolarSearch(
4 const Mat& ref, const Mat& curr,
5 const SE3& T_C_R, const Vector2d& pt_ref,
6 const double& depth_mu, const double& depth_cov,
7 Vector2d& pt_curr )
8 {
9 Vector3d f_ref = px2cam( pt_ref );
10 f_ref.normalize();
11 Vector3d P_ref = f_ref*depth_mu; // 参考帧的 P 向量
12
13 Vector2d px_mean_curr = cam2px( T_C_R*P_ref ); // 按深度均值投影的像素
14 double d_min = depth_mu-3*depth_cov, d_max = depth_mu+3*depth_cov;
15 if ( d_min<0.1 ) d_min = 0.1;
16 Vector2d px_min_curr = cam2px( T_C_R*(f_ref*d_min) ); // 按最小深度投影的像素
17 Vector2d px_max_curr = cam2px( T_C_R*(f_ref*d_max) ); // 按最大深度投影的像素
18
19 Vector2d epipolar_line = px_max_curr - px_min_curr; // 极线(线段形式)
20 Vector2d epipolar_direction = epipolar_line; // 极线方向
21 epipolar_direction.normalize();
22 double half_length = 0.5*epipolar_line.norm(); // 极线线段的半长度
23 if ( half_length>100 ) half_length = 100; // 我们不希望搜索太多东西
24
25 // 取消此句注释以显示极线(线段)
26 // showEpipolarLine( ref, curr, pt_ref, px_min_curr, px_max_curr );
27
28 // 在极线上搜索,以深度均值点为中心,左右各取半长度
29 double best_ncc = -1.0;
30 Vector2d best_px_curr;
31 for ( double l=-half_length; l<=half_length; l+=0.7 ) // l+=sqrt(2)
32 {
33 Vector2d px_curr = px_mean_curr + l*epipolar_direction; // 待匹配点
34 if ( !inside(px_curr) )
35 continue;
36 // 计算待匹配点与参考帧的 NCC
37 double ncc = NCC( ref, curr, pt_ref, px_curr );
38 if ( ncc>best_ncc )
39 {
40 best_ncc = ncc;
41 best_px_curr = px_curr;
42 }
43 }
44 if ( best_ncc < 0.85f ) // 只相信 NCC 很高的匹配
45 return false;
46 pt_curr = best_px_curr;
47 return true;
48 } 

深度滤波器:

bool updateDepthFilter(
const Vector2d& pt_ref,
const Vector2d& pt_curr,
const SE3& T_C_R,
Mat& depth,
Mat& depth_cov
)
{
// 用三角化计算深度
SE3 T_R_C = T_C_R.inverse();
Vector3d f_ref = px2cam( pt_ref );
f_ref.normalize();
Vector3d f_curr = px2cam( pt_curr );
f_curr.normalize(); // 方程
// d_ref * f_ref = d_cur * ( R_RC * f_cur ) + t_RC
// => [ f_ref^T f_ref, -f_ref^T f_cur ] [d_ref] = [f_ref^T t]
// [ f_cur^T f_ref, -f_cur^T f_cur ] [d_cur] = [f_cur^T t]
// 二阶方程用克莱默法则求解并解之
Vector3d t = T_R_C.translation();
Vector3d f2 = T_R_C.rotation_matrix() * f_curr;
Vector2d b = Vector2d ( t.dot ( f_ref ), t.dot ( f2 ) );
double A[4];
A[0] = f_ref.dot ( f_ref );
A[2] = f_ref.dot ( f2 );
A[1] = -A[2];
A[3] = - f2.dot ( f2 );
double d = A[0]*A[3]-A[1]*A[2];
Vector2d lambdavec =
Vector2d ( A[3] * b ( 0,0 ) - A[1] * b ( 1,0 ),
-A[2] * b ( 0,0 ) + A[0] * b ( 1,0 )) /d;
Vector3d xm = lambdavec ( 0,0 ) * f_ref;
Vector3d xn = t + lambdavec ( 1,0 ) * f2;
Vector3d d_esti = ( xm+xn ) / 2.0; // 三角化算得的深度向量
double depth_estimation = d_esti.norm(); // 深度值 // 计算不确定性(以一个像素为误差)
Vector3d p = f_ref*depth_estimation;
Vector3d a = p - t;
double t_norm = t.norm();
double a_norm = a.norm();
double alpha = acos( f_ref.dot(t)/t_norm );
double beta = acos( -a.dot(t)/(a_norm*t_norm));
double beta_prime = beta + atan(1/fx);
double gamma = M_PI - alpha - beta_prime;
double p_prime = t_norm * sin(beta_prime) / sin(gamma);
double d_cov = p_prime - depth_estimation;
double d_cov2 = d_cov*d_cov; // 高斯融合
double mu = depth.ptr<double>( int(pt_ref(1,0)) )[ int(pt_ref(0,0)) ];
double sigma2 = depth_cov.ptr<double>( int(pt_ref(1,0)) )[ int(pt_ref(0,0)) ]; double mu_fuse = (d_cov2*mu+sigma2*depth_estimation) / ( sigma2+d_cov2);
double sigma_fuse2 = ( sigma2 * d_cov2 ) / ( sigma2 + d_cov2 ); depth.ptr<double>( int(pt_ref(1,0)) )[ int(pt_ref(0,0)) ] = mu_fuse;
depth_cov.ptr<double>( int(pt_ref(1,0)) )[ int(pt_ref(0,0)) ] = sigma_fuse2; return true;
}

三角化---深度滤波器---单目稠密重建(高翔slam---十三讲)的更多相关文章

  1. ORB-SLAM (四)tracking单目初始化

    单目初始化以及通过三角化恢复出地图点 单目的初始化有专门的初始化器,只有连续的两帧特征点均>100个才能够成功构建初始化器. ); 若成功获取满足特征点匹配条件的连续两帧,并行计算分解基础矩阵和 ...

  2. ORBSLAM2单目初始化过程

    ORBSLAM2单目初始化过程 转自博客:https://blog.csdn.net/zhubaohua_bupt/article/details/78560966 ORB单目模式的初始化过程可以分为 ...

  3. 单目、双目和RGB-D视觉SLAM初始化比较

    无论单目.双目还是RGB-D,首先是将从摄像头或者数据集中读入的图像封装成Frame类型对象: 首先都需要将彩色图像处理成灰度图像,继而将图片封装成帧. (1) 单目 mCurrentFrame = ...

  4. ORB-SLAM (四)Initializer单目初始化

    一. 通过对极约束并行计算F和H矩阵初始化 VO初始化目的是为了获得准确的帧间相对位姿,并通过三角化恢复出初始地图点.初始化方法要求适用于不同的场景(特别是平面场景),并且不要进行人为的干涉,例如选取 ...

  5. ORB-SLAM2 论文&代码学习 —— 单目初始化

    转载请注明出处,谢谢 原创作者:Mingrui 原创链接:https://www.cnblogs.com/MingruiYu/p/12358458.html 本文要点: ORB-SLAM2 单目初始化 ...

  6. Semantic Monocular SLAM for Highly Dynamic Environments面向高动态环境的语义单目SLAM

    一.摘要 当前单目SLAM系统能够实时稳定地在静态环境中运行,但是由于缺乏明显的动态异常处理能力,在动态场景变化与运动中往往会失败.作者为解决高度动态环境中的问题,提出一种语义单目SLAM架构,结合基 ...

  7. 基于OpenCV单目相机的快速标定--源码、工程、实现过程

    相机的标定是所有人走进视觉世界需要做的第一件事,辣么多的视觉标定原理解释你可以随便在网上找到,这里只讲到底如何去实现,也算是给刚入门的朋友做个简单的分享. 1.单目相机标定的工程源码 首先请到同性交友 ...

  8. ubuntu16.04下用笔记本摄像头和ROS编译运行ORB_SLAM2的单目AR例程

    要编译ORB_SLAM2的ROS例程首先需要安装ROS,以及在ROS下安装usb_cam驱动并调用,最后搭建ORB_SLAM2. 1.ROS的安装 我的电脑安装的是ubuntu16.04系统,所以我安 ...

  9. 多视几何——三角化求解3D空间点坐标

    VINS-Mono / VINS-Fusion中triangulatePoint()函数通过三角化求解空间点坐标,代码所体现的数学描述不是很直观,查找资料,发现参考文献[1]对这个问题进行详细解释,记 ...

随机推荐

  1. idea将项目打成war包

    idea将项目打成war包(转载) 2018年02月28日 20:08:03 沈行的专栏 阅读数:13773更多 个人分类: Java   首先点击这里进入项目的配置页面 在Artifacts栏里点击 ...

  2. week6 10 后端backend server和mongoDB通信

    0 之前我们maogoDB用的是在线的mlab 在线他们帮我们做好了model 也就是那个schma 其实python也有类似的包 帮我们定义这些model 但是呢 我们自己来做吧 用一个传统的意义上 ...

  3. Hibernate 再接触 核心开发接口

    1.可以重载方法进行配置文件的指定 sessionFactory = new AnnotationConfiguration().configure("hibernate.xml" ...

  4. [转载]FMS Dev Guide学习笔记(验证用户)

    一.开发交互式的媒体应用程序 1.使用外部资源验证用户 对于有限数量的客户,请求用户名密码,然后通过外部资源(像数据库.LDAP服务或其它访问授权服务)验证它们,是可行的.     a.SWF在请求连 ...

  5. Python unindent dese not match any out indentation level 问题

    今天写个小程序出现 “unindent dese not  match any out indentation level”. 一直没找到原因,经过仔细对比发现实际上是缩进的问题. 上下两行的缩进用的 ...

  6. zabbix 邮件报警 监控mysql主从

    1)设置邮件模板及邮件服务器 邮箱密码记得写授权密码 2)配置接受报警的邮箱 3)添加报警触发器 配置邮箱服务器 yum -y install mailx yum -y install sendmai ...

  7. 使用rtl8192du安装无线驱动步骤

    *************一.直接操作发********** 步骤:1.去Realtek官网下载无线网卡驱动下载地址:点击这里2.驱动在压缩包中的driver目录(也是一个压缩包),将其解压到/opt ...

  8. Django 基础教程中的Django表单

    在 urls.py 中对应写上这个函数,教程中给的Django 1.7x以下的,我的时2.0.7,应该为 from django.contrib import admin from django.ur ...

  9. beebase

    1.简单介绍 BeeBase是一个在线生物信息学数据库,显示与Apis mellifera.欧洲蜜蜂以及一些病原体和其他物种有关的数据.它是与蜜蜂基因组测序联盟合作开发的.BeeBase是蜜蜂研究社区 ...

  10. 05_ssm基础(一)之mybatis简单使用

    01.mybatis使用引导与准备 1.ssm框架 指: sping+springMVC+mybatis 2.学习mybatis前准备web标准项目结构 model中的Ticket代码如下: pack ...