激光SLAM源码解析S-LOAM(二)激光里程计的计算

10Hz激光雷达点云帧,相临帧的时间间隔是0.1秒。在这0.1秒内激光雷达的位姿变化(平移和旋转)可由这两帧点云的配准计算出来。

以某一时刻为起点,累计帧间位姿变换,得到各帧时刻激光雷达相对起点的位姿(位置和方向),这就是里程计。

配准是求取帧间变换的关键,帧帧配准是指相邻帧点云的配准,比如,配对的双方分别是当前帧与上一帧。

点云共面算法是点云帧帧配准的主要方法。当一个点到一个平面的距离为0时,就认为这个点就与此平面共面。

lidarOdometry.cpp

代码注释较详细,如下所示。

/**
 * Created by haocaichao on 2021/8/29
 *
 * 激光里程计的计算(帧帧配准)
 * (1)接收帧平面特征点云
 * (2)计算当前帧与上一帧之间位姿变换(帧帧配准,ceres优化)
 * (3)累计帧间变换,得到从起始帧开始的里程计
 * (4)发布点云、里程计、轨迹、地图
 */

#include "header.h"

typedef pcl::PointXYZI PointType;

int isDone=1;         //标识运算是否完成
float planeMax=0.5;   //平面判断门槛值
std::mutex mLock;      //多线程锁
pcl::VoxelGrid<PointType> downSizeFilterMap;  //定义点云下采样对象,用于点云抽稀

/**
 * 定义ceres优化中的代价函数
 * 点到平面的距离计算用向量表示
 * 在同一坐标系中,两点间(po,pa)向量值与平面法向量(norm)的点乘可得到距离
 */
struct PlaneFeatureCost{
    const Eigen::Vector3d _po,_pa,_norm;

    PlaneFeatureCost(Eigen::Vector3d po,Eigen::Vector3d pa,Eigen::Vector3d norm):
            _po(po),_pa(pa),_norm(norm){}

    template <typename T>
    bool operator()(const T* q,const T* t,T* residual) const {
        Eigen::Matrix<T,3,1> po_curr{T(_po[0]),T(_po[1]),T(_po[2])};
        Eigen::Matrix<T,3,1> pa_last{T(_pa[0]),T(_pa[1]),T(_pa[2])};
        Eigen::Matrix<T,3,1> p_norm{T(_norm[0]),T(_norm[1]),T(_norm[2])};
        Eigen::Quaternion<T> q_last_curr{q[3],q[0],q[1],q[2]};   //用于坐标系变换统一
        Eigen::Matrix<T,3,1> t_last_curr{t[0],t[1],t[2]};        //用于坐标系变换统一
        Eigen::Matrix<T,3,1> po_last;
        po_last=q_last_curr*po_curr+t_last_curr;
        residual[0]=((po_last-pa_last).dot(p_norm));
        return true;
    }
};

ros::Subscriber sub_plane_frame_cloud;      //接收平面特征点云
ros::Publisher pub_plane_frame_cloud;       //发布平面特征点云
ros::Publisher pub_frame_odometry;          //发布激光雷达里程计,由帧帧配准计算得到
ros::Publisher pub_frame_odom_path;         //发布激光雷达运动轨迹
ros::Publisher pub_sum_lidar_odom_cloud;    //发布拼接后的点云地图
//存储当前帧点云
pcl::PointCloud<PointType>::Ptr lastFramePlanePtr(new pcl::PointCloud<PointType>());
//存储上一帧点云
pcl::PointCloud<PointType>::Ptr currFramePlanePtr(new pcl::PointCloud<PointType>());
//存储拼接后总点云
pcl::PointCloud<PointType>::Ptr sumPlaneCloudPtr(new pcl::PointCloud<PointType>());
std::queue<sensor_msgs::PointCloud2> planeQueue;  //定义点云消息队列
nav_msgs::Path lidarPathInOdom;        //定义激光雷达运动轨迹
std_msgs::Header currHead;             //定义ros消息头变量
double timePlane=0;                    //定义平面点云帧时间戳变量
int numFrame=0;                        //定义帧计数变量
int flagStart=0;                       //定义是否开始标志
double para_q[4]={0,0,0,1};            //定义长度为4的数组,用于构成四元数
double para_t[3]={0,0,0};              //定义长度为3的数组,用于构成位移
Eigen::Quaterniond q_0_curr(1,0,0,0);  //起点到当前帧,四元数
Eigen::Vector3d t_0_curr(0,0,0);       //起点到当前帧,位移
Eigen::Quaterniond q_0_last(1,0,0,0);  //起点到上一帧,四元数
Eigen::Vector3d t_0_last(0,0,0);       //起点到上一帧,位移
//上一帧到当前帧,四元数
Eigen::Quaterniond q_last_curr=Eigen::Map<Eigen::Quaterniond>(para_q);
//上一帧到当前帧,位移
Eigen::Vector3d t_last_curr=Eigen::Map<Eigen::Vector3d>(para_t);

//将当前点坐标变换到上一帧坐标系中
void transformToLast(PointType const *const pi,PointType *const po){
    Eigen::Vector3d curr_point(pi->x,pi->y,pi->z);
    Eigen::Vector3d proj_point;
    proj_point=q_last_curr*curr_point+t_last_curr;
    po->x=proj_point.x();
    po->y=proj_point.y();
    po->z=proj_point.z();
    po->intensity=pi->intensity;
}

//发布点云、里程计、轨迹、地图
void publishResult(){
    //累计帧间变换,得到从起点开始的里程计
    q_0_curr = q_0_last * q_last_curr ;
    t_0_curr = t_0_last + q_0_last * t_last_curr;
    q_0_last = q_0_curr;
    t_0_last = t_0_curr;
    //发布里程计
    nav_msgs::Odometry lidarOdometry;
    lidarOdometry.header.frame_id = "map";
    lidarOdometry.child_frame_id = "map_child";
    lidarOdometry.header.stamp = currHead.stamp;
    lidarOdometry.pose.pose.position.x = t_0_curr[0];
    lidarOdometry.pose.pose.position.y = t_0_curr[1];
    lidarOdometry.pose.pose.position.z = t_0_curr[2];
    lidarOdometry.pose.pose.orientation.x = q_0_curr.x();
    lidarOdometry.pose.pose.orientation.y = q_0_curr.y();
    lidarOdometry.pose.pose.orientation.z = q_0_curr.z();
    lidarOdometry.pose.pose.orientation.w = q_0_curr.w();
    pub_frame_odometry.publish(lidarOdometry);
    //发布里轨迹
    geometry_msgs::PoseStamped pose_stamped;
    pose_stamped.header.stamp = lidarOdometry.header.stamp;
    pose_stamped.header.frame_id = "map";
    pose_stamped.pose = lidarOdometry.pose.pose;
    lidarPathInOdom.poses.push_back(pose_stamped);
    lidarPathInOdom.header.stamp=lidarOdometry.header.stamp;
    lidarPathInOdom.header.frame_id="map";
    pub_frame_odom_path.publish(lidarPathInOdom);
    //发布平面特征点云
    sensor_msgs::PointCloud2 plane_frame_cloud_msgs;
    pcl::toROSMsg(*currFramePlanePtr, plane_frame_cloud_msgs);
    plane_frame_cloud_msgs.header.stamp = lidarOdometry.header.stamp;
    plane_frame_cloud_msgs.header.frame_id = "map";
    pub_plane_frame_cloud.publish(plane_frame_cloud_msgs);
    //发布拼接点云地图
//    if(numFrame % 10 == 0){
//        double r,p,y;
//        tf::Quaternion tq(q_0_curr.x(),q_0_curr.y(),q_0_curr.z(),q_0_curr.w());
//        tf::Matrix3x3(tq).getRPY(r,p,y);
//        Eigen::Affine3d transCurd ;
//        pcl::getTransformation(t_0_curr.x(), t_0_curr.y(), t_0_curr.z(), r,p,y,transCurd);
//        pcl::PointCloud<PointType>::Ptr cloud_res(new pcl::PointCloud<PointType>());
//        pcl::transformPointCloud(*currFramePlanePtr, *cloud_res, transCurd);
//        *sumPlaneCloudPtr += *cloud_res;
//        pcl::PointCloud<PointType>::Ptr cloud_temp(new pcl::PointCloud<PointType>());
//        downSizeFilterMap.setInputCloud(sumPlaneCloudPtr);
//        downSizeFilterMap.filter(*cloud_temp);
//        sensor_msgs::PointCloud2 res_cloud_msgs;
//        pcl::toROSMsg(*cloud_temp, res_cloud_msgs);
//        res_cloud_msgs.header.stamp = lidarOdometry.header.stamp;
//        res_cloud_msgs.header.frame_id = "map";
//        pub_sum_lidar_odom_cloud.publish(res_cloud_msgs);
//    }
}

//计算帧帧配准,得到帧间位姿变换
void frameRegistration(){
    //定义ceres优化对象与参数
    ceres::LossFunction *loss_function=new ceres::HuberLoss(0.1);
    ceres::LocalParameterization *q_parameterization=new ceres::EigenQuaternionParameterization();
    ceres::Problem problem;
    problem.AddParameterBlock(para_q,4,q_parameterization);
    problem.AddParameterBlock(para_t,3);

    pcl::KdTreeFLANN<PointType> kdTreePlanLast;
    int last_plane_num=lastFramePlanePtr->points.size();
    int curr_plane_num=currFramePlanePtr->points.size();
    if(last_plane_num>10){
        kdTreePlanLast.setInputCloud(lastFramePlanePtr);
        for(int i_opt=0;i_opt<2;i_opt++){
            for (int i = 0; i < curr_plane_num; ++i) {    //遍历当前帧各平面点
                PointType pointSeed;
                //将当前帧此平面点坐标变换到上一帧坐标系中
                transformToLast(&currFramePlanePtr->points[i],&pointSeed);
                std::vector<float> pointSearchSqDis1;
                std::vector<int> indx1;
                //将变换后的此点作为种子点,查找上一帧中距离此点最近点的索引
                kdTreePlanLast.nearestKSearch(pointSeed,1,indx1,pointSearchSqDis1);
                int p_ind_a=indx1[0];
                std::vector<float> pointSearchSqDis2;
                std::vector<int> indx2;
                //将上面最近点作为种子点,查找上一帧中距离最近点的索引和距离
                kdTreePlanLast.nearestKSearch(lastFramePlanePtr->points[p_ind_a],30,indx2,pointSearchSqDis2);
                std::vector<int> v_indx5;
                std::vector<int> v_indx_row;
                int p_row=-1;
                if(indx2.size()<5) continue;
                int n=5;
                //挑选5个最近点,尽量满足有2个点不属于同一扫描线
                for (size_t i_kd = 0; i_kd < indx2.size(); ++i_kd) {
                    float f_indx=lastFramePlanePtr->points[indx2[i_kd]].intensity;
                    int i_indx=int(f_indx);
                    int row=100*(f_indx-i_indx+0.002);   //获取点索引
                    if(i_kd==0){
                        p_row=row;
                    }
                    if(i_kd<5){       //先将最近5个点选入
                        v_indx5.push_back(indx2[i_kd]);
                    }else{            //从第6个点开始,寻找与记录不同线的最近2个点
                        if(row != p_row && row>=0 && row <=63){
                            v_indx_row.push_back(indx2[i_kd]);
                            n=i_kd;
                            if(v_indx_row.size()>=2){
                                break;
                            }
                        }
                    }
                }
                if(v_indx_row.size()==1){       //如果不同线的只有1个点
                    v_indx5[4]=v_indx_row[0];
                }
                if(v_indx_row.size()==2){       //如果不同线的有2个点
                    v_indx5[3]=v_indx_row[0];
                    v_indx5[4]=v_indx_row[1];
                }

                if(pointSearchSqDis2[n]<1){     //如果5个点中最远的点小于1米,则利用5点估算平面法线
                    Eigen::Matrix<float, 5, 3> matA0;
                    Eigen::Matrix<float, 5, 1> matB0;
                    Eigen::Vector3f matX0;
                    matA0.setZero();
                    matB0.fill(-1);
                    matX0.setZero();
                    for (int j = 0; j < 5; ++j) {
                        matA0(j,0)=lastFramePlanePtr->points[v_indx5[j]].x;
                        matA0(j,1)=lastFramePlanePtr->points[v_indx5[j]].y;
                        matA0(j,2)=lastFramePlanePtr->points[v_indx5[j]].z;
                    }
                    matX0=matA0.colPivHouseholderQr().solve(matB0);
                    matX0.normalize();  //norm
                    bool planeValid = true;
                    for (int k = 0; k < 4; ++k) {   //利用法向量计算各点到平面的距离
                        Eigen::Vector3d v_temp(
                                lastFramePlanePtr->points[v_indx5[k]].x-lastFramePlanePtr->points[v_indx5[k+1]].x,
                                lastFramePlanePtr->points[v_indx5[k]].y-lastFramePlanePtr->points[v_indx5[k+1]].y,
                                lastFramePlanePtr->points[v_indx5[k]].z-lastFramePlanePtr->points[v_indx5[k+1]].z
                        );
                        if(fabs(matX0(0)*v_temp[0]+matX0(1)*v_temp[1]+matX0(2)*v_temp[2])>planeMax){
                            planeValid=false;       //如果有点到平面的距离太大,则说明此5点不共面
                            break;
                        }
                    }
                    Eigen::Vector3d po(currFramePlanePtr->points[i].x,currFramePlanePtr->points[i].y,currFramePlanePtr->points[i].z);
                    Eigen::Vector3d pa(lastFramePlanePtr->points[p_ind_a].x,lastFramePlanePtr->points[p_ind_a].y,lastFramePlanePtr->points[p_ind_a].z);
                    Eigen::Vector3d norm(matX0[0],matX0[1],matX0[2]);
                    if(planeValid){                 //当找到了共面点,就利用种子点、最近点、平面法向量,构造点与平面共面的优化条件
                        problem.AddResidualBlock(new ceres::AutoDiffCostFunction<PlaneFeatureCost,1,4,3>
                                                         (new PlaneFeatureCost(po,pa,norm)),loss_function,para_q,para_t);
                    }
                }
            }
        }
        //设置优化参数,并优化
        ceres::Solver::Options options;
        options.linear_solver_type=ceres::DENSE_QR;
        options.max_num_iterations=8;
        options.minimizer_progress_to_stdout=false;
        ceres::Solver::Summary summary;
        ceres::Solve(options,&problem,&summary);
        //得到优化结果,构成帧间变换的方向四元数与位移
        q_last_curr=Eigen::Map<Eigen::Quaterniond>(para_q);
        t_last_curr=Eigen::Map<Eigen::Vector3d>(para_t);
    }
}

//接收平面特征点云,添加到消息队列中
void cloudHandler(const sensor_msgs::PointCloud2ConstPtr &cldMsg) {
    mLock.lock();
    planeQueue.push(*cldMsg);
    mLock.unlock();
}

//点云处理多线程函数
void cloudThread(){
    ros::Rate rate(10);                //控制处理频率为10hz,为最终频率
    while(ros::ok()){
        rate.sleep();
        ros::Rate rate2(50);           //内层处理频率为50hz
        if(isDone==0) continue;
        while (planeQueue.size()>0){
            if(isDone==0) continue;
            isDone=0;
            numFrame++;
            rate2.sleep();

            mLock.lock();               //锁线程,取数据
            currFramePlanePtr->clear();
            currHead=planeQueue.front().header;
            timePlane=planeQueue.front().header.stamp.toSec();
            pcl::fromROSMsg(planeQueue.front(),*currFramePlanePtr);
            planeQueue.pop();
            mLock.unlock();

            if(flagStart==0){          //标志起始帧
                flagStart=1;
            }else{                     //从第二帧开始计算帧帧配准
                frameRegistration();
                publishResult();
            }
            *lastFramePlanePtr=*currFramePlanePtr;
            isDone=1;
        }
    }
}

int main(int argc, char **argv) {
    if(N_SCAN_ROW==16){
        planeMax=0.15;
    }
    if(N_SCAN_ROW==64){
        planeMax=0.05;
    }
    downSizeFilterMap.setLeafSize(0.4,0.4,0.4);

    ros::init(argc, argv, "LidarOdometry");
    ros::NodeHandle nh;
    sub_plane_frame_cloud = nh.subscribe<sensor_msgs::PointCloud2>("/plane_frame_cloud1", 10, cloudHandler);
    pub_plane_frame_cloud=nh.advertise<sensor_msgs::PointCloud2>("/plane_frame_cloud2",100);
    pub_frame_odometry = nh.advertise<nav_msgs::Odometry>("/frame_odom2", 100);
    pub_frame_odom_path = nh.advertise<nav_msgs::Path>("/frame_odom_path2", 100);
    pub_sum_lidar_odom_cloud = nh.advertise<sensor_msgs::PointCloud2>("/sum_lidar_odom_cloud2", 10);
    ROS_INFO("\033[1;32m----> LidarOdometry Started.\033[0m");
    std::thread t_cloud_thread(cloudThread);
    ros::spin();
    t_cloud_thread.join();
    return 0;
}

代码地址为(GitHub - haocaichao/S-LOAM: S-LOAM(Simple LOAM) 是一种简单易学的激光SLAM算法,整个程序只有几百行代码,十分方便学习与试验分析。)。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值