本文初始架构转自Cartographer源码阅读11—LocalTrajectoryBuilder2D成员变量解读
PoseExtrapolator定义在/mapping/pose_extrapolator.h中。其主要作用是对IMU、里程计数据进行融合,估计机器人的实时位姿。
从cartographer算法的流程框图(见Cartographer源码阅读1——整体框架介绍(https://zhuanlan.zhihu.com/p/48010119)中第一个图)我们可以知道,PoseExtrapolator的输入有三个:从里程计传来的数据、经过ImuTracker处理过的对重力进行aligned过的IMU数据、从Scan Matching输出的Pose Observation。PoseExtrapolator对这三个输入进行融合后输出估计出来的PoseEstimate。
从该类开头的一段注释我们也可以知道,该类通过从Scan Matching输出的PoseObservation持续一段时间来跟踪Poses,从而估计机器人的线速度和角速度。通过速度来解算机器人的运动。当IMU或里程计数据可用时,可与这些数据融合来提升计算结果。
一、成员变量
<cartographer>/cartographer/mapping/pose_extrapolator.h ------ class PoseExtrapolator : public PoseExtrapolatorInterface { // Duration是在/common/time.h中定义的别名 // using Duration = UniversalTimeScaleClock::duration; //微秒,0.1us // UniversalTimeScaleClock类实现c++11的clock接口,以0.1us为时间精度。 // 在rose_ros,值1000。0.001秒,即1毫秒。这时间基本就是timed_pose_queue_中最多2个单元。 const common::Duration pose_queue_duration_; // 带时间的Pose struct TimedPose { common::Time time; transform::Rigid3d pose; }; // 带时间的Pose队列,存储要持续跟踪的Poses,应该是从ScanMatching输出的PoseObservation std::deque<TimedPose> timed_pose_queue_; // 从持续跟踪一段时间的Pose队列中估计出来的线速度,初始值为0 Eigen::Vector3d linear_velocity_from_poses_ = Eigen::Vector3d::Zero(); // 从持续跟踪一段时间的Pose队列中估计出来的线速度,初始值为0 Eigen::Vector3d angular_velocity_from_poses_ = Eigen::Vector3d::Zero(); //重力的时间间隔? const double gravity_time_constant_; //存储IMU数据的队列 std::deque<sensor::ImuData> imu_data_; //ImuTracker,定义在/mapping/imu_tracker.h中。存放由IMU数据得到的信息 std::unique_ptr<ImuTracker> imu_tracker_; // 存放由里程计得到的信息 std::unique_ptr<ImuTracker> odometry_imu_tracker_; // 存放经过数据融合后的结果 std::unique_ptr<ImuTracker> extrapolation_imu_tracker_; // 缓存的一个带时间的Pose TimedPose cached_extrapolated_pose_; //里程计数据 std::deque<sensor::OdometryData> odometry_data_; // 从里程计获取到的线速度,初始化为0 Eigen::Vector3d linear_velocity_from_odometry_ = Eigen::Vector3d::Zero(); // 从里程计获取到的角速度,初始化为0 Eigen::Vector3d angular_velocity_from_odometry_ = Eigen::Vector3d::Zero(); };
可以看到,与该类的功能相对应,成员变量里包括了:标记持续时间的长短的pose_queue_duration_、记录这段时间的Pose的一个队列timed_pose_queue_、从pose队列中估计出来的线速度和角速度,以及分别从IMU和里程计获得的信息。
根据上面,首先分析下LocalTrajectoryBuilder2D::AddRangeData要调用的ExtrapolatePose。
二、ExtrapolatePose
ExtrapolatePose用于推算指定时刻的Pose。time一般指的是点云中各个点的采集时间,对此次点云的最后一个点,time就是零时刻。
transform::Rigid3d PoseExtrapolator::ExtrapolatePose(const common::Time time) { // 取出Pose队列中的最新值(收到第k次点云的零时刻pose):47.56414 const TimedPose& newest_timed_pose = timed_pose_queue_.back(); // 当前time一定要比队列中的时间更新 CHECK_GE(time, newest_timed_pose.time); if (cached_extrapolated_pose_.time != time) { // 如果缓存的Pose的时间不等于指定时间 // 最新translation等于最新Pose的translation再加上这段时间的平移增长量 const Eigen::Vector3d translation = ExtrapolateTranslation(time) + newest_timed_pose.pose.translation(); // 最新旋转等于最新Pose的rotation再加上这段时间的旋转增长量。47.56414 + 4.45687 = 52.02102。 // 4.45687是(time - k-1次点云的零时刻)这段时间的imu旋转变化量 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; }
会经常调用ExtrapolatePose(time),而且连着数次参数time往往相同,为提高效率,Extrapolator就用了一个缓存变量cached_extrapolated_pose_,缓存着指定time时的位姿。这样对指定time,只要第一次计算,后面就可直接取cached_extrapolated_pose_.pose,直到来了新的time。
ExtrapolatePose怎么计算位姿?来自两处变量:一是timed_pose_queue_队列最新单元中的TimedPose,二是ExtrapolateTranslation和ExtrapolateRotation。
首先看timed_pose_queue_存的是什么。AddPose是入队timed_pose_queue_的唯一入口函数,负责在时刻time往Pose队列中添加一个Pose。
三、AddPose
void PoseExtrapolator::AddPose(const common::Time time, const transform::Rigid3d& pose) { if (imu_tracker_ == nullptr) { // 如果ImuTracker还没有建立,则需要建立一个ImuTracker common::Time tracker_start = time; if (!imu_data_.empty()) { // 如果IMU数据队列不为空,则以当前时间和IMU数据中的最早时刻的较小值为初始时刻建立一个ImuTracker tracker_start = std::min(tracker_start, imu_data_.front().time); } // 生成ImuTracker imu_tracker_ = absl::make_unique<ImuTracker>(gravity_time_constant_, tracker_start); } // 把Pose压入队列 timed_pose_queue_.push_back(TimedPose{time, pose}); // Pose队列大于2,并且时间间隔已经大于我们设定的pose_queue_duration_时,则把队列之前的元素删除 while (timed_pose_queue_.size() > 2 && timed_pose_queue_[1].time <= time - pose_queue_duration_) { timed_pose_queue_.pop_front(); } // 计算机器人的瞬时线速度(linear_velocity_from_poses_)和瞬时角速度(angular_velocity_from_poses_) UpdateVelocitiesFromPoses(); // 更新ImuTracker AdvanceImuTracker(time, imu_tracker_.get()); //更新IMU数据队列 TrimImuData(); //更新里程计数据队列 TrimOdometryData(); // 让两个临时ImuTracker和imu_tracker_保持一致 odometry_imu_tracker_ = absl::make_unique<ImuTracker>(*imu_tracker_); extrapolation_imu_tracker_ = absl::make_unique<ImuTracker>(*imu_tracker_); }
目前不必理会AddPose中自UpdateVelocitiesFromPoses()后的那段逻辑。看是什么时候调用AddPose。当没用imu时,就两处。
- LocalTrajectoryBuilder2D::InitializeExtrapolator。保证在调用ExtrapolatePose(time)时,队列中至少有一个“0”值。
- LocalTrajectoryBuilder2D::AddAccumulatedRangeData。ScanMatch之后结果,再考虑重力后的Pose。
综上所述,在timed_pose_queue_存储的是那些个Scan插入Submap的最优Pose,然后考虑重力方向,将2d的pose变成3d的pose。它指示了机器人在time时的位姿。
上面出现了三个ImuTracker:imu_tracker_、odometry_imu_tracker_和extrapolation_imu_tracker_。在一次LocalTrajectoryBuilder2D::AddRangeData过程中,imu_tracker_中状态只在最后(AddPose)时才会变换。odometry_imu_tracker_、extrapolation_imu_tracker_则是在外推点云中的每个点时就要变换一次,e有时也就叫临时ImuTracker。计算出一次local_pose后,后两个就要和imu_tracker_保持一致,即下次点云来时,是以上一次imu_tracker_状态为基准。
接下看ExtrapolateRotation和ExtrapolateTranslation,它们分别计算旋转和平移的增长量
四、ExtrapolateRotation和ExtrapolateTranslation
ExtrapolateRotation功能是要算出(time - k-1次点云的零时刻)这段时间的旋转变化量。参数imu_tracker不会是imu_tracker_,往往是extrapolation_imu_tracker_。
Eigen::Quaterniond PoseExtrapolator::ExtrapolateRotation( const common::Time time, ImuTracker* const imu_tracker) const { //检查指定时间是否大于等于ImuTracker的时间。 CHECK_GE(time, imu_tracker->time()); //更新ImuTracker到指定的time AdvanceImuTracker(time, imu_tracker); const Eigen::Quaterniond last_orientation = imu_tracker_->orientation(); // 求取旋转变化量:上一时刻旋转的逆乘以当前的旋转。 // -43.22239 + 47.67926 = 4.45687 // 43.22239: imu_tracker_.orientation_要等到AddPose后才会更新,目前值就imu_tracker在k-1次点云零时刻的orientation_。 // 47.67926: time时刻imu_tracker的orientation_。 return last_orientation.inverse() * imu_tracker->orientation(); }
计算(time - newest_timed_pose.time)这段时间平移的变换量
Eigen::Vector3d PoseExtrapolator::ExtrapolateTranslation(common::Time time) { // 取出Pose队列中最新的时刻 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) { // 没有里程计数据,从Pose队列中估计的线速度乘以时间。rose_ros进这入口。 return extrapolation_delta * linear_velocity_from_poses_; } // 有里程计数据,则更信任里程计速度,直接把从里程计处获得的线速度乘以时间 return extrapolation_delta * linear_velocity_from_odometry_; }
从上面两个函数可以看出,对于rotation而言,更信任IMU的数据,如果有IMU数据则用IMU数据;对于Translation而言,更信任里程计数据,如果有里程计数据则优先使用里程计数据。没有的话就用Pose队列估计出来的线速度。而对于IMU的线速度并不信任。这跟IMU的传感器特性是相关的。
不论平移还是旋转,它们都由两部分构成,一是oldest到newest的变化量,二是newest到time的变化量。对oldest到newest的变化量,是UpdateVelocitiesFromPoses在进行计算。
五、UpdateVelocitiesFromPoses
该函数的主要工作就是取出timed_pose_queue_这个队列中最老(oldest)和最新(newest)的两个Pose做差,然后除以这两个点的时间间隔得到机器人的瞬时线速度(linear_velocity_from_poses_)和瞬时角速度(angular_velocity_from_poses_)。
void PoseExtrapolator::UpdateVelocitiesFromPoses() { if (timed_pose_queue_.size() < 2) { // timed_pose_queue_至少得存在两个pose,因为要由首、尾两个pose算出瞬时角/线速度。 // 只有一个的话,做为除数的时间间隔是0,就没意义了。 return; } CHECK(!timed_pose_queue_.empty()); // 取出队列最末尾的一个Pose,也就是最新时间点的Pose,并记录相应的时间 const TimedPose& newest_timed_pose = timed_pose_queue_.back(); const auto newest_time = newest_timed_pose.time; // 取出队列最开头的一个Pose,也就是最旧时间点的Pose,并记录相应的时间 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); // 如果时间差小于1ms,则估计不准,弹出警告信息 if (queue_delta < common::ToSeconds(pose_queue_duration_)) { LOG(WARNING) << "Queue too short for velocity estimation. Queue duration: " << queue_delta << " s"; return; } // 获取两个时刻各自的Pose const transform::Rigid3d& newest_pose = newest_timed_pose.pose; const transform::Rigid3d& oldest_pose = oldest_timed_pose.pose; // 线速度即为两个Pose的translation部分相减后除以间隔时间 linear_velocity_from_poses_ = (newest_pose.translation() - oldest_pose.translation()) / queue_delta; // 角速度是两个Pose的rotation部分的差 angular_velocity_from_poses_ = transform::RotationQuaternionToAngleAxisVector( oldest_pose.rotation().inverse() * newest_pose.rotation()) / queue_delta; }
算出的角速度放在angular_velocity_from_poses_,类型是Eigen::Vector3d,但它其实是个旋转向量。对如何表示旋转向量,Eigen往往用Eigen::AngleAxisd,cartogrphaer没用它,而是Eigen::Vector3d,它的[0]、[1]、[2]分别对应旋转向量的roll、pitch、yaw。carotographer自实现的RotationQuaternionToAngleAxisVector用于把四元数转成旋转向量,AngleAxisVectorToRotationQuaternion则把旋转向量转成四元数。
六、AdvanceImuTracker
PoseExtrapolator::AdvanceImuTracker的主要作用就是从IMU数据队列中取出最新的数据,更新ImuTracker的状态到指定的时间time。
// 从IMU数据队列中取出最新的数据,更新ImuTracker的状态到指定的时间time void PoseExtrapolator::AdvanceImuTracker(const common::Time time, ImuTracker* const imu_tracker) const { //当前时间是否晚于ImuTracker的时间,如果是,需要更新ImuTracker 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数据队列为空,或当前时间要比IMU数据队列中最早期的时间还要早。说明没有可用的IMU数据。 // 这时候根据时间推算。对于角速度,是用从local-pose中估计的角速度或从里程计获得的角速度更新ImuTracker的角速度 imu_tracker->Advance(time); // 用一个fake的重力方向更新线加速度。这个fake的重力方向就是[0,0,1]。理想情况。 imu_tracker->AddImuLinearAccelerationObservation(Eigen::Vector3d::UnitZ()); imu_tracker->AddImuAngularVelocityObservation( odometry_data_.size() < 2 ? angular_velocity_from_poses_ : angular_velocity_from_odometry_); return; } // 如果ImuTracker维护的时间早于IMU数据队列最早期的时间 if (imu_tracker->time() < imu_data_.front().time) { // Advance to the beginning of 'imu_data_'. // 先把ImuTracker更新到IMU数据来临的那一刻 imu_tracker->Advance(imu_data_.front().time); } //然后依次取出IMU数据队列中的数据,更新ImuTracker,直到IMU数据的时间比指定时间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); }
ImuTracker的主要作用就是根据IMU的读数维护传感器当前的姿态、线加速度(经过重力校正的)、当前姿态、重力方向、角速度等量。这些量都是以ImuTracker刚建立时的那一时刻IMU本身的坐标系为基准坐标系。具体函数的实现代码如下。
ImuTracker::Advance功能是把ImuTracker状态更新到指定时刻time,状态包括指示旋转值的orientation_,以及time_、gravity_vector_。函数注解见“计算位姿”中“二、更新旋转值”。
七、ImuTracker::AddImuLinearAccelerationObservation
功能是根据读数更新线加速度。这里的线加速度是经过重力校正的。
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_; 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); }
根据读数更新角速度
void ImuTracker::AddImuAngularVelocityObservation( const Eigen::Vector3d& imu_angular_velocity) { imu_angular_velocity_ = imu_angular_velocity; }
重力校正这一块的公式我每太看懂,但由于时间关系,就不再深入了解了。读者如有兴趣可自行查看。
ImuTracker的主要作用就是根据IMU的读数维护传感器当前的姿态、线加速度(经过重力校正的)、当前姿态、重力方向、角速度等量。这些量都是以ImuTracker刚建立时的那一时刻IMU本身的坐标系为基准坐标系。具体函数的实现代码如下。为全面,再补上ImuTracker构造函数。
ImuTracker::ImuTracker(const double imu_gravity_time_constant, const common::Time time) : imu_gravity_time_constant_(imu_gravity_time_constant), time_(time), last_linear_acceleration_time_(common::Time::min()), //初始姿态为I orientation_(Eigen::Quaterniond::Identity()), //重力方向初始化为[0,0,1] gravity_vector_(Eigen::Vector3d::UnitZ()), //角速度初始化为0 imu_angular_velocity_(Eigen::Vector3d::Zero()) {}