@kokerf
2017-09-19T03:14:40.000000Z
字数 18666
阅读 2564
开源SLAM笔记
建图一致性约束也就是做闭环检测和全局优化。由于论文中采用的是直接法,虽然代码中有fabmap检测闭环的部分,但是其默认检测闭环的方式是帧与帧之间做双向跟踪。
这部分的代码入口在函数SlamSystem::constraintSearchThreadLoop,代码主要分了两种情况:
在测试闭环时调用的函数为SlamSystem::findConstraintsForNewKeyFrames。该函数主要是根据视差、关键帧连接关系,找出并且在删选处候选帧,然后对每个候选帧和测试的关键帧之间进行双向sim3跟踪,如果求解出的两个李代数满足马氏距离在一定范围内,则认为是闭环成功,并且在位姿图中添加边的约束。而图优化的部分在另外一个线程SlamSystem::optimizationThreadLoop,基本就是使用g2o,这部分就略过了。
关于如何选择候选帧,主要步骤如下:
其实第一个步骤是根据每个关键帧求得的位姿取判断的,其实这是一个因果倒置的方式。在代码中把候选帧分为近处的候选帧和远处的候选帧,对于第二个步骤,只是检测处与当前闭环检测关键帧相近的候选帧。第三步则是核心,把删选处的每一个候选帧与检测的关键帧做双向的Sim3跟踪,如果都成功,并且两个的马氏距离足够小,则认为是检测处闭环,并且在位姿图中构建的边。
接下来主要看一下Sim(3)求解以及双向跟踪检测(reciprocal tracking check)。
首先和变换一样,对于两帧图像上对应归一化图像点和通过变换有:
这里的表示从图像帧变换到图像帧上的点。这里在求解的时候也是同求一样,使用了迭代变权重高斯牛顿算法。
注意:论文中提及,把深度图的梯度近似为,因此对求的偏导都可以忽略。
根据高斯牛顿算法,对式子进行处理。由于Huber-Norm是在最外面的,其最终是等效为一个权重。
我们需要计算每个位置处的归一化方差。对于已知的向量,其每一个元素,对于函数的方差有:
疑问1:这里和之前求一样,都是近似可以忽略旋转的情况下计算的。在求得雅可比之后,变量尽量都用的数据来表示,而不是用参考帧上的数据来表示呢?就以为例,完全可以化作的形式。这两者有什么优劣差别?
在Sim3中比SE3多了一个逆深度的雅克比。首先我们看一下光度残差的雅克比,这里和SE3部分唯一不同的地方在连式法则展开后最后一项变换模型有所差异,一个是对求偏导,一个是对求偏导。
然后我们看对逆深度求的雅可比矩阵,而这雅可比矩阵和式子中的两个逆深度都有关。这里的雅可比是在Sim3李代数的扰动求的:
由于图像是非凸的,因此对图像对齐问题,我们需要一个很好的初始化位置。但是对于查找闭环约束的时候,这个条件不容易满足,因此论文中提出了两种增加收敛半径的方法:使用高效二阶最小化方法(Efficient Second Order Minimization,ESM);从粗到细(Coarse-to-Fine)方法,也就是图像金字塔的方式。第二种方法是比较常见的,对于第一种方法之前没有碰到过ESM方法,这里将对ESM方法做个简明的介绍。
我们设代价函数为,做二阶泰勒展开如下:
代码中其他步骤基本都和SE3跟踪相同。对于ESM的详细介绍见论文Real-time image-based tracking of planes using efficient second-order minimization。
Sim3有几个主要的函数,基本和SE3跟踪是一样的。接下里看一下这些代码。
这是Sim3跟踪的主体函数,跟踪图像帧reference到frame求相应的Sim3变换。
Sim3 Sim3Tracker::trackFrameSim3(TrackingReference* reference,Frame* frame,const Sim3& frameToReference_initialEstimate,int startLevel, int finalLevel)
由于主要的框架和SE3都是一样的,我们只看一下具体实现的那几个函数。
我们看下代码,首先得到一些全局不变量,这里的rotMat对应初始Sim3中的,rotMatUnscaled为,transVec是。
// get static valuesint w = frame->width(level);int h = frame->height(level);Eigen::Matrix3f KLvl = frame->K(level);float fx_l = KLvl(0,0);float fy_l = KLvl(1,1);float cx_l = KLvl(0,2);float cy_l = KLvl(1,2);Eigen::Matrix3f rotMat = referenceToFrame.rxso3().matrix().cast<float>();Eigen::Matrix3f rotMatUnscaled = referenceToFrame.rotationMatrix().cast<float>();Eigen::Vector3f transVec = referenceToFrame.translation().cast<float>();
接下来计算了图像平面沿着光轴方向的旋转。我们可以把两帧的相对旋转分为主轴的旋转以及沿着主轴方向的旋转。
// Calculate rotation around optical axis for rotating source frame gradientsEigen::Vector3f forwardVector(0, 0, -1);Eigen::Vector3f rotatedForwardVector = rotMatUnscaled * forwardVector;Eigen::Quaternionf shortestBackRotation;shortestBackRotation.setFromTwoVectors(rotatedForwardVector, forwardVector);Eigen::Matrix3f rollMat = shortestBackRotation.toRotationMatrix() * rotMatUnscaled;float xRoll0 = rollMat(0, 0);float xRoll1 = rollMat(0, 1);float yRoll0 = rollMat(1, 0);float yRoll1 = rollMat(1, 1);
这里的setFromTwoVectors函数,计算得到从rotatedForwardVector到forwardVector的旋转,然后乘以rotMatUnscaled后就是图像平面沿着主轴方向forwardVector的旋转。这里主要是在ESM求梯度时使用的。
在遍历前获取参考帧的3D点、灰度、梯度、图像帧的逆深度、逆深度方差和灰度、梯度的指针。
const Eigen::Vector3f* refPoint_max = reference->posData[level] + reference->numData[level];const Eigen::Vector3f* refPoint = reference->posData[level];const Eigen::Vector2f* refColVar = reference->colorAndVarData[level];const Eigen::Vector2f* refGrad = reference->gradData[level];const float* frame_idepth = frame->idepth(level);const float* frame_idepthVar = frame->idepthVar(level);const Eigen::Vector4f* frame_intensityAndGradients = frame->gradients(level);
然后遍历每个参考帧的3D点,把点通过初始的Sim3变换到图像帧的坐标系下,并且查看对应投影点是不是在图像平面范围内。
for(;refPoint<refPoint_max; refPoint++, refGrad++, refColVar++){Eigen::Vector3f Wxp = rotMat * (*refPoint) + transVec;float u_new = (Wxp[0]/Wxp[2])*fx_l + cx_l;float v_new = (Wxp[1]/Wxp[2])*fy_l + cy_l;// step 1a: coordinates have to be in image:// (inverse test to exclude NANs)if(!(u_new > 1 && v_new > 1 && u_new < w-2 && v_new < h-2))continue;
把数据保存到缓存变量中。如果使用了ESM,则把参考帧上的梯度方向旋转到当前帧的方向。并且这里的的梯度就是按照公式中的形式计算。
*(buf_warped_x+idx) = Wxp(0);*(buf_warped_y+idx) = Wxp(1);*(buf_warped_z+idx) = Wxp(2);Eigen::Vector3f resInterp = getInterpolatedElement43(frame_intensityAndGradients, u_new, v_new, w);// save values#if USE_ESM_TRACKING == 1// get rotated gradient of pointfloat rotatedGradX = xRoll0 * (*refGrad)[0] + xRoll1 * (*refGrad)[1];float rotatedGradY = yRoll0 * (*refGrad)[0] + yRoll1 * (*refGrad)[1];*(buf_warped_dx+idx) = fx_l * 0.5f * (resInterp[0] + rotatedGradX);*(buf_warped_dy+idx) = fy_l * 0.5f * (resInterp[1] + rotatedGradY);#else*(buf_warped_dx+idx) = fx_l * resInterp[0];*(buf_warped_dy+idx) = fy_l * resInterp[1];#endif
疑问2:在ESM求雅克比的时候,对参考帧像素位置求的雅克比得到的梯度为什么需要旋转到图像帧的方向上来?
这里和SE3一样,在计算残差的时候也是使用了迷之仿射变换,先不管这个问题。
float c1 = affineEstimation_a * (*refColVar)[0] + affineEstimation_b;float c2 = resInterp[2];float residual_p = c1 - c2;float weight = fabsf(residual_p) < 2.0f ? 1 : 2.0f / fabsf(residual_p);sxx += c1*c1*weight;syy += c2*c2*weight;sx += c1*weight;sy += c2*weight;sw += weight;*(buf_warped_residual+idx) = residual_p;*(buf_idepthVar+idx) = (*refColVar)[1];
接下来的代码是Sim3特有的部分,计算逆深度的残差,如果当前图像帧对应位置的逆深度方差不为零(对应位置若为-1则表示没有深度),则在缓存变量中赋值。
// new (only for Sim3):int idx_rounded = (int)(u_new+0.5f) + w*(int)(v_new+0.5f);float var_frameDepth = frame_idepthVar[idx_rounded];float ref_idepth = 1.0f / Wxp[2];*(buf_d+idx) = 1.0f / (*refPoint)[2];if(var_frameDepth > 0){float residual_d = ref_idepth - frame_idepth[idx_rounded];*(buf_residual_d+idx) = residual_d;*(buf_warped_idepthVar+idx) = var_frameDepth;}else{*(buf_residual_d+idx) = -1;*(buf_warped_idepthVar+idx) = -1;}
然后这个函数主要的功能都完成了。与SE3Tracker中的calcResidualAndBuffers相对应,差别在于计算梯度的时候按照ESM求的,并且多了两个变量:逆深度残差buf_residual_d和当前图像帧对应逆深度的方差buf_warped_idepthVar。
这个函数主要计算对应的归一化方差以及最终的残差。这里和SE3Tracker中的calcWeightsAndResidual对应,都有旋转很小的假设,认为只有平移。
float tx = referenceToFrame.translation()[0];float ty = referenceToFrame.translation()[1];float tz = referenceToFrame.translation()[2];
接下来遍历每个点,得到buff中对应的数据。
for(int i=0;i<buf_warped_size;i++){float px = *(buf_warped_x+i); // x'float py = *(buf_warped_y+i); // y'float pz = *(buf_warped_z+i); // z'float d = *(buf_d+i); // dfloat rp = *(buf_warped_residual+i); // r_pfloat rd = *(buf_residual_d+i); // r_dfloat gx = *(buf_warped_dx+i); // \delta_x Ifloat gy = *(buf_warped_dy+i); // \delta_y Ifloat s = settings.var_weight * *(buf_idepthVar+i); // \sigma_d^2float sv = settings.var_weight * *(buf_warped_idepthVar+i); // \sigma_d^2'
接下来计算归一化方差,这里比SE3多了g2。通过公式计算得到了雅克比,则对应的方差w_d计算如下
// calc dw/dd (first 2 components):float g0 = (tx * pz - tz * px) / (pz*pz*d);float g1 = (ty * pz - tz * py) / (pz*pz*d);float g2 = (pz - tz) / (pz*pz*d);// calc w_pfloat drpdd = gx * g0 + gy * g1; // ommitting the minusfloat w_p = 1.0f / (cameraPixelNoise2 + s * drpdd * drpdd);float w_d = 1.0f / (sv + g2*g2*s);
然后计算对应的Huber权重
float weighted_rd = fabs(rd*sqrtf(w_d));float weighted_rp = fabs(rp*sqrtf(w_p));float weighted_abs_res = sv > 0 ? weighted_rd+weighted_rp : weighted_rp;float wh = fabs(weighted_abs_res < settings.huber_d ? 1 : settings.huber_d / weighted_abs_res);
最后把保存光度误差的权重和逆深度误差的权重。
*(buf_weight_p+i) = wh * w_p;if(sv > 0)*(buf_weight_d+i) = wh * w_d;else*(buf_weight_d+i) = 0;
这个函数求高斯牛顿迭代的雅克比了。函数传进来的是一个7个参数的最小二乘的类。这个函数与的SE3Tracker的函数calculateWarpUpdate对应,比SE3多了一个参数,就是尺度。在函数开始的时候,初始化了两个最小二乘的类,一个是4个参数的,一个是6个参数的。6个参数的对应于光度残差的雅克比,4参数对应逆深度残差的雅克比,这里都不是7,因为都先不保存非零项。
void Sim3Tracker::calcSim3LGS(NormalEquationsLeastSquares7 &ls7){NormalEquationsLeastSquares4 ls4;NormalEquationsLeastSquares ls6;ls6.initialize(width*height);ls4.initialize(width*height);
然后看循环,遍历每个点
for(int i=0;i<buf_warped_size;i++){float px = *(buf_warped_x+i); // x'float py = *(buf_warped_y+i); // y'float pz = *(buf_warped_z+i); // z'float wp = *(buf_weight_p+i); // wr/wpfloat wd = *(buf_weight_d+i); // wr/wdfloat rp = *(buf_warped_residual+i); // r_pfloat rd = *(buf_residual_d+i); // r_dfloat gx = *(buf_warped_dx+i); // \delta_x Ifloat gy = *(buf_warped_dy+i); // \delta_y Ifloat z = 1.0f / pz;float z_sqr = 1.0f / (pz*pz);
先计算光度残差的雅克比,这里和公式前6项对应,且差一个负号。
Vector6 v;Vector4 v4;v[0] = z*gx + 0;v[1] = 0 + z*gy;v[2] = (-px * z_sqr) * gx +(-py * z_sqr) * gy;v[3] = (-px * py * z_sqr) * gx +(-(1.0 + py * py * z_sqr)) * gy;v[4] = (1.0 + px * px * z_sqr) * gx +(px * py * z_sqr) * gy;v[5] = (-py * z) * gx +(px * z) * gy;
然后是逆深度残差的雅克比,回顾一下公式:
v4[0] = z_sqr;v4[1] = z_sqr * py;v4[2] = -z_sqr * px;v4[3] = z;
然后分别求对应的和。我们先看一下:
l6,这里的雅克比差了一个负号:
ls6.update(v, rp, wp); // Jac = - v
对应调用了NormalEquationsLeastSquares4::update:
inline void NormalEquationsLeastSquares::update(const Vector6& J, const float& res, const float& weight){A_opt.rankUpdate(J, weight);b -= J * (res * weight);error += res * res * weight;num_constraints += 1;}
很明显,这里求的时候的符号已经反过来了。
然后是求l4:
ls4.update(v4, rd, wd); // Jac = v4
对应了NormalEquationsLeastSquares4::update:
inline void NormalEquationsLeastSquares4::update(const Vector4& J, const float& res, const float& weight){// TODO: SSE optimizationA.noalias() += J * J.transpose() * weight;b.noalias() -= J * (res * weight);error += res * res * weight;num_constraints += 1;}
函数的最后计算7参数的最小二乘,这里调用的finish是没有除以约束个数的(SE3中直接除以约束个数了),所以是NoDivide:
ls4.finishNoDivide();ls6.finishNoDivide();ls7.initializeFrom(ls6, ls4);
我们查看NormalEquationsLeastSquares7::initializeFrom:
void NormalEquationsLeastSquares7::initializeFrom(const NormalEquationsLeastSquares& ls6, const NormalEquationsLeastSquares4& ls4){// set zeroA.setZero();b.setZero();// add ls6A.topLeftCorner<6,6>() = ls6.A;b.head<6>() = ls6.b;// add ls4int remap[4] = {2,3,4,6};for(int i=0;i<4;i++){b[remap[i]] += ls4.b[i];for(int j=0;j<4;j++)A(remap[i], remap[j]) += ls4.A(i,j);}num_constraints = ls6.num_constraints + ls4.num_constraints;}
这里按照公式求得最终的和,先拷贝光度残差的l6部分,然后把逆深度残差按照在矩阵块的位置对应拷贝。
最终回到Sim3Tracker::trackFrameSim3的位置就是最终求解增量的部分:
// solve LS system with current lambdaVector7 b = - ls7.b / ls7.num_constraints;Matrix7x7 A = ls7.A / ls7.num_constraints;for(int i=0;i<7;i++) A(i,i) *= 1+LM_lambda;Vector7 inc = A.ldlt().solve(b);
这里和SE3一样,最终把和都除以了约束个数,很是不解。
论文中提及,为了避免误检测,采用双向跟踪检测的方式,对每一个候选帧都计算其与检测关键帧彼此跟踪的,然后计算两者的马氏距离[1](其实是马氏距离的平方):
其实双向跟踪在一开始选取临近的候选帧的时候做SE3跟踪也用到了,但是只是对其中的做了2-Norm距离的判断。
公式其实把变量都转换到计算的坐标系下,也就是对应的关键帧的世界坐标下。为了把两个协方差都用上,这里需要把的协方差转换到对应的切空间(也就是所在的切空间),这里同样使用的方差传播公式。首先把在参考系的位姿变换到参考帧有
到这篇总结结束,LSD-SLAM主要的算法和代码都已经总结完毕了。在本篇总结中,主要是Sim3跟踪,论文中使用双向跟踪的方式,来确保两帧图像是匹配的。这样的策略以前在光流跟踪中看到过,但是用在图像匹配上还是第一次,但是理论上这都是一致的,只是变换的模型有差异而已。
另外这里有几个疑问:
参考: