视觉SLAM 十四讲——建图


主要内容

1. 概述

  在视觉SLAM看来,“建图”是服务于“定位”的。从应用层面来看,地图的作用如下:

  1.1) 定位

  1.2) 导航

    至少需要知道地图中哪些地方可以通过,哪些地方不可通过——至少是一种稠密的地图

  1.3) 避障 —— 稠密地图

  1.4) 重建 —— 稠密地图

  1.5) 交互 —— 语义地图

2. 单目稠密重建

  2.1) 立体视觉

    2.1.1)单副图像像素:只能提供物体与相机成像平面的角度及物体采集到的亮度,而无法提供物体的距离。

    2.1.2)在稠密重建中,获取每一个像素点(或大部分像素点)的距离 的解决方案:

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

      b 使用双目相机,利用左右目的视差计算像素的距离(多目原理相同)

      c 使用RGB-D相机直接获得像素距离

      前两种方式称为立体视觉Stereo Vision,其中移动单目的又称为移动视角的单目视觉Moving View Stereo

    2.1.3) 优缺点

      a 单目,双目:需要花费大量的计算,得到的 深度估计不可靠(fragile

      b RGB-D:量程,应用范围和光照的限制,稠密重建中更常选择

      c 室外,大场景场合中,通过立体视觉估计深度信息

    2.1.4) 单目稠密重建关键点

      a 像素匹配——极线搜索和块匹配技术

      b 三角测量确定深度——深度滤波器技术

  2.2) 极线搜索与块匹配

    2.2.1) 极线概念

      对于参考帧图像中的任意像素,其深度值可能在某个最小值到无穷远之间,像素对应的空间特征点分布在一条射线上;

      在当前帧图像看来,这条射线的投影形成图像平面上的一条线——极线

      当知道两帧图像之间的运动时,极线边可求解得到。

    2.2.2) 块匹配

      a:特征点方法中,通过特征匹配找到确定当前帧中P2的位置,而稠密重建中,需要在极线上搜索确定相似的点。由于单个像素没有区分性,因此选取像素块进行比较

      b: 具体方法是:首先,在参考帧p1点处去w*w的小块;然后在极线上区很多同样大小的小块;最后通过图像块差异的度量方法确定像素点的相似性(算法的假设从像素灰度的不变性到图像块灰度不变性

      c: 图像块相似性度量方法:

        SAD(Sum of Absolute Difference):

        SSD(Sum of Squared Distance):

        NCC(Normalized Cross Correlation):

      (  注:SAD,SSD接近0表示相似,而NCC接近1表示相似;

          实际计算中,可以先对小块进行去均值操作,在进行相似度计算,可避免图像块整体亮度差异的影响,算法更稳定

    NCC计算的代码如下:

double NCC (
    const Mat& ref, const Mat& curr, 
    const Vector2d& pt_ref, const Vector2d& pt_curr
)
{
    // 零均值-归一化互相关
    // 先算均值
    double mean_ref = 0, mean_curr = 0;
    vector<double> values_ref, values_curr; // 参考帧和当前帧的均值
    for ( int x=-ncc_window_size; x<=ncc_window_size; x++ )
        for ( int y=-ncc_window_size; y<=ncc_window_size; y++ )
        {
            double value_ref = double(ref.ptr<uchar>( int(y+pt_ref(1,0)) )[ int(x+pt_ref(0,0)) ])/255.0;
            mean_ref += value_ref;
            
            double value_curr = getBilinearInterpolatedValue( curr, pt_curr+Vector2d(x,y) );
            mean_curr += value_curr;
            
            values_ref.push_back(value_ref);
            values_curr.push_back(value_curr);
        }
        
    mean_ref /= ncc_area;
    mean_curr /= ncc_area;
    
    // 计算 Zero mean NCC
    double numerator = 0, demoniator1 = 0, demoniator2 = 0;
    for ( int i=0; i<values_ref.size(); i++ )
    {
        double n = (values_ref[i]-mean_ref) * (values_curr[i]-mean_curr);
        numerator += n;
        demoniator1 += (values_ref[i]-mean_ref)*(values_ref[i]-mean_ref);
        demoniator2 += (values_curr[i]-mean_curr)*(values_curr[i]-mean_curr);
    }
    return numerator / sqrt( demoniator1*demoniator2+1e-10 );   // 防止分母出现零
}

    2.2.3)极线搜索-块匹配 实际实现

       a: 计算参考帧p1的相机坐标(归一化相机平面)

       b:根据初始的深度信息,结合运动信息,得到在当前帧下的像素坐标p2

       c:  根据初始深度信息的方差信息,得到深度信息的可能范围(正负3倍sigma),投影到当前帧得到极线的信息(极线的方向,投影点在极线上的可能的长度(实际对极线的长度进行限制,避免过长))

      d: 在初始值附近,按照极线的方向,在可能长度的范围内,逐点进行图像块相似性计算,记录在搜索曲线内相似性最大的点和对应的值(注意搜索时步进量的设置,代码中设置sqrt(2)

      e: 相似性门限值检测

    注意: 当前帧中,点的坐标步进以后可能不是整数,需要进行内插  

// 双线性灰度插值 
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;
}

    代码如下:

// 极线搜索
// 方法见书 13.2 13.3 两节
bool epipolarSearch(
    const Mat& ref, const Mat& curr, 
    const SE3& T_C_R, const Vector2d& pt_ref, 
    const double& depth_mu, const double& depth_cov, 
    Vector2d& pt_curr )
{
    Vector3d f_ref = px2cam( pt_ref );
    f_ref.normalize();
    Vector3d P_ref = f_ref*depth_mu;    // 参考帧的 P 向量
    
    Vector2d px_mean_curr = cam2px( T_C_R*P_ref ); // 按深度均值投影的像素
    double d_min = depth_mu-3*depth_cov, d_max = depth_mu+3*depth_cov;
    if ( d_min<0.1 ) d_min = 0.1;
    Vector2d px_min_curr = cam2px( T_C_R*(f_ref*d_min) );    // 按最小深度投影的像素
    Vector2d px_max_curr = cam2px( T_C_R*(f_ref*d_max) );    // 按最大深度投影的像素
    
    Vector2d epipolar_line = px_max_curr - px_min_curr;    // 极线(线段形式)
    Vector2d epipolar_direction = epipolar_line;        // 极线方向 
    epipolar_direction.normalize();
    double half_length = 0.5*epipolar_line.norm();    // 极线线段的半长度
    if ( half_length>100 ) half_length = 100;   // 我们不希望搜索太多东西 
    
    // 取消此句注释以显示极线(线段)
    showEpipolarLine( ref, curr, pt_ref, px_min_curr, px_max_curr );
    
    // 在极线上搜索,以深度均值点为中心,左右各取半长度
    double best_ncc = -1.0;
    Vector2d best_px_curr; 
    for ( double l=-half_length; l<=half_length; l+=0.7 )  // l+=sqrt(2) 
    {
        Vector2d px_curr = px_mean_curr + l*epipolar_direction;  // 待匹配点
        if ( !inside(px_curr) )
            continue; 
        // 计算待匹配点与参考帧的 NCC
        double ncc = NCC( ref, curr, pt_ref, px_curr );
        if ( ncc>best_ncc )
        {
            best_ncc = ncc; 
            best_px_curr = px_curr;
        }
    }
    if ( best_ncc < 0.85f )      // 只相信 NCC 很高的匹配
        return false; 
    pt_curr = best_px_curr;
    return true;
}

 

  2.3) 高斯分布的深度滤波器

    根据极线搜索确定了当前帧中的像素坐标点(可能含有误差),然后通过三角化计算新的深度信息,并量化新的深度信息的不确定度(量测噪声,受极线搜索得到带有误差的像素坐标点影响),然后和前一时刻的深度信息进行融合。

    2.3.1)三角化测量

 

      三角化最早由高斯提出,并应用于测量学中。

      原理:在不同的位置观测同一个三维点P,已知在不同位置处观测到的三维点的二维投影点x1,x2,利用三角关系,恢复出三维点的深度信息z.

 

 

      在SLAM中深度求解的方式常用的有三种,如下:

       a: 克莱默法则求解方法

        

 

 

      代码如下:

        归一化坐标解释:

             

 

         利用Cramer's法则求解得到s1和s2后,得到s1的过程需要注意(分别利用了s1和s2的信息恢复出相机坐标p,在求模得到深度信息)

    // 用三角化计算深度
    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();   // 深度值
  

      附: Cramer's法则如下:

          

      b: 利用叉乘消元进行求解

       

      c: 利用Mid Point Method 进行求解(Hartley 大名鼎鼎的《Mutiple View Geometry》中有讲解)

       

 

    2.3.2)深度滤波器

      对深度的分布假设存在着若干种不同的做法。首先,在比较简单的假设条件下,可以假设深度值服从高斯分布,得到一种类卡尔曼式的方法;而另一些文献中,亦采用了均匀-高斯混合分布的假设,推导了另一种形式更为复杂的深度滤波器。

      高斯分布假设下的滤波器实现:

      a: 设某个像素点的深度d服从:

        

      b: 当新的数据到来,通过计算得到其深度信息,同时包括不确定性结果,假设这次观测亦是一个高斯分布:

        

      c: 使用观测的信息更新原先d的分布(两个高斯分布的乘积依然是一个高斯分布),则假设融合后d的分布为 ,那么根据高斯分布的乘积,有:

        

      d: 代码如下:

    // 高斯融合
    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;

 

    2.3.3) 量测信息方差确定

      关于量测信息的计算亦有一些不同的处理方式。有些文献考虑了几何不确定性和光度不确定性二者之和,而有些文献仅考虑了几何不确定性,下面以仅考虑几何不确定性来进行计算:

      

      a: 首先通过平移信息(t的大小影响三角测量的精度,三角测量矛盾),根据相机坐标系下的坐标(其中涉及特征点深度信息),计算角度值

        

      b: p2扰动一个像素,将使得 产生一个变化量 ,由于相机的焦距为f,于是:

        

        所以:

        

        

      c: 根据正弦定理,可计算 的大小如下:

        

      d: 由此可以确定由单个像素的不确定引起的深度不确定性。如果认为极线搜索的块匹配仅有一个像素的误差,那么就可以设:

        

      e: 代码如下:

    // 计算不确定性(以一个像素为误差)
    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;

      注意:如果极线搜索的不确定性大于一个像素,亦可按照此推导放大这个不确定性。

         实际工程中,当不确定性小于一定阈值之后,就可认为深度数据已经收敛。(实际当中,有些像素点可能观测比较好,其深度和平移t比较合适,量测噪声小,因此收敛比较块,收敛以后这部分像素点可以作为已知位置点,而对于其余的像素点,则需要继续深度滤波,直到观测比较好时,才能收敛)

 

  2.4) 单目稠密深度估计流程

     a: 假设所有像素的深度满足某个初始的高斯分布

    b:当新的数据产生时,遍历所有待估计深度的像素点,通过极线搜索和块匹配确定投影点位置;

    c: 根据几何关系计算三角化后的深度及不确定性

    d: 将当前观测融合进上一次的估计中。检测方差是否收敛,停止解算该像素点深度,否则继续。

    // 第一张图
    Mat ref = imread( color_image_files[0], 0 );                // gray-scale image 
    SE3 pose_ref_TWC = poses_TWC[0];
    double init_depth   = 3.0;    // 深度初始值
    double init_cov2    = 3.0;    // 方差初始值 
    Mat depth( height, width, CV_64F, init_depth );             // 深度图
    Mat depth_cov( height, width, CV_64F, init_cov2 );          // 深度图方差 
    
    for ( int index=1; index<color_image_files.size(); index++ )
    {
        cout<<"*** loop "<<index<<" ***"<<endl;
        Mat curr = imread( color_image_files[index], 0 );       
        if (curr.data == nullptr) continue;
        SE3 pose_curr_TWC = poses_TWC[index];
        SE3 pose_T_C_R = pose_curr_TWC.inverse() * pose_ref_TWC; // 坐标转换关系: T_C_W * T_W_R = T_C_R 
        update( ref, curr, pose_T_C_R, depth, depth_cov );
        plotDepth( depth );
        imshow("image", curr);
        waitKey(1);
    }
    
    cout<<"estimation returns, saving depth map ..."<<endl;
    imwrite( "depth.png", depth );
    cout<<"done."<<endl;
// 根据新的输入图像 对整个深度图进行更新
bool update(const Mat& ref, const Mat& curr, const SE3& T_C_R, Mat& depth, Mat& depth_cov )
{
//#pragma omp parallel for
    for ( int x=boarder; x<width-boarder; x++ )
//#pragma omp parallel for
        for ( int y=boarder; y<height-boarder; y++ )
        {
            // 遍历每个像素
            if ( depth_cov.ptr<double>(y)[x] < min_cov || depth_cov.ptr<double>(y)[x] > max_cov ) // 深度已收敛或发散
                continue;
            // 在极线上搜索 (x,y) 的匹配 
            Vector2d pt_curr; 
            bool ret = epipolarSearch ( 
                ref, 
                curr, 
                T_C_R, 
                Vector2d(x,y), 
                depth.ptr<double>(y)[x], 
                sqrt(depth_cov.ptr<double>(y)[x]),
                pt_curr
            );
            
            if ( ret == false ) // 匹配失败
                continue; 
            
            // 取消该注释以显示匹配
            showEpipolarMatch( ref, curr, Vector2d(x,y), pt_curr );
            
            // 匹配成功,更新深度图 
            updateDepthFilter( Vector2d(x,y), pt_curr, T_C_R, depth, depth_cov );
        }
}

     结果如下:(上为初始估计的深度信息,下为迭代 35 轮观测以后估计的深度结果

    

 

    

    结果分析:

      当迭代次数超过一定值以后,深度图趋于稳定,不再对新的数据产生改变。

      稳定后的结果,整个估计大部分是正确的,但也存在这大量错误的估计

        (表现为深度图中与周围的数据不一致的地方,为过大或过小的估计

        (位于边缘处的地方,由于运动过程中看到的次数比较少,亦没有得到正确的估计

 

  2.5) 实验分析与讨论

    由于真实数据的复杂性,需要周密的考虑和大量的工程技巧。

   2.5.1) 像素梯度的问题

    a: 有明显梯度的小块将具有良好的区分度,不易引起误匹配;对于梯度不明显的像素,由于在块匹配时没有区分性,所以难以有效的估计其深度。对于单目还是双目,立体视觉的重建质量十分依赖于环境纹理——该问题是无法在现有的算法流程上加以改进并解决的(如果依然只关心单个像素周围的邻域小块)

    b: 当像素梯度与极线夹角较大时,极线匹配的不确定性大;而当夹角较小时,匹配的不确定性变小——考虑像素梯度和极线的关系,采用更精确的不确定性模型

   2.5.2) 逆深度

    a: 像素坐标u,v及深度信息d,三维坐标x,y,z不同的参数化形式

    b: 逆深度

      原有的高斯模型假设:室外应用,初始值难以涵盖比较远的点,用高斯模型分布描述会有一些数值计算上的困难。

      假设逆深度,即深度的倒数为高斯分布是比较有效的。在实际应用中,具有更好的数值稳定性,成为一种通用的做法,存在于现有的SLAM方案中的标准做法中。

  2.5.3) 图像间的变换

    在进行块匹配时,假设图像小块在相机运动是保持不变,但是这个假设在相机平移时能够保持成立,在相机发生纯旋转时无法成立——根据仿射变换矩阵,把当前帧(或参考帧)的像素进行变换后再进行块匹配,以期获得对旋转更好的效果

  2.5.4) 并行化:效率的问题

    利用GPU的稠密深度估计是可以在主流GPU上实时化的(每个像素深度计算可以并行执行

  2.5.5) 其他的改进

    a: 假设深度图中相邻的深度变化不会太大,从而给深度估计加上了空间正则项——使得到的深度图更加平滑

    b: 由于遮挡、光照、运动模糊等各种因素影响需要考虑错误匹配的处理——显示地将内点与外点进行区别并进行概率建模,能够较好的处理外点数据。

  2.6) 总结

    存在理论上的困难(环境纹理的依赖,像素梯度与极线方向平行等情况),很难通过调整代码实现来解决。

    截止目前,尽管双目和移动单目能够建立稠密的地图,但通常认为它们过去依赖环境纹理和光照,不够可靠。

 

3. RGB-D稠密建图

  RGB-D稠密建图是相对容易的——利用传感器测量深度信息,无须消耗大量的计算资源,避免对纹理环境的依赖

  3.1 点云地图

    用一组离散的点表示的地图,最基本的点包含三维坐标,也可以带有r,g,b的彩色信息。

    利用RGB-D提供的彩色图和深度图,根据内计算RGB-D点云

    如果得到相机的位姿,只要直接把点云进行加和,就可以获得全局的点云。

    程序实现

      在实际处理中,会对点云加一些滤波处理,以获得更好的视觉效果

      a: 在生成每一帧点云时,去掉深度值太大或无效的点(考虑到Kinect的有效量程,超过量程误差比较大)

      b: 对每一帧生成的点云,利用统计滤波器方法去除孤立点(统计每个点与它距离最近的N个点的距离值分布,去除距离均值过大的点,去掉孤立的噪声点)

      c: 对之后生成的整体的点云,利用体素滤波器(Voxel Filter)进行降采样(由于多个视角存在视野重叠,在重叠区域存在大量位置十分接近的点,体素滤波器确保在某个一定大小的立方体(或称体素)内仅有一个点,相当于对三维空间进行了降采样,从而节省了内存空间)

#include <iostream>
#include <fstream>
using namespace std;

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <Eigen/Geometry> 
#include <boost/format.hpp>  // for formating strings
#include <pcl/point_types.h> 
#include <pcl/io/pcd_io.h> 
#include <pcl/filters/voxel_grid.h>
#include <pcl/visualization/pcl_visualizer.h>
#include <pcl/filters/statistical_outlier_removal.h>

int main( int argc, char** argv )
{
    vector<cv::Mat> colorImgs, depthImgs;    // 彩色图和深度图
    vector<Eigen::Isometry3d> poses;         // 相机位姿
    
    ifstream fin("./data/pose.txt");
    if (!fin)
    {
        cerr<<"cannot find pose file"<<endl;
        return 1;
    }
    
    for ( int i=0; i<5; i++ )
    {
        boost::format fmt( "./data/%s/%d.%s" ); //图像文件格式
        colorImgs.push_back( cv::imread( (fmt%"color"%(i+1)%"png").str() ));
        depthImgs.push_back( cv::imread( (fmt%"depth"%(i+1)%"pgm").str(), -1 )); // 使用-1读取原始图像
        
        double data[7] = {0};
        for ( int i=0; i<7; i++ )
        {
            fin>>data[i];
        }
        Eigen::Quaterniond q( data[6], data[3], data[4], data[5] );
        Eigen::Isometry3d T(q);
        T.pretranslate( Eigen::Vector3d( data[0], data[1], data[2] ));
        poses.push_back( T );
    }
    
    // 计算点云并拼接
    // 相机内参 
    double cx = 325.5;
    double cy = 253.5;
    double fx = 518.0;
    double fy = 519.0;
    double depthScale = 1000.0;
    
    cout<<"正在将图像转换为点云..."<<endl;
    
    // 定义点云使用的格式:这里用的是XYZRGB
    typedef pcl::PointXYZRGB PointT; 
    typedef pcl::PointCloud<PointT> PointCloud;
    
    // 新建一个点云
    PointCloud::Ptr pointCloud( new PointCloud ); 
    for ( int i=0; i<5; i++ )
    {
        PointCloud::Ptr current( new PointCloud );
        cout<<"转换图像中: "<<i+1<<endl; 
        cv::Mat color = colorImgs[i]; 
        cv::Mat depth = depthImgs[i];
        Eigen::Isometry3d T = poses[i];
        for ( int v=0; v<color.rows; v++ )
            for ( int u=0; u<color.cols; u++ )
            {
                unsigned int d = depth.ptr<unsigned short> ( v )[u]; // 深度值
                if ( d==0 ) continue; // 为0表示没有测量到
                if ( d >= 7000 ) continue; // 深度太大时不稳定,去掉
                Eigen::Vector3d point; 
                point[2] = double(d)/depthScale; 
                point[0] = (u-cx)*point[2]/fx;
                point[1] = (v-cy)*point[2]/fy; 
                Eigen::Vector3d pointWorld = T*point;
                
                PointT p ;
                p.x = pointWorld[0];
                p.y = pointWorld[1];
                p.z = pointWorld[2];
                p.b = color.data[ v*color.step+u*color.channels() ];
                p.g = color.data[ v*color.step+u*color.channels()+1 ];
                p.r = color.data[ v*color.step+u*color.channels()+2 ];
                current->points.push_back( p );
            }
        // depth filter and statistical removal 
        PointCloud::Ptr tmp ( new PointCloud );
        pcl::StatisticalOutlierRemoval<PointT> statistical_filter;
        statistical_filter.setMeanK(50);
        statistical_filter.setStddevMulThresh(1.0);
        statistical_filter.setInputCloud(current);
        statistical_filter.filter( *tmp );
        (*pointCloud) += *tmp;
    }
    
    pointCloud->is_dense = false;
    cout<<"点云共有"<<pointCloud->size()<<"个点."<<endl;
    
    // voxel filter 
    pcl::VoxelGrid<PointT> voxel_filter; 
    voxel_filter.setLeafSize( 0.01, 0.01, 0.01 );       // resolution 体素滤波器以后的点云地图分辨啦(参数含义为每立方厘米有一个点)
    PointCloud::Ptr tmp ( new PointCloud );
    voxel_filter.setInputCloud( pointCloud );
    voxel_filter.filter( *tmp );
    tmp->swap( *pointCloud );
    
    cout<<"滤波之后,点云共有"<<pointCloud->size()<<"个点."<<endl;
    
    pcl::io::savePCDFileBinary("map.pcd", *pointCloud );
    return 0;
}

  结果及分析;

 

 

正在将图像转换为点云...
转换图像中: 1
转换图像中: 2
转换图像中: 3
转换图像中: 4
转换图像中: 5
点云共有895060个点.
滤波之后,点云共有448175个点.

  a:  由于滤波方法的实现,左(未加滤波)右图中可以看到异常点的差别

  b: 体素滤波器,可以看到点云数量明显减少,且在当前分辨率参数下,视觉上感受不到差异。

  点云总结:

    可以有RGB-D图像高效生成,不需要额外的处理

    点云地图是“基础的”或“初级的”,更接近于传感器读取的原始数据,。具有一些基本功能,通常用于调试和基本的显示,不变直接用于应用程序

     从点云地图出发,可构建占据网格地图,以供导航算法查询是否可以通过;利用泊松重建方法,通过基本的点云重建物体的网格地图,得到物体的表面信息。

      也可通过Surfel表达物体的表面,以面元作为地图的基本单位。

 

  3.2 八叉树地图

    在导航中比较常用,本身具有较好的压缩性能,灵活的,又能随时更新的地图形式(相比点云,规模大,无法处理运动物体的特点)

    3.2.1) 八叉树相比于点云地图节省空间解释:

      在八叉树地图中,节点存储它是否被占据的信息,然而,当某个方块的所有子节点都被占据或都不被占据时,就没必要展开这个节点。实际当中,由于物体经常连在一起,空白的地方也会常常连在一起,所以大多数八叉树节点无须展开到叶子层面。

    3.2.2) 八叉树节点存储是否被占据信息表达

      采用概率对数值Log-odds动态的建模了地图中的障碍物信息。变换公式为:

       其中,x表示占据概率0~1y为存储的概率对数值。

      当y=0时,对应x0.5,随着不断观测到“占据”时,y值不断增大,此时x逐渐增大,接近于1

      当查询某点是否被占据时,读取节点值y,通过反变换得到概率信息。

    3.2.3) 八叉树更新规律

      在深度值对应的空间点上观察到了一个占据数据,并且,从相机光心出发到这个点的线段上,应该是没有物体的(否则会被遮挡)。

    3.2.4) 实现

      a: octomap 提供了许多种八叉树:带地图的,带占据信息的,亦可自定义节点携带哪些信息

        此处使用最基本的八叉树地图(octomap::OcTree

      b: 先生成点云信息,然后讲点云信息存入八叉树地图,并给相机光心位置信息(使用octomap提供的内部点云结构,相比PCL点云信息稍微简单,只携带点的空间位置信息)

      c: 根据存储的点云信息,更新内部的占据概率,最后保存为压缩后的八叉树地图。

      d: 使用可视化程序octovis查看存储的八叉树地图文件.bt

      e: 在可视化程序中,可调节地图的分辨率

      f: 提供额外功能: 方便地查询任意点的占据概率,以此设计在地图中进行导航的方法

      g: 利用相同的图片信息生成的文件,八叉树地图文件大小明显小于上一小节生成的点云文件,可有效的建模较大的场景

 

    // 计算点云并拼接
    // 相机内参 
    double cx = 325.5;
    double cy = 253.5;
    double fx = 518.0;
    double fy = 519.0;
    double depthScale = 1000.0;
    
    cout<<"正在将图像转换为 Octomap ..."<<endl;
    
    // octomap tree 
    octomap::OcTree tree( 0.05 ); // 参数为分辨率
    
    for ( int i=0; i<5; i++ )
    {
        cout<<"转换图像中: "<<i+1<<endl; 
        cv::Mat color = colorImgs[i]; 
        cv::Mat depth = depthImgs[i];
        Eigen::Isometry3d T = poses[i];
        
        octomap::Pointcloud cloud;  // the point cloud in octomap 
        
        for ( int v=0; v<color.rows; v++ )
            for ( int u=0; u<color.cols; u++ )
            {
                unsigned int d = depth.ptr<unsigned short> ( v )[u]; // 深度值
                if ( d==0 ) continue; // 为0表示没有测量到
                if ( d >= 7000 ) continue; // 深度太大时不稳定,去掉
                Eigen::Vector3d point; 
                point[2] = double(d)/depthScale; 
                point[0] = (u-cx)*point[2]/fx;
                point[1] = (v-cy)*point[2]/fy; 
                Eigen::Vector3d pointWorld = T*point;
                // 将世界坐标系的点放入点云
                cloud.push_back( pointWorld[0], pointWorld[1], pointWorld[2] ); 
            }
            
        // 将点云存入八叉树地图,给定原点,这样可以计算投射线
        tree.insertPointCloud( cloud, octomap::point3d( T(0,3), T(1,3), T(2,3) ) );     
    }
    
    // 更新中间节点的占据信息并写入磁盘
    tree.updateInnerOccupancy();
    cout<<"saving octomap ... "<<endl;
    tree.writeBinary( "octomap.bt" );

 

4. TSDF地图和Fusion系列

  4.1) 之前地图模型总结

    4.1.1)以定位为主体,地图拼接作为后续加工步骤存在于SLAM框架中。该框架成为主流的原因:定位算法可以满足实时性要求,而地图的加工可以在关键帧处进行处理,无须实时响应。

    4.1.2) 并没有对稠密地图进行优化——“鬼影”现象

  4.2)实时三维重建

    希望重建的地图结果是光滑的,完整的,符合对地图的认知,基于此,出现了一种以“建图”为主体,而定位处于次要地位的的做法——实时三维重建

    4.2.1)特点:

      由于三维重建把重建准确地图作为主要目标,所以基本都需要利用GPU进行加速,甚至需要非常高级的GPU或多个GPU进行并行加速。

      SLAM是朝轻量级,小型化方向发展,有些方案甚至放弃了建图和回环检测部分,只保留了视觉里程计;而实施重建在朝大规模、大型动态场景的重建方向发展

      利用RGB-D图像进行实时重建形成了一个重要的发展方向陆续产生了Kinect FusionDynamic FusionElastic FusionFusion4DVolumn Deform等成果。

  4.3TSDFTruncated Signed Distance Function)地图

    是一种网格式的地图。

 

 

参考链接

深度滤波器(1)

视觉SLAM十四讲 第七章 三角化估计深度


 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM