无人驾驶学习笔记-NDT 配准

2023-05-16

目录

1. NDT 的算法处理流程

2. NDT 公式推导 

3. NDT 实例

        3.1 常规NDT的位姿估计

        3.2 front_end_node

1、ROS常规初始化

2、初始化操作:读取传感器数据、获取lidar-to-imu变换矩阵、保证三个传感器时间对齐

3、GNSS初始化位姿、更新里程计位姿

4、前端里程计

3.3 front_end

update()

UpdateNewFrame()


1. NDT 的算法处理流程

 

 

2. NDT 公式推导 

下面部分引自无人驾驶汽车系统入门(十三)——正态分布变换(NDT)配准与无人车定位_AdamShan的博客-CSDN博客_ndt无人驾驶

使用正态分布来表示原本离散的点云有诸多好处,这种分块(通过 一个个cell)的光滑的表示是连续可导的,每一个概率密度函数可以被认为是一个局部表面的近似,它不但描述了这个表面在空间中的位置,同时还包含了这个表面的方向和光滑性等信息。

这里写图片描述

        上图中立方体的边长为1米,其中越明亮的位置表示概率越高。此外,局部表面的方向和光滑性则可以通过协方差矩阵的特征值和特征向量反映出来。我们以三维的概率密度函数为例,如果三个特征值很接近,那么这个正态分布描述的表面是一个球面,如果一个特征值远大于另外两个特征值,则这个正态分布描述的是一条线,如果一个特征值远小于另外两个特征值,则这个正态分布描述的是一个平面。下图描述了协方差矩阵特征值与表面形状之间的关系:

这里写图片描述

变换参数和最大似然

 3. NDT 实例

        NDT并没有比较两个点云点与点之间的差距,而是先将参考点云(即高精度地图)转换为多维变量的正态分布(使用正态分布来表示原本离散的点云),如果变换参数能使得两幅激光数据匹配的很好,那么变换点在参考系中的概率密度将会很大。因此,可以考虑用优化的方法求出使得概率密度之和最大的变换参数,此时两幅激光点云数据将匹配的最好。

PCL 中ndt相关参数

1. setInputCloud (cloud_source) 设置原始点云 
2. setInputTarget (cloud_target) 设置目标点云 
3. setMaxCorrespondenceDistance() 设置最大对应点的欧式距离,只有对应点之间的距离小于该设置值的对应点才作为ICP计算的点对。默认值为:1.7976931348623157e+308,基本上对所有点都计算了匹配点。 
4. 三个迭代终止条件设置: 
1) setMaximumIterations() 设置最大的迭代次数。迭代停止条件之一 
2) setTransformationEpsilon() 设置前后两次迭代的转换矩阵的最大容差(epsilion),一旦两次迭代小于这个最大容差,则认为已经收敛到最优解,迭代停止。迭代停止条件之二,默认值为:0 
3) setEuclideanFitnessEpsilon() 设置前后两次迭代的点对的欧式距离均值的最大容差,迭代终止条件之三,默认值为:-std::numeric_limits::max () 
迭代满足上述任一条件,终止迭代。
5.getFinalTransformation () 获取最终的配准的转化矩阵,即原始点云到目标点云的刚体变换,返回Matrix4数据类型,该数据类型采用了另一个专门用于矩阵计算的开源c++库eigen。 
6. align (PointCloudSource &output) 进行ICP配准,输出变换后点云
7. hasConverged() 获取收敛状态,注意,只要迭代过程符合上述三个终止条件之一,该函数返回true。 
8. min_number_correspondences_  最小匹配点对数量,默认值为3,即由空间中的非共线的三点就能确定刚体变换,建议将该值设置大点,否则容易出现错误匹配。 
9. 最终迭代终止的原因可从convergence_criteria_变量中获取
注意:getFitnessScore()用于获取迭代结束后目标点云和配准后的点云的最近点之间距离的均值。

参考下面作者的代码示例 NDT在slam中前端的用法

从零开始做自动驾驶定位(四): 前端里程计之初试 - 知乎

3.1 常规NDT的位姿估计

#include <iostream>
#include <pcl/io/pcd_io.h>

#include <pcl/registration/ndt.h>
#include <pcl/filters/approximate_voxel_grid.h>

#include <pcl/visualization/pcl_visualizer.h>

// 读取点云pcd文件
pcl::PointCloud<pcl::PointXYZ>::Ptr read_cloud_point(std::string const &file_path){
    // Loading first scan.
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
    if (pcl::io::loadPCDFile<pcl::PointXYZ> (file_path, *cloud) == -1)
    {
        PCL_ERROR ("Couldn't read the pcd file\n");
        return nullptr;
    }
    return cloud;
}

void visualizer(pcl::PointCloud<pcl::PointXYZ>::Ptr target_cloud, pcl::PointCloud<pcl::PointXYZ>::Ptr output_cloud){
    // 1、初始化点云可视化
    boost::shared_ptr<pcl::visualization::PCLVisualizer>
            viewer_final (new pcl::visualization::PCLVisualizer ("3D Viewer"));
    viewer_final->setBackgroundColor (0, 0, 0);

    ///2、对目标点云和匹配后的点云上色
    pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ>
            target_color (target_cloud, 255, 0, 0);
    viewer_final->addPointCloud<pcl::PointXYZ> (target_cloud, target_color, "target cloud");
    viewer_final->setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE,
                                                    1, "target cloud");

    pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ>
            output_color (output_cloud, 0, 255, 0);
    viewer_final->addPointCloud<pcl::PointXYZ> (output_cloud, output_color, "output cloud");
    viewer_final->setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE,
                                                    1, "output cloud");

    // 3、开始可视化,坐标系和相机参数
    viewer_final->addCoordinateSystem (1.0, "global");
    viewer_final->initCameraParameters ();

    ///4、Wait until visualizer window is closed.
    while (!viewer_final->wasStopped ())
    {
        viewer_final->spinOnce (100);
        boost::this_thread::sleep (boost::posix_time::microseconds (100000));
    }
}

int main(int argc, char **argv) {
    // 1、读取pcd文件(source和target)
    auto target_cloud = read_cloud_point(argv[1]);
    std::cout << "Loaded " << target_cloud->size () << " data points from cloud1.pcd" << std::endl;

    auto input_cloud = read_cloud_point(argv[2]);
    std::cout << "Loaded " << input_cloud->size () << " data points from cloud2.pcd" << std::endl;

    // 2、过滤输入点云(减少数据量到10%)
    pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud (new pcl::PointCloud<pcl::PointXYZ>);
    pcl::ApproximateVoxelGrid<pcl::PointXYZ> approximate_voxel_filter;
    approximate_voxel_filter.setLeafSize(0.2, 0.2, 0.2);
    approximate_voxel_filter.setInputCloud(input_cloud);
    approximate_voxel_filter.filter(*filtered_cloud);
    std::cout<<"Filtered cloud contains "<< filtered_cloud->size() << "data points from cloud2.pcd" << std::endl;
    
    // 3、初始化NDT并设置参数
    pcl::NormalDistributionsTransform<pcl::PointXYZ, pcl::PointXYZ> ndt;
    ndt.setTransformationEpsilon(0.01);// 优化过程是否收敛到的阈值
    ndt.setStepSize(0.1);// 设置牛顿法优化的最大步长
    ndt.setResolution(1.0);// 设置网格化时立方体的边长
    ndt.setMaximumIterations(35); // 优化的迭代次数

    ndt.setInputSource(filtered_cloud); // 待匹配点云
    ndt.setInputTarget(target_cloud); // 目标点云

    // 4、初始化变换参数
    Eigen::AngleAxisf init_rotation(0.6931, Eigen::Vector3f::UnitZ());
    Eigen::Translation3f init_translation (1.79387, 0.720047, 0);
    Eigen::Matrix4f init_guess = (init_translation * init_rotation).matrix();

    // 5、开始配准优化并保存配准后的点云图,保存下来
    pcl::PointCloud<pcl::PointXYZ>::Ptr output_cloud (new pcl::PointCloud<pcl::PointXYZ>);
    ndt.align(*output_cloud, init_guess);
    std::cout << "Normal Distribution Transform has converged:" << ndt.hasConverged()
              << "score: " << ndt.getFitnessScore() << std::endl;

    pcl::transformPointCloud(*input_cloud, *output_cloud, ndt.getFinalTransformation());
    pcl::io::savePCDFileASCII("cloud3.pcd", *output_cloud);

    // 6、可视化
    visualizer(target_cloud, output_cloud);

    return 0;
}

3.2 示例的前端激光雷达里程计流程

前端激光雷达里程计主要分为:点云下采样滤波、点云匹配、位姿估计、关键帧选取等步骤。具体流程为:

  • 第一帧点云数据设置为地图;

  • 提取关键帧点云,拼接成地图,保证稀疏点云数据;

  • 除了全局地图,还需要在当前帧附近形成滑动窗局部地图,减小计算量;

  • 匹配之前需要滤波,对点云稀疏化;可以使用PCL中的voxel_filter基本原理是把三维空间划分未等尺寸的立方体格子,在一个立方体格子内最多只留一个点,这样就起到稀疏作用。

  • 点云匹配对位姿预测值比较敏感,所以在载体运动时,不能以它上一帧的位姿作为这一帧的预测值,可以使用IMU预测,也可以使用运动模型预测。

3.3 front_end_node

激光雷达点云前端里程计主要包含以下步骤

  1. ROS常规初始化
  2. 初始化操作:读取传感器、获得lidar-to-imu变换矩阵、保证三个传感器时间对齐
  3. GNSS位姿初始化,更新里程计位姿
  4. 前端里程计

1、ROS常规初始化

        订阅发布节点,包含订阅sub 3个传感器消息:点云、imu、GNSS。
发布pub 5个节点:点云、局部地图、全局地图、点云里程计、GNSS,
坐标转换的Lidar-to-imu变换矩阵T
参数定义上订阅和发布的指针分别命名为XXX_sub_ptr或者XXX_pub_ptr,传感器数据队列命名为XXX_data_ptr,针对地图指针为XXX_map_ptr,最重要的前端里程计指针为front_end_ptr。
重要变量:

// 三个传感器sub消息
cloud_sub_ptr,imu_sub_ptr,gnss_sub_ptr
// 两个传感器发布pub节点
cloud_pub_ptr,gnss_pub_ptr
// 发布的局部、全局地图
local_map_pub_ptr,global_map_pub_ptr
// 发布的点云里程计
laser_odom_pub_ptr
// 前端
front_end_ptr

// 三个传感器队列buff
cloud_data_buff,imu_data_buff,gnss_data_buff
// lidar-to-imu变换矩阵
lidar_to_imu

// 三个指针:扫描点云、局部,全局地图
current_scan_ptr,local_map_ptr,global_map_ptr

ROS初始化

    ros::init(argc, argv, "front_end_node");
    ros::NodeHandle nh;
    // 三个订阅sub消息:点云、imu、GNSS
    std::shared_ptr<CloudSubscriber> cloud_sub_ptr = std::make_shared<CloudSubscriber>(nh, "/kitti/velo/pointcloud", 100000);
    std::shared_ptr<IMUSubscriber> imu_sub_ptr = std::make_shared<IMUSubscriber>(nh, "/kitti/oxts/imu", 1000000);
    std::shared_ptr<GNSSSubscriber> gnss_sub_ptr = std::make_shared<GNSSSubscriber>(nh, "/kitti/oxts/gps/fix", 1000000);
    // lidar-to-imu
    std::shared_ptr<TFListener> lidar_to_imu_ptr = std::make_shared<TFListener>(nh, "velo_link", "imu_link");
    // 发布pub节点:点云、局部地图、全局地图
    std::shared_ptr<CloudPublisher> cloud_pub_ptr = std::make_shared<CloudPublisher>(nh, "current_scan", 100, "/map");
    std::shared_ptr<CloudPublisher> local_map_pub_ptr = std::make_shared<CloudPublisher>(nh, "local_map", 100, "/map");
    std::shared_ptr<CloudPublisher> global_map_pub_ptr = std::make_shared<CloudPublisher>(nh, "global_map", 100, "/map");
    // 点云里程计、GNSS
    std::shared_ptr<OdometryPublisher> laser_odom_pub_ptr = std::make_shared<OdometryPublisher>(nh, "laser_odom", "map", "lidar", 100);
    std::shared_ptr<OdometryPublisher> gnss_pub_ptr = std::make_shared<OdometryPublisher>(nh, "gnss", "map", "lidar", 100);
    // 前端
    std::shared_ptr<FrontEnd> front_end_ptr = std::make_shared<FrontEnd>();

    // 三个数据队列buff
    std::deque<CloudData> cloud_data_buff;
    std::deque<IMUData> imu_data_buff;
    std::deque<GNSSData> gnss_data_buff;
    // lidar到imu的转换矩阵
    Eigen::Matrix4f lidar_to_imu = Eigen::Matrix4f::Identity();

    bool transform_received = false;
    bool gnss_origin_position_inited = false;
    bool front_end_pose_inited = false;
    // 三个指针:点云、局部地图、全局地图
    CloudData::CLOUD_PTR local_map_ptr(new CloudData::CLOUD());
    CloudData::CLOUD_PTR global_map_ptr(new CloudData::CLOUD());
    CloudData::CLOUD_PTR current_scan_ptr(new CloudData::CLOUD());
    
    double run_time = 0.0;
    double init_time = 0.0;
    bool time_inited = false;
    bool has_global_map_published = false;

2、初始化操作:读取传感器数据、获取lidar-to-imu变换矩阵、保证三个传感器时间对齐

主要是为了取出三个传感器数据,在取出之前先把最新扫描的传感器数据添加到队列buff中,然后取出过程中保证三个传感器时间对齐,最后要把读取的传感器数据从队列buff中剔除。

重要变量:

// lidar-to-imu变换矩阵
lidar_to_imu
// 三个传感器数据
cloud_data,imu_data ,gnss_data
    ros::Rate rate(100);
    while (ros::ok()) {
        ros::spinOnce();
        // 1.读取新扫描的数据添加到三个队列buff中,即更新传感器buff
        cloud_sub_ptr->ParseData(cloud_data_buff);
        imu_sub_ptr->ParseData(imu_data_buff);
        gnss_sub_ptr->ParseData(gnss_data_buff);

        if (!transform_received) {
            if (lidar_to_imu_ptr->LookupData(lidar_to_imu)) {
                transform_received = true;
            }
        } else {
            while (cloud_data_buff.size() > 0 && imu_data_buff.size() > 0 && gnss_data_buff.size() > 0) {
                // 1.读取三个传感器数据
                CloudData cloud_data = cloud_data_buff.front();
                IMUData imu_data = imu_data_buff.front();
                GNSSData gnss_data = gnss_data_buff.front();

                if (!time_inited) {
                    time_inited = true;
                    init_time = cloud_data.time;
                } else {
                    run_time = cloud_data.time - init_time;// 点云运行时间
                }
                // 2.保证三个传感器时间对齐
                double d_time = cloud_data.time - imu_data.time;
                if (d_time < -0.05) {
                    cloud_data_buff.pop_front();
                } else if (d_time > 0.05) {
                    imu_data_buff.pop_front();
                    gnss_data_buff.pop_front();
                } else
                 {  // 剔除读取出的传感器数据 
                    cloud_data_buff.pop_front();
                    imu_data_buff.pop_front();
                    gnss_data_buff.pop_front();

三种传感器:雷达点云、imu、GNSS的变量内容为:
激光雷达点云cloud_data.h
构成分为两部分:时间戳+点云指针

  //时间戳+点云指针
class CloudData {
  public:
    using POINT = pcl::PointXYZ;// 三维点
    using CLOUD = pcl::PointCloud<POINT>;// 点云
    using CLOUD_PTR = CLOUD::Ptr;// 点云指针

  public:
    CloudData()
      :cloud_ptr(new CLOUD()) {
    }

  public:
    double time = 0.0;
    CLOUD_PTR cloud_ptr;
};

imu_data.h
imu包含时间戳、线速度、角速度、方向角类(四元数+归一化)
类成员函数:四元数转旋转矩阵GetOrientationMatrix()

class IMUData {
  public:
    struct LinearAcceleration {
      double x = 0.0;
      double y = 0.0;
      double z = 0.0;
    };

    struct AngularVelocity {
      double x = 0.0;
      double y = 0.0;
      double z = 0.0;
    };
    // 方向角类(四元数+归一化)
    class Orientation {
      public:
        double x = 0.0;
        double y = 0.0;
        double z = 0.0;
        double w = 0.0;
      
      public:
        void Normlize() {
          double norm = sqrt(pow(x, 2.0) + pow(y, 2.0) + pow(z, 2.0) + pow(w, 2.0));
          x /= norm;
          y /= norm;
          z /= norm;
          w /= norm;
        }
    };

    double time = 0.0;
    LinearAcceleration linear_acceleration;
    AngularVelocity angular_velocity;
    Orientation orientation;
  
  public:
    // 把四元数转换成旋转矩阵送出去
    Eigen::Matrix3f GetOrientationMatrix();

gnss_data.h
GNSS数据包括:时间戳、地理坐标系、东北天坐标系。

class GNSSData {
  public:
    double time = 0.0;// 时间戳
    // 地理坐标系:经纬度、高度
    double longitude = 0.0;
    double latitude = 0.0;
    double altitude = 0.0;
    // 东北天坐标系
    double local_E = 0.0;
    double local_N = 0.0;
    double local_U = 0.0;

    int status = 0;
    int service = 0;

  private:
    static GeographicLib::LocalCartesian geo_converter;
    static bool origin_position_inited;

类成员函数:初始化位置、更新位姿。

    // 初始化位置reset
void GNSSData::InitOriginPosition() {
    geo_converter.Reset(latitude, longitude, altitude);
    origin_position_inited = true;
}
// 更新位姿
void GNSSData::UpdateXYZ() {
    if (!origin_position_inited) {
        LOG(WARNING) << "GeoConverter has not set origin position";
    }
    geo_converter.Forward(latitude, longitude, altitude, local_E, local_N, local_U);
}

获取Lidar-to-IMU的转换矩阵
test_frame_node.cpp中:初始化为

// 一个坐标系转换指针ptr:基坐标系雷达、子坐标系imu
    std::shared_ptr<TFListener> lidar_to_imu_ptr = std::make_shared<TFListener>(nh, "velo_link", "imu_link");

主要利用tf_lisener.cpp中的两个函数:

// 给lidar-to-imu赋值
bool TFListener::LookupData(Eigen::Matrix4f& transform_matrix) {
    try {
        tf::StampedTransform transform;
        listener_.lookupTransform(base_frame_id_, child_frame_id_, ros::Time(0), transform);
        TransformToMatrix(transform, transform_matrix);
        return true;
    } catch (tf::TransformException &ex) {
        return false;
    }
}
bool TFListener::TransformToMatrix(const tf::StampedTransform& transform, Eigen::Matrix4f& transform_matrix) {
    Eigen::Translation3f tl_btol(transform.getOrigin().getX(), transform.getOrigin().getY(), transform.getOrigin().getZ());
    
    double roll, pitch, yaw;
    tf::Matrix3x3(transform.getRotation()).getEulerYPR(yaw, pitch, roll);
    Eigen::AngleAxisf rot_x_btol(roll, Eigen::Vector3f::UnitX());
    Eigen::AngleAxisf rot_y_btol(pitch, Eigen::Vector3f::UnitY());
    Eigen::AngleAxisf rot_z_btol(yaw, Eigen::Vector3f::UnitZ());

    // 此矩阵为 child_frame_id 到 base_frame_id 的转换矩阵 zyx
    transform_matrix = (tl_btol * rot_z_btol * rot_y_btol * rot_x_btol).matrix();

    return true;
}

3、GNSS初始化位姿、更新里程计位姿

利用GNSS数据作为初始化位姿,然后从LIdar坐标系转化到imu坐标系,最后把估计位姿发布出去。
重要变量

// 里程计估计位姿
odometry_matrix
                   Eigen::Matrix4f odometry_matrix = Eigen::Matrix4f::Identity();
                    // 1.GNSS数据初始化位姿
                    if (!gnss_origin_position_inited) {
                        gnss_data.InitOriginPosition();
                        gnss_origin_position_inited = true;
                    }
                    // 2.GNSS数据更新里程计位姿
                    gnss_data.UpdateXYZ();
                    odometry_matrix(0,3) = gnss_data.local_E;
                    odometry_matrix(1,3) = gnss_data.local_N;
                    odometry_matrix(2,3) = gnss_data.local_U;
                    odometry_matrix.block<3,3>(0,0) = imu_data.GetOrientationMatrix();
                    // 从lidar坐标系转换到imu系
                    odometry_matrix *= lidar_to_imu;
                    // 3、发布消息
                    gnss_pub_ptr->Publish(odometry_matrix);

使用GNSS数据(平移)和IMU(旋转)数据初始化位姿、更新位姿的两个函数为
gnss_data.InitOriginPosition();
gnss_data.UpdateXYZ();
函数内部实现为:

geo_converter.Reset(latitude, longitude, altitude);
geo_converter.Forward(latitude, longitude, altitude, local_E, local_N, local_U);

4、前端里程计

这是最重要的里程计部分,其实就是对front_end文件进行详细解读。

  1. 前端里程计初始化
  2. 更新预测位姿为当前估计位姿
  3. 更新并发布位姿
  4. 获得当前帧点云和局部地图并发布(都是下采样)
// 1.前端里程计初始化
                    if (!front_end_pose_inited) {
                        front_end_pose_inited = true;
                        front_end_ptr->SetInitPose(odometry_matrix);
                    }
                    // 2.更新预测位姿为当前估计位姿
                    front_end_ptr->SetPredictPose(odometry_matrix);
                    // 3.更新位姿
                    Eigen::Matrix4f laser_matrix = front_end_ptr->Update(cloud_data);
                    // 4.发布位姿
                    laser_odom_pub_ptr->Publish(laser_matrix);
                    
                    // 5.获得当前帧点云和局部地图并发布(都是下采样)
                    // 获得当前扫描并发布
                    front_end_ptr->GetCurrentScan(current_scan_ptr);
                    cloud_pub_ptr->Publish(current_scan_ptr);
                    // 获得新局部地图并发布
                    if (front_end_ptr->GetNewLocalMap(local_map_ptr))
                        local_map_pub_ptr->Publish(local_map_ptr);
                }
                // 6.如果运行时间超了且全局地图还没发布,获得下采样后的全局地图并发布
                if (run_time > 460.0 && !has_global_map_published) {
                    if (front_end_ptr->GetNewGlobalMap(global_map_ptr)) {
                        global_map_pub_ptr->Publish(global_map_ptr);
                        has_global_map_published = true;
                    }
                }

接下来专门对front_end.cpp进行讲解。

3.4 front_end

首先建立一个Frame类数据结构:
Frame类两个:位姿+点云数据

class Frame {
      public:  
        Eigen::Matrix4f pose = Eigen::Matrix4f::Identity();
        CloudData cloud_data;
    };

重要变量

// 三个滤波器
cloud_filter_,local_map_filter_,display_filter_
// ndt匹配方法的对象
pcl::NormalDistributionsTransform<>::PTR ndt_ptr_
// 局部、全局地图队列
local_map_frames_,global_map_frames_
// 指针:局部、全局地图、点云结果
local_map_ptr_,global_map_ptr_,result_cloud_ptr_
// Frame对象
current_frame_
// 初始位姿,预测位姿
init_pose,predict_pose

初始化

    // 构造函数初始化
FrontEnd::FrontEnd()
    :ndt_ptr_(new pcl::NormalDistributionsTransform<CloudData::POINT, CloudData::POINT>()),
     local_map_ptr_(new CloudData::CLOUD()),
     global_map_ptr_(new CloudData::CLOUD()),
     result_cloud_ptr_(new CloudData::CLOUD()) {

    // 给个默认参数,以免类的使用者在匹配之前忘了设置参数
    cloud_filter_.setLeafSize(1.3, 1.3, 1.3);
    local_map_filter_.setLeafSize(0.6, 0.6, 0.6);
    display_filter_.setLeafSize(0.5, 0.5, 0.5);

    ndt_ptr_->setResolution(1.0);// NDT网格结构的分辨率
    ndt_ptr_->setStepSize(0.1);// 为More-Thuente线搜索设置最大步长
    ndt_ptr_->setTransformationEpsilon(0.01);// 为终止条件设置最大转换差异
    ndt_ptr_->setMaximumIterations(30);// 匹配迭代最大次数
}

主要的两个函数为更新位姿Update()更新关键帧UpdateNewFrame()

update()

输入当前帧扫描的点云图,对位姿进行更新
主要分为以下流程:

  1. 参数初始化
  2. 第一帧作为地图
  3. 其余帧进行正常匹配
  4. 每隔2米设置一个关键帧

1、参数初始化
获得时间戳,剔除无效点云,对点云下采样滤波,初始化四个位姿。

 // 1.参数初始化
    current_frame_.cloud_data.time = cloud_data.time;// 时间戳
    // 剔除无效点云:输入点云,输出点云,索引
    std::vector<int> indices;
    pcl::removeNaNFromPointCloud(*cloud_data.cloud_ptr, *current_frame_.cloud_data.cloud_ptr, indices);
    // 下采样滤波
    CloudData::CLOUD_PTR filtered_cloud_ptr(new CloudData::CLOUD());// 过滤的点云指针
    cloud_filter_.setInputCloud(current_frame_.cloud_data.cloud_ptr);
    cloud_filter_.filter(*filtered_cloud_ptr);
    // 四个位姿
    static Eigen::Matrix4f step_pose = Eigen::Matrix4f::Identity();// 初始化下一个位姿
    static Eigen::Matrix4f last_pose = init_pose_;// 更新上一帧位姿
    static Eigen::Matrix4f predict_pose = init_pose_;// 更新预测位姿
    static Eigen::Matrix4f last_key_frame_pose = init_pose_;// 更新上一帧位姿

2、第一帧为地图
此时把当前帧数据作为第一个关键帧,并更新局部地图容器和全局地图容器

 if (local_map_frames_.size() == 0) {
        current_frame_.pose = init_pose_;
        UpdateNewFrame(current_frame_);// 关键函数
        return current_frame_.pose;
    }

3、正常匹配

    ndt_ptr_->setInputSource(filtered_cloud_ptr);// 要匹配的点云
    ndt_ptr_->align(*result_cloud_ptr_, predict_pose);// 点云配准,变换后的点云在result中
    current_frame_.pose = ndt_ptr_->getFinalTransformation();// 获得位姿

4、关键帧选取

// 4.隔2米设置一个关键帧,匹配之后根据距离判断是否需要生成新的关键帧,如果需要,则做相应更新
    if (fabs(last_key_frame_pose(0,3) - current_frame_.pose(0,3)) + 
        fabs(last_key_frame_pose(1,3) - current_frame_.pose(1,3)) +
        fabs(last_key_frame_pose(2,3) - current_frame_.pose(2,3)) > 2.0) {
        //更新帧
        UpdateNewFrame(current_frame_);
        last_key_frame_pose = current_frame_.pose;
    }
    return current_frame_.pose;

UpdateNewFrame()

主要分为四步骤

创建Frame对象,保存关键帧点云
更新局部地图
更新ndt匹配的目标点云
更新全局地图


1、创建Frame对象,保存关键帧点云
由于用的是共享指针,所以直接复制只是复制了一个指针而已,此时无论你放多少个关键帧在容器里,这些关键帧点云指针都是指向的同一个点云

    Frame key_frame = new_key_frame;
    
    key_frame.cloud_data.cloud_ptr.reset(new CloudData::CLOUD(*new_key_frame.cloud_data.cloud_ptr));
    CloudData::CLOUD_PTR transformed_cloud_ptr(new CloudData::CLOUD());

2.更新局部地图

// 先加到滑窗队列中
    local_map_frames_.push_back(key_frame);
    while (local_map_frames_.size() > 20) {// 滑窗大小固定
        local_map_frames_.pop_front();
    }
    // 再把局部地图指针进行更新
    local_map_ptr_.reset(new CloudData::CLOUD());
    for (size_t i = 0; i < local_map_frames_.size(); ++i) {// 遍历滑动窗
       // 恢复下采样的点云数据,参数:输入,输出,估计的姿态
        pcl::transformPointCloud(*local_map_frames_.at(i).cloud_data.cloud_ptr, 
                                 *transformed_cloud_ptr, 
                                 local_map_frames_.at(i).pose);
        // 局部地图指针
        *local_map_ptr_ += *transformed_cloud_ptr;
    }
    // 最后表示局部地图更新完成
    has_new_local_map_ = true;

3.更新ndt匹配的目标点云

// 如果滑动窗数量小于10个,直接设置目标点云
    if (local_map_frames_.size() < 10) {
        ndt_ptr_->setInputTarget(local_map_ptr_);
    } 
    else // 否则,先对局部地图进行下采样,再加进去。
    {
        CloudData::CLOUD_PTR filtered_local_map_ptr(new CloudData::CLOUD());
        local_map_filter_.setInputCloud(local_map_ptr_);
        local_map_filter_.filter(*filtered_local_map_ptr);
        ndt_ptr_->setInputTarget(filtered_local_map_ptr);
    }

4.更新全局地图

  global_map_frames_.push_back(key_frame);
    if (global_map_frames_.size() % 100 != 0) {
        return;
    } else {
        global_map_ptr_.reset(new CloudData::CLOUD());
        for (size_t i = 0; i < global_map_frames_.size(); ++i) {
            pcl::transformPointCloud(*global_map_frames_.at(i).cloud_data.cloud_ptr, 
                                    *transformed_cloud_ptr, 
                                    global_map_frames_.at(i).pose);
            *global_map_ptr_ += *transformed_cloud_ptr;
        }
        has_new_global_map_ = true;
    }

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

无人驾驶学习笔记-NDT 配准 的相关文章

  • 【OpenFOAM】-olaFlow-算例3- currentWaveFlume

    算例路径 olaFlow tutorials currentWaveFlume 算例描述 波流耦合模拟 该算例提供了四种工况 1 Waves and forward current 2 Waves and backward current
  • 【OpenFOAM】-olaFlow-算例4- irreg45degTank

    算例路径 olaFlow tutorials irreg45degTank 算例描述 不规则波浪模拟 学习目标 不规则波浪模拟 olaFlow中单向不规则波采用线性波浪叠加法生成 基本原理如图2所受 需要提供对应波谱的周期 波高和相位的离散
  • 【OpenFOAM】-olaFlow-算例5- oppositeSolitariesFlume

    算例路径 olaFlow tutorials oppositeSolitariesFlume 算例描述 两列反向的孤立波相互作用 学习目标 熟练掌握olaFlow的造波设置 波浪方向与消波方向设置 算例快照 图1 两列反向孤立波相互作用 文
  • 【OpenFOAM】-olaFlow-算例6- waveFloatingObject

    算例路径 olaFlow tutorials waveFloatingObject 算例描述 波浪作用下的浮体的刚体运动 属于流固耦合 FSI 问题 学习目标 动网格设置和使用 网格变形控制 浮体的物理参数设置 浮体做刚体运动的约束设置 算
  • 【OpenFOAM】-olaFlow-算例7-波面自适应网格

    算例路径 none 算例描述 波面附近采用自适应网格划分 学习目标 动网格设置和使用 dynamicFvMesh dynamicRefineFvMesh 的各参数含义 学习体会 1 在结构附近的加密网格 自适应网格依然会对细网格进一步细化
  • 【OpenFOAM】-olaFlow-算例8-setOlaFlume

    算例路径 olaFlow tutorials setOlaFlume 算例描述 不规则底部的二维波浪水槽 且波场被 setOla 工具设置为初始条件 学习目标 使用 setOla 工具设置初始条件为波浪场 不规则底部数值波浪水槽的设置 学习
  • 【OpenFOAM】-olaFlow-算例9-pistonFlumeABS

    算例路径 olaFlow tutorials pistonFlumeABS 算例描述 采用 piston 形式的动边界进行消波 学习目标 了解 olaDyMFlow 的使用 理解动网格使用和参数设置 理解 dynamicMotionSolv
  • 【OpenFOAM】-olaFlow-算例10-wavemakerTank

    算例路径 olaFlow tutorials wavemakerTank 算例描述 采用 Flap和Piston两种方式的动网格进行造波 学习目标 了解 olaDyMFlow 的使用 理解动网格使用和参数设置 理解 dynamicMotio
  • 【OpenFOAM】-算例解析合集(备份目录)

    OpenFOAM 算例解析合集 OlaFlowinterFoampimpleFoam OlaFlow OpenFOAM olaFlow 算例1 baseWaveFlume OpenFOAM olaFlow 算例2 breakwater Op
  • 关于MATLAB中使用Link函数和SerialLink建模

    关于MATLAB中使用Link函数和SerialLink建模 Link函数默认使用的是标准D H法建立模型 xff0c 如果想用改进D H法建立模型 xff0c 则应在参数后添加 modified 如下所示 xff1a 建立机器人模型 th
  • 【OpenFOAM】-interFoam-laminar-算例11-wave

    算例路径 OpenFOAM 8 tutorials multiphase interFoam laminar wave 算例描述 使用 interFoam 求解器的造波功能 学习目标 extrudeMesh 网格操作 了解 setWaves
  • 解决Ubunt20.04安装Sogou输入法失败进不去桌面 及 中文输入法安装

    目录 解决Ubunt20 04安装Sogou输入法失败进不去桌面中文输入法安装解决wps无法输入中文 解决Ubunt20 04安装Sogou输入法失败进不去桌面 问题 xff1a Ubuntu20 04 安装了 fcitx 和 sogou
  • 【OpenFOAM】-pimpleFoam-RAS-算例12-wingMotion

    算例路径 OpenFOAM 8 tutorials incompressible pimpleFoam RAS wingMotion 算例描述 该路径下包含三个目录 分别为 1 wingMotion snappyHexMesh 使用 sna
  • 【OpenFOAM】-算例解析合集

    OpenFOAM 算例解析合集 OlaFlow interFoam pimpleFoam OlaFlow OpenFOAM olaFlow 算例1 baseWaveFlume OpenFOAM olaFlow 算例2 breakwater
  • 我的第一篇博客

    我的第一篇博客 很高兴来到这里 xff0c 加油 xff01 我会写更多有用的文章的 xff01
  • 为Termux安装图形化界面

    在学校闲着没事就逛逛论坛 博客 以及贴吧 突然发现一个好玩的东西 xff0c 它就是 Termux 也是咕哝了好久 xff0c 在贴吧看到Termux可以装xfce桌面 于是便有此篇文章留作纪念 xff0c 也同时感谢大佬们的默默努力 xf
  • 在华为平板的Termux上安装Debian Linux图形化界面的详细教程,向生产力更近一步。

    Termux 安装 Debian Linux 图形化界面 文章目录 前言一 准备材料二 安装Debian Linux步骤1 进入Termux安装Debian Linux2 开启远程桌面 xff08 两种方式选一种即可 xff09 总结 前言
  • 在Termux的Debian Linux中设置中文界面

    在Termux的Debian Linux容器中设置中文界面 文章目录 前言Debian汉化 前言 上次在平板中安装了Debian Linux 并可以连接远程xfce桌面 xff0c 有兴趣的可以去看这里 xff0c 但是系统界面确是英文实在

随机推荐

  • 在Termux的Debian Linux中安装VScode

    文章目录 前言安装VScode 前言 有兴趣的伙伴可以看上次安装Debian这里和汉化Debian的文章这里 安装VScode 1 下载火狐浏览器 span class token function sudo span span class
  • 一步一步教你使用uCOS-II

    第一篇 UCOS介绍 第一篇 UCOS介绍 这个大家都知道 呵呵 考虑到咱们学习的完整性还是在这里唠叨一下 让大家再熟悉一下 高手们忍耐一下吧 xff01 uC OS II Micro Control Operation System Tw
  • MATLAB绘制空间曲线和曲面图像

    MATLAB绘制空间曲线和曲面图像 之前考研的时候做到2010年数一试卷第19题时 xff0c 一直无法想象 Sigma 的图像到底是什么样的 当时由于时间紧迫且不知道如何用MATLAB画图 xff0c 因此就这么草草了事 现在正好学到了这
  • 学习笔记|元学习(Meta-learning)——让机器学习如何学习

    文章目录 1 元学习概述2 MAML2 1 MAML概述2 2 MAML的训练 3 元学习在N ways K shot上的应用 1 元学习概述 元学习的意思即 学会如何学习 在机器学习中 xff0c 工作量最大也是最无聊的事情就是调参 我们
  • 串口调试助手 安卓版 附下载地址

    平时工作中和硬件同事对接的比较多 xff0c 软件和硬件的通讯 xff0c 串口用的也比较多的 在网上找了很多串口调试工具 xff0c 大都年代久远 xff0c 没有继续更新维护的了 于是 xff0c 自己抽空写了一个 xff1a 串口调试
  • cv_bridge 与opencv 版本不匹配的解决

    问题描述 xff1a ubuntu18 04安装的ros 默认的opencv版本和cv bridge 版本为3 2 0 但是在使用其他程序包的时候有时候需要用到其他版本的opencv 再调用cv bridge的时候会发生调用冲突 xff1b
  • ROS包nmea_navsat_driver读取GPS、北斗定位信息笔记

    硬件 ATGM332D 43 串口调试工具 43 GPS 天线 软件 xff1a ubunutu 18 04 43 ros 1 串口 读取数据 sudo apt install cutecom sudo cutecom 设置 波特率9600
  • realsense 选型大对比D455 D435i D415 T265 3D硬件对比

    Intel Realsense D455 D435i D415 T265 3D实感硬件对比 xiaodeng6185的博客 CSDN博客 体感摄像头 realsense 系列硬件资料 果匠 CSDN博客 体感摄像头 哪款适合你 xff1f
  • ImportError: /lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.29‘ not found

    解决方案 下载地址 xff1a http ftp gnu org pub gnu glibc 下载 xff1a wget http ftp gnu org gnu glibc glibc 2 29 tar gz 过程有些慢 解压安装 xff
  • 解决ImportError: /usr/lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.26‘ not found

    报错信息如题所示 原因 xff1a 这个是默认路径下的libstdc 43 43 so 6缺少GLIBCXX 3 4 26 xff0c 你有可能缺少其它版本的比如3 4 26 xff0c 解决方法一样 xff0c 如下所示 xff1a xf
  • Eigen 库常用基本用法 备忘

    ps xff1a eigen 看到的时候较多 xff0c 自己写的时候总有一些用法想不起来具体函数名 xff0c 特此总结一下以备忘 官方doc eigen 官网最权威 目录 Eigen 矩阵定义 Eigen 基础使用 Eigen 特殊矩阵
  • Hector slam算法原理解析与代码详解

    写了markdown 上传 xff0c 公式都乱码 xff0c 无果 xff0c 截图上传吧 目录 1 hector 原理解析 1 4 多重分辨率地图 2 代码框架 2 1 回调函数 2 2 更新 3 扫描匹配 3 1 多分辨率匹配 3 2
  • Logistic映射的简单理解

    Logistic映射 在看论文时看到了这个概念 xff0c 于是就去简单了解了一下 参考博客 1 前言 谈到Logistic映射就要先谈一谈什么是混沌系统 百度百科上的解释是 xff0c 混沌系统是指在一个确定性系统中 xff0c 存在着貌
  • _findnext 报错

    ps 编译环境 qt 43 mingw32 编译没问题 xff1b 换到qt 43 msvc 2017 64 就出现问题 xff1b 报错信息 xff1a Stopped in thread 0 by Exception at 0x7ffb
  • bug解决: ffmpeg 在window下使用 PRId64 报错

    在timestamp h 中 调用 av ts make string报错 error expected before PRId64 原因 xff1a 该宏定义给c用的 xff0c C 43 43 要用它 xff0c 就要定义一个 STDC
  • qt: error: C2001: 常量中有换行符

    PS 这两天搞工程系统移植 xff0c 搞得疯掉了 xff0c 代码复用还不如重写呢 如下一句带有中文的程序 xff0c mingw 43 linux 运行没有任何问题 xff0c window下msvc 运行就报错C2001 time s
  • Eigen内存分配器aligned_allocator

    在使用Eigen的时候 xff0c 如果STL容器中的元素是Eigen数据库结构 xff0c 比如下面用vector容器存储Eigen Matrix4f类型或用map存储Eigen Vector4f数据类型时 xff1a vector lt
  • Ubuntu 升级cmake 版本

    PS 在编译一些包时需要更高的版本 xff0c 需要升级 cmake 千万别执行下面的命令 xff0c 这样会把之前用 cmake 编译好的包都给卸载掉 xff0c 包括ros sudo apt get autoremove cmake 比
  • 视觉slam十四讲(ch6) Ubuntu18.04安装 g2o库 报错error: FixedArray ... has no member named ‘fill’

    ps 再学习14讲第二版的时候 xff0c 运行g2o 报错 error FixedArray aka class ceres internal FixedArray lt double 6 gt has no member named f
  • 无人驾驶学习笔记-NDT 配准

    目录 1 NDT 的算法处理流程 2 NDT 公式推导 3 NDT 实例 3 1 常规NDT的位姿估计 3 2 front end node 1 ROS常规初始化 2 初始化操作 xff1a 读取传感器数据 获取lidar to imu变换