【多传感器融合定位】【代码阅读】【A_LOAM及详细注释】

2023-09-24 8 0

【多传感器融合定位】【代码阅读】【A_LOAM及详细注释】

  • 0 前言
  • 1 概述介绍
    • 1.1 各模块说明
  • 2 代码演示
    • 2.1 kittiHelper.cpp
    • 2.2 laserMapping.cpp
    • 2.3 laserOdometry.cpp
    • 2.4 lidarFactor.hpp
    • 2.5 scanRegistration.cpp
  • 3 运行结果

0 前言

  • 主要特点
  1. 去掉了和IMU相关的部分
  2. 使用Eigen(四元数)做位姿转换,简化了代码
  3. 使用ceres做位姿优化,简化了代码,但降低了效率
  • A_LOAM代码:https://github.com/HKUST-Aerial-Robotics/A-LOAM
  • A_LOAM_NOTED代码注释版:https://github.com/cgbcgb/A-LOAM-NOTED
  • 注释参考:A-LOAM源码解读
  • A_LOAM代码带我的注释自取:链接:链接:https://pan.baidu.com/s/1lXvtkuyiavHAEBWjoxGWBQ
    提取码:iusz ,将所得工程包放入catkin/src路径下,参考A_LOAM_NOTED的编译方法
  • nsh_indoor_outdoor.bag:链接:https://pan.baidu.com/s/19I66agf-xb8edHJ1c9y8oQ 提取码:r4mw
    catkin_makesource ~/catkin_ws/devel/setup.bash

1 概述介绍

1.1 各模块说明

  1. scanRegistration.cpp
    scanRegistration.cpp的功能是对激光点云预处理,包括移除空点和近点、计算每个点的水平和垂直角度并分成不同的scan、然后就是最重要的曲率计算、将每条激光线分成6个扇区然后根据曲率提取出特征点(极大边线点sharp、less sharp次极大边线点、flat极小平面点、less flat次极小平面点),对于得到的less flat进行滤波处理,最后把四种特征点,和含有intensity(scanID和对应时间)的点云发布出来;

  2. laserOdometry.cpp
    laserOdometry.cpp的功能是通过求解最小二乘问题进行点云匹配,从而得到状态估计,包括畸变补偿(上面是用IMU补偿)、查找最近的对应特征点、计算点到直线和点到平面的距离、求雅克比矩阵、迭代解最小二乘问题、最后发布估计的状态里程计和path、以及上一帧的边线点、平面点并且重新把点云原封不动地发布出去。

  3. laserMapping.cpp
    laserMapping.cpp的功能是不断拼接每帧点云建立地图,包括对点云坐标变换、然后还是求雅克比矩阵和解最小二乘问题这一套、最后把环境地图发布出来就不管了;这里是把地图分成很多个cube,然后进行管理,基于地图和上一帧的角点和边点匹配优化位姿,将局部地图发布、发布全局地图、优化后位姿的点云、优化后的位姿,是世界坐标系下当前帧的位姿、高频位姿、路径

2 代码演示

2.1 kittiHelper.cpp

  • 其代码主要功能是将kitti数据集读取并数据发布,可选择是否生成对应的bag包

2.2 laserMapping.cpp

  • 该代码未细看

2.3 laserOdometry.cpp

  • 参考:五.激光SLAM框架学习之A-LOAM框架—项目工程代码介绍—3.laserOdometry.cpp–前端雷达里程计和位姿粗估计
// This is an advanced implementation of the algorithm described in the following paper:
//   J. Zhang and S. Singh. LOAM: Lidar Odometry and Mapping in Real-time.
//     Robotics: Science and Systems Conference (RSS). Berkeley, CA, July 2014. // Modifier: Tong Qin               qintonguav@gmail.com
// 	         Shaozu Cao 		    saozu.cao@connect.ust.hk// Copyright 2013, Ji Zhang, Carnegie Mellon University
// Further contributions copyright (c) 2016, Southwest Research Institute
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
//    this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
// 3. Neither the name of the copyright holder nor the names of its
//    contributors may be used to endorse or promote products derived from this
//    software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.//laserOdometry的作用就是对相邻帧的两帧点云做帧间匹配得到位姿的变换
#include <cmath>
#include <nav_msgs/Odometry.h>
#include <nav_msgs/Path.h>
#include <geometry_msgs/PoseStamped.h>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/kdtree/kdtree_flann.h>
#include <pcl_conversions/pcl_conversions.h>
#include <ros/ros.h>
#include <sensor_msgs/Imu.h>
#include <sensor_msgs/PointCloud2.h>
#include <tf/transform_datatypes.h>
#include <tf/transform_broadcaster.h>
#include <eigen3/Eigen/Dense>
#include <mutex>
#include <queue>#include "aloam_velodyne/common.h"
#include "aloam_velodyne/tic_toc.h"
#include "lidarFactor.hpp"#define DISTORTION 0/*
laserOdometry这个节点订阅了5个话题:
有序点云、极大边线点、次极大边线点、极小平面点、次极小平面点。发布了4个话题:
有序点云、上一帧的平面点、上一帧的边线点、当前帧位姿粗估计。主要功能是前端的激光里程计和位姿粗估计。
*/int corner_correspondence = 0, plane_correspondence = 0;constexpr double SCAN_PERIOD = 0.1;
constexpr double DISTANCE_SQ_THRESHOLD = 25;
constexpr double NEARBY_SCAN = 2.5;int skipFrameNum = 5;
bool systemInited = false;double timeCornerPointsSharp = 0;
double timeCornerPointsLessSharp = 0;
double timeSurfPointsFlat = 0;
double timeSurfPointsLessFlat = 0;
double timeLaserCloudFullRes = 0;pcl::KdTreeFLANN<pcl::PointXYZI>::Ptr kdtreeCornerLast(new pcl::KdTreeFLANN<pcl::PointXYZI>());
pcl::KdTreeFLANN<pcl::PointXYZI>::Ptr kdtreeSurfLast(new pcl::KdTreeFLANN<pcl::PointXYZI>());pcl::PointCloud<PointType>::Ptr cornerPointsSharp(new pcl::PointCloud<PointType>());
pcl::PointCloud<PointType>::Ptr cornerPointsLessSharp(new pcl::PointCloud<PointType>());
pcl::PointCloud<PointType>::Ptr surfPointsFlat(new pcl::PointCloud<PointType>());
pcl::PointCloud<PointType>::Ptr surfPointsLessFlat(new pcl::PointCloud<PointType>());pcl::PointCloud<PointType>::Ptr laserCloudCornerLast(new pcl::PointCloud<PointType>());
pcl::PointCloud<PointType>::Ptr laserCloudSurfLast(new pcl::PointCloud<PointType>());
pcl::PointCloud<PointType>::Ptr laserCloudFullRes(new pcl::PointCloud<PointType>());int laserCloudCornerLastNum = 0;
int laserCloudSurfLastNum = 0;// Lidar Odometry线程估计的frame在world坐标系的位姿P,Transformation from current frame to world frame
Eigen::Quaterniond q_w_curr(1, 0, 0, 0);
Eigen::Vector3d t_w_curr(0, 0, 0);// 点云特征匹配时的优化变量
double para_q[4] = {0, 0, 0, 1};
double para_t[3] = {0, 0, 0};// 下面的2个分别是优化变量para_q和para_t的映射:表示的是两个world坐标系下的位姿P之间的增量,例如△P = P0.inverse() * P1
Eigen::Map<Eigen::Quaterniond> q_last_curr(para_q);
Eigen::Map<Eigen::Vector3d> t_last_curr(para_t);// 存放数据的queue// 只能访问 queue<T> 容器适配器的第一个和最后一个元素。只能在容器的末尾添加新元素,只能从头部移除元素。
std::queue<sensor_msgs::PointCloud2ConstPtr> cornerSharpBuf;
std::queue<sensor_msgs::PointCloud2ConstPtr> cornerLessSharpBuf;
std::queue<sensor_msgs::PointCloud2ConstPtr> surfFlatBuf;
std::queue<sensor_msgs::PointCloud2ConstPtr> surfLessFlatBuf;
std::queue<sensor_msgs::PointCloud2ConstPtr> fullPointsBuf;
std::mutex mBuf;// undistort lidar point
//将当前帧点云转换到上一帧,根据运动模型对点云去畸变
/*
激光雷达在运动过程中存在一定的运动畸变,即同一帧点云中,
各个点在采集时,LiDAR的位姿是不同的,就如同高速移动相机时拍摄的照片一样。那么如何进行运动补偿呢?即将所有的点云补偿到某一个时刻。常用的做法是补偿到起始时刻,如果有IMU,我们通过IMU得到的
雷达高频位姿,可以求出每个点相对起始点的位姿,就可以补偿回去。如果没有IMU,我们可以使用匀速模型假设,使?上?个帧间?程记的
结果作为当前两帧之间的运动,假设当前帧也是匀速运动,可以估计出每个点相对起始时刻的位姿。最后,当前点云中的点相对第一个点去除因运动产生的畸变,
效果相当于静止扫描得到的点云。下面是去除运动畸变的函数。*/
/*
TransformToStart:将当前帧Lidar坐标系下的点云变换到上一帧Lidar坐标系下(也就是当前帧的初始位姿,起始位姿,
所以函数名是TransformToStart),因为kitti点云已经去除了畸变,所以不再考虑运动补偿。(如果点云没有去除畸变,
用slerp差值的方式计算出每个点在fire时刻的位姿,然后进行TransformToStart的坐标变换,一方面实现了变换到上一
帧Lidar坐标系下;另一方面也可以理解成点都将fire时刻统一到了开始时刻,即去除了畸变,完成了运动补偿)
*/
void TransformToStart(PointType const *const pi, PointType *const po)
{//interpolation ratiodouble s;// 由于kitti数据集上的lidar已经做过了运动补偿,因此这里就不做具体补偿了if (DISTORTION)//需要去除畸变s = (pi->intensity - int(pi->intensity)) / SCAN_PERIOD;elses = 1.0;// s = 1.0说明全部补偿到点云结束的时刻// 所有点的操作方式都是一致的,相当于从结束时刻补偿到起始时刻,相当于是一个匀速模型的假设Eigen::Quaterniond q_point_last = Eigen::Quaterniond::Identity().slerp(s, q_last_curr);Eigen::Vector3d t_point_last = s * t_last_curr; // 平移量插值Eigen::Vector3d point(pi->x, pi->y, pi->z);// 当前帧的点Eigen::Vector3d un_point = q_point_last * point + t_point_last;//q_point_last :为此帧在上一阵lider坐标系下位姿po->x = un_point.x();po->y = un_point.y();po->z = un_point.z();po->intensity = pi->intensity;
}// transform all lidar points to the start of the next frame
//转换所有的激光雷达指向下一帧的开始void TransformToEnd(PointType const *const pi, PointType *const po)
{// undistort point firstpcl::PointXYZI un_point_tmp;TransformToStart(pi, &un_point_tmp);Eigen::Vector3d un_point(un_point_tmp.x, un_point_tmp.y, un_point_tmp.z);//Eigen::Vector3d point_end = q_last_curr.inverse() * (un_point - t_last_curr);po->x = point_end.x();po->y = point_end.y();po->z = point_end.z();//Remove distortion time infopo->intensity = int(pi->intensity);
}
// 之后的5个Handler函数为接受上游5个topic的回调函数,作用是将消息缓存到对应的queue中,以便后续处理。
// 操作都是送去各自的队列中,加了线程锁以避免线程数据冲突
void laserCloudSharpHandler(const sensor_msgs::PointCloud2ConstPtr &cornerPointsSharp2)
{mBuf.lock();cornerSharpBuf.push(cornerPointsSharp2);mBuf.unlock();
}void laserCloudLessSharpHandler(const sensor_msgs::PointCloud2ConstPtr &cornerPointsLessSharp2)
{mBuf.lock();cornerLessSharpBuf.push(cornerPointsLessSharp2);mBuf.unlock();
}void laserCloudFlatHandler(const sensor_msgs::PointCloud2ConstPtr &surfPointsFlat2)
{mBuf.lock();surfFlatBuf.push(surfPointsFlat2);mBuf.unlock();
}void laserCloudLessFlatHandler(const sensor_msgs::PointCloud2ConstPtr &surfPointsLessFlat2)
{mBuf.lock();surfLessFlatBuf.push(surfPointsLessFlat2);mBuf.unlock();
}//receive all point cloud
void laserCloudFullResHandler(const sensor_msgs::PointCloud2ConstPtr &laserCloudFullRes2)
{mBuf.lock();fullPointsBuf.push(laserCloudFullRes2);mBuf.unlock();
}
/*
laserOdometry这个节点订阅了5个话题:有序点云、极大边线点、次极大边线点、极小平面点、次极小平面点。发布了4个话题:有序点云、上一帧的平面点、上一帧的边线点、当前帧位姿粗估计。主要功能是前端的激光里程计和位姿粗估计。
*/
int main(int argc, char **argv)
{//1 初始化发布和订阅ros::init(argc, argv, "laserOdometry");ros::NodeHandle nh;nh.param<int>("mapping_skip_frame", skipFrameNum, 2);//    设定里程计的帧率//if 1, do mapping 10 Hz, if 2, do mapping 5 Hz. printf("Mapping %d Hz \n", 10 / skipFrameNum);// 从scanRegistration节点订阅的消息话题,分别为极大边线点  次极大边线点   极小平面点  次极小平面点 全部点云点ros::Subscriber subCornerPointsSharp = nh.subscribe<sensor_msgs::PointCloud2>("/laser_cloud_sharp", 100, laserCloudSharpHandler);ros::Subscriber subCornerPointsLessSharp = nh.subscribe<sensor_msgs::PointCloud2>("/laser_cloud_less_sharp", 100, laserCloudLessSharpHandler);ros::Subscriber subSurfPointsFlat = nh.subscribe<sensor_msgs::PointCloud2>("/laser_cloud_flat", 100, laserCloudFlatHandler);ros::Subscriber subSurfPointsLessFlat = nh.subscribe<sensor_msgs::PointCloud2>("/laser_cloud_less_flat", 100, laserCloudLessFlatHandler);ros::Subscriber subLaserCloudFullRes = nh.subscribe<sensor_msgs::PointCloud2>("/velodyne_cloud_2", 100, laserCloudFullResHandler);// 注册发布上一帧的边线点话题  ros::Publisher pubLaserCloudCornerLast = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_corner_last", 100);// 注册发布上一帧的平面点话题ros::Publisher pubLaserCloudSurfLast = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_surf_last", 100);// 注册发布全部有序点云话题,就是从scanRegistration订阅来的点云,未经其他处理ros::Publisher pubLaserCloudFullRes = nh.advertise<sensor_msgs::PointCloud2>("/velodyne_cloud_3", 100);// 注册发布帧间的位姿变换话题 ros::Publisher pubLaserOdometry = nh.advertise<nav_msgs::Odometry>("/laser_odom_to_init", 100);// 注册发布帧间的平移运动话题 ros::Publisher pubLaserPath = nh.advertise<nav_msgs::Path>("/laser_odom_path", 100);nav_msgs::Path laserPath;int frameCount = 0;ros::Rate rate(100);// 循环频率//2 进行数据处理
/*
下面是主函数的主循环,主要是帧间位姿估计的过程,目
标是希望找到位姿变换T,使得第k帧点云左乘T得到第k+1帧点云,
或者说左乘T得到k+1帧点云的误差最小。
*/while (ros::ok()){ros::spinOnce();// 只触发一次回调,所以每次都要调用一次;等待回调函数执行完毕,执行一次后续代码,参考https://www.cnblogs.com/liu-fa/p/5925381.html//2.1 判断特征点是否有空的 // 首先确保订阅的五个消息都有,有一个队列为空都不行if (!cornerSharpBuf.empty() && !cornerLessSharpBuf.empty() &&!surfFlatBuf.empty() && !surfLessFlatBuf.empty() &&!fullPointsBuf.empty()){//2.1.1  获取特征点集合的首歌元素时间戳// 5个queue记录了最新的极大/次极大边线点,极小/次极小平面点,全部有序点云// 分别求出队列第一个时间,用来分配时间戳timeCornerPointsSharp = cornerSharpBuf.front()->header.stamp.toSec();timeCornerPointsLessSharp = cornerLessSharpBuf.front()->header.stamp.toSec();timeSurfPointsFlat = surfFlatBuf.front()->header.stamp.toSec();timeSurfPointsLessFlat = surfLessFlatBuf.front()->header.stamp.toSec();timeLaserCloudFullRes = fullPointsBuf.front()->header.stamp.toSec();//2.1.2 如果关键点的时间戳和完整数据的时间戳有一个不相等,那么报错// 因为同一帧的时间戳都是相同的,这里比较是否是同一帧if (timeCornerPointsSharp != timeLaserCloudFullRes ||timeCornerPointsLessSharp != timeLaserCloudFullRes ||timeSurfPointsFlat != timeLaserCloudFullRes ||timeSurfPointsLessFlat != timeLaserCloudFullRes){printf("unsync messeage!");ROS_BREAK();}//2.1.3 5个点云的时间同步// 分别将五个点云消息取出来,同时转成pcl的点云格式mBuf.lock();//数据多个线程使用,这里先进行加锁,避免线程冲突cornerPointsSharp->clear();pcl::fromROSMsg(*cornerSharpBuf.front(), *cornerPointsSharp);cornerSharpBuf.pop();cornerPointsLessSharp->clear();pcl::fromROSMsg(*cornerLessSharpBuf.front(), *cornerPointsLessSharp);cornerLessSharpBuf.pop();surfPointsFlat->clear();pcl::fromROSMsg(*surfFlatBuf.front(), *surfPointsFlat);surfFlatBuf.pop();surfPointsLessFlat->clear();pcl::fromROSMsg(*surfLessFlatBuf.front(), *surfPointsLessFlat);surfLessFlatBuf.pop();laserCloudFullRes->clear();pcl::fromROSMsg(*fullPointsBuf.front(), *laserCloudFullRes);fullPointsBuf.pop();mBuf.unlock();//数据取出来后进行解锁TicToc t_whole;//计算整个激光雷达里程计的时间// initializing 一个什么也不干的初始化,没有延迟时间,主要用来跳过第一帧数据,直接作为第二帧的上一帧//2.1.4 是否进行初始化if (!systemInited)// 第一帧不进行匹配,仅仅将 cornerPointsLessSharp 保存至 laserCloudCornerLast//                       将 surfPointsLessFlat    保存至 laserCloudSurfLast// 为下次匹配提供target{systemInited = true;std::cout << "Initialization finished \n";}else// 第二帧开始 特征点匹配、位姿估计{// 2.1.4.1 获取sharp和flat点的个数// 取出比较突出的特征点数量,极大边线点和极小平面点int cornerPointsSharpNum = cornerPointsSharp->points.size();int surfPointsFlatNum = surfPointsFlat->points.size();//2.1.4.2 进行点到线以及点到面的ICP,迭代2次TicToc t_opt;//计算优化的时间// 点到线以及点到面的非线性优化,迭代2次(选择当前优化位姿的特征点匹配,并优化位姿(4次迭代),然后重新选择特征点匹配并优化)for (size_t opti_counter = 0; opti_counter < 2; ++opti_counter){//2.1.4.2.1 定义ceres优化器corner_correspondence = 0;// 角点的误差项数量plane_correspondence = 0;// 平面点的误差项数量//ceres::LossFunction *loss_function = NULL;// 定义一下ceres的核函数,使用Huber核函数来减少外点的影响,即去除outliersceres::LossFunction *loss_function = new ceres::HuberLoss(0.1);// 由于旋转不满足一般意义的加法,因此这里使用ceres自带的local paramceres::LocalParameterization *q_parameterization =new ceres::EigenQuaternionParameterization();ceres::Problem::Options problem_options;ceres::Problem problem(problem_options);//实例化求解最优化问题// 待优化的变量是帧间位姿,包括平移和旋转,这里旋转使用四元数来表示problem.AddParameterBlock(para_q, 4, q_parameterization);problem.AddParameterBlock(para_t, 3);pcl::PointXYZI pointSel;std::vector<int> pointSearchInd;std::vector<float> pointSearchSqDis;TicToc t_data;//计算寻找关联点的时间//2.1.4.2.2 基于最近邻原理建立corner特征点之间关联,find correspondence for corner features/*基于最近邻原理建立corner特征点(边线点)之间的关联,每一个极大边线点去上一帧的次极大边线点中找匹配;采用边线点匹配方法:假如在第k+1帧中发现了边线点i,通过KD-tree查询在第k帧中的最近邻点j,查询j的附近扫描线上的最近邻点l,j与l相连形成一条直线l-j,让点i与这条直线的距离最短。构建一个非线性优化问题:以点i与直线lj的距离为代价函数,以位姿变换T(四元数表示旋转+位移t)为优化变量。下面是优化的程序。*/// find correspondence for corner featurefor (int i = 0; i < cornerPointsSharpNum; ++i)//先进行线点的匹配{//2.1.4.2.2.1 变换sharp点,并寻找最近点// 运动补偿去畸变 TransformToStart(&(cornerPointsSharp->points[i]), &pointSel);// 将当前帧的corner_sharp特征点O_cur,从当前帧的Lidar坐标系下变换到//上一帧的Lidar坐标系下(记为点O,注意与前面的点O_cur不同),以利于寻找corner特征点的correspondencekdtreeCornerLast->nearestKSearch(pointSel, 1, pointSearchInd, pointSearchSqDis);// kdtree中的点云是上一帧的corner_less_sharp,所以这是在上一帧// 的corner_less_sharp中寻找当前帧corner_sharp特征点O的最近邻点(记为A)int closestPointInd = -1, minPointInd2 = -1;//2.1.4.2.2.2 如果最近邻的 corner 特征点之间距离平方小于阈值,则最近邻点A有效if (pointSearchSqDis[0] < DISTANCE_SQ_THRESHOLD){// 2.1.4.2.2.2.1 获取近邻点A的所属激光束closestPointInd = pointSearchInd[0];// 目标点对应的最近距离点的索引取出来// 找到其所在线束id,线束信息是intensity的整数部分int closestPointScanID = int(laserCloudCornerLast->points[closestPointInd].intensity);double minPointSqDis2 = DISTANCE_SQ_THRESHOLD;//2.1.4.2.2.2.2 在激光束增大的方向,寻找点O的另外一个最近邻的点(记为点B) in the direction of increasing scan line// 在刚刚角点(次极大边线点)id附近扫描线分别继续寻找最邻近点for (int j = closestPointInd + 1; j < (int)laserCloudCornerLast->points.size(); ++j)// laserCloudCornerLast 来自上一帧的corner_less_sharp特征点,由于提取特征时是{                                                                                   // 按照scan的顺序提取的,所以laserCloudCornerLast中的点也是按照scanID递增的顺序存放的//2.1.4.2.2.2.2.1 判断是否是同一个激光束上的点   // if in the same scan line, continueif (int(laserCloudCornerLast->points[j].intensity) <= closestPointScanID)// intensity整数部分存放的是scanIDcontinue;//2.1.4.2.2.2.2.2 判断该点是否是近邻点,即要求找到的线束距离当前线束不能太远// if not in nearby scans, end the loopif (int(laserCloudCornerLast->points[j].intensity) > (closestPointScanID + NEARBY_SCAN))break;//2.1.4.2.2.2.2.3 计算所得近邻点B与转换后的角点O之间的距离二次方double pointSqDis = (laserCloudCornerLast->points[j].x - pointSel.x) *(laserCloudCornerLast->points[j].x - pointSel.x) +(laserCloudCornerLast->points[j].y - pointSel.y) *(laserCloudCornerLast->points[j].y - pointSel.y) +(laserCloudCornerLast->points[j].z - pointSel.z) *(laserCloudCornerLast->points[j].z - pointSel.z);//2.1.4.2.2.2.2.4 判断近邻点B与角点的距离平方是否小于阈值if (pointSqDis < minPointSqDis2)// 第二个最近邻点有效,,更新点B{// find nearer point//2.1.4.2.2.2.2.4.1 更新特征点之间的最小距离平方// find nearer point,寻找距离最小的角点(次极大边线点)及其索引,记录其索引minPointSqDis2 = pointSqDis;minPointInd2 = j;}}//2.1.4.2.2.2.3 在激光束减小的方向,寻找点O的另外一个最近邻的点B in the direction of decreasing scan line// 同样另一个方向寻找对应角点(次极大边线点)for (int j = closestPointInd - 1; j >= 0; --j){// if in the same scan line, continueif (int(laserCloudCornerLast->points[j].intensity) >= closestPointScanID)continue;// if not in nearby scans, end the loopif (int(laserCloudCornerLast->points[j].intensity) < (closestPointScanID - NEARBY_SCAN))break;double pointSqDis = (laserCloudCornerLast->points[j].x - pointSel.x) *(laserCloudCornerLast->points[j].x - pointSel.x) +(laserCloudCornerLast->points[j].y - pointSel.y) *(laserCloudCornerLast->points[j].y - pointSel.y) +(laserCloudCornerLast->points[j].z - pointSel.z) *(laserCloudCornerLast->points[j].z - pointSel.z);if (pointSqDis < minPointSqDis2)// 第二个最近邻点有效,更新点B{// find nearer pointminPointSqDis2 = pointSqDis;minPointInd2 = j;}}}//2.1.4.2.2.3 特征点O的两个最近邻点A和B都有效 构造误差项// 如果特征点i的两个最近邻点j和m都有效,构建非线性优化问题if (minPointInd2 >= 0) // both closestPointInd and minPointInd2 is valid{                  // 取出当前点和上一帧的两个角点    Eigen::Vector3d curr_point(cornerPointsSharp->points[i].x,cornerPointsSharp->points[i].y,cornerPointsSharp->points[i].z);Eigen::Vector3d last_point_a(laserCloudCornerLast->points[closestPointInd].x,laserCloudCornerLast->points[closestPointInd].y,laserCloudCornerLast->points[closestPointInd].z);Eigen::Vector3d last_point_b(laserCloudCornerLast->points[minPointInd2].x,laserCloudCornerLast->points[minPointInd2].y,laserCloudCornerLast->points[minPointInd2].z);double s;// 运动补偿系数,kitti数据集的点云已经被补偿过,所以s = 1.0if (DISTORTION)s = (cornerPointsSharp->points[i].intensity - int(cornerPointsSharp->points[i].intensity)) / SCAN_PERIOD;elses = 1.0;// 用点O,A,B构造点到线的距离的残差项,注意这三个点都是在上一帧的Lidar坐标系下,即,残差 = 点O到直线AB的距离// 具体到介绍lidarFactor.cpp时再说明该残差的具体计算方法ceres::CostFunction *cost_function = LidarEdgeFactor::Create(curr_point, last_point_a, last_point_b, s);problem.AddResidualBlock(cost_function, loss_function, para_q, para_t);corner_correspondence++;}}//2.1.4.2.3 基于最近邻原理建立surf特征点之间关联// 下面说的点符号与上述相同// 与上面的建立corner特征点之间的关联类似,寻找平面特征点O的最近邻点ABC,即基于最近邻原理建立surf特征点之间的关联,find correspondence for plane features/*下面采用平面点匹配方法:假如在第k+1帧中发现了平面点i,通过KD-tree查询在第k帧(上一帧)中的最近邻点j,查询j的附近扫描线上的最近邻点l和同一条扫描线的最近邻点m,这三点确定一个平面,让点i与这个平面的距离最短;构建一个非线性优化问题:以点i与平面lmj的距离为代价函数,以位姿变换T(四元数表示旋转+t)为优化变量。*/for (int i = 0; i < surfPointsFlatNum; ++i){TransformToStart(&(surfPointsFlat->points[i]), &pointSel);// 先寻找上一帧距离这个面点最近的面点kdtreeSurfLast->nearestKSearch(pointSel, 1, pointSearchInd, pointSearchSqDis);int closestPointInd = -1, minPointInd2 = -1, minPointInd3 = -1;if (pointSearchSqDis[0] < DISTANCE_SQ_THRESHOLD)// 找到的最近邻点A有效{closestPointInd = pointSearchInd[0];// get closest point's scan IDint closestPointScanID = int(laserCloudSurfLast->points[closestPointInd].intensity);double minPointSqDis2 = DISTANCE_SQ_THRESHOLD, minPointSqDis3 = DISTANCE_SQ_THRESHOLD;// search in the direction of increasing scan linefor (int j = closestPointInd + 1; j < (int)laserCloudSurfLast->points.size(); ++j){// if not in nearby scans, end the loopif (int(laserCloudSurfLast->points[j].intensity) > (closestPointScanID + NEARBY_SCAN))break;double pointSqDis = (laserCloudSurfLast->points[j].x - pointSel.x) *(laserCloudSurfLast->points[j].x - pointSel.x) +(laserCloudSurfLast->points[j].y - pointSel.y) *(laserCloudSurfLast->points[j].y - pointSel.y) +(laserCloudSurfLast->points[j].z - pointSel.z) *(laserCloudSurfLast->points[j].z - pointSel.z);// if in the same or lower scan lineif (int(laserCloudSurfLast->points[j].intensity) <= closestPointScanID && pointSqDis < minPointSqDis2){minPointSqDis2 = pointSqDis;// 找到的第2个最近邻点有效,更新点B,注意如果scanID准确的话,一般点A和点B的scanID相同minPointInd2 = j;}// if in the higher scan lineelse if (int(laserCloudSurfLast->points[j].intensity) > closestPointScanID && pointSqDis < minPointSqDis3){minPointSqDis3 = pointSqDis;// 找到的第3个最近邻点有效,更新点C,注意如果scanID准确的话,一般点A和点B的scanID相同,且与点C的scanID不同,与LOAM的paper叙述一致minPointInd3 = j;}}// search in the direction of decreasing scan linefor (int j = closestPointInd - 1; j >= 0; --j){// if not in nearby scans, end the loopif (int(laserCloudSurfLast->points[j].intensity) < (closestPointScanID - NEARBY_SCAN))break;double pointSqDis = (laserCloudSurfLast->points[j].x - pointSel.x) *(laserCloudSurfLast->points[j].x - pointSel.x) +(laserCloudSurfLast->points[j].y - pointSel.y) *(laserCloudSurfLast->points[j].y - pointSel.y) +(laserCloudSurfLast->points[j].z - pointSel.z) *(laserCloudSurfLast->points[j].z - pointSel.z);// if in the same or higher scan lineif (int(laserCloudSurfLast->points[j].intensity) >= closestPointScanID && pointSqDis < minPointSqDis2){minPointSqDis2 = pointSqDis;minPointInd2 = j;}else if (int(laserCloudSurfLast->points[j].intensity) < closestPointScanID && pointSqDis < minPointSqDis3){// find nearer pointminPointSqDis3 = pointSqDis;minPointInd3 = j;}}if (minPointInd2 >= 0 && minPointInd3 >= 0)// 如果三个最近邻点都有效{Eigen::Vector3d curr_point(surfPointsFlat->points[i].x,surfPointsFlat->points[i].y,surfPointsFlat->points[i].z);Eigen::Vector3d last_point_a(laserCloudSurfLast->points[closestPointInd].x,laserCloudSurfLast->points[closestPointInd].y,laserCloudSurfLast->points[closestPointInd].z);Eigen::Vector3d last_point_b(laserCloudSurfLast->points[minPointInd2].x,laserCloudSurfLast->points[minPointInd2].y,laserCloudSurfLast->points[minPointInd2].z);Eigen::Vector3d last_point_c(laserCloudSurfLast->points[minPointInd3].x,laserCloudSurfLast->points[minPointInd3].y,laserCloudSurfLast->points[minPointInd3].z);double s;if (DISTORTION)s = (surfPointsFlat->points[i].intensity - int(surfPointsFlat->points[i].intensity)) / SCAN_PERIOD;elses = 1.0;// 用点O,A,B,C构造点到面的距离的残差项,注意这三个点都是在上一帧的Lidar坐标系下,即,残差 = 点O到平面ABC的距离// 同样的,具体到介绍lidarFactor.cpp时再说明该残差的具体计算方法ceres::CostFunction *cost_function = LidarPlaneFactor::Create(curr_point, last_point_a, last_point_b, last_point_c, s);problem.AddResidualBlock(cost_function, loss_function, para_q, para_t);plane_correspondence++;}}}// 输出寻找关联点消耗的时间//printf("coner_correspondance %d, plane_correspondence %d \n", corner_correspondence, plane_correspondence);printf("data association time %f ms \n", t_data.toc());// 如果总的线约束和面约束太少,就打印一下if ((corner_correspondence + plane_correspondence) < 10){printf("less correspondence! *************************************************\n");}//2.1.4.2.4 使用ceres求解// 调用ceres求解器求解 ,设定求解器类型,最大迭代次数,不输出过程信息,优化报告存入summaryTicToc t_solver;ceres::Solver::Options options;options.linear_solver_type = ceres::DENSE_QR;options.max_num_iterations = 4;options.minimizer_progress_to_stdout = false;ceres::Solver::Summary summary;// 基于构建的所有残差项,求解最优的当前帧位姿与上一帧位姿的位姿增量:para_q和para_tceres::Solve(options, &problem, &summary);printf("solver time %f ms \n", t_solver.toc());}// 经过两次LM优化消耗的时间printf("optimization twice time %f \n", t_opt.toc());//2.1.4.3 新计算出的位姿增量// 用最新计算出的位姿增量,更新上一帧的位姿,得到当前帧的位姿,注意这里说的位姿都指的是世界坐标系下的位姿// 这里右式的w_curr 实际上是 w_last,即上一帧// 更新帧间匹配的结果,得到lidar odom位姿t_w_curr = t_w_curr + q_w_curr * t_last_curr;q_w_curr = q_w_curr * q_last_curr;}TicToc t_pub;//计算发布运行时间// 发布lidar里程计结果//2.1.5 发布里程计odom话题和path// publish odometry// 创建nav_msgs::Odometry消息类型,把信息导入,并发布nav_msgs::Odometry laserOdometry;laserOdometry.header.frame_id = "/camera_init";//选择相机坐标系laserOdometry.child_frame_id = "/laser_odom";laserOdometry.header.stamp = ros::Time().fromSec(timeSurfPointsLessFlat);// 以四元数和平移向量发出去laserOdometry.pose.pose.orientation.x = q_w_curr.x();laserOdometry.pose.pose.orientation.y = q_w_curr.y();laserOdometry.pose.pose.orientation.z = q_w_curr.z();laserOdometry.pose.pose.orientation.w = q_w_curr.w();laserOdometry.pose.pose.position.x = t_w_curr.x();laserOdometry.pose.pose.position.y = t_w_curr.y();laserOdometry.pose.pose.position.z = t_w_curr.z();pubLaserOdometry.publish(laserOdometry);// geometry_msgs::PoseStamped消息是laserOdometry的部分内容geometry_msgs::PoseStamped laserPose;laserPose.header = laserOdometry.header;laserPose.pose = laserOdometry.pose.pose;laserPath.header.stamp = laserOdometry.header.stamp;laserPath.poses.push_back(laserPose);laserPath.header.frame_id = "/camera_init";pubLaserPath.publish(laserPath);//2.1.6 将角特征和面特征变换到扫描端点  // transform corner features and plane features to the scan end point// transform corner features and plane features to the scan end point//去畸变,没有调用if (0){int cornerPointsLessSharpNum = cornerPointsLessSharp->points.size();for (int i = 0; i < cornerPointsLessSharpNum; i++){TransformToEnd(&cornerPointsLessSharp->points[i], &cornerPointsLessSharp->points[i]);}int surfPointsLessFlatNum = surfPointsLessFlat->points.size();for (int i = 0; i < surfPointsLessFlatNum; i++){TransformToEnd(&surfPointsLessFlat->points[i], &surfPointsLessFlat->points[i]);}int laserCloudFullResNum = laserCloudFullRes->points.size();for (int i = 0; i < laserCloudFullResNum; i++){TransformToEnd(&laserCloudFullRes->points[i], &laserCloudFullRes->points[i]);}}//2.1.7 更新配准的source 当前帧变上一帧// 位姿估计完毕之后,当前边线点和平面点就变成了上一帧的边线点和平面点,把索引和数量都转移过去pcl::PointCloud<PointType>::Ptr laserCloudTemp = cornerPointsLessSharp;cornerPointsLessSharp = laserCloudCornerLast;laserCloudCornerLast = laserCloudTemp;laserCloudTemp = surfPointsLessFlat;surfPointsLessFlat = laserCloudSurfLast;laserCloudSurfLast = laserCloudTemp;laserCloudCornerLastNum = laserCloudCornerLast->points.size();laserCloudSurfLastNum = laserCloudSurfLast->points.size();// std::cout << "the size of corner last is " << laserCloudCornerLastNum << ", and the size of surf last is " << laserCloudSurfLastNum << '\n';// 使用上一帧的点云更新kd-tree,如果是第一帧的话是直接将其保存为这个的// kdtree设置当前帧,用来下一帧lidar odom使用kdtreeCornerLast->setInputCloud(laserCloudCornerLast);// 更新kdtree的点云kdtreeSurfLast->setInputCloud(laserCloudSurfLast);//2.1.8 每5帧执行一次发布// 控制后端节点的执行频率,降频后给后端发送,只有整除时才发布结果if (frameCount % skipFrameNum == 0)//skipFrameNum = 2{frameCount = 0;// 发布上一帧的Corner点 // 发布边线点sensor_msgs::PointCloud2 laserCloudCornerLast2;pcl::toROSMsg(*laserCloudCornerLast, laserCloudCornerLast2);laserCloudCornerLast2.header.stamp = ros::Time().fromSec(timeSurfPointsLessFlat);laserCloudCornerLast2.header.frame_id = "/camera";pubLaserCloudCornerLast.publish(laserCloudCornerLast2);// 发布上一帧的Surf点 // 发布平面点sensor_msgs::PointCloud2 laserCloudSurfLast2;pcl::toROSMsg(*laserCloudSurfLast, laserCloudSurfLast2);laserCloudSurfLast2.header.stamp = ros::Time().fromSec(timeSurfPointsLessFlat);laserCloudSurfLast2.header.frame_id = "/camera";pubLaserCloudSurfLast.publish(laserCloudSurfLast2);// 发布全部完整点云 // 原封不动的转发当前帧点云sensor_msgs::PointCloud2 laserCloudFullRes3;pcl::toROSMsg(*laserCloudFullRes, laserCloudFullRes3);laserCloudFullRes3.header.stamp = ros::Time().fromSec(timeSurfPointsLessFlat);laserCloudFullRes3.header.frame_id = "/camera";pubLaserCloudFullRes.publish(laserCloudFullRes3);}printf("publication time %f ms \n", t_pub.toc());printf("whole laserOdometry time %f ms \n \n", t_whole.toc());if(t_whole.toc() > 100)ROS_WARN("odometry process over 100ms");frameCount++;}rate.sleep();}return 0;
}

2.4 lidarFactor.hpp

// Author:   Tong Qin               qintonguav@gmail.com
// 	         Shaozu Cao 		    saozu.cao@connect.ust.hk#include <ceres/ceres.h>
#include <ceres/rotation.h>
#include <eigen3/Eigen/Dense>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>
#include <pcl/kdtree/kdtree_flann.h>
#include <pcl_conversions/pcl_conversions.h>// 点到线的残差距离计算
struct LidarEdgeFactor
{LidarEdgeFactor(Eigen::Vector3d curr_point_, Eigen::Vector3d last_point_a_,Eigen::Vector3d last_point_b_, double s_): curr_point(curr_point_), last_point_a(last_point_a_), last_point_b(last_point_b_), s(s_) {}template <typename T>bool operator()(const T *q, const T *t, T *residual) const{// 将double数组转成eigen的数据结构,注意这里必须都写成模板Eigen::Matrix<T, 3, 1> cp{T(curr_point.x()), T(curr_point.y()), T(curr_point.z())};Eigen::Matrix<T, 3, 1> lpa{T(last_point_a.x()), T(last_point_a.y()), T(last_point_a.z())};Eigen::Matrix<T, 3, 1> lpb{T(last_point_b.x()), T(last_point_b.y()), T(last_point_b.z())};//Eigen::Quaternion<T> q_last_curr{q[3], T(s) * q[0], T(s) * q[1], T(s) * q[2]};Eigen::Quaternion<T> q_last_curr{q[3], q[0], q[1], q[2]};Eigen::Quaternion<T> q_identity{T(1), T(0), T(0), T(0)};// 考虑运动补偿,ktti点云已经补偿过所以可以忽略下面的对四元数slerp插值以及对平移的线性插值// 计算的是上一帧到当前帧的位姿变换,因此根据匀速模型,计算该点对应的位姿// 这里暂时不考虑畸变,因此这里不做任何变换q_last_curr = q_identity.slerp(T(s), q_last_curr);Eigen::Matrix<T, 3, 1> t_last_curr{T(s) * t[0], T(s) * t[1], T(s) * t[2]};Eigen::Matrix<T, 3, 1> lp;// Odometry线程时,下面是将当前帧Lidar坐标系下的cp点变换到上一帧的Lidar坐标系下,然后在上一帧的Lidar坐标系计算点到线的残差距离// Mapping线程时,下面是将当前帧Lidar坐标系下的cp点变换到world坐标系下,然后在world坐标系下计算点到线的残差距离lp = q_last_curr * cp + t_last_curr;// 点到线的计算如下图所示Eigen::Matrix<T, 3, 1> nu = (lp - lpa).cross(lp - lpb);Eigen::Matrix<T, 3, 1> de = lpa - lpb;// 最终的残差本来应该是residual[0] = nu.norm() / de.norm(); 为啥也分成3个,我也不知// 道,从我试验的效果来看,确实是下面的残差函数形式,最后输出的pose精度会好一点点,这里需要// 注意的是,所有的residual都不用加fabs,因为Ceres内部会对其求 平方 作为最终的残差项residual[0] = nu.x() / de.norm();residual[1] = nu.y() / de.norm();residual[2] = nu.z() / de.norm();return true;}static ceres::CostFunction *Create(const Eigen::Vector3d curr_point_, const Eigen::Vector3d last_point_a_,const Eigen::Vector3d last_point_b_, const double s_){return (new ceres::AutoDiffCostFunction<LidarEdgeFactor, 3, 4, 3>(
//					             ^  ^  ^
//					             |  |  |
//			      残差的维度 ____|  |  |
//			 优化变量q的维度 _______|  |
//			 优化变量t的维度 __________|new LidarEdgeFactor(curr_point_, last_point_a_, last_point_b_, s_)));}Eigen::Vector3d curr_point, last_point_a, last_point_b;double s;
};// 计算Odometry线程中点到面的残差距离
struct LidarPlaneFactor
{LidarPlaneFactor(Eigen::Vector3d curr_point_, Eigen::Vector3d last_point_j_,Eigen::Vector3d last_point_l_, Eigen::Vector3d last_point_m_, double s_): curr_point(curr_point_), last_point_j(last_point_j_), last_point_l(last_point_l_),last_point_m(last_point_m_), s(s_){// 点l、j、m就是搜索到的最近邻的3个点,下面就是计算出这三个点构成的平面ljlm的法向量ljm_norm = (last_point_j - last_point_l).cross(last_point_j - last_point_m);// 归一化法向量ljm_norm.normalize();}template <typename T>bool operator()(const T *q, const T *t, T *residual) const{Eigen::Matrix<T, 3, 1> cp{T(curr_point.x()), T(curr_point.y()), T(curr_point.z())};Eigen::Matrix<T, 3, 1> lpj{T(last_point_j.x()), T(last_point_j.y()), T(last_point_j.z())};//Eigen::Matrix<T, 3, 1> lpl{T(last_point_l.x()), T(last_point_l.y()), T(last_point_l.z())};//Eigen::Matrix<T, 3, 1> lpm{T(last_point_m.x()), T(last_point_m.y()), T(last_point_m.z())};Eigen::Matrix<T, 3, 1> ljm{T(ljm_norm.x()), T(ljm_norm.y()), T(ljm_norm.z())};//Eigen::Quaternion<T> q_last_curr{q[3], T(s) * q[0], T(s) * q[1], T(s) * q[2]};Eigen::Quaternion<T> q_last_curr{q[3], q[0], q[1], q[2]};Eigen::Quaternion<T> q_identity{T(1), T(0), T(0), T(0)};q_last_curr = q_identity.slerp(T(s), q_last_curr);Eigen::Matrix<T, 3, 1> t_last_curr{T(s) * t[0], T(s) * t[1], T(s) * t[2]};Eigen::Matrix<T, 3, 1> lp;lp = q_last_curr * cp + t_last_curr;// 计算点到平面的残差距离,如下图所示residual[0] = (lp - lpj).dot(ljm);return true;}static ceres::CostFunction *Create(const Eigen::Vector3d curr_point_, const Eigen::Vector3d last_point_j_,const Eigen::Vector3d last_point_l_, const Eigen::Vector3d last_point_m_,const double s_){return (new ceres::AutoDiffCostFunction<LidarPlaneFactor, 1, 4, 3>(
//				 	              ^  ^  ^
//			 		              |  |  |
//			       残差的维度 ____|  |  |
//			  优化变量q的维度 _______|  |
//		 	  优化变量t的维度 __________|new LidarPlaneFactor(curr_point_, last_point_j_, last_point_l_, last_point_m_, s_)));}Eigen::Vector3d curr_point, last_point_j, last_point_l, last_point_m;Eigen::Vector3d ljm_norm;double s;
};struct LidarPlaneNormFactor
{LidarPlaneNormFactor(Eigen::Vector3d curr_point_, Eigen::Vector3d plane_unit_norm_,double negative_OA_dot_norm_) : curr_point(curr_point_), plane_unit_norm(plane_unit_norm_),negative_OA_dot_norm(negative_OA_dot_norm_) {}template <typename T>bool operator()(const T *q, const T *t, T *residual) const{Eigen::Quaternion<T> q_w_curr{q[3], q[0], q[1], q[2]};Eigen::Matrix<T, 3, 1> t_w_curr{t[0], t[1], t[2]};Eigen::Matrix<T, 3, 1> cp{T(curr_point.x()), T(curr_point.y()), T(curr_point.z())};Eigen::Matrix<T, 3, 1> point_w;point_w = q_w_curr * cp + t_w_curr;Eigen::Matrix<T, 3, 1> norm(T(plane_unit_norm.x()), T(plane_unit_norm.y()), T(plane_unit_norm.z()));residual[0] = norm.dot(point_w) + T(negative_OA_dot_norm);return true;}static ceres::CostFunction *Create(const Eigen::Vector3d curr_point_, const Eigen::Vector3d plane_unit_norm_,const double negative_OA_dot_norm_){return (new ceres::AutoDiffCostFunction<LidarPlaneNormFactor, 1, 4, 3>(new LidarPlaneNormFactor(curr_point_, plane_unit_norm_, negative_OA_dot_norm_)));}Eigen::Vector3d curr_point;Eigen::Vector3d plane_unit_norm;double negative_OA_dot_norm;
};struct LidarDistanceFactor
{LidarDistanceFactor(Eigen::Vector3d curr_point_, Eigen::Vector3d closed_point_) : curr_point(curr_point_), closed_point(closed_point_){}template <typename T>bool operator()(const T *q, const T *t, T *residual) const{Eigen::Quaternion<T> q_w_curr{q[3], q[0], q[1], q[2]};Eigen::Matrix<T, 3, 1> t_w_curr{t[0], t[1], t[2]};Eigen::Matrix<T, 3, 1> cp{T(curr_point.x()), T(curr_point.y()), T(curr_point.z())};Eigen::Matrix<T, 3, 1> point_w;point_w = q_w_curr * cp + t_w_curr;residual[0] = point_w.x() - T(closed_point.x());residual[1] = point_w.y() - T(closed_point.y());residual[2] = point_w.z() - T(closed_point.z());return true;}static ceres::CostFunction *Create(const Eigen::Vector3d curr_point_, const Eigen::Vector3d closed_point_){return (new ceres::AutoDiffCostFunction<LidarDistanceFactor, 3, 4, 3>(new LidarDistanceFactor(curr_point_, closed_point_)));}Eigen::Vector3d curr_point;Eigen::Vector3d closed_point;
};

2.5 scanRegistration.cpp

  • 前端雷达处理和特征提取
  • 参考:四.激光SLAM框架学习之A-LOAM框架—项目工程代码介绍—2.scanRegistration.cpp–前端雷达处理和特征提取
// This is an advanced implementation of the algorithm described in the following paper:
//   J. Zhang and S. Singh. LOAM: Lidar Odometry and Mapping in Real-time.
//     Robotics: Science and Systems Conference (RSS). Berkeley, CA, July 2014. // Modifier: Tong Qin               qintonguav@gmail.com
// 	         Shaozu Cao 		    saozu.cao@connect.ust.hk// Copyright 2013, Ji Zhang, Carnegie Mellon University
// Further contributions copyright (c) 2016, Southwest Research Institute
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
//    this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
// 3. Neither the name of the copyright holder nor the names of its
//    contributors may be used to endorse or promote products derived from this
//    software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.//scanRegistration的功能就是提取特征点。
#include <cmath>
#include <vector>
#include <string>
#include "aloam_velodyne/common.h"
#include "aloam_velodyne/tic_toc.h"
#include <nav_msgs/Odometry.h>
#include <opencv/cv.h>
#include <pcl_conversions/pcl_conversions.h>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/kdtree/kdtree_flann.h>
#include <ros/ros.h>
#include <sensor_msgs/Imu.h>
#include <sensor_msgs/PointCloud2.h>
#include <tf/transform_datatypes.h>
#include <tf/transform_broadcaster.h>using std::atan2;
using std::cos;
using std::sin;const double scanPeriod = 0.1;const int systemDelay = 0; 
int systemInitCount = 0;
bool systemInited = false;
int N_SCANS = 0;
float cloudCurvature[400000];
int cloudSortInd[400000];
int cloudNeighborPicked[400000];
int cloudLabel[400000];//comp函数,比较两个点的曲率
bool comp (int i,int j) { return (cloudCurvature[i]<cloudCurvature[j]); }ros::Publisher pubLaserCloud;
ros::Publisher pubCornerPointsSharp;
ros::Publisher pubCornerPointsLessSharp;
ros::Publisher pubSurfPointsFlat;
ros::Publisher pubSurfPointsLessFlat;
ros::Publisher pubRemovePoints;
std::vector<ros::Publisher> pubEachScan;bool PUB_EACH_LINE = false;double MINIMUM_RANGE = 0.1; //removeClosedPointCloud函数,丢弃一定距离以内的点
template <typename PointT>
void removeClosedPointCloud(const pcl::PointCloud<PointT> &cloud_in,pcl::PointCloud<PointT> &cloud_out, float thres)
{// 假如输入输出点云不使用同一个变量,则需要将输出点云的时间戳和容器大小与输入点云同步if (&cloud_in != &cloud_out){cloud_out.header = cloud_in.header;cloud_out.points.resize(cloud_in.points.size());}size_t j = 0;// 把点云距离小于给定阈值的去除掉for (size_t i = 0; i < cloud_in.points.size(); ++i){if (cloud_in.points[i].x * cloud_in.points[i].x + cloud_in.points[i].y * cloud_in.points[i].y + cloud_in.points[i].z * cloud_in.points[i].z < thres * thres)continue;cloud_out.points[j] = cloud_in.points[i];j++;}// 重新调整输出容器大小if (j != cloud_in.points.size()){cloud_out.points.resize(j);}// 这里是对每条扫描线上的点云进行直通滤波,因此设置点云的高度为1,宽度为数量,稠密点云cloud_out.height = 1;cloud_out.width = static_cast<uint32_t>(j);cloud_out.is_dense = true;
}//这个函数主要就是对接收到的点云数据进行处理,
//最终发布关键点(laser_cloud_sharp、laser_cloud_less_sharp、laser_cloud_flat、laser_cloud_less_flat),以方便后续使用。
void laserCloudHandler(const sensor_msgs::PointCloud2ConstPtr &laserCloudMsg)
{//下面是做初始化,但是这里延时为0,相当于没有延时,只是留下了初始化的接口。if (!systemInited){ systemInitCount++;if (systemInitCount >= systemDelay){systemInited = true;}elsereturn;}//作者自己设计的计时类,以构造函数为起始时间,以toc()函数为终止时间,并返回时间间隔(ms)TicToc t_whole;//计算整个回调函数的时间TicToc t_prepare;//计算雷达点云有序化的时间//每条雷达扫描线上的可以计算曲率的点云点的起始索引和结束索引,分别用scanStartInd数组和scanEndInd数组记录std::vector<int> scanStartInd(N_SCANS, 0);std::vector<int> scanEndInd(N_SCANS, 0);pcl::PointCloud<pcl::PointXYZ> laserCloudIn;// 把点云从ros格式转到pcl的格式pcl::fromROSMsg(*laserCloudMsg, laserCloudIn);std::vector<int> indices;// 首先对点云滤波,去除NaN值得无效点云pcl::removeNaNFromPointCloud(laserCloudIn, laserCloudIn, indices);// 去除在Lidar坐标系原点 MINIMUM_RANGE 距离以内的点,这里调用作者写的函数removeClosedPointCloud(laserCloudIn, laserCloudIn, MINIMUM_RANGE);// 计算起始点和终止点角度// 解释一下:atan2的范围是[-180,180],atan的范围是[-90,90]/*下面要计算点云角度范围,是为了使点云有序,需要做到两件事:为每个点找到它所对应的扫描线(SCAN);为每条扫描线上的点分配时间戳。要计算每个点的时间戳, 首先我们需要确定这个点的角度范围。可以使用<cmath>中的atan2( )函数计算点云点的水平角度。这块在后面的激光框架基本都不需要了,因为后面雷达的驱动将每个点的线号、角度、时间戳都发出来了。所以说这一块可以跳过不看。*/// 计算起始点和结束点的角度,由于激光雷达是顺时针旋转,这里取反就相当于转成了逆时针int cloudSize = laserCloudIn.points.size();float startOri = -atan2(laserCloudIn.points[0].y, laserCloudIn.points[0].x);// 第一个扫描点的角度/** atan2()函数是atan(y, x)函数的增强版,不仅可以求取arctran(y/x)还能够确定象限* startOri和endOri分别为起始点和终止点的方位角* atan2范围是[-Pi,PI],这里加上2PI是为了保证起始到结束相差2PI符合实际*/float endOri = -atan2(laserCloudIn.points[cloudSize - 1].y,     // 最后一个扫描点的角度laserCloudIn.points[cloudSize - 1].x) +2 * M_PI;// 但总有一些例外,比如这里大于3PI,和小于PI,就需要调整到合理范围// 最后一个点的角度 - 第一个点的角度> PIif (endOri - startOri > 3 * M_PI){endOri -= 2 * M_PI;}else if (endOri - startOri < M_PI){endOri += 2 * M_PI;}//printf("end Ori %f\n", endOri);//printf("[scanRegistration] start Ori %f\n",startOri);//printf("[scanRegistration] end Ori %f\n", endOri);//对每一个点计算其 scanID 和该点对应的扫描到的时间,将结果放在intensity中。//为点云点找到对应的扫描线scanID,每条扫描线都有它固定的俯仰角,//我们可以根据点云点的垂直角度为其寻找对应的扫描线。bool halfPassed = false;int count = cloudSize;  // 所有的点数PointType point;std::vector<pcl::PointCloud<PointType>> laserCloudScans(N_SCANS);// 遍历每一个点for (int i = 0; i < cloudSize; i++){point.x = laserCloudIn.points[i].x;point.y = laserCloudIn.points[i].y;point.z = laserCloudIn.points[i].z;//通过计算垂直视场角确定激光点在哪个扫描线上(N_SCANS线激光雷达)float angle = atan(point.z / sqrt(point.x * point.x + point.y * point.y)) * 180 / M_PI;int scanID = 0;if (N_SCANS == 16){// 如果是16线激光雷达,结算出的angle应该在-15~15之间,//+-15°的垂直视场,垂直角度分辨率2°,则-15°时的scanID = 0。scanID = int((angle + 15) / 2 + 0.5);if (scanID > (N_SCANS - 1) || scanID < 0){count--;continue;}}else if (N_SCANS == 32){scanID = int((angle + 92.0/3.0) * 3.0 / 4.0);if (scanID > (N_SCANS - 1) || scanID < 0){count--;continue;}}else if (N_SCANS == 64){   if (angle >= -8.83)scanID = int((2 - angle) * 3.0 + 0.5);elsescanID = N_SCANS / 2 + int((-8.83 - angle) * 2.0 + 0.5);// use [0 50]  > 50 remove outlies if (angle > 2 || angle < -24.33 || scanID > 50 || scanID < 0){count--;continue;}}else{printf("wrong scan number\n");ROS_BREAK();}//printf("angle %f scanID %d \n", angle, scanID);// 获取每一个点对应的角度float ori = -atan2(point.y, point.x);// 每一个点的水平角度// 根据扫描线是否旋转过半选择与起始位置还是终止位置进行差值计算,//从而进行补偿,如果此时扫描没有过半,则halfPassed为falseif (!halfPassed) // 判断当前点过没过一半(开始的时候是没过一半的){ // 确保-PI / 2 < ori - startOri < 3 / 2 * PI, //如果ori-startOri小于-0.5pi或大于1.5pi,则调整ori的角度if (ori < startOri - M_PI / 2){ori += 2 * M_PI;}else if (ori > startOri + M_PI * 3 / 2){ori -= 2 * M_PI;}//扫描点过半则设定halfPassed为true,如果超过180度,就说明过了一半了if (ori - startOri > M_PI){halfPassed = true;}}else// 后半段时间{// 确保-PI * 3 / 2 < ori - endOri < PI / 2ori += 2 * M_PI;if (ori < endOri - M_PI * 3 / 2){ori += 2 * M_PI;}else if (ori > endOri + M_PI / 2){ori -= 2 * M_PI;}}/** relTime 是一个0~1之间的小数,代表占用一帧扫描时间的比例,乘以扫描时间得到真实扫描时刻,* scanPeriod扫描时间默认为0.1s* 角度的计算是为了计算相对起始时刻的时间*/float relTime = (ori - startOri) / (endOri - startOri);// 时间占比// 整数部分是scan线束的索引,小数部分是相对起始时刻的时间point.intensity = scanID + scanPeriod * relTime;// intensity=scanID+点对应的在线上的时间laserCloudScans[scanID].push_back(point); // 根据每条线的idx送入各自数组,表示这一条扫描线上的点}// cloudSize是有效的点云的数目cloudSize = count;printf("points size %d \n", cloudSize);// 记录16个scan的每一个的startInd,endIndpcl::PointCloud<PointType>::Ptr laserCloud(new pcl::PointCloud<PointType>());// 全部集合到一个点云里面去,但是使用两个数组标记起始和结果,//这里分别+5和-6是为了计算曲率方便for (int i = 0; i < N_SCANS; i++){ scanStartInd[i] = laserCloud->size() + 5;// 记录每个scan的开始index,忽略前5个点*laserCloud += laserCloudScans[i];scanEndInd[i] = laserCloud->size() - 6;// 记录每个scan的结束index,忽略后5个点,开始和结束处的点云scan容易产生不闭合的“接缝”,对提取edge feature不利}// 将一帧无序点云转换成有序点云消耗的时间,这里指的是前面处理雷达数据的时间printf("prepare time %f \n", t_prepare.toc());// 有序地计算曲率// 计算每一个点的曲率,这里的laserCloud是有序的点云,故可以直接这样计算// 但是在每条scan的交界处计算得到的曲率是不准确的,这可通过scanStartInd[i]、scanEndInd[i]来选取for (int i = 5; i < cloudSize - 5; i++)// 忽略前后的5个点{ float diffX = laserCloud->points[i - 5].x + laserCloud->points[i - 4].x + laserCloud->points[i - 3].x + laserCloud->points[i - 2].x + laserCloud->points[i - 1].x - 10 * laserCloud->points[i].x + laserCloud->points[i + 1].x + laserCloud->points[i + 2].x + laserCloud->points[i + 3].x + laserCloud->points[i + 4].x + laserCloud->points[i + 5].x;float diffY = laserCloud->points[i - 5].y + laserCloud->points[i - 4].y + laserCloud->points[i - 3].y + laserCloud->points[i - 2].y + laserCloud->points[i - 1].y - 10 * laserCloud->points[i].y + laserCloud->points[i + 1].y + laserCloud->points[i + 2].y + laserCloud->points[i + 3].y + laserCloud->points[i + 4].y + laserCloud->points[i + 5].y;float diffZ = laserCloud->points[i - 5].z + laserCloud->points[i - 4].z + laserCloud->points[i - 3].z + laserCloud->points[i - 2].z + laserCloud->points[i - 1].z - 10 * laserCloud->points[i].z + laserCloud->points[i + 1].z + laserCloud->points[i + 2].z + laserCloud->points[i + 3].z + laserCloud->points[i + 4].z + laserCloud->points[i + 5].z;//存储每个点的曲率的索引/* * cloudSortInd[i] = i相当于所有点的初始自然序列,每个点得到它自己的序号(索引)* 对于每个点,选择了它附近的特征点数量初始化为0,* 每个点的点类型初始设置为0(次极小平面点)*/cloudCurvature[i] = diffX * diffX + diffY * diffY + diffZ * diffZ;cloudSortInd[i] = i;cloudNeighborPicked[i] = 0;// 点有没有被选选择为feature点cloudLabel[i] = 0;// Label 2: corner_sharp// Label 1: corner_less_sharp, 包含Label 2// Label -1: surf_flat// Label 0: surf_less_flat, 包含Label -1,因为点太多,最后会降采样}/*
曲率计算完成后进行特征分类,提取特征点有几点原则:1.为了提高效率,每条扫描线分成6个扇区,在每个扇区内,寻找曲率最大的20个点,作为次极大边线点,其中最大的2个点,同时作为极大边线点;2. 寻找曲率最小的4个点,作为极小平面点,剩下未被标记的点,全部作为次极小平面点。3. 为了防止特征点过多聚堆,每提取一个特征点(极大/次极大边线点,极小平面点),都要将这个点和它附近的点全都标记为“已选中”,在下次提取特征点时,将会跳过这些点。对于次极小平面点,采取降采样的方法避免过多聚堆。
*/TicToc t_pts;//计算特征提取的时间pcl::PointCloud<PointType> cornerPointsSharp;// 极大边线点pcl::PointCloud<PointType> cornerPointsLessSharp;// 次极大边线点pcl::PointCloud<PointType> surfPointsFlat;// 极小平面点pcl::PointCloud<PointType> surfPointsLessFlat;// 次极小平面// 提取特征点float t_q_sort = 0;// 用来记录排序花费的总时间// 遍历每个scan,对每条线进行操作(曲率排序,选取对应特征点)for (int i = 0; i < N_SCANS; i++)// 按照scan的顺序提取4种特征点{// 如果最后一个可算曲率的点与第一个的数量差小于6,说明无法分成6个扇区,跳过if( scanEndInd[i] - scanStartInd[i] < 6)// 如果该scanx线的点数少于7个点,就跳过continue;// 用来存储次极小平面点,后面会进行降采样pcl::PointCloud<PointType>::Ptr surfPointsLessFlatScan(new pcl::PointCloud<PointType>);//为了使特征点均匀分布,将一个scan线分成6个扇区for (int j = 0; j < 6; j++)// 将该scan线分成6小段执行特征检测{// 每个等分的起始和结束点int sp = scanStartInd[i] + (scanEndInd[i] - scanStartInd[i]) * j / 6;// subscan的起始indexint ep = scanStartInd[i] + (scanEndInd[i] - scanStartInd[i]) * (j + 1) / 6 - 1;// subscan的结束indexTicToc t_tmp;//计算排序时间// 对点云按照曲率进行升序排序,小的在前,大的在后std::sort (cloudSortInd + sp, cloudSortInd + ep + 1, comp);// t_q_sort累计每个扇区曲率排序时间总和t_q_sort += t_tmp.toc();// 选取极大边线点(2个)和次极大边线点(20个)int largestPickedNum = 0;// 挑选曲率比较大的部分,从最大曲率往最小曲率遍历,寻找边线点,并要求大于0.1for (int k = ep; k >= sp; k--)// 从后往前,即从曲率大的点开始提取corner feature{// 排序后顺序就乱了,这个时候索引的作用就体现出来了int ind = cloudSortInd[k]; // 看看这个点是否是有效点,同时曲率是否大于阈值,即没被选过 && 曲率 > 0.1if (cloudNeighborPicked[ind] == 0 &&cloudCurvature[ind] > 0.1)// 如果该点没有被选择过,并且曲率大于0.1{largestPickedNum++;// 每段选2个曲率大的点if (largestPickedNum <= 2)// 该subscan中曲率最大的前2个点认为是corner_sharp特征点{              // label为2是曲率大的标记,即设置标签为极大边线点          cloudLabel[ind] = 2;// cornerPointsSharp用来存放大曲率的点,既放入极大边线点,也放入次极大边线点cornerPointsSharp.push_back(laserCloud->points[ind]);cornerPointsLessSharp.push_back(laserCloud->points[ind]);}// 以及20个曲率稍微大一些的点else if (largestPickedNum <= 20)// 该subscan中曲率最大的前20个点认为是corner_less_sharp特征点{           // label置1表示曲率稍微大一些,超过2个选择点以后,设置为次极大边线点,放入次极大边线点容器                       cloudLabel[ind] = 1; cornerPointsLessSharp.push_back(laserCloud->points[ind]);}// 超过20个就跳过else{break;}// 这个点被选中后pick标志位置1 ,记录这个点已经被选择了cloudNeighborPicked[ind] = 1;// 标记该点被选择过了// 与当前点距离的平方 <= 0.05的点标记为选择过,避免特征点密集分布// 为了保证特征点不过度集中,将选中的点周围5个点都置1,避免后续会选到for (int l = 1; l <= 5; l++){// 查看相邻点距离是否差异过大,如果差异过大说明点云在此不连续,//是特征边缘,就会是新的特征,因此就不置位了float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l - 1].x;float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l - 1].y;float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l - 1].z;// 相邻scan点距离的平方 <= 0.05的点标记为选择过,避免特征点密集分布if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05){break;}// 与临近点的距离的平方 <= 0.05的点标记为选择过,避免特征点密集分布,越往上scanID越大越远,所以可以直接breakcloudNeighborPicked[ind + l] = 1;}for (int l = -1; l >= -5; l--){float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l + 1].x;float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l + 1].y;float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l + 1].z;if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05){break;}cloudNeighborPicked[ind + l] = 1;}}}// 下面开始挑选面点,选取极小平面点(4个)// 提取surf平面feature,与上述类似,选取该subscan曲率最小的前4个点为surf_flatint smallestPickedNum = 0;for (int k = sp; k <= ep; k++) // 寻找平的,曲率由小到大排序{int ind = cloudSortInd[k];// 判断是否满足平面点的条件// 确保这个点没有被pick且曲率小于阈值if (cloudNeighborPicked[ind] == 0 &&cloudCurvature[ind] < 0.1){// -1认为是平坦的点cloudLabel[ind] = -1; surfPointsFlat.push_back(laserCloud->points[ind]);// 这里不区分平坦和比较平坦,因为剩下的点label默认是0,就是比较平坦smallestPickedNum++;if (smallestPickedNum >= 4){ break;}cloudNeighborPicked[ind] = 1;// 根据scanID往上查找,防止关键点密集for (int l = 1; l <= 5; l++){ float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l - 1].x;float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l - 1].y;float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l - 1].z;if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05){break;}cloudNeighborPicked[ind + l] = 1;}for (int l = -1; l >= -5; l--){float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l + 1].x;float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l + 1].y;float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l + 1].z;if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05){break;}cloudNeighborPicked[ind + l] = 1;}}}// 其他的非corner特征点与surf_flat特征点一起组成surf_less_flat特征点// 选取次极小平面点,除了极小平面点,剩下的都是次极小平面点for (int k = sp; k <= ep; k++){if (cloudLabel[k] <= 0){surfPointsLessFlatScan->push_back(laserCloud->points[k]);}}}// 最后对该scan点云中提取的所有surf_less_flat特征点进行降采样,因为点太多了pcl::PointCloud<PointType> surfPointsLessFlatScanDS;pcl::VoxelGrid<PointType> downSizeFilter;downSizeFilter.setInputCloud(surfPointsLessFlatScan);downSizeFilter.setLeafSize(0.2, 0.2, 0.2);downSizeFilter.filter(surfPointsLessFlatScanDS);surfPointsLessFlat += surfPointsLessFlatScanDS;}printf("sort q time %f \n", t_q_sort);printf("seperate points time %f \n", t_pts.toc());sensor_msgs::PointCloud2 laserCloudOutMsg;// 创建publish msg实例pcl::toROSMsg(*laserCloud, laserCloudOutMsg);// 有序点云转化为msglaserCloudOutMsg.header.stamp = laserCloudMsg->header.stamp;// 时间戳保持不变laserCloudOutMsg.header.frame_id = "/camera_init"; // frame_id名字,坐标系pubLaserCloud.publish(laserCloudOutMsg);sensor_msgs::PointCloud2 cornerPointsSharpMsg;pcl::toROSMsg(cornerPointsSharp, cornerPointsSharpMsg);cornerPointsSharpMsg.header.stamp = laserCloudMsg->header.stamp;cornerPointsSharpMsg.header.frame_id = "/camera_init";pubCornerPointsSharp.publish(cornerPointsSharpMsg);sensor_msgs::PointCloud2 cornerPointsLessSharpMsg;pcl::toROSMsg(cornerPointsLessSharp, cornerPointsLessSharpMsg);cornerPointsLessSharpMsg.header.stamp = laserCloudMsg->header.stamp;cornerPointsLessSharpMsg.header.frame_id = "/camera_init";pubCornerPointsLessSharp.publish(cornerPointsLessSharpMsg);sensor_msgs::PointCloud2 surfPointsFlat2;pcl::toROSMsg(surfPointsFlat, surfPointsFlat2);surfPointsFlat2.header.stamp = laserCloudMsg->header.stamp;surfPointsFlat2.header.frame_id = "/camera_init";pubSurfPointsFlat.publish(surfPointsFlat2);sensor_msgs::PointCloud2 surfPointsLessFlat2;pcl::toROSMsg(surfPointsLessFlat, surfPointsLessFlat2);surfPointsLessFlat2.header.stamp = laserCloudMsg->header.stamp;surfPointsLessFlat2.header.frame_id = "/camera_init";pubSurfPointsLessFlat.publish(surfPointsLessFlat2);// pub each scam// 可以按照每个scan发出去,不过这里是falseif(PUB_EACH_LINE){for(int i = 0; i< N_SCANS; i++){sensor_msgs::PointCloud2 scanMsg;pcl::toROSMsg(laserCloudScans[i], scanMsg);scanMsg.header.stamp = laserCloudMsg->header.stamp;scanMsg.header.frame_id = "/camera_init";pubEachScan[i].publish(scanMsg);}}printf("scan registration time %f ms *************\n", t_whole.toc());if(t_whole.toc() > 100)ROS_WARN("scan registration process over 100ms");
}int main(int argc, char **argv)
{ros::init(argc, argv, "scanRegistration"); //节点名称ros::NodeHandle nh; //注册ROS句柄//从launch文件参数服务器中获取多少线的激光雷达,如果没有则默认16线nh.param<int>("scan_line", N_SCANS, 16);// 从launch文件参数服务器中获取激光雷达的最小扫描距离MINIMUM_RANGE,//小于MINIMUM_RANGE的点将被滤除,单位为M,如果没有则默认0.1。nh.param<double>("minimum_range", MINIMUM_RANGE, 0.1);printf("scan line number %d \n", N_SCANS);// 只有线束是16、32、64的才可以继续if(N_SCANS != 16 && N_SCANS != 32 && N_SCANS != 64){printf("only support velodyne with 16, 32 or 64 scan line!");return 0;}// 订阅初始的激光雷达数据,并注册回调函数laserCloudHandlerros::Subscriber subLaserCloud = nh.subscribe<sensor_msgs::PointCloud2>("/velodyne_points", 100, laserCloudHandler);// 发布话题:有序点云(删除过近点、设置索引),极大边线点集合,次极大边线点集合,极小平面点集合,次极小平面点集合,删除的点云pubLaserCloud = nh.advertise<sensor_msgs::PointCloud2>("/velodyne_cloud_2", 100);pubCornerPointsSharp = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_sharp", 100);pubCornerPointsLessSharp = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_less_sharp", 100);pubSurfPointsFlat = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_flat", 100);pubSurfPointsLessFlat = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_less_flat", 100);pubRemovePoints = nh.advertise<sensor_msgs::PointCloud2>("/laser_remove_points", 100);if(PUB_EACH_LINE)//这个条件没有进去{for(int i = 0; i < N_SCANS; i++){ros::Publisher tmp = nh.advertise<sensor_msgs::PointCloud2>("/laser_scanid_" + std::to_string(i), 100);pubEachScan.push_back(tmp);}}ros::spin();// 循环执行回调函数return 0;
}

3 运行结果

roslaunch aloam_velodyne aloam_velodyne_VLP_16.launch
rosbag play YOUR_DATASET_FOLDER/nsh_indoor_outdoor.bag

在这里插入图片描述

代码编程
赞赏

相关文章

数据结构与算法 二维迷宫问题
数据结构与算法 约瑟夫环问题 Josephus问题
数据结构与算法 哈希表的特点
数据结构与算法 Farmer John 问题 农夫锯木板问题
数据结构与算法 前缀、中缀、后缀表达式求值和相互转换
链表(图文详解)