直接法相机位姿估计

前面用了好几篇文章介绍特征点法的相机位姿估计,本文则换一种思路,介绍近年来日渐流行的直接法。

一、直接法

与“光流法”类似,直接法也不需要特征匹配,甚至不需要提取特征。显然,这会节约大量时间,但与此同时,由于缺少准确的匹配,无法得知两张图像间像素的对应关系,所以必须依靠大量像素点的整体优化才有可能得到正确的结果。

这里提到了“大量像素点”,不是特征点也不是全部像素点。这是因为直接法可以根据使用像素点的多少分为三类:稀疏直接法,半稠密直接法,稠密直接法。稀疏直接法仍然使用特征点,不过不必计算描述子,可以节约一些时间。半稠密直接法使用像素梯度大的像素点,舍弃那些像素梯度非常小的点。稠密直接法使用所有像素点(一般几十万至几百万个),计算量非常大,无法在CPU上实时计算,但好处是可以建立稠密地图。

在特征点法中,我们优化相机位姿使重投影误差最小。而在直接法中,由于没有匹配点对,无法计算重投影误差,取而代之的是光度误差。光度误差是两张图片中由变换矩阵关联起来的两个像素点的灰度差异。理想情况下,灰度差异应该为0,它们应该是同一个点(仍然基于灰度不变假设)。但实际中由于相机位姿变换矩阵不准确会造成一些差异,我们据此构建一个非线性优化问题,把大量像素点的光度误差的平方和作为总误差,优化相机位姿使该误差最小。

构建这样一个非线性优化问题需要计算误差对相机位姿的导数。这里的理论性又很强了,笔者也是搞不定,建议大家看高翔博士《视觉SLAM十四讲》的第8讲原文。接下来我们还是看看怎么使用g2o实现直接法吧。

二、使用g2o实现直接法

由于稠密直接法需要借助GPU编程,所以这里不予考虑。只介绍稀疏直接法和半稠密直接法的实现。

仍然可以借助g2o提供的VertexSE3Expmap类作为顶点,但需要自己编写代表误差项的边,代码如下。

// project a 3d point into an image plane, the error is photometric error
// an unary edge with one vertex SE3Expmap (the pose of camera)
class EdgeSE3ProjectDirect: public BaseUnaryEdge< 1, double, VertexSE3Expmap>
{
public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW

    EdgeSE3ProjectDirect() {}

    EdgeSE3ProjectDirect ( Eigen::Vector3d point, float fx, float fy, float cx, float cy, cv::Mat* image )
        : x_world_ ( point ), fx_ ( fx ), fy_ ( fy ), cx_ ( cx ), cy_ ( cy ), image_ ( image )
    {}

    virtual void computeError()
    {
        const VertexSE3Expmap* v  =static_cast<const VertexSE3Expmap*> ( _vertices[0] );
        Eigen::Vector3d x_local = v->estimate().map ( x_world_ );
        float x = x_local[0]*fx_/x_local[2] + cx_;
        float y = x_local[1]*fy_/x_local[2] + cy_;
        // check x,y is in the image
        if ( x-4<0 || ( x+4 ) >image_->cols || ( y-4 ) <0 || ( y+4 ) >image_->rows )
        {
            _error ( 0,0 ) = 0.0;
            this->setLevel ( 1 );
        }
        else
        {
            _error ( 0,0 ) = getPixelValue ( x,y ) - _measurement;
        }
    }

    // plus in manifold
    virtual void linearizeOplus( )
    {
        if ( level() == 1 )
        {
            _jacobianOplusXi = Eigen::Matrix<double, 1, 6>::Zero();
            return;
        }
        VertexSE3Expmap* vtx = static_cast<VertexSE3Expmap*> ( _vertices[0] );
        Eigen::Vector3d xyz_trans = vtx->estimate().map ( x_world_ );   // q in book

        double x = xyz_trans[0];
        double y = xyz_trans[1];
        double invz = 1.0/xyz_trans[2];
        double invz_2 = invz*invz;

        float u = x*fx_*invz + cx_;
        float v = y*fy_*invz + cy_;

        // jacobian from se3 to u,v
        // NOTE that in g2o the Lie algebra is (\omega, \epsilon), where \omega is so(3) and \epsilon the translation
        Eigen::Matrix<double, 2, 6> jacobian_uv_ksai;

        jacobian_uv_ksai ( 0,0 ) = - x*y*invz_2 *fx_;
        jacobian_uv_ksai ( 0,1 ) = ( 1+ ( x*x*invz_2 ) ) *fx_;
        jacobian_uv_ksai ( 0,2 ) = - y*invz *fx_;
        jacobian_uv_ksai ( 0,3 ) = invz *fx_;
        jacobian_uv_ksai ( 0,4 ) = 0;
        jacobian_uv_ksai ( 0,5 ) = -x*invz_2 *fx_;

        jacobian_uv_ksai ( 1,0 ) = - ( 1+y*y*invz_2 ) *fy_;
        jacobian_uv_ksai ( 1,1 ) = x*y*invz_2 *fy_;
        jacobian_uv_ksai ( 1,2 ) = x*invz *fy_;
        jacobian_uv_ksai ( 1,3 ) = 0;
        jacobian_uv_ksai ( 1,4 ) = invz *fy_;
        jacobian_uv_ksai ( 1,5 ) = -y*invz_2 *fy_;

        Eigen::Matrix<double, 1, 2> jacobian_pixel_uv;

        jacobian_pixel_uv ( 0,0 ) = ( getPixelValue ( u+1,v )-getPixelValue ( u-1,v ) ) /2;
        jacobian_pixel_uv ( 0,1 ) = ( getPixelValue ( u,v+1 )-getPixelValue ( u,v-1 ) ) /2;

        _jacobianOplusXi = jacobian_pixel_uv*jacobian_uv_ksai;
    }

    // dummy read and write functions because we don't care...
    virtual bool read ( std::istream& in ) {}
    virtual bool write ( std::ostream& out ) const {}

protected:
    // get a gray scale value from reference image (bilinear interpolated)
    inline float getPixelValue ( float x, float y )
    {
        uchar* data = & image_->data[ int ( y ) * image_->step + int ( x ) ];
        float xx = x - floor ( x );
        float yy = y - floor ( y );
        return float (
                   ( 1-xx ) * ( 1-yy ) * data[0] +
                   xx* ( 1-yy ) * data[1] +
                   ( 1-xx ) *yy*data[ image_->step ] +
                   xx*yy*data[image_->step+1]
               );
    }
public:
    Eigen::Vector3d x_world_;   // 3D point in world frame
    float cx_=0, cy_=0, fx_=0, fy_=0; // Camera intrinsics
    cv::Mat* image_=nullptr;    // reference image
};

这是一个一元边,因为我们每次只对一个相机位姿做优化。覆写了两个函数,在computeError中计算光度误差,并在linearizeOplus中计算误差关于相机位姿的导数,即雅克比矩阵。另外,getPixelValue中对图像亮度做了插值,以得到更准确的结果。

在主函数中,初始化g2o求解器,添加顶点和边,开始优化等等。稀疏直接法和半稠密直接法的区别是,前者事先提取FAST关键点,而后者遍历所有像素,找出其中像素梯度大的点。再往后的内容就趋于一致了。下面给出程序运行效果。

稀疏直接法
半稠密直接法

可以看到,半稠密直接法使用了更多的像素点,准确度更高,但是速度比稀疏直接法慢了许多,大概需要100多毫秒,而稀疏直接法只需要十几毫秒。

完整代码下载地址:https://github.com/jingedawang/DirectMethod

三、直接法的优缺点

总结起来,直接法的优点有:

  • 省去计算特征点和描述子的时间。
  • 不需要特征点,只需要有像素梯度。因此可以用于特征缺失的场合,比如渐变的图案。
  • 可以构建半稠密乃至稠密地图。

同时也有如下的缺点:

  • 非凸性。优化很容易进入局部极小。
  • 单个像素没有区分度。应当使用图像块来代替单个像素的计算。
  • 灰度值不变假设难以满足。相机的曝光参数会自动调整,导致图像整体变亮或变暗。

四、参考资料

《视觉SLAM十四讲》第8讲 视觉里程计2 高翔

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,478评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,825评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,482评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,726评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,633评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,018评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,168评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,320评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,264评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,288评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,995评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,587评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,909评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,284评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,862评论 2 339

推荐阅读更多精彩内容

  • 本文首先介绍如何使用OpenCV中的PnP求解3D-2D位姿变换,再介绍如何使用g2o对前面得出的结果进行集束调整...
    金戈大王阅读 10,631评论 5 6
  • 直接法是用于视觉里程计估计相机位姿的一种重要方法。相比于其他依赖几何特征做相机位姿估计的方法,直接法具有无需计算特...
    变胖是梦想2014阅读 5,327评论 0 2
  • 目录 1999年冬天的一晚,马雪拉守着女儿学习到了...
    共山阅读 789评论 7 13
  • 我想我的骨头是黑色的,就像乌鸡的骨头是黑的,但乌鸡不坏,我坏。我坏到了骨髓。其实坏是一种与生俱来的天赋,我从来没有...
    雪鹰飞阅读 768评论 10 8
  • 距离墨白的突然表白已经过去十天了,这十天里,两人之间的相处与平常一样,墨白负责打理屋子里的花花草草,而南星则是继续...
    邡素阅读 275评论 0 0