LOAM论文与代码阅读(三)IMU运动补偿相关代码

LOAM代码中使用的是IMU进行运动补偿。这部分在思想上比较简单,即用IMU积分得到各个点相对开始时刻的位姿。但实际上涉及一些坐标系转换,积分,时间戳问题,因此对代码进一步阅读。

IMU数据处理

对于发布的IMU消息,首先还原加速度,然后利用加速度积分得到当前imu的速度,位置,而姿态可以直接使用,存成IMUState,加入队列。

IMU设置后一般可以直接输出四元数,由加速度积分得到。但不会输出速度,因为加速度不准,积分很容易发散。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 传入IMU消息
void ScanRegistration::handleIMUMessage(const sensor_msgs::Imu::ConstPtr& imuIn) // imuIn的结构--1
{
tf::Quaternion orientation;
tf::quaternionMsgToTF(imuIn->orientation, orientation);
double roll, pitch, yaw;
tf::Matrix3x3(orientation).getRPY(roll, pitch, yaw); //将IMU的姿态转roll,pitch,yaw

Vector3 acc;
acc.x() = float(imuIn->linear_acceleration.y - sin(roll) * cos(pitch) * 9.81);
acc.y() = float(imuIn->linear_acceleration.z - cos(roll) * cos(pitch) * 9.81);
acc.z() = float(imuIn->linear_acceleration.x + sin(pitch) * 9.81); //减去重力得到真实的加速度--2

IMUState newState; // IMUState的结构--3
newState.stamp = fromROSTime( imuIn->header.stamp);
newState.roll = roll;
newState.pitch = pitch;
newState.yaw = yaw;
newState.acceleration = acc;

updateIMUData(acc, newState); // 将新的IMU状态更新到全局信息中--4
}

1 imu消息的结构

ROS中sensor_msgs::Imu的定义:包含一个四元数表示姿态,角速度和加速度为三维向量

1
2
3
4
5
6
7
8
9
10
Header header

geometry_msgs/Quaternion orientation
float64[9] orientation_covariance # Row major about x, y, z axes

geometry_msgs/Vector3 angular_velocity
float64[9] angular_velocity_covariance # Row major about x, y, z axes

geometry_msgs/Vector3 linear_acceleration
float64[9] linear_acceleration_covariance

2 减去重力得到真实的加速度

由于IMU测量的是比力,在物体静止时会测量到一个垂直向上,大小为g的加速度,因此测量值需要减去g。但由于IMU在车辆运动中跟随车辆旋转,我们需要先将世界坐标系下的 \(\textbf{g}=[0,0,9.8]\) 用orientation转换到当前IMU坐标系下,即:

\[ a_ {true} '=a '_ {measure} - R_ {ZYX} *\textbf{g} = \left [\begin{matrix}a_{x}'\\ a_{y}' \\ a_{z}' \end{matrix} \right ] - \left [\begin{matrix}-sin(p)g \\ sin(r)cos(p)g \\ cos(r)cos(p)g \end{matrix} \right ] \]

3 IMUState的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
typedef struct IMUState
{
Time stamp;
Angle roll;
Angle pitch;
Angle yaw;

Vector3 position; // IMU在世界坐标系的位置

Vector3 velocity; // IMU相对于世界坐标系的速度

Vector3 acceleration;

// 一个插值函数,输入一个IMU开始状态,一个结束状态,一个插值的位置比例,输出插值得到的IMU状态
static void interpolate(const IMUState& start,
const IMUState& end,
const float& ratio,
IMUState& result)
{
float invRatio = 1 - ratio;

result.roll = start.roll.rad() * invRatio + end.roll.rad() * ratio;
result.pitch = start.pitch.rad() * invRatio + end.pitch.rad() * ratio;
if (start.yaw.rad() - end.yaw.rad() > M_PI)
{
result.yaw = start.yaw.rad() * invRatio + (end.yaw.rad() + 2 * M_PI) * ratio;
}
else if (start.yaw.rad() - end.yaw.rad() < -M_PI)
{
result.yaw = start.yaw.rad() * invRatio + (end.yaw.rad() - 2 * M_PI) * ratio;
}
else
{
result.yaw = start.yaw.rad() * invRatio + end.yaw.rad() * ratio;
}

result.velocity = start.velocity * invRatio + end.velocity * ratio;
result.position = start.position * invRatio + end.position * ratio;
};
} IMUState;

4 将新的IMU状态更新到全局信息中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void BasicScanRegistration::updateIMUData(Vector3& acc, IMUState& newState)
{
if (_imuHistory.size() > 0) {
// 把加速度转到世界坐标系下
rotateZXY(acc, newState.roll, newState.pitch, newState.yaw);

// 对位置和速度进行积分
const IMUState& prevState = _imuHistory.last();
float timeDiff = toSec(newState.stamp - prevState.stamp);
newState.position = prevState.position
+ (prevState.velocity * timeDiff)
+ (0.5 * acc * timeDiff * timeDiff);
newState.velocity = prevState.velocity
+ acc * timeDiff;
}

_imuHistory.push(newState); // 推入_imuHistory队列
}

这里根据当前的加速度,前一个时刻的速度对位置和时间做了积分

\[p_{new} = p_{pre} + v_{prev} * \Delta t + \frac{1}{2} a (\Delta t)^2\]

\[v_{new} = v_{pre} + a \Delta t\]

问题1:对加速度做2次积分应该不太准吧,代码中有用反馈信息来纠正吗?

运动补偿

把点云投影到这一帧开始时刻:

1
2
3
4
5
6
7
8
void BasicScanRegistration::projectPointToStartOfSweep(pcl::PointXYZI& point, float relTime) //relTime: 该点相对于这个scan初始时刻的时间
{
if (hasIMUData()) // hasIMUData: _imuHistory.size() > 0
{
setIMUTransformFor(relTime); // 计算该点相对于这一sweep初始时刻的位姿--1
transformToStartIMU(point); // 把该点投影到这一sweep初始时刻--2
}
}

1 计算该点相对于这一sweep初始时刻的位姿

1
2
3
4
5
6
7
void BasicScanRegistration::setIMUTransformFor(const float& relTime) 
{
interpolateIMUStateFor(relTime, _imuCur); // 获得relTime时间的IMU状态

float relSweepTime = toSec(_scanTime - _sweepStart) + relTime; //在代码中_sweepStart设为_scanTime,所以这里就是relTime
_imuPositionShift = _imuCur.position - _imuStart.position - _imuStart.velocity * relSweepTime; // ??
}

获得relTime时间的IMU状态

找到当前时刻之后最近的一帧IMU,用这一帧IMU和前一帧插值得到relTime时间的IMU状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void BasicScanRegistration::interpolateIMUStateFor(const float &relTime, IMUState &outputState)
{
double timeDiff = toSec(_scanTime - _imuHistory[_imuIdx].stamp) + relTime; // 要补偿的点和当前的IMU帧(IMU根据用的需要idx不断往后加)的时间差
while (_imuIdx < _imuHistory.size() - 1 && timeDiff > 0) {
_imuIdx++;
timeDiff = toSec(_scanTime - _imuHistory[_imuIdx].stamp) + relTime;
} // 当这个IMU时间戳超过我们要补偿的点的时间时,停止

if (_imuIdx == 0 || timeDiff > 0) {
outputState = _imuHistory[_imuIdx];
} else {
float ratio = -timeDiff / toSec(_imuHistory[_imuIdx].stamp - _imuHistory[_imuIdx - 1].stamp);
IMUState::interpolate(_imuHistory[_imuIdx], _imuHistory[_imuIdx - 1], ratio, outputState);
} // 用这一帧IMU和上一帧IMU插值得到这个时刻的IMU状态
}

问题2:_imuPositionShift为什么是当前位置-(scan开始位置+开始速度*relTime)。_imuStart看代码scan开始时的imu状态。那_imuPositionShift就是由加速运动导致的点的位置偏移?

2 把该点投影到scan初始时刻

先把点转换到全局坐标系,只加入由加速度得到的偏移?再旋转回初始时刻的imu局部坐标系

1
2
3
4
5
6
7
8
9
10
11
12
13
void BasicScanRegistration::transformToStartIMU(pcl::PointXYZI& point)
{
// 把点旋转到全局坐标系
rotateZXY(point, _imuCur.roll, _imuCur.pitch, _imuCur.yaw);

// 加入平移?
point.x += _imuPositionShift.x();
point.y += _imuPositionShift.y();
point.z += _imuPositionShift.z();

// rotate point back to local IMU system relative to the start IMU state
rotateYXZ(point, -_imuStart.yaw, -_imuStart.pitch, -_imuStart.roll);
}

问题3:这里两次旋转相对于对姿态做了补偿,但是平移方向是只对加速度产生偏差做补偿?


LOAM论文与代码阅读(三)IMU运动补偿相关代码
https://sisyphus-99.github.io/2023/09/23/LOAM论文与代码阅读3/
Author
sisyphus
Posted on
September 23, 2023
Licensed under