hdl-graph-slam思想解读(2)-后端处理


任何形式的里程计包括imu、编码器、激光、视觉等均存在累计误差致命缺点,会导致随着SLAM距离的增加,其与真实位置偏差逐渐变大。因此具有闭环能力的后端处理十分必要;HDL后端采用g2o中图优化思想进行后端处理,关键帧作为顶点,而平面,gps,imu,闭环等信息作为边进行约束。
整个后端主要包括3个进程进行处理:顶点和边添加、定时优化、定时发布地图;

顶点

将帧间匹配中激光雷达的pose作为顶点,在hdl中引入了关键帧概念,实际上在整个SLAM过程中,选取一定距离或角度间隔的点云数据作为关键帧,其中将关键帧中的pose作为顶点。

其中优化前的顶点由关键帧(激光里程计)提供的pose作为顶点初始值;每次添加顶点时,同时记录整个slam轨迹的累加值,用于后续闭环条件判断。

约束边

激光里程计约束

关键帧的pose,作为顶点,相邻顶点的里程计位置变换矩阵作为边。

      // 获取相对位置
      Eigen::Isometry3d relative_pose = keyframe->odom.inverse() * prev_keyframe->odom;
      // 信息矩阵
      Eigen::MatrixXd information = inf_calclator->calc_information_matrix(keyframe->cloud, prev_keyframe->cloud, relative_pose);
      // 添加边
      auto edge = graph_slam->add_se3_edge(keyframe->node, prev_keyframe->node, relative_pose, information);
      // 增加鲁棒性
      graph_slam->add_robust_kernel(edge, private_nh.param<std::string>("odometry_edge_robust_kernel", "NONE"), private_nh.param<double>("odometry_edge_robust_kernel_size", 1.0));

闭环约束

  1. 闭环检测

闭环约束边前提需要检测闭环可能。即robot在slam中到了曾经经历过环境,并检测出来,称为闭环检测。由于所有顶点存储均为关键帧,因此闭环也就是新的关键帧是否出现在历史关键帧附近。HDL采用了较为暴力的方法,即关键帧与符合初步条件的关键帧逐一进行匹配。其中闭环检测初步条件包括:

  • 两次闭环需要有一定距离间隔,因为短距离内闭环无意义(即防止同一位置附近自己闭环)。
  • 闭环检测的两个keyframe 需要经历一定轨迹累计间隔,防止假闭环出现(即相当于里程计功能);
  • 两个keyframe 在优化前距离一定范围内;
  std::vector<KeyFrame::Ptr> find_candidates(const std::vector<KeyFrame::Ptr>& keyframes, const KeyFrame::Ptr& new_keyframe) const {
   // too close to the last registered loop edge
   // 累计距离过近,则无需进行闭环检测,无意义
   // 即闭环需要经过一定距离间隔
   if(new_keyframe->accum_distance - last_edge_accum_distance < distance_from_last_edge_thresh) {
     return std::vector<KeyFrame::Ptr>();
   }

   std::vector<KeyFrame::Ptr> candidates;
   candidates.reserve(32);

   for(const auto& k : keyframes) {
     // traveled distance between keyframes is too small
     // 闭环匹配的两帧累计距离间隔须有一定范围
     if(new_keyframe->accum_distance - k->accum_distance < accum_distance_thresh) {
       continue;
     }

     // 两帧关键帧的坐标间隔小于一定范围则认为是匹配对
     const auto& pos1 = k->node->estimate().translation();
     const auto& pos2 = new_keyframe->node->estimate().translation();

     // estimated distance between keyframes is too small
     double dist = (pos1.head<2>() - pos2.head<2>()).norm();
     if(dist > distance_thresh) {
       continue;
     }
     // 
     candidates.push_back(k);
   }

   return candidates;
 }

如果满足以上条件的,则可作为闭环可能匹配的候选。将所有候选keyFrame对进行一一点云匹配;如果匹配相关度高于一定值时,则检测到闭环。并获取新的关键帧与变换历史帧的相对位姿;

  1. 添加闭环边
    闭环边约束与激光里程计边约束完全一样;
      Eigen::Isometry3d relpose(loop->relative_pose.cast<double>());
      // 计算信息矩阵
      Eigen::MatrixXd information_matrix = inf_calclator->calc_information_matrix(loop->key1->cloud, loop->key2->cloud, relpose);
      // 添加闭环的约束边
      auto edge = graph_slam->add_se3_edge(loop->key1->node, loop->key2->node, relpose, information_matrix);
      graph_slam->add_robust_kernel(edge, private_nh.param<std::string>("loop_closure_edge_robust_kernel", "NONE"), private_nh.param<double>("loop_closure_edge_robust_kernel_size", 1.0));
  1. 关键帧说明
    在做闭环时,包含两类keyframe,已经检测过的keyframe和未检测过闭环的新添加的new_keyframe。满足条件的两个关键帧,则是将new_keyframe中的每一个和keyframe中每一个进行判断和点云匹配,较为暴力;

平面约束

  1. 说明
    从功能上显然能看出,主要目的是约束SLAM过程中高度上产生的误差。由于实际环境中,无论室内还是室外,采用的lidar传感器主要为水平安装,因此在高度上约束较少,在室外可采用gps提供的高度信息进行约束,如果没有gps信号,则高度误差较大。
    如果提前已知robot运行的为平面环境,则可根据平面信息进行约束,即锁定高度信息,防止地图倾斜或闭环上上下重影;(注:非平面环境不可使用,否则会加大误差,因为平面约束就是认为slam所行走的轨迹均在一个平面上

在hdl整体介绍中已经说明采用RANSAC拟合出平面参数。通过以下收集函数进行收集对应关键帧的平面信息。

void floor_coeffs_callback(const hdl_graph_slam::FloorCoeffsConstPtr& floor_coeffs_msg) {
    if(floor_coeffs_msg->coeffs.empty()) {
      return;
    }

    std::lock_guard<std::mutex> lock(floor_coeffs_queue_mutex);
    floor_coeffs_queue.push_back(floor_coeffs_msg);
  }

将每帧检测的平面参数信息放入队列缓存中,待处理;
2. 添加约束边

    bool updated = false;
    for(const auto& floor_coeffs : floor_coeffs_queue) {
      if(floor_coeffs->header.stamp > latest_keyframe_stamp) {
        break;
      }

      // 找到平面时间戳对应的关键帧
      auto found = keyframe_hash.find(floor_coeffs->header.stamp);
      if(found == keyframe_hash.end()) {
        continue;
      }

遍历每一个平面参数,并根据时间戳找到对应的keyframe,其目的即约束该关键帧pose。

      // 初始化平面节点
      if(!floor_plane_node) {
        floor_plane_node = graph_slam->add_plane_node(Eigen::Vector4d(0.0, 0.0, 1.0, 0.0));
        floor_plane_node->setFixed(true);
      }

约束的第一个平面作为初始值,即固定平面。后续所有平面都以第一帧平面作为地平面信息参考。

      const auto& keyframe = found->second;
      // 添加平面约束
      Eigen::Vector4d coeffs(floor_coeffs->coeffs[0], floor_coeffs->coeffs[1], floor_coeffs->coeffs[2], floor_coeffs->coeffs[3]);
      Eigen::Matrix3d information = Eigen::Matrix3d::Identity() * (1.0 / floor_edge_stddev);
      auto edge = graph_slam->add_se3_plane_edge(keyframe->node, floor_plane_node, coeffs, information);
      graph_slam->add_robust_kernel(edge, private_nh.param<std::string>("floor_edge_robust_kernel", "NONE"), private_nh.param<double>("floor_edge_robust_kernel_size", 1.0));

      // 记录
      keyframe->floor_coeffs = coeffs;

      updated = true;
    }

其中平面约束边在g2o中有现成定义,不需要创建。构建的约束边为平面与定顶点之间的边。

imu约束

  void imu_callback(const sensor_msgs::ImuPtr& imu_msg) {
    //都不需要
    if(!enable_imu_orientation && !enable_imu_acceleration) {
      return;
    }

    std::lock_guard<std::mutex> lock(imu_queue_mutex);
    imu_msg->header.stamp += ros::Duration(imu_time_offset);
    imu_queue.push_back(imu_msg);
  }

hdl采用imu信息主要采用姿态角和重力加速度方向进行约束。实时收集imu数据并放入缓存队列中。

      auto closest_imu = imu_cursor;
      for(auto imu = imu_cursor; imu != imu_queue.end(); imu++) {
        auto dt = ((*closest_imu)->header.stamp - keyframe->stamp).toSec();
        auto dt2 = ((*imu)->header.stamp - keyframe->stamp).toSec();
        if(std::abs(dt) < std::abs(dt2)) {
          break;
        }

        closest_imu = imu;
      }

从imu队列中遍历找到对应的时间戳最近的keyframe。

      // 获取姿态角和线加速度
      const auto& imu_ori = (*closest_imu)->orientation;
      const auto& imu_acc = (*closest_imu)->linear_acceleration;

      geometry_msgs::Vector3Stamped acc_imu;
      geometry_msgs::Vector3Stamped acc_base;
      geometry_msgs::QuaternionStamped quat_imu;
      geometry_msgs::QuaternionStamped quat_base;

      quat_imu.header.frame_id = acc_imu.header.frame_id = (*closest_imu)->header.frame_id;
      quat_imu.header.stamp = acc_imu.header.stamp = ros::Time(0);
      acc_imu.vector = (*closest_imu)->linear_acceleration;
      quat_imu.quaternion = (*closest_imu)->orientation;

      // 将imu位置转换到base坐标系下
      try {
        tf_listener.transformVector(base_frame_id, acc_imu, acc_base);
        tf_listener.transformQuaternion(base_frame_id, quat_imu, quat_base);
      } catch(std::exception& e) {
        std::cerr << "failed to find transform!!" << std::endl;
        return false;
      }

      keyframe->acceleration = Eigen::Vector3d(acc_base.vector.x, acc_base.vector.y, acc_base.vector.z);
      keyframe->orientation = Eigen::Quaterniond(quat_base.quaternion.w, quat_base.quaternion.x, quat_base.quaternion.y, quat_base.quaternion.z);
      keyframe->orientation = keyframe->orientation;

获取imu中对应历史关键帧最新匹配的一帧数据,并根据静态tf变换,转换到baseframe坐标系下的姿态和加速度信息。

  1. 添加姿态约束
 		Eigen::MatrixXd info = Eigen::MatrixXd::Identity(3, 3) / imu_orientation_edge_stddev;
        auto edge = graph_slam->add_se3_prior_quat_edge(keyframe->node, *keyframe->orientation, info);
        graph_slam->add_robust_kernel(edge, private_nh.param<std::string>("imu_orientation_edge_robust_kernel", "NONE"), private_nh.param<double>("imu_orientation_edge_robust_kernel_size", 1.0));
  1. 添加重力加速度约束
        Eigen::MatrixXd info = Eigen::MatrixXd::Identity(3, 3) / imu_acceleration_edge_stddev;
        g2o::OptimizableGraph::Edge* edge = graph_slam->add_se3_prior_vec_edge(keyframe->node, -Eigen::Vector3d::UnitZ(), *keyframe->acceleration, info);
        graph_slam->add_robust_kernel(edge, private_nh.param<std::string>("imu_acceleration_edge_robust_kernel", "NONE"), private_nh.param<double>("imu_acceleration_edge_robust_kernel_size", 1.0));

疑问:姿态约束是否准确,因为时间戳并没有严格对齐,即没有进行根据时间进行插值

gps约束

GPS较为简单,因为直接提供的是全局绝对位置,仅需将gps经纬度转换为大地坐标系,并且将初始位置进行坐标初始处理即可。
方法同样将每帧gps信息放入缓存队列中,然后根据时间戳找到对应的keyframe即顶点进行约束。

      // 减去初始偏移量(即初始gps位置)
      xyz -= (*zero_utm);

      // 关键帧添加gps位置
      keyframe->utm_coord = xyz;

      // 根据gps是否存在高度值,添加概率图边
      g2o::OptimizableGraph::Edge* edge;
      if(std::isnan(xyz.z())) {
        Eigen::Matrix2d information_matrix = Eigen::Matrix2d::Identity() / gps_edge_stddev_xy;
        edge = graph_slam->add_se3_prior_xy_edge(keyframe->node, xyz.head<2>(), information_matrix);
      } else {
        Eigen::Matrix3d information_matrix = Eigen::Matrix3d::Identity();
        information_matrix.block<2, 2>(0, 0) /= gps_edge_stddev_xy;
        information_matrix(2, 2) /= gps_edge_stddev_z;
        edge = graph_slam->add_se3_prior_xyz_edge(keyframe->node, xyz, information_matrix);
      }
      // 增加鲁棒性
      graph_slam->add_robust_kernel(edge, private_nh.param<std::string>("gps_edge_robust_kernel", "NONE"), private_nh.param<double>("gps_edge_robust_kernel_size", 1.0));

定时优化

之所以前面所经所有传感器信息在优化前均需要放入buffer队列中,是因为hdl采用后端优化机制是定时进行调用优化。因此在每次优化间隔间,不同传感器可独立进行收集数据缓存,在优化时根据最新的keyframe的时间戳去匹配所有其他传感器数据信息,添加对应边约束,并剔除过时的传感器数据;

    // 处理关键帧队列
    bool keyframe_updated = flush_keyframe_queue();

    if(!keyframe_updated) {
      std_msgs::Header read_until;
      read_until.stamp = ros::Time::now() + ros::Duration(30, 0);
      read_until.frame_id = points_topic;
      read_until_pub.publish(read_until);
      read_until.frame_id = "/filtered_points";
      read_until_pub.publish(read_until);
    }

    // 如果4类传感器数据均无处理结果,则无需进行优化
    if(!keyframe_updated & !flush_floor_queue() & !flush_gps_queue() & !flush_imu_queue()) {
      return;
    }

其中keyframe中pose作为优化图中的顶点,为核心数据。如果所有传感器约束均没有添加,则无需进行优化;

    // loop detection
    std::vector<Loop::Ptr> loops = loop_detector->detect(keyframes, new_keyframes, *graph_slam);
    // 存在闭环,则将闭环约束加入概率图中
    for(const auto& loop : loops) {
      Eigen::Isometry3d relpose(loop->relative_pose.cast<double>());
      // 计算信息矩阵
      Eigen::MatrixXd information_matrix = inf_calclator->calc_information_matrix(loop->key1->cloud, loop->key2->cloud, relpose);
      // 添加闭环的约束边
      auto edge = graph_slam->add_se3_edge(loop->key1->node, loop->key2->node, relpose, information_matrix);
      graph_slam->add_robust_kernel(edge, private_nh.param<std::string>("loop_closure_edge_robust_kernel", "NONE"), private_nh.param<double>("loop_closure_edge_robust_kernel_size", 1.0));
    }

    // 将new_keyframes复制到keyframes,用于历史记录, 而new_keyframes 用于与历史keyframes闭环检测
    std::copy(new_keyframes.begin(), new_keyframes.end(), std::back_inserter(keyframes));
    // 清除所有新添加的关键帧
    new_keyframes.clear();

其中闭环检测在每次优化过程中进行,找到所有可能闭环条件,并增加闭环约束。当处理闭环后,则新添加的关键帧作为历史关键帧进行保存,并清除新关键帧队列,从而保证新添加的keyframe仅有一次机会与历史keyframe进行闭环约束。

    // 优化
    int num_iterations = private_nh.param<int>("g2o_solver_num_iterations", 1024);
    graph_slam->optimize(num_iterations);

调用g2o进行图优化。

    const auto& keyframe = keyframes.back();
    Eigen::Isometry3d trans = keyframe->node->estimate() * keyframe->odom.inverse();
    trans_odom2map_mutex.lock();
    trans_odom2map = trans.matrix().cast<float>();
    trans_odom2map_mutex.unlock();

将最新帧的keyframe的优化后的pose作为 里程计进行发布;

    std::vector<KeyFrameSnapshot::Ptr> snapshot(keyframes.size());
    std::transform(keyframes.begin(), keyframes.end(), snapshot.begin(), [=](const KeyFrame::Ptr& k) { return std::make_shared<KeyFrameSnapshot>(k); });

    // 每优化一次将优化的结果复制到keyframes_snapshot中,用于发布地图
    // 采用交换的方法可以让keyframes_snapshot之前的数据内存进行回收
    keyframes_snapshot_mutex.lock();
    keyframes_snapshot.swap(snapshot);
    keyframes_snapshot_mutex.unlock();

复制一份优化后的历史关键帧,用于地图发布进程进行地图发布;

定时发布地图

    // 将复制的关键帧队列,拼接成map
    auto cloud = map_cloud_generator->generate(snapshot, map_cloud_resolution);
    if(!cloud) {
      return;
    }

    cloud->header.frame_id = map_frame_id;
    cloud->header.stamp = snapshot.back()->cloud->header.stamp;

    sensor_msgs::PointCloud2Ptr cloud_msg(new sensor_msgs::PointCloud2());
    pcl::toROSMsg(*cloud, *cloud_msg);
    // 发布map
    map_points_pub.publish(cloud_msg);

根据优化后提供的keyframe队列,进行点云拼接形成slam点云地图,并进行发布。

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值