这部分主要代码在函数FullSystem::trackNewCoarse中。首先,DSO设置了一系列的候选位姿lastF_2_fh_tries,作为前一关键帧到当前帧的相对位姿的初值。这里主要参考前两帧和前一关键帧的位姿,就静止、恒定速度等猜想设了一些初值,另外主要针对旋转设置了许多微小的初始值。然后开始不断地尝试,从图像金字塔顶层开始就这些初值进行追踪CoarseTracker::trackNewestCoarse,如果找到一个合适的初值,就跳出循环。先来看一下追踪部分是如何实现的。函数CoarseTracker::trackNewestCoarse,传入参数有当前帧newFrameHessian,预测的相对位姿lastToNew_out,预测的相对光度aff_g2l_out(初始化为0),金字塔层数coarsestLvl,用来判断是否合适的误差minResForAbort,返回一个表明是否成功的bool值。该函数从输入的金字塔层级开始,由粗到精地计算最佳位姿。
2.1 误差计算
先是计算当前误差的大小CoarseTracker::calcRes。这一步里还没有改变位姿的大小,仅仅将host帧(参考帧,前一关键帧)的点, 通过反投影变换 以及投影变换 变换到Target帧(最新帧)的图像平面,然后将误差累计起来返回,并保存了后续计算雅克比矩阵需要的变量。这里和初始化时不同的是,追踪时已经有了一定的点,因此只考虑位姿加光度共8个参数。
将host帧上的点x1通过反投影,投影变换到target帧的图像平面上
X1x2=ρ1−1K−1x1=Kρ2(R21ρ1−1K−1x1+t21)=Kx2′
上式中的 x2′ 就是归一化平面坐标,可以写作x2′=[u2′,v2′,1]T
设误差函数
r21=(I2(p2)−b2)−(exp(a21)I1(p1)−b1)
对相对光度参数求导:
∂a21∂r21=−exp(a21)I1(p1)
∂b21∂r21=−1
对位姿增量求导
∂ξ21∂r21=∂x2∂r21∂ξ21∂x2
其中,∂x2∂r21=∂x2∂I2[x2]=[dxdy]
通过链式法则
∂ξ21∂x2=∂x2′∂x2∂ξ21∂x2′
∂x2′∂x2=⎣⎡fx000fy0000⎦⎤
x2′=ρ2(R21X1+t21)=ρ2X2,ρ2是点的深度
∂ξ21∂X2=⎣⎢⎡∂ξ21∂X2∂ξ21∂Y2∂ξ21∂Z2⎦⎥⎤=⎣⎡1000100010−Z2Y2Z20−X2−Y2X20⎦⎤∂ξ21∂x2′=⎣⎢⎡∂ξ21∂ρ2X2∂ξ21∂ρ2Y2∂ξ21∂ρ2Z2⎦⎥⎤+ρ2∂ξ21∂X2
最终可得:
∂δξ21∂r21=[dx,dy,0]⎣⎡fxρ2000fyρ20−fxρ2u2′−fyρ2v2′0−fxu2′v2′−fy(1+v2′2)0fx(1+u2′2)fyu2′v2′0−fxv2′fyu2′0⎦⎤
逆深度的雅可比
∂ρ1∂r21=∂x2∂r21∂ρ1∂x2=∂x2∂I2[x2]∂ρ1∂x2=[dxdy0][fxρ1−1ρ2(t21x−u2′t21z)fyρ1−1ρ2(t21y−v2′t21z)]=(dxfxρ1−1ρ2(t21x−u2′t21z)+dyfyρ1−1ρ2(t21y−v2′t21z))
2.2 计算增量方程
接下来在函数CoarseTracker::calcGSSSE中计算增量方程中的H和b。上一节已经求得了对应变量的导数,它们在代码中的表示为
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| _mm_mul_ps(id,dx), _mm_mul_ps(id,dy), _mm_sub_ps(zero, _mm_mul_ps(id,_mm_add_ps(_mm_mul_ps(u,dx), _mm_mul_ps(v,dy)))), _mm_sub_ps(zero, _mm_add_ps( _mm_mul_ps(_mm_mul_ps(u,v),dx), _mm_mul_ps(dy,_mm_add_ps(one, _mm_mul_ps(v,v))))), _mm_add_ps( _mm_mul_ps(_mm_mul_ps(u,v),dy), _mm_mul_ps(dx,_mm_add_ps(one, _mm_mul_ps(u,u)))), _mm_sub_ps(_mm_mul_ps(u,dy), _mm_mul_ps(v,dx)), _mm_mul_ps(a,_mm_sub_ps(b0, _mm_load_ps(buf_warped_refColor+i))), minusOne, _mm_load_ps(buf_warped_residual+i), _mm_load_ps(buf_warped_weight+i));
|
可以得到: H=JTWJ
$\mathbf{b}=-\mathbf{J}^{T} \mathbf{W} f(\mathbf{x}) $
初始的lambda设为0.01(即列文伯格方法中的拉格让日乘子λ\lambdaλ),然后求解增量方程
(H+λI)Δx=b
得到增量后对原来的状态变量进行更新x←x+Δx
2.3 迭代求解
每一层的最大迭代次数是固定的,且各不相同,高层的次数多,低层的次数少
初始的lambda设为0.01(即列文伯格方法中的拉格让日乘子λ\lambdaλ),然后求解增量方程
得到增量后对原来的状态变量进行更新
注意位姿的更新不是李代数相加,而是指数映射到李群后相乘
1 2 3 4
| SE3 refToNew_new = SE3::exp((Vec6)(incScaled.head<6>())) * refToNew_current; AffLight aff_g2l_new = aff_g2l_current; aff_g2l_new.a += incScaled[6]; aff_g2l_new.b += incScaled[7];
|
用新的状态变量重新计算误差,然后将新的误差和旧的误差做比较,考虑是否接受这次优化
bool accept = (resNew[0] / resNew[1]) < (resOld[0] / resOld[1]);
这里的resNew[0]是总的误差,resNew[1]是对应点的数量,当平均误差减小时,认为这次优化可以接受。如果可以接受,那么就缩小lambda(每次缩小为原来的二分之一),如果优化失败,那么就说明高斯牛顿法的二次函数近似效果在这里不太好,通过增大lambda(放大为原来的4倍)来改善。当某一次得到的增量小于一定值时,认为收敛了,迭代过程终止。
DSO通过比较前后两次误差的大小关系来判断,它要求每一次得到的结果至少要比上一次尝试(上一个候选位姿下的)好,否则就直接跳过;如果比上一次好,那么再看它是否小于设定的阈值,如果小的话就结束尝试。
3. 判断是否为关键帧
关键帧的选择主要考虑当前帧和前一关键帧(参考帧)在点的光流变化,不考虑旋转情况下的光流变化,曝光参数的变化,三者加权相加大于1时新建关键帧
3.1非关键帧
如果当前帧被认为是非关键帧,那么该帧就用来对活动窗口中所有的关键帧中还未成熟的点进行逆深度更新: traceNewCoarse(fh);(深度滤波)
基本原理是沿着极线进行搜索ImmaturePoint::traceOn
3.1.1 极线搜索
首先,将未成熟的点根据相对位姿和之前的逆深度投影到当前帧上
1 2 3 4
| Vec3f pr = hostToFrame_KRKi * Vec3f(u,v, 1); Vec3f ptpMin = pr + hostToFrame_Kt*idepth_min; float uMin = ptpMin[0] / ptpMin[2]; float vMin = ptpMin[1] / ptpMin[2];
|
这里的(uMin
,vMin
)就是设逆深度最小时投影得到的像素坐标。
接下来确定极线,如果逆深度无限大,随便设一个逆深度0.01,得到另一个投影点的坐标(uMax
,vMax
),
1 2 3 4
| // project to arbitrary depth to get direction. ptpMax = pr + hostToFrame_Kt*0.01; uMax = ptpMax[0] / ptpMax[2]; vMax = ptpMax[1] / ptpMax[2];
|
这样就得到了极线的方向
1 2 3 4
| // direction. float dx = uMax-uMin; float dy = vMax-vMin; float d = 1.0f / sqrtf(dx*dx+dy*dy);
|
这样,极线可以表示为L:={l0+λ[lx,ly]T}
其中l0就是[ umin, vmin]T ,λ是离散的步长(视差),[lx,ly]T表示极线的方向(单位向量)。根据前面设的最大搜索范围,得到像素的最大范围
1 2 3
| dist = maxPixSearch; uMax = uMin + dist*dx*d; vMax = vMin + dist*dy*d;
|
然后在最大范围内按一定步长进行离散搜索,找到最小的和第二小的误差,比较两者的比值。最后在最小误差的位置上进行高斯牛顿优化(只有一个变量),每次迭代过程中如果误差大于前面得到的最小误差,就缩小优化步长重新来过,当增量小于一定值时停止。
3.1.2 逆深度范围更新
设Pr=KRK−1[u1v11]T=[m1m2m3]T
Kt=[n1n2n3]T
则投影后的像素坐标
$u_{2}=\frac{m_{1}+n_{1} \rho_{1}}{m_{3}+n_{3} \rho_{1}} $$v_{2}=\frac{m_{2}+n_{2} \rho_{1}}{m_{3}+n_{3} \rho_{1}}$
把逆深度放在左边,$\rho_{1}=\frac{m_{3} u_{2}-m_{1}}{n_{1}-n_{3} u_{2}} $ (1) $ \rho_{1}=\frac{m_{3} v_{2}-m_{2}}{n_{2}-n_{3} v_{2}}$ (2)
设[u2∗,v2∗]T为前面得到的最佳位置,并设当前像素位置的误差范围为α,离散搜索的单位步长在x,y方向上的投影分别为Δu,Δv,当x方向梯度较大时,我们根据公式(1)来确定逆深度范围:
ρ1min=n1−n3(u2∗−αΔu)m3(u2∗−αΔu)−m1 ρ1max=n1−n3(u2∗+αΔu)m3(u2∗+αΔu)−m1
当y方向梯度较大时,根据公式(2)来确定逆深度范围:
ρ1min=n2−n3(v2∗−αΔv)m3(v2∗−αΔv)−m2 ρ1max=n2−n3(v2∗+αΔv)m3(v2∗+αΔv)−m2
1 2 3 4 5 6 7 8 9 10
| if(dx*dx>dy*dy) { idepth_min = (pr[2]*(bestU-errorInPixel*dx) - pr[0]) / (hostToFrame_Kt[0] - hostToFrame_Kt[2]*(bestU-errorInPixel*dx)); idepth_max = (pr[2]*(bestU+errorInPixel*dx) - pr[0]) / (hostToFrame_Kt[0] - hostToFrame_Kt[2]*(bestU+errorInPixel*dx)); } else { idepth_min = (pr[2]*(bestV-errorInPixel*dy) - pr[1]) / (hostToFrame_Kt[1] - hostToFrame_Kt[2]*(bestV-errorInPixel*dy)); idepth_max = (pr[2]*(bestV+errorInPixel*dy) - pr[1]) / (hostToFrame_Kt[1] - hostToFrame_Kt[2]*(bestV+errorInPixel*dy)); }
|
接下来考虑α。为什么这里要有个α呢?前面通过离散搜索加上高斯牛顿优化的方式得到了最佳的匹配点,如果假设没有其他任何误差存在的话,我们完全可以令α=1,这样逆深度的最大最小值就可以通过简单地扰动一个单位步长来得到。但考虑误差的话,我们会发现极线和梯度的夹角对结果有着非常大的影响。如果极线的方向和梯度的方向接近垂直的,那么稍微有一点位姿误差(必然存在)使得投影点和真实点产生了一定的误差,沿着极线搜索得到的结果就会产生巨大的偏差,如图1所示。因此,非常有必要考虑这个α,事实上,在代码中,α的计算是在极线搜索之前做的,如果得到的α太大(这意味这极线和梯度的夹角接近90度),就没有做极线搜索的必要。
DSO似乎没有直接根据公式计算视差的不确定度,α更像是一个根据人工经验设计的置信系数(我目前是这么理解的,因为实在推不出这个公式),代码如下所示:
1 2 3 4 5 6
| float dx = setting_trace_stepsize*(uMax-uMin); float dy = setting_trace_stepsize*(vMax-vMin);
float a = (Vec2f(dx,dy).transpose() * gradH * Vec2f(dx,dy)); float b = (Vec2f(dy,-dx).transpose() * gradH * Vec2f(dy,-dx)); float errorInPixel = 0.2f + 0.2f * (a+b) / a;
|
令点在主导帧中的梯度雅克比为J∇=[∇x,∇y]T,gradH就是∑J∇J∇T ,这里的求和符号表示对一个小块中的8个点求和,因此a则可以理解为极线与梯度的点乘的平方,b则可以理解为极线旋转90度后与梯度的点乘的平方,errorInPixel就是这里的α。可以看到,errorInPixel基本来自于变量b/a,当b/a接近于0时(此时极线和梯度方向基本平行),α≈0.4,逆深度只更新大约0.4个单位步长;而当b/a大于一定阈值时,则后续步骤直接跳过,该点被标记为IPS_BADCONDITION。