首页 > 编程语言 >cartographer 源码解析(六)

cartographer 源码解析(六)

时间:2022-12-21 13:36:13浏览次数:70  
标签:odometry cartographer const pose 源码 imu tracker time 解析

这一章节呢,主要讲外推器。我们上一章节说到激光的畸变与矫正,最关键的是什么呢?其实最关键的就是关于每个发射点的发射位置的估计推测,这里就用到了外推器去推测每个发射点的位置。
local_trajectory_builder_2d.cc

if (synchronized_data.ranges.empty(std::unique_ptr<LocalTrajectoryBuilder2D::MatchingResult>
LocalTrajectoryBuilder2D::AddRangeData(
const std::string& sensor_id,
const sensor::TimedPointCloudData& unsynchronized_data) {
......
for (size_t i = 0; i < synchronized_data.ranges.size(); ++i) {
......
const Eigen::Vector3f origin_in_local =
range_data_poses[i] *
synchronized_data.origins.at(synchronized_data.ranges[i].origin_index);
sensor::RangefinderPoint hit_in_local =
range_data_poses[i] * sensor::ToRangefinderPoint(hit);
......
}
}
......
}

其中可以看到每个点都有一个外推的​​pose​​​------​​range_data_poses[i]​​​。
再往前追溯,就可以知道,其实每个点都是外推器给推出来的,我们可以找到代码看一下。
local_trajectory_builder_2d.cc

for (const auto& range : synchronized_data.ranges) {
......
range_data_poses.push_back(
extrapolator_->ExtrapolatePose(time_point).cast<float>());
}

其中​​time_point​​​是每一个激光点发射对应的时间戳。那么今天就仔细讲一下这个外推器是怎么进行位姿推算的。首先我们看​​ExtrapolatePose​​​是怎么实现的。
pose_extrapolator.cc

transform::Rigid3d PoseExtrapolator::ExtrapolatePose(const common::Time time) {
const TimedPose& newest_timed_pose = timed_pose_queue_.back();
CHECK_GE(time, newest_timed_pose.time);
if (cached_extrapolated_pose_.time != time) {
const Eigen::Vector3d translation =
ExtrapolateTranslation(time) + newest_timed_pose.pose.translation();
const Eigen::Quaterniond rotation =
newest_timed_pose.pose.rotation() *
ExtrapolateRotation(time, extrapolation_imu_tracker_.get());
cached_extrapolated_pose_ =
TimedPose{time, transform::Rigid3d{translation, rotation}};
}
return cached_extrapolated_pose_.pose;
}

下面进行逐行讲解

CHECK_GE(time, newest_timed_pose.time);

首先检查time是否大于等于外推最新的pose的时间,​​newest_timed_pose.time​​​。其中,​​cached_extrapolated_pose_​​是外推器外推的pose.

if (cached_extrapolated_pose_.time != time) {

这句的意思是在某个时间点上进行不重复的外推。也就是曾经没有外推过的时间,这都是一些时间上的检查。
我们既然要外推​​​pose​​​,那么就是两方面,一个是平移​​translation​​​,一个是旋转​​rotation​​ 先看平移。

const Eigen::Vector3d translation =
ExtrapolateTranslation(time) + newest_timed_pose.pose.translation();

那么这个平移是什么呢?其实就是在先前​​pose​​​的基础上​​newest_timed_pose.pose.translation()​​​,再加上一个cartographer 源码解析(六)_3d ​​​ExtrapolateTranslation(time)​​​。
pose_extrapolator.cc

Eigen::Vector3d PoseExtrapolator::ExtrapolateTranslation(common::Time time) {
const TimedPose& newest_timed_pose = timed_pose_queue_.back();
const double extrapolation_delta =
common::ToSeconds(time - newest_timed_pose.time);
if (odometry_data_.size() < 2) {
return extrapolation_delta * linear_velocity_from_poses_;
}
return extrapolation_delta * linear_velocity_from_odometry_;
}

我们外推器里面存储了一系列的​​pose​​​,​​linear_velocity_from_poses_​​​。比如说滑动窗口,一个10个pose,新来了pose,就把最老的pose给干掉。然后我们用最新的减去最旧的,最后再除以时间差,得到这个速度,也就是linear_velocity_from_poses_,得到了之后呢。然后我们要用我们要外推需要的时间time,减去最新的pose的时间。其中要外推的时间肯定比最新的pose时间大,相减之后得到的时间是cartographer 源码解析(六)_3dt,​​​extrapolation_delta​​​,然后乘以从历史存储得到的速度,也就是​​linear_velocity_from_poses_​​​,然后得到了在cartographer 源码解析(六)_3dt时间内的平移。这是在里程计数据比较少的情况下,​​​if (odometry_data_.size() < 2)​​​。
但是当里程计数据足够呢?
那么我们就用时间cartographer 源码解析(六)_3dt乘以从里程计获得的速度。代码如下:

return extrapolation_delta * linear_velocity_from_odometry_;

其中​​linear_velocity_from_odometry_​​​是用里程计算出的一个速度。
如果里程计数据足够,就用里程计做外推,如果没有,就之前存储的位姿来推算速度。
好了,平移说完了,那么就再说说旋转​​​Rotation​​。

const Eigen::Quaterniond rotation =
newest_timed_pose.pose.rotation() *
ExtrapolateRotation(time, extrapolation_imu_tracker_.get());

同样是最新的位姿的旋转向量​​newest_timed_pose.pose.rotation()​​​的基础上,乘以cartographer 源码解析(六)_3d​​​rotation​​​,那么如何算cartographer 源码解析(六)_3d​​​rotation​​​,可以看到传入了两个向量,不仅有​​time​​​,而且还有​​extrapolation_imu_tracker_.get()​​​,那么​​extrapolation_imu_tracker_.get()​​​这个是什么东西呢。我们展开来看一下。
pose_extrapolator.h

std::unique_ptr<ImuTracker> imu_tracker_;
std::unique_ptr<ImuTracker> odometry_imu_tracker_;
std::unique_ptr<ImuTracker> extrapolation_imu_tracker_;

我们可以看到有三个​​imu_tracker​​​。一个是​​imu_tracker_​​​,一个是​​odometry_imu_tracker_​​​,还有一个是​​extrapolation_imu_tracker_​​。这具体是干什么的呢?后面我们再讲。我们先回归pose_extrapolator.cc到​​ExtrapolateRotation()​​这个函数中进行展开讲。

Eigen::Quaterniond PoseExtrapolator::ExtrapolateRotation(
const common::Time time, ImuTracker* const imu_tracker) const {
CHECK_GE(time, imu_tracker->time());
AdvanceImuTracker(time, imu_tracker);
const Eigen::Quaterniond last_orientation = imu_tracker_->orientation();
return last_orientation.inverse() * imu_tracker->orientation();
}

传入的​​imu_tracker​​​是外推器​​imu_tracker​​​------​​extrapolation_imu_tracker_​

CHECK_GE(time, imu_tracker->time());

我们不会往历史时刻做外推,所以检查​​time​​​大于等于​​imu_tracker->time()​​​。​​AdvanceImuTracker(time, imu_tracker);​​这个先不展开讲,再看接下来做了什么。

const Eigen::Quaterniond last_orientation = imu_tracker_->orientation();

首先是把这一次的​​imu_tracker_->orientation()​​​作为上一次的​​last_orientation​​​,也就是更新值,其中imu_tracker_是有一个下划线。从这个带下划线的​​imu_tracker_​​​拿出一个一个朝向角,从这个朝向角乘以外推器的朝向角。他们做一个差,得到一个​​orientation​​​。说明​​imu_tracker_​​​是一直实时记录最新估计的​​imu​​​的朝向角的。然后传入的​​imu_tracker​​​是往前外推的​​imu_tracker​​​的朝向角。所以是往前外推一下的orientation乘以上一次位姿的​​orientation​​​的逆,也就是差,得到了cartographer 源码解析(六)_3d​​​orientation​​​。得到了这个旋转,最后再乘以最新的旋转​​newest_timed_pose.pose.rotation()​​​。那么我们再来看一下,​​imu_tracker->orientation()​​​是怎么计算的。也即是​​AdvanceImuTracker(time, imu_tracker);​​​函数。
pose_extrapolator.cc

void PoseExtrapolator::AdvanceImuTracker(const common::Time time,
ImuTracker* const imu_tracker) const {
CHECK_GE(time, imu_tracker->time());
if (imu_data_.empty() || time < imu_data_.front().time) {
// There is no IMU data until 'time', so we advance the ImuTracker and use
// the angular velocities from poses and fake gravity to help 2D stability.
imu_tracker->Advance(time);
imu_tracker->AddImuLinearAccelerationObservation(Eigen::Vector3d::UnitZ());
imu_tracker->AddImuAngularVelocityObservation(
odometry_data_.size() < 2 ? angular_velocity_from_poses_
: angular_velocity_from_odometry_);
return;
}
if (imu_tracker->time() < imu_data_.front().time) {
// Advance to the beginning of 'imu_data_'.
imu_tracker->Advance(imu_data_.front().time);
}
auto it = std::lower_bound(
imu_data_.begin(), imu_data_.end(), imu_tracker->time(),
[](const sensor::ImuData& imu_data, const common::Time& time) {
return imu_data.time < time;
});
while (it != imu_data_.end() && it->time < time) {
imu_tracker->Advance(it->time);
imu_tracker->AddImuLinearAccelerationObservation(it->linear_acceleration);
imu_tracker->AddImuAngularVelocityObservation(it->angular_velocity);
++it;
}
imu_tracker->Advance(time);
}

接下来进行逐行解析吧,首先第一行。还是老套路,检查时间。

CHECK_GE(time, imu_tracker->time());

首先假设我们不直接使用​​imu​​​,来做轨迹推算,直接用一个比较好的里程计来做推算。​​imu​​​就是和编码器融合推演出一个比较好的里程计上面。不把​​imu​​​直接用到​​cartographer​​上面。首先我们看这一种情况下是怎么操作的。

if (imu_data_.empty() || time < imu_data_.front().time)

首先是imu数据是空的,或者是外推的时间小于了imu的时间。那么我们再看看下面是怎么操作的。

imu_tracker->Advance(time);
imu_tracker->AddImuLinearAccelerationObservation(Eigen::Vector3d::UnitZ());
imu_tracker->AddImuAngularVelocityObservation(
odometry_data_.size() < 2 ? angular_velocity_from_poses_
: angular_velocity_from_odometry_);
return;

首先看看​​imu_tracker->Advance(time);​​​是干什么的?
imu_tracker.cc

void ImuTracker::Advance(const common::Time time) {
CHECK_LE(time_, time);
const double delta_t = common::ToSeconds(time - time_);
const Eigen::Quaterniond rotation =
transform::AngleAxisVectorToRotationQuaternion(
Eigen::Vector3d(imu_angular_velocity_ * delta_t));
orientation_ = (orientation_ * rotation).normalized();
gravity_vector_ = rotation.conjugate() * gravity_vector_;
time_ = time;
}

从函数名​​Advance​​​中就知道,就是前进到这个时刻​​time​​​。其中有一个是​​time_​​​,我们可能就比较困惑了,这个是从哪里来的呢,其实这是imu的编码规范,其实就是类的成员,至于这里是哪个类呢,显然是​​ImuTracker​​​这个类的成员。这是​​time_​​​具体是什么呢?其实就是我们一次一次​​Advance​​​到的时间。可以看到代码最后一行​​time_ = time;​​,这就是我们最后的是时间,赋值给它。

const double delta_t = common::ToSeconds(time - time_);

而​​delta_t​​是我们将要赶到的时间点距离上一次赶到的时间点之间的差。

const Eigen::Quaterniond rotation =
transform::AngleAxisVectorToRotationQuaternion(
Eigen::Vector3d(imu_angular_velocity_ * delta_t));

其中​​imu_angular_velocity_​​​是imu的角速度。如果不用imu了,那么他就是0,那么​​rotation​​就是0。

orientation_ = (orientation_ * rotation).normalized();

​orientation_​​​就保持不变,乘以0还是原来的​​orientation_​

gravity_vector_ = rotation.conjugate() * gravity_vector_;

​gravity_vector_​​​就是考虑重力对齐的关系。主要考虑的是imu航向角是否与水平面水平,如果不是,那么就要将重力划分到imu坐标系中。
回过头来,我们看一下​​​Advance​​​到底做了什么?
如果我们有​​​imu​​​数据的时候,也就是有了​​imu​​​的角速度​​imu_angular_velocity_​​​。通过这个角速度乘以时间cartographer 源码解析(六)_3dt ,得到一个​​​rotation​​​。有了imu自己的​​rotation​​​,然后更新自己的重力向量。然后还有什么呢?这一些的​​ImuTracker Rotation​​​其实就是在算cartographer 源码解析(六)_3d ​​​rotation​​​。然后更新自己的​​orientation_​​​。
现在​​​imu_tracker->Advance()​​​是干什么,就是更新朝向角,再更新一下重力向量的表示。
然后再接着回溯代码

imu_tracker->AddImuLinearAccelerationObservation(Eigen::Vector3d::UnitZ());

其中​​imu_tracker​​​是外推器的​​imu_tracker​​​。这一步是添加​​imu​​​的线速度加速度观测。因为我们假设我们没有​​imu​​​数据,再进一步假设我们在2D平面上运行。那么我们只有一个在Z方向上的重力向量​​Eigen::Vector3d::UnitZ()​​​。
imu_tracker.cc

void ImuTracker::AddImuLinearAccelerationObservation(
const Eigen::Vector3d& imu_linear_acceleration) {
// Update the 'gravity_vector_' with an exponential moving average using the
// 'imu_gravity_time_constant'.
const double delta_t =
last_linear_acceleration_time_ > common::Time::min()
? common::ToSeconds(time_ - last_linear_acceleration_time_)
: std::numeric_limits<double>::infinity();
last_linear_acceleration_time_ = time_;
const double alpha = 1. - std::exp(-delta_t / imu_gravity_time_constant_);
gravity_vector_ =
(1. - alpha) * gravity_vector_ + alpha * imu_linear_acceleration;
// Change the 'orientation_' so that it agrees with the current
// 'gravity_vector_'.
const Eigen::Quaterniond rotation = FromTwoVectors(
gravity_vector_, orientation_.conjugate() * Eigen::Vector3d::UnitZ());
orientation_ = (orientation_ * rotation).normalized();
CHECK_GT((orientation_ * gravity_vector_).z(), 0.);
CHECK_GT((orientation_ * gravity_vector_).normalized().z(), 0.99);
}

虽然我们之前假设没有imu数据,但是当我们有imu数据的时候,我们添加imu线性加速度的作用是什么吧。我们先不管传入的是什么东西,我们先看这个函数吧。

void ImuTracker::AddImuLinearAccelerationObservation(
const Eigen::Vector3d& imu_linear_acceleration) {
......
}

首先填入进入的是一个线性加速度​​imu_linear_acceleration​​​。主要是做了指数平均滤波cartographer 源码解析(六)_3d_10,也就是互补滤波

cartographer 源码解析(六)_ci_11
指数平均滤波的权值是以指数式递减的
指数移动平均无论多旧的数据其总会被给予一定的权值
参考:​​javascript:void(0)​​ 相关链接:​​https://zhuanlan.zhihu.com/p/76188487​

首先是一个条件表达式,其中​​time_​​​我们可以从之前讲的代码中得知已经更新到最新了,​​last_linear_acceleration_time_​​​是上一次的线性加速度时间,两者想减得到cartographer 源码解析(六)_3d_12,​​​last_linear_acceleration_time_ > common::Time::min()​​这个是验证时间的有效性。

const double delta_t =
last_linear_acceleration_time_ > common::Time::min()
? common::ToSeconds(time_ - last_linear_acceleration_time_)
: std::numeric_limits<double>::infinity();

然后更新一下时间

last_linear_acceleration_time_ = time_;

​下一步就是刚才引用的指数平均滤波的代码表示,其中​​imu_gravity_time_constant_​​​是一个常数,可以知道,当cartographer 源码解析(六)_3d_12越小的时候,cartographer 源码解析(六)_算法_14就越大。

const double alpha = 1. - std::exp(-delta_t / imu_gravity_time_constant_);
gravity_vector_ =
(1. - alpha) * gravity_vector_ + alpha * imu_linear_acceleration;

其中​​imu_linear_acceleration​​​为​​imu​​​的测量值,如果cartographer 源码解析(六)_3d, ​​​IMU​​​短时间内角速度还是比较准确的。 所以cartographer 源码解析(六)_3d_12越大,我们越不相信之前的角速度​​​(1. - alpha) * gravity_vector_​​​,就越相信当前的测量值​​alpha * imu_linear_acceleration​​。虽然不太准,但这就是重力向量在机器人体内,也就是机体内怎么表示的。

接下来呢,我们首先算一个旋转向量​​rotation​​。

const Eigen::Quaterniond rotation = FromTwoVectors(
gravity_vector_, orientation_.conjugate() * Eigen::Vector3d::UnitZ());

​FromTwoVectors​​​就是从一个向量转到另一个向量是怎么旋转的。第一个向量是重力向量​​gravity_vector_​​​。第二个向量是什么呢?是​​orientation_​​​(四元数)共轭 也就是旋转的逆。就是把这个四元数转换成向量的形式。Z轴上的向量​​Eigen::Vector3d::UnitZ()​​​都旋转到 ​​orientation_.conjugate()​​这个方向上。变成这么一个朝向上的向量。

这里我画图解释一下,首先假设我们有一个车,车体自己有一个坐标系以及航向。蓝色线为水平线

cartographer 源码解析(六)_3d_17

然后再画出我们已经算出来的重力向量。

cartographer 源码解析(六)_3d_18

我们假设算出的重力向量很准。然后呢,假设我们还知道航向与水平面的夹角cartographer 源码解析(六)_3d_19。首先我们要知道,航向的四元数的共轭向量,本质就是旋转的逆。正向旋转是逆时针,反向旋转是顺时针,

cartographer 源码解析(六)_算法_20


如果这个旋转的逆,乘以这个UnitZ ,也就是意味着UnitZ 顺时针旋转cartographer 源码解析(六)_3d_19角度。

cartographer 源码解析(六)_ci_22


绿色线为旋转之后的​​UnitZ​​​。然后我们再求出​​UnitZ​​与重力向量直线的旋转向量,再将这个旋转向量乘以航向角,来纠正航向角,航向角向下旋转的角度就是新的UnitZ与重力向量之间的夹角。实现代码如下:

orientation_ = (orientation_ * rotation).normalized();

效果如下:紫色为纠正后的航向角

cartographer 源码解析(六)_3d_23

使用重力加速度来纠正我们估计的朝向。
怎么纠正呢,理论上来说,我们车体坐标系上的Z轴上的单位向量,乘以航向角的逆,也就是逆航向角进行旋转,应该是跟重量向量方向是重合的,如果不重合,那么就对航向角进行矫正,矫正的大小就是旋转后的Z轴与重力向量之间的旋转向量​​​rotation​​​。
这就是使用重力加速度来纠正估计航向的原理

CHECK_GT((orientation_ * gravity_vector_).z(), 0.);
CHECK_GT((orientation_ * gravity_vector_).normalized().z(), 0.99);

关于这两行代码呢。我有我自己的个人理解。可能不对,大家姑且一听,首先呢,​​orientation_ * gravity_vector_​​ 是对重力向量的矫正,让重力向量的精度更高,尽可能的垂直与水平面,之后,判断校正后的重力向量的z分量是否存在,存在是否大于0,理论上重力向量是朝上的,所以z的值是大于0的。为什么重力向量朝上,请看下面的引用。

重力向量为什么朝上,重力和器件之间作用力和反作用力,重力向下,测量值朝上。

这部分的代码我们讲完了,我们回溯一下,当前函数的名字是​​ImuTracker::AddImuLinearAccelerationObservation​​ 这个是IMU加入线性加速度观测,加入观测的目的是什么呢?纠正当前的状态。我们之前利用角速度,求出朝向角。然后求出重力朝向,重力朝向去矫正航向角。

然后再回溯到上一层。
pose_extrapolator.cc

void PoseExtrapolator::AdvanceImuTracker(const common::Time time,
ImuTracker* const imu_tracker) const {
CHECK_GE(time, imu_tracker->time());
......
imu_tracker->AddImuLinearAccelerationObservation(Eigen::Vector3d::UnitZ());
imu_tracker->AddImuAngularVelocityObservation(
odometry_data_.size() < 2 ? angular_velocity_from_poses_
: angular_velocity_from_odometry_);
return;
}
}

执行完IMU线速度观测,继续知行IMU角速度观测。也就是​​AddImuAngularVelocityObservation​​​。
假设我们第一次进入​​​AdvanceImuTracker​​​这个函数,且有IMU数据,并且我们满足If条件中的​​time < imu_data_.front().time​​​这个条件。在这个条件下,我们继续看​​AddImuAngularVelocityObservation​​​这个函数是什么意思。这个函数是添加了IMU角速度观测值。
pose_extrapolator.cc

void ImuTracker::AddImuAngularVelocityObservation(
const Eigen::Vector3d& imu_angular_velocity) {
imu_angular_velocity_ = imu_angular_velocity;
}

直接就是IMU的角速度​​imu_angular_velocity​​​就是最新的角速度,直接更新一下就好了。
那我们再分析​​​AdvanceImuTracker​​​这个函数的整体逻辑吧。再次回顾,形参里面的​​imu_tracker​​​是什么呀?是​​extrapolator->imu_tracker​pose_extrapolator.cc

Eigen::Quaterniond PoseExtrapolator::ExtrapolateRotation(
const common::Time time, ImuTracker* const imu_tracker) const {
CHECK_GE(time, imu_tracker->time());
AdvanceImuTracker(time, imu_tracker);
const Eigen::Quaterniond last_orientation = imu_tracker_->orientation();
return last_orientation.inverse() * imu_tracker->orientation();
}

再往回推​​ExtrapolateRotation​​​这个函数。找到函数​​PoseExtrapolator::ExtrapolatePose​​如下

transform::Rigid3d PoseExtrapolator::ExtrapolatePose(const common::Time time) {
const TimedPose& newest_timed_pose = timed_pose_queue_.back();
CHECK_GE(time, newest_timed_pose.time);
if (cached_extrapolated_pose_.time != time) {
const Eigen::Vector3d translation =
ExtrapolateTranslation(time) + newest_timed_pose.pose.translation();
const Eigen::Quaterniond rotation =
newest_timed_pose.pose.rotation() *
ExtrapolateRotation(time, extrapolation_imu_tracker_.get());
cached_extrapolated_pose_ =
TimedPose{time, transform::Rigid3d{translation, rotation}};
}
return cached_extrapolated_pose_.pose;
}

回溯完毕,我们继续掉回头去讲​​AdvanceImuTracker​​​。如果不用IMU数据​​imu_data_.empty()​​​ 那么继续执行​​imu_tracker->Advance(time);​​。具体干了什么呢,就是更新了一下imu_tracker 记录的朝向。

void ImuTracker::Advance(const common::Time time) {
CHECK_LE(time_, time);
const double delta_t = common::ToSeconds(time - time_);
const Eigen::Quaterniond rotation =
transform::AngleAxisVectorToRotationQuaternion(
Eigen::Vector3d(imu_angular_velocity_ * delta_t));
orientation_ = (orientation_ * rotation).normalized();
gravity_vector_ = rotation.conjugate() * gravity_vector_;
time_ = time;
}

首先求出cartographer 源码解析(六)_3d_12,在这个基础上乘以角速度,算出旋转,更新朝向,同时更新重力向量。用于后面加速度数据来了,用到重力向量​​​gravity_vector_​​​上,再进一步更新朝向角。因为我们假设没有IMU数据,所以角速度是0,也没有旋转,所以重力向量是没有更新的。
接下来执行的代码是​​​imu_tracker->AddImuLinearAccelerationObservation(Eigen::Vector3d::UnitZ());​​​ 基于没有IMU数据,所以先给一个假数据​​Eigen::Vector3d::UnitZ()​​,也就是没有做矫正。接下来添加IMU的角速度观测。

imu_tracker->AddImuAngularVelocityObservation(
odometry_data_.size() < 2 ? angular_velocity_from_poses_
: angular_velocity_from_odometry_);

没有角速度

odometry_data_.size() < 2 ? angular_velocity_from_poses_
: angular_velocity_from_odometry_

如果里程计的数据很少,那么就从之前存储的位姿中计算出一个角速度,如果里程计速度够多,那么就从里程计中算出一个角速度。所以我们的IMU数据并不一定是0 ,有可能是历史的位姿上算出来的速度,有可能是里程计测算出来的速度。
imu_tracker.cc

void ImuTracker::Advance(const common::Time time) {
CHECK_LE(time_, time);
const double delta_t = common::ToSeconds(time - time_);
const Eigen::Quaterniond rotation =
transform::AngleAxisVectorToRotationQuaternion(
Eigen::Vector3d(imu_angular_velocity_ * delta_t));
orientation_ = (orientation_ * rotation).normalized();
gravity_vector_ = rotation.conjugate() * gravity_vector_;
time_ = time;
}

所以​​ImuTracker::Advance​​​ 里面的​​imu_angular_velocity_​​​ 并不一定是零,因此,其他的变量也并不是不会改变的,当没有IMU数据的时候。​​rotation​​​ 并不是0,​​orientation_​​​也是一直更新的。
以上是假设没有IMU数据的情况下。其实就是用历史位姿或者里程计更新朝向角和重力向量。
AdvanceImuTracker 重点就是更新形参​​​imu_tracker​​​的角度。
pose_extrapolator.cc

Eigen::Quaterniond PoseExtrapolator::ExtrapolateRotation(
const common::Time time, ImuTracker* const imu_tracker) const {
CHECK_GE(time, imu_tracker->time());
AdvanceImuTracker(time, imu_tracker);
const Eigen::Quaterniond last_orientation = imu_tracker_->orientation();
return last_orientation.inverse() * imu_tracker->orientation();
}

​AdvanceImuTracker​​执行完之后,再看下一行

const Eigen::Quaterniond last_orientation = imu_tracker_->orientation();
return last_orientation.inverse() * imu_tracker->orientation();

我们要算cartographer 源码解析(六)_Time_25 就是拿​​​imu_tracker_​​​的朝向角与​​imu_tracker​​​作差就得到了cartographer 源码解析(六)_Time_25。得到的这个旋转​​​ExtrapolateRotation(time, extrapolation_imu_tracker_.get())​​​,再乘到最新的位姿​​newest_timed_pose.pose.rotation()​​上去。得到了最新的外推的位姿

transform::Rigid3d PoseExtrapolator::ExtrapolatePose(const common::Time time) {
const TimedPose& newest_timed_pose = timed_pose_queue_.back();
CHECK_GE(time, newest_timed_pose.time);
if (cached_extrapolated_pose_.time != time) {
const Eigen::Vector3d translation =
ExtrapolateTranslation(time) + newest_timed_pose.pose.translation();
const Eigen::Quaterniond rotation =
newest_timed_pose.pose.rotation() *
ExtrapolateRotation(time, extrapolation_imu_tracker_.get());
cached_extrapolated_pose_ =
TimedPose{time, transform::Rigid3d{translation, rotation}};
}
return cached_extrapolated_pose_.pose;
}

其中,我们再求IMU角速度的时候,当没有IMU角速度,我们可以从历史位姿的角速度​​angular_velocity_from_poses_​​​或者里程计的角速度​​angular_velocity_from_odometry_​​​中推算出来,那么这两个速度是什么时候计算的呢?
首先看历史位姿角速度是在哪里更新的。
carto_ws/src/cartographer/cartographer/mapping/pose_extrapolator.h

void PoseExtrapolator::UpdateVelocitiesFromPoses() {
......
angular_velocity_from_poses_ =
transform::RotationQuaternionToAngleAxisVector(
oldest_pose.rotation().inverse() * newest_pose.rotation()) /
queue_delta;
}

就是从历史存储的位姿计算速度。
下面按行分析

if (timed_pose_queue_.size() < 2) {
// We need two poses to estimate velocities.
return;
}

首先就是数据太少,就返回。再看下面就是一些变量的定义

const TimedPose& newest_timed_pose = timed_pose_queue_.back();
const auto newest_time = newest_timed_pose.time;
const TimedPose& oldest_timed_pose = timed_pose_queue_.front();
const auto oldest_time = oldest_timed_pose.time;
const double queue_delta = common::ToSeconds(newest_time - oldest_time);

其中他也是用最新的时间减去最老的时间来算​​queue_delta​​。

const TimedPose& newest_timed_pose = timed_pose_queue_.back();

拿出最新的位姿

const auto newest_time = newest_timed_pose.time;

同时拿出最新的时间

const TimedPose& oldest_timed_pose = timed_pose_queue_.front();

拿出最老的位姿

const auto oldest_time = oldest_timed_pose.time;=

同时拿出最老的位姿。

linear_velocity_from_poses_ =
(newest_pose.translation() - oldest_pose.translation()) / queue_delta;

最新位姿和最老位姿作差,除以这个时间跨度。算出一个线速度

angular_velocity_from_poses_ =
transform::RotationQuaternionToAngleAxisVector(
oldest_pose.rotation().inverse() * newest_pose.rotation()) /
queue_delta;

再算一个角速度。

最后看一下这个函数​​UpdateVelocitiesFromPoses()​​是在哪里进行调用更新的。

void PoseExtrapolator::AddPose(const common::Time time,
const transform::Rigid3d& pose) {
......
UpdateVelocitiesFromPoses();
......
}

是在​​AddPose()​​​中调用的,那么​​AddPose​​​又在哪里调用的呢
src/cartographer/cartographer/mapping/internal/2d/local_trajectory_builder_2d.cc

std::unique_ptr<LocalTrajectoryBuilder2D::MatchingResult>
LocalTrajectoryBuilder2D::AddAccumulatedRangeData(
......
std::unique_ptr<transform::Rigid2d> pose_estimate_2d =
ScanMatch(time, pose_prediction, filtered_gravity_aligned_point_cloud);
if (pose_estimate_2d == nullptr) {
LOG(WARNING) << "Scan matching failed.";
return nullptr;
}
const transform::Rigid3d pose_estimate =
transform::Embed3D(*pose_estimate_2d) * gravity_alignment;
extrapolator_->AddPose(time, pose_estimate);
......
}

前端我们经过​​ScanMatch​​​之后得到一个Pose​​pose_estimate_2d​​我们再将这个Pose放到外推器里面。刚才不是讲了一个外推器里面的有一个位姿队列,然后就把这个位姿存储到位姿队列中去。

UpdateVelocitiesFromPoses();

然后计算更新后Pose的速度。

前端:我们看完了从pose里面算出来的速度,再看从odom算出来的角速度。
首先再回忆一下前端的整个过程,首先来了一帧激光,就用这一帧激光建立一个子图,然后就是运动,运动到哪个位置就用外推器计算出来大概的位置,大概是用里程计去计算大概的位置。计算出来之后,我们拿着当前的激光去跟之前建立的子图做匹配,进一步匹配精细化我跟刚刚匹配的位姿。匹配得到了位姿之后,就将位姿放到外推器里面,同时在这个位姿上将新的激光插入到子图上,这就是我们的前端了

可以看到ScanMatch修正之后得到的​​Pose​

std::unique_ptr<transform::Rigid2d> pose_estimate_2d =
ScanMatch(time, pose_prediction, filtered_gravity_aligned_point_cloud);

然后再将位姿放到外推器里面去,也就是调用​​AddPose​​函数。如下代码所示

const transform::Rigid3d pose_estimate =
transform::Embed3D(*pose_estimate_2d) * gravity_alignment;
extrapolator_->AddPose(time, pose_estimate);

然后再在​​AddPose()​​​中去调用​​UpdateVelocitiesFromPoses();​​​函数,用Pose队列,去更新下速度。
接下来看用​​​Odom​​​算出来的角速度​​angular_velocity_from_odometry_​​​是在哪里定义的。是在​​AddOdometryData​​中定义的。

void PoseExtrapolator::AddOdometryData(
const sensor::OdometryData& odometry_data) {
......
angular_velocity_from_odometry_ =
transform::RotationQuaternionToAngleAxisVector(
odometry_pose_delta.rotation()) /
odometry_time_delta;
......
}

每次新来了Odom数据​​odometry_data​​​之后,就用最新的队列去计算一下角速度。这里不仅计算角速度,而且还有​​ExtrapolateRotation(odometry_data_newest.time, odometry_imu_tracker_.get());​​​ 值得一提的是,我们再通过里程计计算角速度的这个地方遇到了​​odometry_imu_tracker_​​​,这里我们暂时提一下,后面会进行详细讲解。
接下来看一下​​​AddOdometryData()​​​是在哪里调用的,
local_trajectory_builder_2d.cc

void LocalTrajectoryBuilder2D::AddOdometryData(
const sensor::OdometryData& odometry_data) {
if (extrapolator_ == nullptr) {
// Until we've initialized the extrapolator we cannot add odometry data.
LOG(INFO) << "Extrapolator not yet initialized.";
return;
}
extrapolator_->AddOdometryData(odometry_data);
}

主要就是我们前端在接受了里程计数据​​odometry_data​​​之后,然后把它送到外推器里面去​​extrapolator_->AddOdometryData(odometry_data);​​​。然后就是刚才提到的计算角速度。具体代码如下:
pose_extrapolator.cc

void PoseExtrapolator::AddOdometryData(
const sensor::OdometryData& odometry_data) {
CHECK(timed_pose_queue_.empty() ||
odometry_data.time >= timed_pose_queue_.back().time);
odometry_data_.push_back(odometry_data);
TrimOdometryData();
if (odometry_data_.size() < 2) {
return;
}
// TODO(whess): Improve by using more than just the last two odometry poses.
// Compute extrapolation in the tracking frame.
const sensor::OdometryData& odometry_data_oldest = odometry_data_.front();
const sensor::OdometryData& odometry_data_newest = odometry_data_.back();
const double odometry_time_delta =
common::ToSeconds(odometry_data_oldest.time - odometry_data_newest.time);
const transform::Rigid3d odometry_pose_delta =
odometry_data_newest.pose.inverse() * odometry_data_oldest.pose;
angular_velocity_from_odometry_ =
transform::RotationQuaternionToAngleAxisVector(
odometry_pose_delta.rotation()) /
odometry_time_delta;
if (timed_pose_queue_.empty()) {
return;
}
const Eigen::Vector3d
linear_velocity_in_tracking_frame_at_newest_odometry_time =
odometry_pose_delta.translation() / odometry_time_delta;
const Eigen::Quaterniond orientation_at_newest_odometry_time =
timed_pose_queue_.back().pose.rotation() *
ExtrapolateRotation(odometry_data_newest.time,
odometry_imu_tracker_.get());
linear_velocity_from_odometry_ =
orientation_at_newest_odometry_time *
linear_velocity_in_tracking_frame_at_newest_odometry_time;
}

下面进行逐行解析,首先是接收到一帧里程计数据​​odometry_data​​​之后,首先是进行一个检查
src/cartographer/cartographer/mapping/pose_extrapolator.cc

CHECK(timed_pose_queue_.empty() ||
odometry_data.time >= timed_pose_queue_.back().time);

检查如果这个​​pose​​​队列是空的,或者当前的里程计的时间大于pose队列最老的时间,满足这些条件我们才能进行下去。因为我们是往前推数据,而不是推已经存在的数据。然后有一个​​TrimOdometryData​​函数,会把time之前的odometry_data数据都给删掉。

void PoseExtrapolator::TrimOdometryData() {
while (odometry_data_.size() > 2 && !timed_pose_queue_.empty() &&
odometry_data_[1].time <= timed_pose_queue_.back().time) {
odometry_data_.pop_front();
}
}

在保证里程计数据大于2的情况下,且位姿队列不为空的情况下,也就是说,直到位姿队列中只剩下一个位姿的时候,循环才会停止。并且,如果队列就剩下两个数据,timed_pose_queue_.back().time的时间就等于odometry_data_[1].time的时间,满足条件,就一直往外抛出数据。因为之前的数据都没有用了,我们要使用的是pose之前的数据,之后的没有用了,只保留2个就可以了。
然后继续回溯到之前的代码的位置。
src/cartographer/cartographer/mapping/pose_extrapolator.cc

if (odometry_data_.size() < 2) {
return;
}

如果数据不够,就继续返回。

const sensor::OdometryData& odometry_data_oldest = odometry_data_.front();
const sensor::OdometryData& odometry_data_newest = odometry_data_.back();
const double odometry_time_delta =
common::ToSeconds(odometry_data_oldest.time - odometry_data_newest.time);
const transform::Rigid3d odometry_pose_delta =
odometry_data_newest.pose.inverse() * odometry_data_oldest.pose;
angular_velocity_from_odometry_ =
transform::RotationQuaternionToAngleAxisVector(
odometry_pose_delta.rotation()) /
odometry_time_delta;

接下来是老套路了,都是拿到最老和最新的odom数据,然后再计算得到cartographer 源码解析(六)_算法_27,根据最新和最老的位姿算出cartographer 源码解析(六)_3d_28。然后再求出角速度。

const Eigen::Vector3d
linear_velocity_in_tracking_frame_at_newest_odometry_time =
odometry_pose_delta.translation() / odometry_time_delta;

然后还有一串长名字​​linear_velocity_in_tracking_frame_at_newest_odometry_time​​​。最新的里程计时间点上的​​tracking_frame​​上的线速度。为什么起这么个名字,我们还是要讲一下。

linear_velocity_from_odometry_ =
orientation_at_newest_odometry_time *
linear_velocity_in_tracking_frame_at_newest_odometry_time;

上面代码主要是让速度从机体坐标转化成local坐标系。

const Eigen::Quaterniond orientation_at_newest_odometry_time =
timed_pose_queue_.back().pose.rotation() *
ExtrapolateRotation(odometry_data_newest.time,
odometry_imu_tracker_.get());

上面代码算的是刚刚使用到的朝向,朝向怎么算的,跟之前的外推器计算的朝向差不多,就是外推器存储的最后的一个pose,​​timed_pose_queue_.back().pose.rotation()​​​,在这个基础上,乘以一个cartographer 源码解析(六)_Time_29​​​ExtrapolateRotation(odometry_data_newest.time, odometry_imu_tracker_.get())​​​。 但是这个cartographer 源码解析(六)_Time_29是怎么算的呢?他使用了一个​​​odometry_imu_tracker_​​​。 当然这函数是一样的,只不过参数​​imu_tracker​​不一样。

我们为了通过里程计,计算出来线速度,我们需要把线速度转化到local坐标系里面去。计算这个速度的时候,本身就要外推一下,然后使用一个外推器,推出一个朝向角,计算出一个速度。然后再用这个速度去外推一个pose。然后计算里程计的速度去做一个外推,用到了imu_tracker,为什么又搞了一个新的呢?

Eigen::Quaterniond PoseExtrapolator::ExtrapolateRotation(
const common::Time time, ImuTracker* const imu_tracker) const {
CHECK_GE(time, imu_tracker->time());
AdvanceImuTracker(time, imu_tracker);
const Eigen::Quaterniond last_orientation = imu_tracker_->orientation();
return last_orientation.inverse() * imu_tracker->orientation();
}

可以看到我们的​​odometry_tracker​​​ 也是先通过​​AdvanceImuTracker​​​ 这个来得到一个朝向角。然后再从后缀_的imu_tracker_中得到上一次的朝向角。算出来一个cartographer 源码解析(六)_3d
我们什么时候要用外推器外推的时间?
比如一帧激光进来了,你要外推当前机器人的pose。要外推一次。
然后每一帧的激光点,都要给一个点外推一个发射原点。
我们的外推器,每来一个IMU数据,就要更新当前时刻的朝向。我们记录我们的,外推器记录外推器的,大家各自一套,分开就可以了。

void PoseExtrapolator::AdvanceImuTracker(const common::Time time,
ImuTracker* const imu_tracker) const {
CHECK_GE(time, imu_tracker->time());
if (imu_data_.empty() || time < imu_data_.front().time) {
// There is no IMU data until 'time', so we advance the ImuTracker and use
// the angular velocities from poses and fake gravity to help 2D stability.
imu_tracker->Advance(time);
imu_tracker->AddImuLinearAccelerationObservation(Eigen::Vector3d::UnitZ());
imu_tracker->AddImuAngularVelocityObservation(
odometry_data_.size() < 2 ? angular_velocity_from_poses_
: angular_velocity_from_odometry_);
return;
}
if (imu_tracker->time() < imu_data_.front().time) {
// Advance to the beginning of 'imu_data_'.
imu_tracker->Advance(imu_data_.front().time);
}
auto it = std::lower_bound(
imu_data_.begin(), imu_data_.end(), imu_tracker->time(),
[](const sensor::ImuData& imu_data, const common::Time& time) {
return imu_data.time < time;
});
while (it != imu_data_.end() && it->time < time) {
imu_tracker->Advance(it->time);
imu_tracker->AddImuLinearAccelerationObservation(it->linear_acceleration);
imu_tracker->AddImuAngularVelocityObservation(it->angular_velocity);
++it;
}
imu_tracker->Advance(time);
}

为了实时的把里程计的速度转化到​​local​​​坐标系下,我们就单独用了​​odometry_imu_tracker​​。

我们再回顾一下​​imu_tracker​​​。
src/cartographer/cartographer/mapping/pose_extrapolator.h

std::unique_ptr<ImuTracker> imu_tracker_;
std::unique_ptr<ImuTracker> odometry_imu_tracker_;
std::unique_ptr<ImuTracker> extrapolation_imu_tracker_;

​extrapolation_imu_tracker_​​​是用来做外推的主力。​​odometry_imu_tracker_​​​只为了用于把里程计的速度转化为local坐标系里去。​​imu_tracker_​​​还没讲到。但他时刻记录着过程中,起到的作用,记录最近一次pose的朝向角,我们总是在这个基础上,计算cartographer 源码解析(六)_Time_29

我们首先想想​​AddPose​​​什么时候用的,就是​​ScanMatch​​​结束之后,我们得到了一个精细的​​Pose​​​,然后将这个​​Pose​​​传入到​​AddPose​​函数中,放入到外推器里面的位姿队列中。

void PoseExtrapolator::AddPose(const common::Time time,
const transform::Rigid3d& pose) {
......
odometry_imu_tracker_ = absl::make_unique<ImuTracker>(*imu_tracker_);
extrapolation_imu_tracker_ = absl::make_unique<ImuTracker>(*imu_tracker_);
}

​odometry_imu_tracker_​​​的作用是只要来一帧里程计数据,就要更新里程计的速度,但是速度要转化到local坐标系里面去,怎么转化呢?就是算cartographer 源码解析(六)_Time_29,再添加到最新的​​​pose​​​上面去。
为什么两个位姿可以相减得到KaTeX parse error: Undefined control sequence: \Deltata at position 1: \̲D̲e̲l̲t̲a̲t̲a̲_{rotation},因为每一次数据来之后,我们得到一个很精细的pose。完了我们把​​​imu_tracker_​​​记录的角度给统一到​​odometry_imu_tracker_​​​,​​extrapolation_imu_tracker_​​​中去。那么下次我们再计算出角度之后,就去减掉​​imu_tracker_​​​。就相当于减掉自己的上一时刻的位姿。imu_tracker_记录的是每一次我们​​ScanMatch​​​结束之后,我们记录的精细的角度。他始终记录的是结果。
​​​odometry_imu_tracker_,extrapolation_imu_tracker_​​​是两个工具人,始终在​​imu_tracker_​​​的基础上去计算cartographer 源码解析(六)_Time_29,然后再把cartographer 源码解析(六)_Time_29放置在存储pose队列里面最后的一个​​​pose​​​上面去,得到所需要时刻的​​pose​​。


标签:odometry,cartographer,const,pose,源码,imu,tracker,time,解析
From: https://blog.51cto.com/u_12606187/5959825

相关文章

  • C#解析XML字符串
    stringsql="实际sql语句";DataTablesourseDt=OracleHelper.ExecuteDataTable(sql);for(vari=0;i<sourseDt.Rows.Count;i++)......
  • linux内核源码阅读(四)Linux进程调度时机
    调度程序虽然特别重要,但它不过是一个存在于内核空间中的函数而已,并不神秘。Linux的调度程序是一个叫Schedule()的函数,这个函数被调用的频率很高,由它来决定是否要进行进程的切......
  • MyBatis源码分析(二)prepareStatement预编译的执行流程
    通常我们如果自己写建立数据库连接的代码的时候,都会这么写pstmt=conn.prepareStatement(sql);pstmt.setString(1,email);result=pstmt.executeQuery();而Mybatis是怎么......
  • JDK源码之CompletableFuture(二)链式调用原理
    ​​JDK源码之CompletableFuture(一)结果返回原理​​JDK源码之CompletableFuture(二)链式调用原理JDK源码之CompletableFuture(三)anyOf,allOf是怎么实现的?目录​​一、第一步调......
  • SpringMVC父子容器加载与关系源码
    我们都知道SpringMVC父子容器加载是通过dispatcherServlet与ContextLoaderListener类:他们的关系源码如下:先说父容器加载:Context'Loader'Listener源码如下:@Overridepublic......
  • AliMQ(RocketMQ)源码(一)创建producer
    公司现在在使用阿里云的AliMQ也就是RocketMQ,趁着工作之余,将RocketMQ的使用心得分析一下,关于RocketMQ的Producer、Consumer、Broker、NameServer等架构问题,在此处先不做分析......
  • AliMQ(RocketMQ)源码(二)producer.start()
    在创建完成Producer后,就进入了Producer的start()方法。start()方法DefaultMQProducerImpl的start()方法。this.serviceState=ServiceState.START_FAILED;......
  • Dubbo架构设计与源码解析(二) 服务注册
    一、Dubbo简介Dubbo是一款典型的高扩展、高性能、高可用的RPC微服务框架,用于解决微服务架构下的服务治理与通信问题。其核心模块包含【RPC通信】和【服务治理】,其中......
  • Postman核心功能解析-参数化和测试报告
    每天进步一点点,关注我们哦,每天分享测试技术文章本文章出自【码同学软件测试】码同学公众号:自动化软件测试,领取资料可加:magetest码同学抖音号:小码哥聊软件测试1.参数化......
  • Python 配置参数解析器configparse
    1.configparse介绍configparser是python自带的配置参数解析器。可以用于解析.config文件中的配置参数。ini文件中由sections(节点)-key-value组成2.安装:pipinstallc......