当前位置: 华文问答 > 数码

研究SLAM,对编程的要求有多高?

2016-10-18数码

我还没写你们就开始点赞了,有点过意不去啊……

题主说MATLAB,主要原因是大多数人本科阶段接触的都是MATLAB,所以希望之后研究SLAM也用它。

MATLAB确实有很多优点:语法简单,开发速度快,调试方便,功能丰富。然而,在SLAM领域,MATLAB缺点也很明显,主要是这两个:

  • 需要正版软件(你不能实机上也装个盗版MATLAB吧);
  • 运行效率不高;
  • 需要一个巨大的安装包;
  • 而相对的,C++的优势在于直接使用,有很高的运行效率,不过开发速度和调试方面慢于MATLAB。不过光运行效率这一条,就够许多SLAM方案选择C++作为开发语言了,因为运行效率真的很重要。同一个算法,拿MATLAB写出来实现不能实时,拿C++写的能实时,你说用哪个?

    当然MATLAB也有一些用武之地。我见过一些SLAM相关的公开课程,让学生用MATLAB做仿真,交作业,这没有问题,比如SLAM toolbox 。同样的,比较类似于MATLAB的Python(以及octave)亦常被用于此道。它们在开发上的快捷带来了很多便利,当你想要验证一些数学理论、思想时,这些都是不错的工具。所谓技多不压身,题主掌握MATLAB和Python当然是很棒的。


    但是一牵涉到实用,你会发现几乎所有的方案都在用C++。 因为运行效率实在是太重要了。


    那既然有心思学MATLAB,为什么不学好C++呢?


    ---------------------分割线------------------

    接下来说说C++大概要学到什么程度。用程序员的话说,C++语言比较特殊,你可以说自己精通了Java,但千万不要说自己精通了C++。C++非常之博大精深,有数不清的特性,而且随着时间还会不断变化更新。不过,大多数人都用不着学会所有的C++特性,因为许多东西一辈子都用不到。


    作为SLAM研究人员,我们面对的主要是算法层面的开发,所以更关心如何有效地实现各种相关的算法。而相对的,那些复杂的软件架构,设计模式,我个人认为在SLAM中倒是占次要地位的。毕竟您用SLAM的目的是计算一个位置以及建个地图,并不是要去写一套能够自动更新的、多人网上对战功能的机器人大战平台。您的主要精力可能会花在矩阵运算、分块、非线性优化的实现、图像处理上面;您可能对并发、指令集加速、GPU加速等话题感兴趣,也可以花点时间学习;你还可能想用模板来拓展你的算法,也不妨一试。相应的,很多功能性的东西,比如说UI、网络通信等等,当你用到的时候不妨接触一下,但专注于SLAM上时就不必专门去学习了。


    话虽如此,SLAM所需的C++水平,大抵要高于你在书本上看到的那些个示例代码。因为那些代码是作者用来向初学者介绍语法的,所以会尽量简单。而实际见到的代码往往结合了各种奇特的技巧,乍看起来会显得高深莫测。比方说你在教科书里看的大概是这样:


    int main ( int argc, char** argv ) { vector<string> vec; vec.push_back("abc"); for ( int i=0; i<vec.size(); i++ ) { // ... } return 0; }


    你看了C++ Primer Plus,觉得C++也不过如此,并没有啥特别难以理解的地方。然而实际代码大概是这样的:


    嵌套的模板类(来自g2o的块求解器):

    g2o :: BlockSolver < g2o :: BlockSolverTraits < 3 , 1 > >:: LinearSolverType * linearSolver = new g2o :: LinearSolverDense < g2o :: BlockSolver < g2o :: BlockSolverTraits < 3 , 1 > >:: PoseMatrixType > (); g2o :: BlockSolver < g2o :: BlockSolverTraits < 3 , 1 > >* solver_ptr = new g2o :: BlockSolver < g2o :: BlockSolverTraits < 3 , 1 > > ( linearSolver ); g2o :: OptimizationAlgorithmLevenberg * solver = new g2o :: OptimizationAlgorithmLevenberg ( solver_ptr ); g2o :: SparseOptimizer optimizer ; optimizer . setAlgorithm ( solver );


    模板元(来自ceres的自动求导):

    virtual bool Evaluate ( double const * const * parameters , double * residuals , double ** jacobians ) const { if ( ! jacobians ) { return internal :: VariadicEvaluate < CostFunctor , double , N0 , N1 , N2 , N3 , N4 , N5 , N6 , N7 , N8 , N9 > :: Call ( * functor_ , parameters , residuals ); } return internal :: AutoDiff < CostFunctor , double , N0 , N1 , N2 , N3 , N4 , N5 , N6 , N7 , N8 , N9 >:: Differentiate ( * functor_ , parameters , SizedCostFunction < kNumResiduals , N0 , N1 , N2 , N3 , N4 , N5 , N6 , N7 , N8 , N9 >:: num_residuals (), residuals , jacobians ); }


    C11新特性(来自SVO特征提取部分)

    void Frame :: setKeyPoints () { for ( size_t i = 0 ; i < 5 ; ++ i ) if ( key_pts_ [ i ] != NULL ) if ( key_pts_ [ i ] -> point == NULL ) key_pts_ [ i ] = NULL ; std :: for_each ( fts_ . begin (), fts_ . end (), [ & ]( Feature * ftr ){ if ( ftr -> point != NULL ) checkKeyPoints ( ftr ); }); }



    谜之运算(来自SVO的深度滤波器):

    void DepthFilter :: updateSeed ( const float x , const float tau2 , Seed * seed ) { float norm_scale = sqrt ( seed -> sigma2 + tau2 ); if ( std :: isnan ( norm_scale )) return ; boost :: math :: normal_distribution < float > nd ( seed -> mu , norm_scale ); float s2 = 1. / ( 1. / seed -> sigma2 + 1. / tau2 ); float m = s2 * ( seed -> mu / seed -> sigma2 + x / tau2 ); float C1 = seed -> a / ( seed -> a + seed -> b ) * boost :: math :: pdf ( nd , x ); float C2 = seed -> b / ( seed -> a + seed -> b ) * 1. / seed -> z_range ; float normalization_constant = C1 + C2 ; C1 /= normalization_constant ; C2 /= normalization_constant ; float f = C1 * ( seed -> a + 1. ) / ( seed -> a + seed -> b + 1. ) + C2 * seed -> a / ( seed -> a + seed -> b + 1. ); float e = C1 * ( seed -> a + 1. ) * ( seed -> a + 2. ) / (( seed -> a + seed -> b + 1. ) * ( seed -> a + seed -> b + 2. )) + C2 * seed -> a * ( seed -> a + 1.0f ) / (( seed -> a + seed -> b + 1.0f ) * ( seed -> a + seed -> b + 2.0f )); // update parameters float mu_new = C1 * m + C2 * seed -> mu ; seed -> sigma2 = C1 * ( s2 + m * m ) + C2 * ( seed -> sigma2 + seed -> mu * seed -> mu ) - mu_new * mu_new ; seed -> mu = mu_new ; seed -> a = ( e - f ) / ( f - e / f ); seed -> b = seed -> a * ( 1.0f - f ) / f ; }


    我不知道你们看到这些代码是什么心情,总之我当时内心的感受是:卧槽这怎么和教科书里的完全不一样啊!而且研究了半天发现人家居然是对的啊!


    [我不是很擅长贴表情图总之你们脑补一下就好]


    总而言之,对C++的水平要求应该是在教科书之上的。而且这个水平的提高,多数时候建立在你不断地看别人代码、码自己代码的过程之上。它是反复练习出来的,并不是仅仅通过看书就能领会的。特别是对于视觉SLAM问题,很多时候你没法照着论文把一套方案实现出来,这很大程度上取决于你的理论和代码功底。


    所以,请尽早开始学习C++,尽早开始使用C++,才是研究SLAM的正确之道。不要长期彷徨在自己的舒适区里犹豫不决,这样是没有进步的。(同样的道理亦适用于想研究SLAM但不愿意学习Linux的朋友们)


    ---------------------分割线------------------

    题主还提到闭环检测的库,稍微列几个:

    DBoW系列,来自TRO12的一篇文章,里头用k-means++训练的字典树。与OpenCV结合紧密,原理亦比较简单。

    GitHub - dorian3d/DBoW2: Enhanced hierarchical bag-of-word library for C++

    GitHub - rmsalinas/DBow3: Improved version of DBow2


    FABMAP系列,用了Chow-Liu树,来自Cummins的一系列论文。作者自己提供过一个开源版本,OpenCV也有人实现了一个,所以一共两种。

    FabMap原版

    OpenCV:OpenFABMAP


    DLoopDetector:在DBoW2基础上开发的回环检测库

    https://github.com/dorian3d/DLoopDetector


    建议题主从DBoW2或者DBoW3开始入手研究。原理和实现都相对简单一些,效果也比较好。


    我自己也在写一本SLAM的书籍,里头带有大量的算法实验。现在还在草稿期,明年出版后题主可以参考一下。鉴于评论区对新书比较感兴趣,那我贴个草稿的目录吧。(由于是草稿所以最后见到的可能会有出入,而且有几节我也还没写完。)所有写着「实践部分」的都附有实例代码。