利用IMU进行激光点云运动畸变校正

利用IMU进行激光点云运动畸变校正

1.思想原理

在激光SLAM定位与建图过程中,当激光雷达运动得比较慢没有比较大幅度的旋转时,可以不用考虑点云的运动畸变,在帧间匹配时依然能用ICP配准估计出雷达的位姿。但是在激光雷达运动幅度比较大时,就不得不考虑激光点云的运动畸变,可以采用IMU或者编码器提供的短时间位姿估计信息去除点云的运动畸变。在这里我采用的是IMU对点云的运动畸变进行去除,IMU输出的加速度信息积分很发散,如果是更差的IMU设备,更用不了加速度信息,在这里之考虑了利用IMU的角速度信息。在1帧的点云时间内,对所有的IMU的角速度进行积分得到IMU的旋转角度,再转换到激光雷达上,对雷达的姿态进行姿态四元数球面线性插值将对应点云转换到末状态的姿态上实现去点云运动畸变去除。

2.操作流程

使用的设备:

  • 激光雷达:Velodyne VLP16 ,频率为10HZ,机械旋转式,16线,在垂直方向为 ( − 1 5 ∘ , 1 5 ∘ ) (-15^{\circ} , 15^{\circ} ) (15,15),每条线之间夹角为 2 ∘ 2^{\circ} 2,在水平面上有1800个激光点,平均分辨率 0. 2 ∘ 0.2^{\circ} 0.2
  • IMU: D435i,频率为200HZ。

具体流程:

  1. 对IMU进行角速度积分:角速度积分参照了VINS-Mono的预积分流程,激光雷达的频率远低于IMU频率。在一个点云数据的一个周期内(100ms),IMU大约会产生 200 ÷ 10 = 20 200{\div} 10 = 20 200÷10=20个,根据公式:
    α t = ω t 0 + ω t 2 ⋅ d t d t = t − t 0 α = ∑ i = 0 n − 1 α i \alpha_{t} = \frac{\omega_{t_0} + \omega_{t}}{2}\cdot dt \newline dt = t - t_0 \\ \alpha = \sum_{i =0}^{n-1} \alpha_i αt=2ωt0+ωtdtdt=tt0α=i=0n1αi
    其中 α t \alpha_{t} αt t t t时刻相对于前一时刻 t 0 t_0 t0的角度变化, ω t 0 , ω t \omega_{t_0} , \omega_{t} ωt0,ωt分别为对应时刻的角速度, n n n个IMU数据。这样子不断积分累加迭代即可得到一个周期内IMU旋转角度。

    在这里,采用的是四元数的计算方式。四元数 q = [ c o s ( θ 2 ) s i n ( θ 2 ) ⋅ r ] = [ w x y z ] q=\begin{bmatrix} cos(\frac{\theta}{2} ) \\ sin(\frac{\theta}{2})\cdot r \end{bmatrix} = \begin{bmatrix} w \\ x \\ y \\ z \end{bmatrix} q=[cos(2θ)sin(2θ)r]=wxyz,对于相邻的IMU数据 θ 2 \frac{\theta}{2} 2θ都趋近于0,可用等价无穷小
    lim ⁡ 狗 → 0 s i n ( 狗 ) 狗 = 1 lim ⁡ 狗 → 0 c o s ( 狗 ) = 1 \lim_{狗 \to 0} \frac{sin(狗)}{狗} = 1 \\ \lim_{狗 \to 0} cos(狗) = 1 0limsin()=10limcos()=1
    则有:
    q t = q t 0 ⋅ q d t q d t = ( 1 , α x 2 , α y 2 , α z 2 ) q_t = q_{t_0}\cdot q_{dt} \\ q_{dt} = (1, \frac{\alpha_x}{2} , \frac{\alpha_y}{2}, \frac{\alpha_z}{2}) qt=qt0qdtqdt=(1,2αx,2αy,2αz)
    其中为 q t q_t qt时刻的姿态四元数, q t 0 q_{t_0} qt0为前一时刻姿态四元数, q d t q_{dt} qdt为该时间段内四元数变化值, α x \alpha_x αx, α y \alpha_y αy , α z \alpha_z αz为对应的XYZ三轴角度变化.

    附上部分ROS+Eigen的代码

    std::vector<sensor_msgs::Imu::ConstPtr> imu_msg_vector;
    std::mutex mutex_lock;
    
    // IMU消息回调存储
    void imu_cb(const sensor_msgs::Imu::ConstPtr &imu_msg)
    {
        mutex_lock.lock();
        imu_msg_vector.push_back(imu_msg);
        mutex_lock.unlock();
    }
    
    // IMU角度积分
    Eigen::Quaterniond imu_q = Eigen::Quaterniond::Identity(); // R
    Eigen::Vector3d angular_velocity_1;
    angular_velocity_1 << imu_msg_v[0]->angular_velocity.x, imu_msg_v[0]->angular_velocity.y, imu_msg_v[0]->angular_velocity.z;
    double lastest_time = imu_msg_v[0]->header.stamp.toSec();
    for (int i = 1; i < imu_msg_v.size(); i++)
    {
        double t = imu_msg_v[i]->header.stamp.toSec();
        double dt = t - lastest_time;
        lastest_time = t;
        Eigen::Vector3d angular_velocity_2;
        angular_velocity_2 << imu_msg_v[i]->angular_velocity.x, imu_msg_v[i]->angular_velocity.y, imu_msg_v[i]->angular_velocity.z;
        Eigen::Vector3d aver_angular_vel = (angular_velocity_1 + angular_velocity_2) / 2.0;
        imu_q = imu_q * Eigen::Quaterniond(1, 0.5 * aver_angular_vel(0) * dt, 0.5 * aver_angular_vel(1) * dt, 0.5 * aver_angular_vel(2) * dt);
    }
    
  2. IMU->雷达的姿态变换:以上测量出IMU的姿态角变化,需要映射到激光雷达坐标系上。实现需要知道激光雷达与IMU的外参变换变换矩阵(不用平移向量,但一定要旋转矩阵), R l i d a r I M U R_{lidar}^{IMU} RlidarIMU为从lidar到IMU的旋转矩阵(需要事先标定)。在lidar与IMU一起运动过程中,可以认为 R l i d a r I M U R_{lidar}^{IMU} RlidarIMU是不变的。则有关系:
    R l i d a r 0 l i d a r 1 ⋅ R l i d a r I M U = R l i d a r I M U ⋅ R I M U 0 I M U 1 R_{lidar_0}^{lidar_1} \cdot R_{lidar}^{IMU} = R_{lidar}^{IMU} \cdot R_{IMU_0}^{IMU_1} Rlidar0lidar1RlidarIMU=RlidarIMURIMU0IMU1
    其中 R l i d a r 0 l i d a r 1 R_{lidar_0}^{lidar_1} Rlidar0lidar1为雷达始状态到末状态姿态变换,$R_{IMU_0}^{IMU_1} 为 I M U 始 状 态 到 末 状 态 姿 态 变 换 ( 也 就 是 上 面 积 分 出 来 I M U 姿 态 角 四 元 数 为IMU始状态到末状态姿态变换(也就是上面积分出来IMU姿态角四元数 IMU姿IMU姿q$)。将该公式同时右乘逆,可得:
    R l i d a r 0 l i d a r 1 = R l i d a r I M U ⋅ R I M U 0 I M U 1 ⋅ R l i d a r I M U − 1 R_{lidar_0}^{lidar_1} = R_{lidar}^{IMU} \cdot R_{IMU_0}^{IMU_1} \cdot {R_{lidar}^{IMU}}^{-1} Rlidar0lidar1=RlidarIMURIMU0IMU1RlidarIMU1
    Eigen代码:

    Eigen::Matrix3d lidar_imu_R << -1,     -1.22465e-16,            0,
              1.22465e-16,          -1,            0,
               0,            0,           1;     // lidar->IMU 外参数旋转矩阵
    Eigen::Matrix3d lidar_R;
    lidar_R = lidar_imu_R * (imu_q.toRotationMatrix()) * lidar_imu_R.inverse();
    

    效果如下:

  3. 对雷达的姿态四元数进行球面线性插值转换:在1个周期(100ms),雷达在俯视图上按照顺时针旋转,水平角度如下:
    在这里插入图片描述

    在一个水平面360度,分成1800份,先计算每个点云所对应的角度 θ i \theta_i θi:
    θ i = a r c c o s ( x i x i 2 + y i 2 ) , i f   y i < 0 θ i = 2 π − a r c c o s ( x i x i 2 + y i 2 ) , i f   y i > 0 \theta_i = arccos\left ( \frac{x_i}{\sqrt{x_i^2 + y_i^2}} \right ) ,if \ y_i < 0 \\ \theta_i = 2\pi - arccos\left ( \frac{x_i}{\sqrt{x_i^2 + y_i^2}} \right ) ,if \ y_i > 0 θi=arccos(xi2+yi2 xi),if yi<0θi=2πarccos(xi2+yi2 xi),if yi>0
    再进行插值,注意是将当前时刻的点云转换到末状态雷达姿态上:
    q s l e r p = S l e r p ( q 0 , q 1 ; t ) = q 0 ( q 0 − 1 q 1 ) t = q 1 ( q 1 − 1 q 0 ) 1 − t = ( q 0 q 1 − 1 ) 1 − t q 1 = ( q 1 q 0 − 1 ) t q 0 q_{slerp}={Slerp}\left(q_{0}, q_{1} ; t\right)=q_{0}\left(q_{0}^{-1} q_{1}\right)^{t}=q_{1}\left(q_{1}^{-1} q_{0}\right)^{1-t}=\left(q_{0} q_{1}^{-1}\right)^{1-t} q_{1}=\left(q_{1} q_{0}^{-1}\right)^{t} q_{0} qslerp=Slerp(q0,q1;t)=q0(q01q1)t=q1(q11q0)1t=(q0q11)1tq1=(q1q01)tq0
    其中 q 0 q_0 q0为起始四元数, q 1 q_1 q1为末状态四元数, t t t为插值的步长, t ∈ [ 0 , 1 ] t\in [0,1] t[0,1]

    将四元数 q s l e r p q_{slerp} qslerp转化成旋转矩阵 R s l e r p R_{slerp} Rslerp形式接着就可以利用坐标转换公式:
    P 1 = [ x 1 y 1 z 1 1 ] = T s l e r p ⋅ P 0 = [ R s l e r p 0 0 1 ] ⋅ [ x 0 y 0 z 0 1 ] P_1 = \begin{bmatrix} x_1 \\ y_1 \\ z_1 \\ 1 \end{bmatrix} = T_{slerp} \cdot P_0 = \begin{bmatrix} R_{slerp} & \boldsymbol 0 \\ \boldsymbol 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} x_0 \\ y_0 \\ z_0 \\ 1 \end{bmatrix} P1=x1y1z11=TslerpP0=[Rslerp001]x0y0z01
    其中 P 0 P_0 P0为原始点云坐标, P 1 P_1 P1为转化后的点云坐标。

    附上Eigen部分代码:

    pcl::PointCloud<pcl::PointXYZI> cloud_undistorted;
    pcl::copyPointCloud(cloud_in, cloud_undistorted);
    
    for (int i = 0; i < cloud_undistorted.points.size(); i++)
    {
        double horizon_angle; // 化为角度为单位
        if (cloud_rgb.points[i].y < 0)
        {
            double theta = acos(cloud_undistorted.points[i].x / sqrt(cloud_undistorted.points[i].x * cloud_undistorted.points[i].x + cloud_undistorted.points[i].y * cloud_undistorted.points[i].y));
            horizon_angle = theta * 180.0 / M_PI;
        }
        else
        {
            double theta = acos(cloud_undistorted.points[i].x / sqrt(cloud_undistorted.points[i].x * cloud_undistorted.points[i].x + cloud_undistorted.points[i].y * cloud_undistorted.points[i].y));
            horizon_angle = (2 * M_PI - theta) * 180 / M_PI;
        }
        int id = int(horizon_angle / 360.0 * 1800);
        // std::cout << "horizon_angle: " << horizon_angle << ", id: " << id << std::endl;
    
        Eigen::Quaterniond q_slerp = lidar_q0.slerp((1.0 - horizon_angle / 360.0), lidar_q);
        Eigen::Matrix3d R_slerp = q_slerp.toRotationMatrix();
        Eigen::Matrix4d T_lidar_slerp;
        T_lidar_slerp << R_slerp(0, 0), R_slerp(0, 1), R_slerp(0, 2), 0,
            R_slerp(1, 0), R_slerp(1, 1), R_slerp(1, 2), 0,
            R_slerp(2, 0), R_slerp(2, 1), R_slerp(2, 2), 0,
            0, 0, 0, 1;
        Eigen::Vector4d Pi, Pj;
        Pi << cloud_undistorted.points[i].x, cloud_undistorted.points[i].y, cloud_undistorted.points[i].z, 1;
        Pj = T_lidar_slerp.inverse() * Pi;
        cloud_undistorted.points[i].x = Pj(0);
        cloud_undistorted.points[i].y = Pj(1);
        cloud_undistorted.points[i].z = Pj(2);
    }
    
  4. 效果:

    单帧点云处理效果:
    在这里插入图片描述
    在图中白色点云为原始数据,绿色为去畸变点云效果。这样子看不不出来实际效果,接着有个建图例子来说明。

    室外的走廊环境:
    在这里插入图片描述
    红色为手持激光雷达行进方向。采用A-LOAM进行建图,未去畸变点云建图:
    在这里插入图片描述
    去畸变后点云在有大旋转大环境范围下建图依然稳如老狗,效果如下:
    在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

雨霖 X

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

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

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

打赏作者

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

抵扣说明:

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

余额充值