PoseExtrapolator(位姿外推器)

本文初始架构转自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()) {}

全部评论: 0

    写评论: