利用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。
具体流程:
-
对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+ωt⋅dtdt=t−t0α=i=0∑n−1α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 狗→0lim狗sin(狗)=1狗→0limcos(狗)=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=qt0⋅qdtqdt=(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); }
-
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} Rlidar0lidar1⋅RlidarIMU=RlidarIMU⋅RIMU0IMU1
其中 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=RlidarIMU⋅RIMU0IMU1⋅RlidarIMU−1
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();
效果如下:
-
对雷达的姿态四元数进行球面线性插值转换:在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+yi2xi),if yi<0θi=2π−arccos(xi2+yi2xi),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(q0−1q1)t=q1(q1−1q0)1−t=(q0q1−1)1−tq1=(q1q0−1)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⎦⎥⎥⎤=Tslerp⋅P0=[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); }
-
效果:
单帧点云处理效果:
在图中白色点云为原始数据,绿色为去畸变点云效果。这样子看不不出来实际效果,接着有个建图例子来说明。室外的走廊环境:
红色为手持激光雷达行进方向。采用A-LOAM进行建图,未去畸变点云建图:
去畸变后点云在有大旋转大环境范围下建图依然稳如老狗,效果如下: