机器人自主探索系列解析(一)——仿真环境解析

机器人自主探索系列解析(一)——仿真环境解析

最近实验室又开始了新一轮的无人小车自主探索研究,之前已经在仿真环境中测试了自主导航,效果挺不错的。但是由于一直没来得及详细阅读代码。趁这段时间有空,记录以下对代码的阅读情况。

CMU开源了两个自主探索的代码 TASE和FAR_planner。 同时也提供了全套的导航框架,包括状态估计、地形可通行性分析、避障等算法模块,本文主要对仿真环境部分的代码进行分析。

仿真环境代码链接:
https://github.com/HongbiaoZ/autonomous_exploration_development_environment

1. system_real_robot.launch

因为更多的是想适配实验室自己的设备,所以重点关注了代码中关于实车适配,对应的是system_real_robot.launch文件

system_real_robot.launch代码文件:

<launch>

  <arg name="cameraOffsetZ" default="0"/>
  <arg name="vehicleX" default="0"/>
  <arg name="vehicleY" default="0"/>
  <arg name="checkTerrainConn" default="false"/>

  <!-- <include file="$(find lio_sam)/launch/run.launch" /> -->
  <include file="$(find fast_lio)/launch/mapping_velodyne.launch" />

  <include file="$(find ps3joy)/launch/ps3.launch" />

  <include file="$(find local_planner)/launch/local_planner.launch" >
    <arg name="cameraOffsetZ" value="$(arg cameraOffsetZ)"/>
    <arg name="goalX" value="$(arg vehicleX)"/>
    <arg name="goalY" value="$(arg vehicleY)"/>
  </include>

  <include file="$(find terrain_analysis)/launch/terrain_analysis.launch" />

  <include file="$(find terrain_analysis_ext)/launch/terrain_analysis_ext.launch" >
    <arg name="checkTerrainConn" value="$(arg checkTerrainConn)"/>
  </include>

  <include file="$(find sensor_scan_generation)/launch/sensor_scan_generation.launch" />

  <include file="$(find loam_interface)/launch/loam_interface.launch" />

  <node launch-prefix="nice" pkg="rviz" type="rviz" name="rvizGA" args="-d $(find vehicle_simulator)/rviz/vehicle_simulator.rviz" respawn="true"/>

</launch>

实车配置和仿真环境不同在于,仿真中里程计使用的是gazebo得到的真实位姿,且点云是直接发布在map全局坐标系下的。在适配到自己设备时,需要外部的SLAM算法提供对应的里程计 /topic和点云地图 /topic

2.loam_interface

launch文件

loam_interface提供了一个和slam状态估计节点通讯的接口,可以很方便的在launch文件中通过修改slam算法的topic,适配不同类型的slam算法 实例

对应的launch文件如下:

<launch>
  <node pkg="loam_interface" type="loamInterface" name="loamInterface" output="screen" required="true">
    <!-- <param name="stateEstimationTopic" type="string" value="/lio_sam/mapping/odometry" />
    <param name="registeredScanTopic" type="string" value="/lio_sam/mapping/cloud_registered" /> -->
    <param name="stateEstimationTopic" type="string" value="/Odometry" />
    <param name="registeredScanTopic" type="string" value="/cloud_registered" />
    <param name="flipStateEstimation" type="bool" value="false" />
    <param name="flipRegisteredScan" type="bool" value="false" />
    <param name="sendTF" type="bool" value="true" />
    <param name="reverseTF" type="bool" value="false" />
  </node>

</launch>

其中stateEstimationTopicstateEstimationTopic需要替换为实际SLAM发布的话题。后续一些参数是tf变换的一些操作,例如翻转点云、翻转位姿等。

本文以lio-sam为状态估计节点

ros::Subscriber subOdometry = nh.subscribe<nav_msgs::Odometry> (stateEstimationTopic, 5, odometryHandler);
 
ros::Subscriber subLaserCloud = nh.subscribe<sensor_msgs::PointCloud2> (registeredScanTopic, 5, laserCloudHandler);

odometryHandler

里程计回调函数。订阅里程计信息,liosam中是/lio_sam/mapping/odometry(机器人在世界坐标系下的位姿),执行回调函数odometryHandler(将机器人在世界坐标系下的位姿换个话题名/state_estimation发布,同时发布TF变换)

void odometryHandler(const nav_msgs::Odometry::ConstPtr& odom)
{
  double roll, pitch, yaw;
  geometry_msgs::Quaternion geoQuat = odom->pose.pose.orientation;
  odomData = *odom;

  if (flipStateEstimation) { // flip the odometry data 将原始ROS下的ENU坐标系转换为LOAM下的ENU坐标系  X’ = Z, Y’ = X, Z’ = Y
    tf::Matrix3x3(tf::Quaternion(geoQuat.z, -geoQuat.x, -geoQuat.y, geoQuat.w)).getRPY(roll, pitch, yaw);

    pitch = -pitch;
    yaw = -yaw;

    geoQuat = tf::createQuaternionMsgFromRollPitchYaw(roll, pitch, yaw);

    odomData.pose.pose.orientation = geoQuat;
    odomData.pose.pose.position.x = odom->pose.pose.position.z;
    odomData.pose.pose.position.y = odom->pose.pose.position.x;
    odomData.pose.pose.position.z = odom->pose.pose.position.y;
  }

  // publish odometry messages
  odomData.header.frame_id = "map";
  odomData.child_frame_id = "sensor";
  pubOdometryPointer->publish(odomData);

  // publish tf messages
  odomTrans.stamp_ = odom->header.stamp;
  odomTrans.frame_id_ = "map";
  odomTrans.child_frame_id_ = "sensor";
  odomTrans.setRotation(tf::Quaternion(geoQuat.x, geoQuat.y, geoQuat.z, geoQuat.w));
  odomTrans.setOrigin(tf::Vector3(odomData.pose.pose.position.x, odomData.pose.pose.position.y, odomData.pose.pose.position.z));

  if (sendTF) {
    if (!reverseTF) {
      tfBroadcasterPointer->sendTransform(odomTrans);
    } else {
      tfBroadcasterPointer->sendTransform(tf::StampedTransform(odomTrans.inverse(), odom->header.stamp, "sensor", "map"));
    }
  }
}

laserCloudHandler

点云回调函数。订阅点云信息,liosam中是/lio_sam/mapping/cloud_registered(世界坐标系下的关键帧),执行回调函数laserCloudHandler,将世界坐标系下的关键帧点云换个话题名/registered_scan重新发布出去

void laserCloudHandler(const sensor_msgs::PointCloud2ConstPtr& laserCloudIn)
{
  laserCloud->clear();
  pcl::fromROSMsg(*laserCloudIn, *laserCloud);

  if (flipRegisteredScan) { // flip the point cloud data 将原始ROS下的ENU坐标系转换为LOAM下的ENU坐标系  X’ = Z, Y’ = X, Z’ = Y
    int laserCloudSize = laserCloud->points.size();
    for (int i = 0; i < laserCloudSize; i++) {
      float temp = laserCloud->points[i].x;
      laserCloud->points[i].x = laserCloud->points[i].z;
      laserCloud->points[i].z = laserCloud->points[i].y;
      laserCloud->points[i].y = temp;
    }
  }

  // publish registered scan messages
  sensor_msgs::PointCloud2 laserCloud2;
  pcl::toROSMsg(*laserCloud, laserCloud2);
  laserCloud2.header.stamp = laserCloudIn->header.stamp;
  laserCloud2.header.frame_id = "map";
  pubLaserCloudPointer->publish(laserCloud2);
}

总的来说 loam_interface提供了一个slam接口,不断接收slam部分提供的世界坐标系下的当前机器人的位姿/Odometry和关键帧点云/cloud。并以新的话题/state_estimation和/registered_scan话题名发布出去。

3.sensor_scan_generation

这部分的内容主要是提供了一个坐标转换的功能:将世界坐标系下的点云转换到传感器坐标下
即订阅/state_estimation和/registered_scan 话题,发布/state_estimation_at_scan和/sensor_scan话题。

主要代码如下:

void laserCloudAndOdometryHandler(const nav_msgs::Odometry::ConstPtr& odometry,
                                  const sensor_msgs::PointCloud2ConstPtr& laserCloud2)
{
  laserCloudIn->clear();
  laserCLoudInSensorFrame->clear();

  pcl::fromROSMsg(*laserCloud2, *laserCloudIn);//将ROS消息转换为PCL点云

  odometryIn = *odometry;//接收里程计数据  
  // 创建一个transformToMap变换, 表示传感器坐标系在地图坐标系下的位置和方向  
  transformToMap.setOrigin(
      tf::Vector3(odometryIn.pose.pose.position.x, odometryIn.pose.pose.position.y, odometryIn.pose.pose.position.z));
  transformToMap.setRotation(tf::Quaternion(odometryIn.pose.pose.orientation.x, odometryIn.pose.pose.orientation.y,odometryIn.pose.pose.orientation.z, odometryIn.pose.pose.orientation.w));

  int laserCloudInNum = laserCloudIn->points.size();

  pcl::PointXYZ p1;
  tf::Vector3 vec;

  /*将点云数据从地图坐标系转换到传感器坐标系。
   *对于每个点,使用transformToMap变换将其
   *从地图坐标系变换到传感器坐标系,然后将变换
   *后的点云数据存储在 laserCloudInSensorFrame中。
   */ 

  for (int i = 0; i < laserCloudInNum; i++)
  {
    p1 = laserCloudIn->points[i];
    vec.setX(p1.x);
    vec.setY(p1.y);
    vec.setZ(p1.z);

    vec = transformToMap.inverse() * vec;// 将点从地图坐标系转换到传感器坐标系 
    //p_a=T_ab*p_b --> p_b=(T_ab)-1 *p_a

    p1.x = vec.x();
    p1.y = vec.y();
    p1.z = vec.z();

    laserCLoudInSensorFrame->points.push_back(p1);
  }
  //发布里程计信息 map到sensor_at_scan的odom
  odometryIn.header.stamp = laserCloud2->header.stamp;
  odometryIn.header.frame_id = "map";
  odometryIn.child_frame_id = "sensor_at_scan";
  pubOdometryPointer->publish(odometryIn);
  
  //发布tf变换
  transformToMap.stamp_ = laserCloud2->header.stamp;
  transformToMap.frame_id_ = "map";
  transformToMap.child_frame_id_ = "sensor_at_scan";
  tfBroadcasterPointer->sendTransform(transformToMap);

  //发布转换后的在传感器坐标下的点云
  sensor_msgs::PointCloud2 scan_data;
  pcl::toROSMsg(*laserCLoudInSensorFrame, scan_data);
  scan_data.header.stamp = laserCloud2->header.stamp;
  scan_data.header.frame_id = "sensor_at_scan";
  pubLaserCloud.publish(scan_data);
}

总的来说 sensor_scan_generation 提供了一个坐标转换功能,作用如下:

  • 订阅/state_estimation和/registered_scan
  • 发布/state_estimation_at_scan(等同于/state_estimation)
  • 发布/sensor_scan (将世界坐标系下的点云转换到传感器坐标系下)

之所以要进行坐标变换,是因为我们在导航、规划过程中,往往希望点云是在传感器坐标系,而不是世界坐标系下。这部分在后续的pathFollwer中介绍。

4.总结

在这篇文章中,我们主要介绍了机器人自主探索中的仿真环境部分,重点介绍了几个接口内容。例如loam_interface和sensor_scan_generation。这些接口很好的适配了不同的SLAM接口和导航接口。 接口的作用非常重要,直接会影响到仿真以及实车的适配性。

仿真环境和实车调试的区别主要在:

  1. 机器人在全局坐标系的位姿获取不同,仿真来源于gazebo提供的真实位姿,实车则需要SLAM提供对应位姿
  2. 仿真中,所有的点云都是在/map坐标系下,而实车中则希望是在车辆坐标系base_link或者传感器坐标系上。因此代码提供了很多从全局坐标转换到车辆坐标系的操作。例如点云裁剪、距离判断等。
  3. loam_interface提供了slam的接口,sensor_scan_generation将世界坐标系下的点云转换到传感器坐标上

参考链接

posted @ 2025-06-12 17:46  遥感摆烂人  阅读(202)  评论(0)    收藏  举报