cartographer源码阅读---位姿推测器

2023-05-16

工程化的cartographer框架为了实现实时可靠的位姿推断,在算法中实现了位姿推测器(pose_extrapolator), 其作用是通过现有的有关机器人位姿的数据, 给前端匹配一个可靠的目标值进行ceres优化. 也就是以位姿推测器推出来的位姿为前端扫描匹配的目标位姿. 所以前端得到的位姿结果相当依赖于位姿推测器. 前端和位姿推测器的交集如下, 在local_trajectory_builder_2d.cc中:

   range_data_poses.push_back(
        extrapolator_->ExtrapolatePose(time_point).cast<float>());
   ......
   const transform::Rigid3d non_gravity_aligned_pose_prediction =
      extrapolator_->ExtrapolatePose(time);
   ......
  extrapolator_->AddPose(time, pose_estimate);
  ......
  extrapolator_->AddPose(time, transform::Rigid3d::Identity());
  ......
  extrapolator_->AddOdometryData(odometry_data);
  

cartographer为位姿推测器写了个接口, 文件为pose_extrapolator_Interface.cc/h,都是虚函数与结构体定义, 其接口的实现在pose_extrapolator.cc/h中.

1. PoseExtrapolator类

这个类构造函数如下

/**
 * @brief 构造函数
 * 
 * @param[in] pose_queue_duration 时间差 0.001s
 * @param[in] imu_gravity_time_constant 10
 */
PoseExtrapolator::PoseExtrapolator(const common::Duration pose_queue_duration,
                                   double imu_gravity_time_constant)
    : pose_queue_duration_(pose_queue_duration),
      gravity_time_constant_(imu_gravity_time_constant),
      cached_extrapolated_pose_{common::Time::min(),
                                transform::Rigid3d::Identity()} {}

pose_queue_duration_与gravity_time_constant_都是从lua配置文件中读取的参数, 意思是两次预测位姿需要最小间隔多长时间 与 重力常数.
定义在头文件中的私有变量:

  // 保存一定时间内的pose
  const common::Duration pose_queue_duration_;
  struct TimedPose {
    common::Time time;
    transform::Rigid3d pose;
  };
  std::deque<TimedPose> timed_pose_queue_; //带时间戳的pose
  // 线速度有2种计算途径
  Eigen::Vector3d linear_velocity_from_poses_ = Eigen::Vector3d::Zero();
  Eigen::Vector3d angular_velocity_from_poses_ = Eigen::Vector3d::Zero();

  const double gravity_time_constant_;
  std::deque<sensor::ImuData> imu_data_;

  // c++11: std::unique_ptr 是独享被管理对象指针所有权的智能指针
  // 它无法复制到其他 unique_ptr, 也无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL) 算法
  // 只能通过 std::move() 来移动unique_ptr
  // std::make_unique 是 C++14 才有的特性

  std::unique_ptr<ImuTracker> imu_tracker_;               // 保存与预测当前姿态
  std::unique_ptr<ImuTracker> odometry_imu_tracker_;      // 用于计算里程计的姿态的ImuTracker
  std::unique_ptr<ImuTracker> extrapolation_imu_tracker_; // 用于预测姿态的ImuTracker
  TimedPose cached_extrapolated_pose_;

  std::deque<sensor::OdometryData> odometry_data_;
  Eigen::Vector3d linear_velocity_from_odometry_ = Eigen::Vector3d::Zero();
  Eigen::Vector3d angular_velocity_from_odometry_ = Eigen::Vector3d::Zero();

2. 通过odometer进行位姿推测

// 向odom数据队列中添加odom数据,并进行数据队列的修剪,并计算角速度与线速度
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);

  // 修剪odom的数据队列
  TrimOdometryData();
  // 数据队列中至少有2个数据
  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();
  // 最新与最老odom数据间的时间差
  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;

  // 两个位姿间的旋转量除以时间得到 tracking frame 的角速度
  angular_velocity_from_odometry_ =
      transform::RotationQuaternionToAngleAxisVector(
          odometry_pose_delta.rotation()) /
      odometry_time_delta;
  if (timed_pose_queue_.empty()) {
    return;
  }
  // 平移量除以时间得到 tracking frame 的线速度, 只在x方向有数值(机器人只能向前走)
  const Eigen::Vector3d
      linear_velocity_in_tracking_frame_at_newest_odometry_time =
          odometry_pose_delta.translation() / odometry_time_delta;

  // 根据位姿队列中最后一个位姿 乘以 上次添加位姿时的姿态预测到time时刻的姿态变化量
  // 得到预测的 最新里程计数据时刻 tracking frame 在 local 坐标系下的姿态
  const Eigen::Quaterniond orientation_at_newest_odometry_time =
      timed_pose_queue_.back().pose.rotation() *
      ExtrapolateRotation(odometry_data_newest.time,
                          odometry_imu_tracker_.get());
  // 将tracking frame的线速度进行旋转, 得到 local 坐标系下 tracking frame 的线速度
  linear_velocity_from_odometry_ =
      orientation_at_newest_odometry_time *
      linear_velocity_in_tracking_frame_at_newest_odometry_time;
}

上面判断是不是有两个有效的odometer数据来判断是不是用了odometer. 在odom数据修剪后的队列中得到最新与最老的数据, 计算时间差, 然后通过odometer的位移和角度与时间差微分得到速度和角速度,然后在通过积分得到从上一时刻的位姿(这个位姿是优化后的位姿, 不是odom的位姿)到现在时刻的位姿的转换矩阵,从而推出现在的估计位姿.
修剪odomdata

// 修剪odom的数据队列,丢掉过时的odom数据
void PoseExtrapolator::TrimOdometryData() {
  // 保持odom队列中第二个数据的时间要大于最后一个位姿的时间, odometry_data_最少是2个
  while (odometry_data_.size() > 2 && !timed_pose_queue_.empty() &&
         odometry_data_[1].time <= timed_pose_queue_.back().time) { //同上
    odometry_data_.pop_front();
  }
}

3. 通过IMU进行位姿预测

实际用到的IMU位姿预测没有在pose_extrapolator中实现, 而是调用的imu_tracker. 其核心的预测代码在Advance中, 如下:

/**
 * @brief 预测出time时刻的姿态与重力方向
 * 
 * @param[in] time 要预测的时刻
 */
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_ = (orientation_ * rotation).normalized();

  // 根据预测出的姿态变化量,预测旋转后的线性加速度的值
  gravity_vector_ =
      rotation.conjugate() * // 一般不用inverse,在表示旋转时,共轭即可表示相反的的旋转
      gravity_vector_; 
  // 更新时间
  time_ = time;
}

可以看到, Advance只用到了IMU的角速度信息(陀螺仪), 并没有用到加速度信息(加速度计). 只用陀螺仪积分得到了角度变化, 用角度的变化更新重力的方向.
在PoseExtrapolator类中, 与 ImuTracker类进行交互的方法是AdvanceImuTracker:

/**
 * @brief 更新imu_tracker的状态, 并将imu_tracker的状态预测到time时刻
 * 
 * @param[in] time 要预测到的时刻
 * @param[in] imu_tracker 给定的先验状态
 */
void PoseExtrapolator::AdvanceImuTracker(const common::Time time,
                                         ImuTracker* const imu_tracker) const {
  // 检查指定时间是否大于等于 ImuTracker 的时间
  CHECK_GE(time, imu_tracker->time());

  // 不使用imu 或者 预测时间之前没有imu数据的情况
  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.
    // 在time之前没有IMU数据, 因此我们推进ImuTracker, 并使用姿势和假重力产生的角速度来帮助2D稳定
    
    // 预测当前时刻的姿态与重力方向
    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的时间比imu数据队列中第一个数据的时间早, 就先预测到imu数据队列中第一个数据的时间
  if (imu_tracker->time() < imu_data_.front().time) {
    // Advance to the beginning of 'imu_data_'.
    imu_tracker->Advance(imu_data_.front().time); //Advance就是进行预测
  }

  // c++11: std::lower_bound() 是在区间内找到第一个大于等于 value 的值的位置并返回, 如果没找到就返回 end() 位置
  // 在第四个参数位置可以自定义比较规则,在区域内查找第一个 **不符合** comp 规则的元素

  // 在imu数据队列中找到第一个时间上 大于等于 imu_tracker->time() 的数据的索引
  auto it = std::
      lower_bound(  // 遍历的imu_data_传给lambda表达式的第一个参数imu_data,
                    // 条件imu_tracker->time()传给lambda表达式的第二个参数time
          imu_data_.begin(), imu_data_.end(), imu_tracker->time(),
          [](const sensor::ImuData& imu_data, const common::Time& time) {
            return imu_data.time < time;
          });

  // 然后依次对imu数据进行预测, 以及添加观测, 直到imu_data_的时间大于等于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的状态预测到time时刻
  imu_tracker->Advance(time);
}

上面代码说明了有或者没有IMU的情况下, 怎么预测旋转. 大家可能会疑惑, 为啥没有IMU还能调用imu_tracker进行Advance? 这要看一下ImuTracker的构造函数

/**
 * @brief Construct a new Imu Tracker:: Imu Tracker object
 * 
 * @param[in] imu_gravity_time_constant 这个值在2d与3d情况下都为10
 * @param[in] time 
 */
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()),
      orientation_(Eigen::Quaterniond::Identity()), // 初始方向角
      gravity_vector_(Eigen::Vector3d::UnitZ()),    // 重力方向初始化为[0,0,1]
      imu_angular_velocity_(Eigen::Vector3d::Zero()) {}

可以看到构造函数里面都有初值, 那么初值是在哪更新的呢?
如果有IMU的话, 直接通过IMU得到的角速度更新:

imu_tracker->AddImuAngularVelocityObservation(it->angular_velocity);

如果没有IMU的话, 就只能通过其他方式获得估计的角速度, 即有odometer数据就用odom推测的角速度, 没有的话就用pose推测的角速度.

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

4. 通过Pose进行位姿预测

由于cartographer中只有激光雷达是必须的, 如果没有odom和IMU, 只用激光雷达推测出的Pose也是可以用位姿推测器的.

// 根据pose队列计算tracking frame 在 local坐标系下的线速度与角速度
void PoseExtrapolator::UpdateVelocitiesFromPoses() {
  if (timed_pose_queue_.size() < 2) {
    // We need two poses to estimate velocities.
    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);
  // 如果时间差小于pose_queue_duration_(1ms), 不进行计算
  if (queue_delta < common::ToSeconds(pose_queue_duration_)) {
    LOG(WARNING) << "Queue too short for velocity estimation. Queue duration: "
                 << queue_delta << " s";
    return;
  }
  const transform::Rigid3d& newest_pose = newest_timed_pose.pose;
  const transform::Rigid3d& oldest_pose = oldest_timed_pose.pose;
  // 平移量除以时间得到 tracking frame 在 local坐标系下的线速度
  linear_velocity_from_poses_ =
      (newest_pose.translation() - oldest_pose.translation()) / queue_delta;
  // 角度变化量除以时间得到角速度得到 tracking frame 在 local坐标系下的角速度
  angular_velocity_from_poses_ =
      transform::RotationQuaternionToAngleAxisVector(
          oldest_pose.rotation().inverse() * newest_pose.rotation()) /
      queue_delta;
}

方法和odom的预测方式大同小异, 就不介绍了.

5. 插入历史位姿AddPose

AddPose是位姿推测器中最重要的函数, 作用是通过插入上一帧前端匹配后的位姿来进行当前时间点的位姿预测. 这个函数调用了上面提到的函数, 预测出角速度与线速度.

// 将扫描匹配后的pose加入到pose队列中,计算线速度与角速度,并将imu_tracker_的状态更新到time时刻
void PoseExtrapolator::AddPose(const common::Time time,
                               const transform::Rigid3d& pose) {
  // 如果imu_tracker_没有初始化就先进行初始化
  if (imu_tracker_ == nullptr) {
    common::Time tracker_start = time;
    if (!imu_data_.empty()) {
      tracker_start = std::min(tracker_start, imu_data_.front().time);
    }
    // imu_tracker_的初始化
    imu_tracker_ =
        absl::make_unique<ImuTracker>(gravity_time_constant_, tracker_start);
  }

  // 在timed_pose_queue_中保存pose
  timed_pose_queue_.push_back(TimedPose{time, pose});

  // 保持pose队列中第二个pose的时间要大于 time - pose_queue_duration_
  while (timed_pose_queue_.size() > 2 && // timed_pose_queue_最少是2个数据
         timed_pose_queue_[1].time <= time - pose_queue_duration_) {
    timed_pose_queue_.pop_front(); //丢弃旧的
  }

  // 根据加入的pose计算线速度与角速度
  UpdateVelocitiesFromPoses();

  // 将imu_tracker_更新到time时刻
  AdvanceImuTracker(time, imu_tracker_.get());

  // pose队列更新了,之前imu及里程计数据已经过时了
  // 因为pose是匹配的结果,之前的imu及里程计数据是用于预测的,现在结果都有了,之前的用于预测的数据肯定不需要了
  TrimImuData();
  TrimOdometryData();

  // 用于根据里程计数据计算线速度时姿态的预测
  odometry_imu_tracker_ = absl::make_unique<ImuTracker>(*imu_tracker_);
  // 用于位姿预测时的姿态预测
  extrapolation_imu_tracker_ = absl::make_unique<ImuTracker>(*imu_tracker_);
}

AddPose只用了pose和imu进行预测, 为啥没见到odom呢?这需要结合local_trajectory_builder_2d.cc这个文件看, 不同传感器运用的方案有不同位姿推测器的方案.

6. 位姿推测器的总结

有3中位姿推测的方法, 但是不同情况下用哪一个呢? 当然是哪个准用哪个, 具体哪个准是工程经验得到的. 预测位姿分为4中情况, 共同点是都是用匀速模型进行预测:

  1. 使用imu, 使用里程计
    平移的预测: 通过里程计数据队列开始和末尾的2个数据计算出的线速度乘以时间
    姿态的预测: 通过imu的角速度乘以时间
  2. 使用imu, 不使用里程计
    平移的预测: 通过pose数据队列开始和末尾的2个数据计算出的线速度乘以时间
    姿态的预测: 通过imu的角速度乘以时间
  3. 不使用imu, 使用里程计
    平移的预测: 通过里程计数据队列开始和末尾的2个数据计算出的线速度乘以时间
    姿态的预测: 通过里程计数据队列开始和末尾的2个数据计算出的角速度乘以时间
  4. 不使用imu, 不用里程计
    平移的预测: 通过pose数据队列开始和末尾的2个数据计算出的线速度乘以时间
    姿态的预测: 通过pose数据队列开始和末尾的2个数据计算出的角速度乘以时间

总结:

  1. 预测平移时: 有里程计就用里程计的线速度, 没有里程计就用pose计算的线速度进行预测
  2. 预测姿态时: 有IMU就用IMU的角速度, 没有IMU时, 如果有里程计就用里程计计算出的角速度, 没有里程计就用pose计算的角速度进行预测
  3. 预测的都是相对值, 要加上最后一个pose的位姿
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

cartographer源码阅读---位姿推测器 的相关文章

随机推荐

  • 移动程序后出现Cannot resolve symbol AppCompatActivity等错误

    移动android studio 程序包到不同电脑后 xff0c 出现Cannot resolve symbol AppCompatActivity等错误 xff0c 网上有说没有包含lib包 import android support
  • Gradle sync failed: Could not GET gradle-3.0.0-beta4.pom

    在不电脑间移动android工程项目时出现Gradle sync fail Could not resolve com android tools build gradle 3 0 0 beta4 以为是android studio版本的问
  • undefined reference to `dlopen' 等dlfcn.h类问题的另一种错误解决

    linux下或者android下编程经常遇到dlopen dlclose等未定义函数 xff0c 其实 只要我们链接上dl库就可以解决这个问题 xff0c 在makefile 加上 ldl选项 xff0c 便可以解决这个问题 很多文章都讲
  • openwrt交叉编译libffi zlib libiconv gettext glib gstreamer

    libffi 如果你是直接从git托管源构建libffi xff0c 那么configure还不存在 首先运行 autogen sh 这将要求您安装autoconf xff0c automake和libtool 您可能想要告诉configu
  • Python中链表、栈、队列、二叉树的封装

    Python中链表 栈 队列 二叉树的封装 1 链表的封装2 栈的封装3 队列4 二叉树的封装 1 链表的封装 链表是一种物理存储单元上非连续 非顺序的存储结构 xff0c 数据元素的逻辑顺序是通过链表中的指针链接次序实现的 链表由一系列结
  • 进程中产生僵尸和孤儿

    孤儿进程 父进程先于子进程结束 xff0c 则子进程成为孤儿进程 xff0c 子进程的父进程成为init进程 xff0c 称为init进程领养孤儿进程 孤儿进程其实对于操作系统来说是没有太大的危害 xff0c 在孤儿进程中父进程结束了之后
  • C++Primer Plus第6版&C Primer Plus第6版 中文版免费分享啦

    最近在学习C 43 43 xff0c 用的资料是师兄分享的经典书籍 C 43 43 Primer Plus第6版中文版 的PDF xff0c 自带书签 xff0c 使用很方便 但师兄说这个是他在网上花了积分才下载下来的 xff0c 这让我很
  • www服务器是什么

    WWW是一种服务 我们在浏览器的地址栏里输入的网站地址叫做URL Uniform Resource Locator xff0c 统一资源定位符 就像每家每户都有一个门牌地址一样 xff0c 每个网页也都有一个Internet地址 当你在浏览
  • 快速配置VSCode(Visual Studio Code) C/C++开发环境

    简介 VSCode只是一个纯文本编辑器 editor xff0c 不是IDE 集成开发环境 xff0c 不含编译器 compiler 和许多其他功能 xff0c 所以需要自己先搭建环境 但是对小白朋友来说 xff0c 面对一堆的环境变量操作
  • c++:如何定义一个头文件来使用类

    定义自己的头文件 定义类为了方便起见 xff0c 一般不在某个函数内部去定义 要在不同文件中使用同一个类 xff0c 类的定义就必须保持一致 因此考虑拆分一个类 xff0c 在头文件内声明类和其中的成员函数 xff0c 在源文件中写该类的实
  • Ubuntu can口状态检查

    1 配置can0 设置比特率 sudo ip link set can0 type can bitrate 250000 2 打开can0 sudo ip link set can0 up 3 查看 信息 ip details link s
  • Arduino读取DHT11的温湿度显示在基于I2C的1602上

    背景摘要 又到了周末 xff0c B站的番还未更新 闲来无事 xff0c 搞搞吃灰已久的Arduino 南方的冬天真冷啊 xff0c 测测屋子里的温湿度怎么样 xff01 为了测量屋子里的温湿度 xff0c 就用简单大方的Arduino x
  • 终于解决:CUDA: OpenCV requires enabled ‘cudev‘ module from ‘opencv_contrib‘

    把opencv contrib放到opencv文件夾下就行了 編譯到99 時會卡好久 大概半個小時 16線程的CPU 耐心等待即可 編譯的選項 cmake D CMAKE INSTALL PREFIX 61 usr local D CMAK
  • rviz出现Transform [sender=unknown_publisher]For frame [hokuyo_link]: Fixed Frame [map] does not exist

    这是因为本身建模的时候没有用tf包工具发布global fixed frame到topic所在坐标系的tf关系 xff0c 通俗来讲就是模型fixed frame与rviz的不匹配 解决办法 xff1a 修改Fixed frame xff0
  • 镜像后的图片的ORB特征提取

    原图1 xff0c 原图2 xff0c 水平镜像后的原图2 xff1a 用opencv4自带的ORB提取 xff1a 所有的配对如下 筛选后的配对如下 xff1a 可见 xff0c 过滤了一些匹配错误特征点 xff0c 但是还是有很多错误的
  • 构造函数与重载构造函数

    发现slam程序里有个这种写法 在mappoint h中写的如下 MapPoint XXX 重载构造函数 一个默认 new的时候用 一个含参 MapPoint long id Vec3 position 在mappoint cpp中有含参构
  • 内存指针mm_struct

    我们在使用fork 产生了一个子进程 xff0c 子进程和父进程都是执行的相同的代码 xff0c 此时我们又是如何进程执行的呢 虚拟地址空间和物理内存 这就是我们的虚拟地址空间 xff0c 他实际上是使用一个结构体mm struct结构体去
  • Cartographer源码阅读---开始轨迹的相关处理

    上一节我们看了一下node main cc的内容 发现其中最重要的部分就是Node类 这个类吃下了map builder类 且里面实现了传感器数据的处理与分发 sensor bridge 还有整个Cartographer算法的调用 map
  • C++程序, 执行程序加上-v显示版本号的方法

    先宏定义 span class token macro property span class token directive hash span span class token directive keyword define span
  • cartographer源码阅读---位姿推测器

    工程化的cartographer框架为了实现实时可靠的位姿推断 在算法中实现了位姿推测器 pose extrapolator 其作用是通过现有的有关机器人位姿的数据 给前端匹配一个可靠的目标值进行ceres优化 也就是以位姿推测器推出来的位