点云基本操作汇总

        本节目录如下:

目录

1、ICP变换

1.1 激光点云的ICP

1.1.1 ICP代码 

1.1.2 ICP的优缺点

1.2 视觉SLAM中的ICP

1.2.1 SVD方法

1.2.2 非线性优化

2、NDT变换

2.1 NDT的代码

3、点云滤波

3.1 点云数据结构

3.2 点云滤波基本操作

3.2.1 直通滤波器 

3.2.2 体素滤波器 

3.2.3 统计滤波器

3.3.4 半径滤波器

3.3.5 均匀采样

3.3.6 条件滤波器

3.3.7 索引提取

3.3.8 其他点云滤波方法

3.2 RGB-D SLAM中的点云滤波操作

4、回环检测中的scan-context

4.1 RGB-D SLAM讲到的视觉的回环检测方法



        激光SLAM里程计方案有以下几种方式:

         其中基于栅格和基于匹配的方法有所了解,基于栅格的方法可以看gmapping的相关博客,这里介绍基于直接匹配ICPNDT的方案。

        激光点云的帧间匹配问题主要由两种方式实现,一种是ICP,另一种是更加常用的NDT(Normal Distributions Transform,正太分布变换)

1、ICP变换

        ICP(Iterative Closest Point,ICP)求解是3D-3D位姿估计问题中并没有出现相机模型,也就是说,仅考虑3D点之间的变换时,和相机并没有关系。

        在激光SLAM和视觉SLAM中都会遇到ICP问题,不过由于激光数据特征不够丰富,我们无从知道两个点集之间的帧间匹配关系,只能认为距离最近的两个点为同一个,所以这个方法称为迭代最近点。而在视觉中,特征点为我们提供了较好的匹配关系,所以整个问题就变得更简单了。在RGB-D SLAM中,可以用这种方式估计相机位姿。下文我们用ICP指代匹配好的两组点之间的运动估计问题。

        视觉上的ICP求解分为两种方式:利用线性代数的求解(主要是SVD),以及利用非线性优化的方法。

1.1 激光点云的ICP

        ICP系列有很多方案,基本组合思路如下:

        我们这里记录的方法是基于点到点的ICP

1.1.1 ICP代码 

 https://github.com/jiamen/Self-driving-vehicle-Practice/blob/master/01_Localization/ICP_PCL_DEMO/iterative_icp_space.cpp

相关问题汇总参考如下博客:

https://blog.csdn.net/lilynothing/article/details/80170521

1.1.2 ICP的优缺点

        1. 在较好的初值情况下,可以得到很好的算法收敛性。但是这也是一个缺点,就是初始值不好的话,结果也会收到影响。

        2. 在找匹配点的时候,认为距离最近的点就是对应点,这点比较果断。CPD会在每个距离之前加上权值,所以相比ICP有一定的优化效果。

        3. ICP对于形状相似,但是角度偏差比较大的情况,匹配效果比较差。因为是基于最近点找的。CPD的话是全局的考虑,如果是局部和全局进行匹配的话,CPD不一定能取得比ICP更好的结果。

1.2 视觉SLAM中的ICP

        给两组已经匹配好的3D点,计算相对位姿变换,写代码。

        匹配两组已知坐标的3D点当然是采用ICP,参考《视觉SLAM十四讲》,ICP的解法一共有两种:SVD方法非线性优化方法,下面过一遍SVD方法的推导过程:   
参考:给两组已经匹配好的3D点,计算相对位姿变换,写代码

1.2.1 SVD方法

构建最小二乘的代价函数,求得使误差平方和达到最小的Rt

 定义两组点的质心

 对代价函数做如下处理(后面添加的这两步会达到R与t分离的目的):

上面三项中最后一项求和为零,因此代价函数变为:

第一项只和R有关,因此我们可以先求得一个R使得第一项最小,然后再求t

        我们记去质心的点分别为q_{i} = p_{i} - p 和q_{i}^{'} = p_{i}^{'} - p^{'},我们对第一项展开得:

第一项和第二项都与R无关,因此最后优化目标函数变为:

最后通过SVD方法求得使得上述代价函数最小的R,先定义矩阵:

对其进行SVD分解

当W满秩时,R为:

解得R之后,就可以进一步求得t。代码如下:具体的应用代码在《SLAM14讲中》pose_estimate_3d3d.cpp文件中,具体的做法是先对匹配好的3D点使用SVD求解,再对求解出的R和t进行BA优化。代码在如下网址:

https://github.com/jiamen/SLAMStudy/blob/main/ch7_ICP/pose_estimate_3d3d.cpp

// 《SLAM14讲》SVD求解  P174
void pose_estimation_3d3d(
        const vector<Point3f>& pts1,
        const vector<Point3f>& pts2,
        Mat& R, Mat& t)
{
    Point3f p1, p2;                     // center of mass  质心
    int N = pts1.size();
    
    // 求和
    for(int i=0; i<N; i ++)
    {
        p1 += pts1[i];
        p2 += pts2[i];
    }
    // 求平均值
    p1 = Point3f ( Vec3f(p1) / N );
    p2 = Point3f ( Vec3f(p2) / N );

    vector<Point3f> q1(N), q2(N);       // remove the center,去中心化之后的点
    for(int i=0; i<N; i ++)
    {
        q1[i] = pts1[i] - p1;
        q2[i] = pts2[i] - p2;
    }

    // compute q1*q2^T 求出《SLAM14讲》P174页的 W
    Eigen::Matrix3d W = Eigen::Matrix3d::Zero();
    for(int i=0; i<N; i ++)
    {
        W += Eigen::Vector3d(q1[i].x, q1[i].y, q1[i].z) * Eigen::Vector3d(q2[i].x, q2[i].y, q2[i].z).transpose();
    }
    cout << "W = " << W << endl;

    // SVD on W
    Eigen::JacobiSVD<Eigen::Matrix3d> svd(W, Eigen::ComputeFullU | Eigen::ComputeThinV);
    Eigen::Matrix3d U = svd.matrixU();      // 得到U矩阵
    Eigen::Matrix3d V = svd.matrixV();      // 得到V矩阵

    // 第2帧到第1帧的变换,与PnP中第1    determinant行列式
    if( U.determinant() * V.determinant() < 0 )
    {
        for(int x=0; x < 3; x ++)
        {
            U(x, 2) *= -1;
        }
    }

    cout << "U = " << U << endl;
    cout << "V = " << V << endl;

    Eigen::Matrix3d R_ = U * (V.transpose());
    Eigen::Vector3d t_ = Eigen::Vector3d( p1.x, p1.y, p1.z ) - R_ * Eigen::Vector3d(p2.x, p2.y, p2.z);      // P174页 式7.53 t* = p - R p'


    // convert to cv::Mat 将矩阵和向量统一转换为转换为Mat矩阵形式
    R = (Mat_<double> (3,3) <<
            R_(0,0), R_(0,1), R_(0,2),
            R_(1,0), R_(1,1), R_(1,2),
            R_(2,0), R_(2,1), R_(2,2)
        );

    t = (Mat_<double>(3,1) <<t_(0,0), t_(1,0), t_(2,0));
}

其他代码也是类似:

#include <iostream>
#include <Eigen/Core>
#include <Eigen/Geometry>

using namespace std;

int main()
{
    // 生成旋转矩阵
    Eigen::Matrix3d R = Eigen::Matrix3d::Identity();
    Eigen::AngleAxisd rotationVector(M_PI/4, Eigen::Vector3d(0,0,1));
    R = rotationVector.toRotationMatrix();
    cout<<R<<endl<<endl;

    // 生成一系列原始点
    Eigen::Vector3d p1 = Eigen::Vector3d(1,2,3);
    Eigen::Vector3d p2 = Eigen::Vector3d(6,5,4);
    Eigen::Vector3d p3 = Eigen::Vector3d(8,7,9);
    vector<Eigen::Vector3d> pA = {p1, p2, p3};

    // 生成旋转后的点
    vector<Eigen::Vector3d> pB;
    for(auto p:pA)
    {
        pB.emplace_back(R*p);
    }

    // 求两个点云的中心
    Eigen::Vector3d qA = Eigen::Vector3d(0,0,0), qB = Eigen::Vector3d(0,0,0);
    for(int i = 0; i< pA.size(); i++)
    {
        for(int j = 0; j<3; j++)
        {
            qA[j] += pA[i][j];// pA[i][j]中每个向量是纵着排列,所以循环一次相当于求每一行的和
            qB[j] += pB[i][j];
        }
    }
    qA = qA/pA.size();
    qB = qB/pB.size();

    // 求去中心的点云
    for(int i = 0; i<pA.size(); i++)
    {
        pA[i] = pA[i]-qA;
        pB[i] = pB[i]-qB;
    }

    // 求矩阵W
    Eigen::Matrix3d W = Eigen::Matrix3d::Zero();
    for(int i = 0; i<pA.size(); i++)
    {
        W += pA[i]*pB[i].transpose();
    }

    // SVD分解
    Eigen::JacobiSVD<Eigen::Matrix3d> svd(W,Eigen::ComputeFullU|Eigen::ComputeFullV);
    Eigen::Matrix3d U = svd.matrixU();
    Eigen::Matrix3d V = svd.matrixV();

    Eigen::Matrix3d Rr = U*V.transpose();
    cout<<Rr<<endl;
}

1.2.2 非线性优化

                求解可以看前面PnP部分重投影误差的推导过程,由于这里没有是直接对3D点进行操作,没有相机模型,所以可以直接用下面公式;

         可以得到:

         在对ICP问题使用非线性优化来计算时,我们依然希望使用李代数来表达相机位姿。与SVD思路不同的地方在于,在优化过程中我们不仅考虑相机的位姿,同时会优化3D点的空间位置。对我们来说,RGB-D相机每次可以观测到路表单的三维位置,从而产生一个观测数据

代码如下:


2、NDT变换

        NDT(Normal Distribution Transform):NDT算法的基本思想是构建多维变量的正太分布,如果变换参数是两个点云的最佳匹配,则变换点的概率密度很大。因此,用优化的方法求出使得概率密度之和最大的变换参数。

NDT与ICP的不同:

        1、ICP要求点云之间尽量对应,而NDT是求栅格内点云的概率分布,对单点匹配的依赖程度降低,允许存在小部分差异的匹配;

        2、效率上,NDT也高于ICP。原因是ICP需要迭代匹配。

        对原分布求最大化,相当于对负对数求最小化。

2.1 NDT的代码


3、点云滤波

3.1 点云数据结构

        在RGB-D SLAM中用到的点数据结构和点云数据结构分别是: 

// 类定义
typedef pcl::PointXYZRGBA PointT;
typedef pcl::PointCloud<PointT> PointCloud;

3.2 点云滤波基本操作

        点云滤波属于点云数据预处理操作,点云数据预处理解决的问题有:

  • 点云范围、处理对象区域
  • 点云遮挡、局部视角
  • 点云数据量大
  • 点云采集噪声、平滑处理

3.2.1 直通滤波器 

 具体的代码操作如下:

pcl::PassThroungh<pcl::PointXYZ> pass;

// 创建滤波器对象
pcl::PassThrough<pcl::PointXYZ> pass;

// 设置输入点云
pass.setInputCloud(cloud);

// 滤波字段名被设置为z轴方向
pass.setFilterFieldName("z");

// 可接受的范围为(0.0, 0.5)
pass.setFilterLimits(0.0, 0.5);

// 设置保留范围内,还是过滤掉范围内
// pass.setFilterLimitsNegative(true);

// 执行滤波,保存过滤结果在cloud_filtered
pass.filter(*cloud_filtered);

3.2.2 体素滤波器 

        体素滤波器算法流程如下:

 代码如下:

pcl::VoxelGrid<pcl::PCLPointCloud2> vox;

// 创建滤波器对象
pcl::VoxelGrid<pcl::PointXYZ> vox;

// 设置输入点云
pcl::setInputCloud(cloud);

// 设置滤波时创建的体素体积为1cm的立方体
vox.setLeafSize(0.01f, 0.01f, 0.01f);

// 执行滤波,保存过滤结果过在cloud_filtered
vox.filter(*cloud_filtered);

3.2.3 统计滤波器

         统计滤波器算法流程如下:

设置代码如下:

pcl::StatisticalOutLierRemoval<pcl::PointXYZ> sor;

// 创建滤波器对象
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;

// 设置输入点云
sor.setInputCloud(cloud);

// 设置在进行统计时考虑查询点临近点数
sor.setMeanK(50);

// 设置判断是否为离群点的阈值
sor.setStddevMulThresh(1.0);

// 执行滤波,保存过滤结果在cloud_filtered
sor.filter(*cloud_filtered);

效果如下:

3.3.4 半径滤波器

        算法思路:

        1. 为每个点设置搜索半径r,在半径r内统计邻域点个数;

        2. 设置邻域点集数阈值min,邻域点数少于min的点会被删除。

代码如下:

 效果如下:

3.3.5 均匀采样

代码如下:

效果如下:

3.3.6 条件滤波器

     

3.3.7 索引提取

3.3.8 其他点云滤波方法

3.3.11 双边滤波

3.3.12 高斯滤波

3.2 RGB-D SLAM中的点云滤波操作

        RGB-D SLAM在最后做点云拼接融合时,用到了上述两种滤波操作,分别是直通滤波器pcl::PassThrought<pcl::PointXYZ> pass体素(网格)滤波器pcl::VoxelGrid<pcl::PCLPointCloud2> vox。原因是使用网格滤波器,用来调整地图分辨率;使用Z方向区间滤波器,由于rgbd有效深度空间有限,把太远的点去掉。

        代码如下:

    /*
     * 拼接点云地图
     * */
    cout << "saving the point cloud map ..." << endl;
    PointCloud::Ptr output(new PointCloud());       // 全局地图
    PointCloud::Ptr tmp(new PointCloud());
 
    pcl::VoxelGrid<PointT> voxel;       // 网格滤波器,调整地图分辨率
    pcl::PassThrough<PointT> pass;      // z方向区间滤波器,由于rgbd相机的有效深度区间有限,把太远的去掉
 
    pass.setFilterFieldName("z");
    pass.setFilterLimits(0.0, 4.0);     // 4m以上的就不要了
    
    // ① 设置滤波时创建的体素体积为1cm的立方体
    double gridsize = atof(pd.getData("voxel_grid").c_str());   // 分辨图可以在parameters.txt里调
    voxel.setLeafSize(gridsize, gridsize, gridsize);
 
    for (size_t i=0; i<keyframes.size(); i ++)
    {
        // 从g2o里取出一帧
        g2o::VertexSE3* vertex = dynamic_cast<g2o::VertexSE3*>(globalOptimizer.vertex(keyframes[i].frameID));
        Eigen::Isometry3d pose = vertex->estimate();        // 该帧优化后的位姿
        PointCloud::Ptr newCloud = image2PointCloud(keyframes[i].rgb, keyframes[i].depth, camera);  // 转成点云
 
        // 以下是两种滤波操作,分别是网格滤波器和区间滤波
        voxel.setInputCloud(newCloud);
        voxel.filter(*tmp);
        pass.setInputCloud(tmp);
        pass.filter(*newCloud);
 
        // 把点云变换后加入到全局地图中
        pcl::transformPointCloud(*newCloud, *tmp, pose.matrix());
        *output += *tmp;
 
        tmp->clear();
        newCloud->clear();
    }
     
    // ②设置输入点云
    voxel.setInputCloud(output);
    // ③执行滤波,保存过滤结果在tmp
    voxel.filter(*tmp);
 
 
    // 存储
    pcl::io::savePCDFile("./result.pcd", *tmp);
 
    cout << "Final map is saved." << endl;
 
    return 0;
}

4、回环检测中的scan-context

4.1 RGB-D SLAM讲到的视觉的回环检测方法

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

家门Jm

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值