互动
最近评论
标签
寻找感兴趣的领域

ROS1入门

视频链接机器人操作系统 ROS 快速入门教程

github6-robot/wpr_simulation

硬件配置:本人买了一台N5095的小主机,安装ubunut20.04系统,你也可以安装双系统或者使用虚拟机,我已经安装了双系统,autoware会在笔记本的ubuntu环境下进行学习,使用wsl安装ubuntu你不仅得解决图形化界面的问题,还有xrdp默认参数导致界面卡顿的问题。

本文档依据上述教程编写

更多推荐:ROS机器人入门(赵虚左) 配套的官方文档:Introduction · Autolabor-ROS机器人入门课程《ROS理论与实践》零基础教程


环境搭建

每个人遇到的问题都不一样,自行解决,给个参考文章:

ROS环境搭建参考(你实在不会安装就去看鱼香ros,一键安装)


运行小海龟例程

自己琢磨


获取源码

请进入github仓库查看与获取


ROS中的Node与Package

在机器人操作系统(Robot Operating System,简称ROS)中,Node和Package是两个非常重要的概念,它们是构建ROS程序的基本单位。下面我将分别解释这两个概念:

Package(包)

  • 定义:Package是ROS中用于组织代码的单元,它包含了节点(nodes)、库(libraries)、配置文件、依赖关系等。一个package可以包含多个节点,但一个节点只能属于一个package。

  • 作用:Packages使得代码模块化,便于管理和复用。它们定义了节点之间的依赖关系,使得构建和编译过程更加清晰。

  • 组成:通常包括以下文件和目录:

    • CMakeLists.txt:用于指定如何构建package中的代码。

    • package.xml:定义package的元数据,如名称、版本、依赖关系等。

    • src/:存放源代码的目录。

    • include/:存放头文件的目录。

    • msg/:存放自定义消息类型的目录。

    • srv/:存放自定义服务类型的目录。

    • action/:存放自定义动作类型的目录。

    • launch/:存放启动文件的目录。

Node(节点)

  • 定义:Node是ROS中的一个进程,它可以发布消息、订阅话题、提供服务、调用服务或处理传入的数据。节点是ROS通信的基本单元。

  • 作用:节点允许不同的程序或程序片段在ROS网络中进行通信和数据交换。

  • 类型

    • Publisher(发布者):发布消息到话题。

    • Subscriber(订阅者):订阅话题并接收消息。

    • Service Server(服务提供者):提供服务。

    • Service Client(服务请求者):请求服务。

    • Parameter Server(参数服务器):存储配置参数。

  • 通信方式

    • 话题(Topics):用于节点之间的异步消息传递。

    • 服务(Services):用于节点之间的同步消息传递,通常用于请求/响应模式。

    • 动作(Actions):用于处理可能需要较长时间完成的目标,允许请求者监控进度并支持取消操作。

Package(包)与 Node(节点)的 创建

在/catkin_ws/src目录下执行:catkin_create_pkg hello_world_node rospy roscpp std_msgs

这个命令做了以下几件事情:

  1. 创建一个新的ROS包:名为hello_world_node的新包将被创建。这个包名是你自定义的,可以根据你的项目需求来命名。

  2. 添加依赖rospyroscppstd_msgs是ROS中的一些常用包和库。rospy是ROS的Python客户端库,roscpp是ROS的C++客户端库,而std_msgs是ROS中的标准消息类型的包。通过在命令中指定这些依赖,catkin_create_pkg会在新包的package.xml文件中添加对应的依赖声明,同时在CMakeLists.txt文件中添加相应的find_package调用,以确保在构建过程中这些依赖能够被正确地找到和链接。

  3. 生成包结构:这个命令会在当前目录下创建一个名为hello_world_node的新目录,并在其中生成标准的ROS包结构,包括CMakeLists.txtpackage.xmlsrc/等文件和目录。

  4. 配置构建系统CMakeLists.txtpackage.xml文件会被配置好,以便使用catkin构建系统进行构建。

C++版本的ROS节点

在src目录下创建hello_world.cpp并编写如下代码:

#include <ros/ros.h> // 包含ROS的头文件,这是编写ROS节点所必需的。

// 程序的入口点,main函数。argc和argv是标准的C++程序参数,分别代表命令行参数的数量和参数值。
int main(int argc, char *argv[]){

    // 初始化ROS节点。这里需要传递argc和argv参数,以及节点的名称。
    ros::init(argc,argv,"hello_world_node"); 

    // 打印信息到控制台,告知用户节点正在运行。
    printf("hello world node is running"); 

    // 一个循环,只要ROS系统处于OK状态(即没有关闭),循环就会继续执行。
    while (ros::ok()) {
        // 在控制台打印"hello world !"信息,每次循环都会打印一次,后面跟着一个换行符。
        printf("hello world !\n");
    }
    
    // 程序正常退出,返回0表示成功。
    return 0;
}
添加编译规则:在CmakeLists.txt中
找到 ## Declare a C++ executable
将以下文件复制到文件末尾
# add_executable(${PROJECT_NAME}_node src/hello_world_node.cpp) 
 
找到  ## Specify libraries to link a library or executable target against
将以下文件复制到文件末尾
# target_link_libraries(${PROJECT_NAME}_node
#   ${catkin_LIBRARIES}
# )

最终修改为:
add_executable(hello_world_node src/hello_world_node.cpp)
 target_link_libraries(hello_world_node
   ${catkin_LIBRARIES}
 )

ROS中的通信方式之Topic与消息(Message)类型

在 ROS(Robot Operating System)中,Topic 是一种通信机制,允许节点之间进行异步的消息传递。节点可以发布(publish)消息到一个话题,也可以订阅(subscribe)一个话题来接收消息。这种机制是 ROS 中实现节点解耦和数据分发的核心方式之一。

Topic 的特点:

  1. 异步通信:发布者和订阅者不需要同时在线,它们可以在不同的时间发布和接收消息。

  2. 一对多:一个节点可以向一个话题发布消息,而多个节点可以订阅同一个话题来接收这些消息。

  3. 无状态:话题通信是无状态的,每个消息都是独立的,不依赖于之前的消息。

  4. 类型安全:每个话题都有指定的消息类型,ROS 会确保只有正确类型的消息可以被发布和接收。

  5. 松耦合:发布者和订阅者不需要知道对方的存在,它们通过话题进行通信,从而降低了系统的耦合度。

使用 Topic 的基本步骤:

  1. 定义消息类型:首先,你需要定义或选择一个消息类型,这个消息类型将被用作话题通信的数据格式。

  2. 发布消息:节点使用 ros::Publisher 对象来发布消息到一个话题。

  3. 订阅话题:节点使用 ros::Subscriber 对象来订阅一个话题,并提供一个回调函数来处理接收到的消息。

消息(Message)类型

在ROS中,消息是一种数据结构,用于在节点之间传递数据。消息类型定义了消息中包含的数据字段和它们的数据类型。ROS提供了一些预定义的消息类型,例如:

  • std_msgs:包含基本的消息类型,如StringInt32Float64等。

  • sensor_msgs:包含传感器相关的消息类型,如ImageLaserScanPointCloud等。

  • geometry_msgs:包含几何消息类型,如PoseTwistPoint等。

  • nav_msgs:包含导航相关的消息类型,如OdometryPath等。

用户也可以根据需要定义自定义消息类型。自定义消息类型需要在.msg文件中定义,并使用rosmsg工具来查看消息结构或者前往index.ros.org查看。

接下来通过一个实例来实现话题的通信

首先进入ROS的工作空间:cd catkin_ws/src/

接着新建一个ROS功能包: catkin_create_pkg turtle_vel_ctrl_pkg rospy roscpp geometry_msgs

编写如下代码实现话题的发布:

#include <ros/ros.h>  // 包含ROS的头文件
#include <geometry_msgs/Twist.h>  // 包含Twist消息类型的头文件,用于发送速度指令

int main(int argc, char *argv[]) {
    // 初始化ROS节点,节点名称为"turtle_vel_ctrl_node"
    ros::init(argc, argv, "turtle_vel_ctrl_node");

    // 创建一个节点句柄,用于创建发布者和订阅者
    ros::NodeHandle n;

    // 创建一个发布者,发布到"/turtle1/cmd_vel"话题,消息类型为geometry_msgs::Twist,队列大小为20
    ros::Publisher vel_pub = n.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel", 20);

    // 创建一个Twist消息类型的实例,用于存储速度指令
    geometry_msgs::Twist vel_cmd;

    // 设置线速度为0.1米/秒,沿x轴移动
        vel_cmd.linear.x = 0.1;
        vel_cmd.linear.y = 0.0;
        vel_cmd.linear.z = 0.0;
        
        // 设置角速度为0弧度/秒,不进行旋转
        vel_cmd.angular.x = 0.0;
        vel_cmd.angular.y = 0.0;
        vel_cmd.angular.z = 0.0;


    // ROS的主循环,只要ROS系统运行正常,这个循环就会一直执行
    while (ros::ok()) {

        // 通过发布者发布速度指令
        vel_pub.publish(vel_cmd);

        // 让当前线程休眠一段时间,这里设置为10毫秒
        // 这个休眠时间决定了发送速度指令的频率,即100Hz
        ros::Duration(0.01).sleep();

        // 处理ROS回调函数,例如订阅话题的消息等 本代码中未使用回调函数
        ros::spinOnce();
    }

    // 节点正常退出
    return 0;
}

/* 为本文件添加编译:在CMakeLists.txt 文件添加如下代码 */

# 添加一个名为turtle_vel_ctrl_node的可执行文件,源文件为src/turtle_vel_ctrl_node.cpp
add executablevel turtle_vel_ctrl_node src/turtle_vel_ctrl_node.cpp)
# 添加依赖关系,确保在构建turtle_vel_ctrl_node之前,先构建了项目和catkin中导出的目标
add dependencies(turtle_vel_ctrl_node ${${PROJECT NAME}EXPORTED TARGETS} ${catkin EXPORTED TARGETS})
# 链接catkin提供的库到可执行文件turtle_vel_ctrl_node
target link libraries(turtle_vel_ctrl_node ${catkin LIBRARIES}

/* 运行 */
//启动ros内核:roscore
//启动小海龟节点:rosrun turtlesim turtlesim_node 
//运行上述代码: rosrun turtle_vel_ctrl_pkg turtle_vel_ctrl_node

python 代码实现:

#新建一个名为scripts文件夹存放代码文件,文件夹与src同级

#!/usr/bin/env python3
#coding=utf-8
import rospy  # 导入ROS的Python库
from geometry_msgs.msg import Twist  # 导入Twist消息类型的库

def main():
    # 初始化ROS节点,节点名称为"turtle_vel_ctrl_node"
    rospy.init_node('turtle_vel_ctrl_node', anonymous=True)

    # 创建一个发布者,发布到"/turtle1/cmd_vel"话题,消息类型为geometry_msgs/Twist,队列大小为20
    vel_pub = rospy.Publisher('/turtle1/cmd_vel', Twist, queue_size=20)

    # 创建一个Twist消息类型的实例,用于存储速度指令
    vel_cmd = Twist()

    # 设置线速度为0.1米/秒,沿x轴移动
    vel_cmd.linear.x = 0.1
    vel_cmd.linear.y = 0.0
    vel_cmd.linear.z = 0.0

    # 设置角速度为0弧度/秒,不进行旋转
    vel_cmd.angular.x = 0.0
    vel_cmd.angular.y = 0.0
    vel_cmd.angular.z = 0.0

    # 设置ROS循环的频率,即100Hz
    rate = rospy.Rate(100)  # 10毫秒循环一次

    # ROS的主循环,只要ROS系统运行正常,这个循环就会一直执行
    while not rospy.is_shutdown():
        # 通过发布者发布速度指令
        vel_pub.publish(vel_cmd)

        # 按照设置的频率休眠
        rate.sleep()

        # 处理ROS回调函数,例如订阅话题的消息等 本代码中未使用回调函数
        rospy.spin()

    # 节点正常退出
    return 0

if __name__ == '__main__':
    try:
        main()
    except rospy.ROSInterruptException:
        pass

接下来实现话题的接收:

#include <ros/ros.h>  // 包含ROS的头文件
#include <geometry_msgs/Twist.h>  // 包含Twist消息类型的头文件

// 回调函数,当接收到消息时被调用
void cmdVelCallback(const geometry_msgs::Twist::ConstPtr& msg)
{
    // 打印接收到的消息
    ROS_INFO("Received Twist message:");
    ROS_INFO("  Linear: x: [%f], y: [%f], z: [%f]", 
             msg->linear.x, msg->linear.y, msg->linear.z);
    ROS_WARN("  Angular: x: [%f], y: [%f], z: [%f]", 
             msg->angular.x, msg->angular.y, msg->angular.z);
}

int main(int argc, char *argv[])
{
    // 初始化ROS节点,节点名称为"turtle_vel_listener_node"
    ros::init(argc, argv, "turtle_vel_listener_node");

    // 创建一个节点句柄,用于创建发布者和订阅者
    ros::NodeHandle n;

    // 创建一个订阅者,订阅"/turtle1/cmd_vel"话题,消息类型为geometry_msgs::Twist
    ros::Subscriber sub = n.subscribe("turtle1/cmd_vel", 10, cmdVelCallback);

    // ROS的主循环,只要ROS系统运行正常,这个循环就会一直执行
    ros::spin();

    // 节点正常退出
    return 0;
}
#!/usr/bin/env python
import rospy
from geometry_msgs.msg import Twist

# 回调函数,当接收到消息时被调用
def cmdVelCallback(msg):
    # 打印接收到的消息
    rospy.loginfo("Received Twist message:")
    rospy.loginfo("  Linear: x: [%f], y: [%f], z: [%f]", 
                 msg.linear.x, msg.linear.y, msg.linear.z)
    rospy.logwarn("  Angular: x: [%f], y: [%f], z: [%f]", 
                 msg.angular.x, msg.angular.y, msg.angular.z)

def listener():
    # 初始化ROS节点,节点名称为"turtle_vel_listener_node"
    rospy.init_node('turtle_vel_listener_node', anonymous=True)

    # 创建一个订阅者,订阅"/turtle1/cmd_vel"话题,消息类型为geometry_msgs/Twist
    rospy.Subscriber("/turtle1/cmd_vel", Twist, cmdVelCallback)

    # ROS的主循环,只要ROS系统运行正常,这个循环就会一直执行
    rospy.spin()

if __name__ == '__main__':
    try:
        listener()
    except rospy.ROSInterruptException:
        pass

其他通信方式自行了解:服务(Services)、动作(Actions)、参数(Parameters)


ROS中的实用工具

  1. launch文件

  2. TF 坐标变换

  3. Gazebo仿真

  4. Rviz可视化工具


ROS中的2D激光雷达

在此之前,你需要先自行了解2D激光雷达的原理,在ros中2D激光雷达的消息包格式如下:

Header

  • header:这个消息头包含了一些元数据,如时间戳(timestamp)和坐标系(frame_id)。时间戳指的是扫描中第一条射线的获取时间。frame_id指的是这个扫描数据所属的坐标系。

Angles

  • angle_min:扫描的起始角度,单位是弧度(rad)。通常,这个角度是以正Z轴为基准,逆时针方向测量的,其中0角度是沿着X轴正方向。

  • angle_max:扫描的结束角度,单位是弧度(rad)。

  • angle_increment:测量之间的角距离,单位是弧度(rad)。这个值表示相邻射线之间的夹角。

Time

  • time_increment:测量之间的时间间隔,单位是秒(seconds)。如果你的扫描仪是移动的,这个值将用于插值3D点的位置。

  • scan_time:两次扫描之间的时间间隔,单位是秒(seconds)。

Range

  • range_min:最小范围值,单位是米(m)。这是扫描仪能够检测到的最小距离。

  • range_max:最大范围值,单位是米(m)。这是扫描仪能够检测到的最大距离。

Data

  • ranges:范围数据,单位是米(m)。这个数组包含了每个射线的距离测量值。如果测量值小于range_min或大于range_max,则应该丢弃这些值。

  • intensities:强度数据,单位是设备特定的单位。如果设备不提供强度数据,则数组应该为空。

以下为雷达的实际消息内容:

以下是获取雷达消息的示例代码:

//在~/catkin_ws/src执行catkin_create_pkg your_package_name std_msgs rospy roscpp sensor_msgs

#include <ros/ros.h> // 包含ROS的头文件,用于访问ROS的功能
#include <std_msgs/String.h> // 包含标准消息类型的头文件,虽然在这个程序中并未使用
#include <sensor_msgs/LaserScan.h> // 包含激光扫描消息类型的头文件

// 定义一个回调函数,当接收到激光雷达(LIDAR)数据时被调用
void LidarCallback(const sensor_msgs::LaserScan msg) {
    // 获取激光雷达数据中的测距数组的大小
    int nNum = msg.ranges.size();
    
    // 计算数组的中间位置,用于获取正前方的测距数据
    int nMid = nNum/2;
    // 获取正前方的测距值
    float fMidDist = msg.ranges[nMid];
    // 打印正前方的测距值
    ROS_INFO("前方测距 ranges[%d] = %f 米", nMid, fMidDist); 
}

// 主函数
int main(int argc, char** argv) {
    // 设置程序的区域设置,这里设置为默认区域设置
    setlocale(LC_ALL,"");
    // 初始化ROS节点,节点名为"demo_lidar_data"
    ros::init(argc,argv,"demo_lidar_data");
    
    // 创建ROS节点句柄
    ros::NodeHandle n;
    // 创建一个订阅者,订阅"/scan"主题,队列大小为10,并且当接收到消息时调用LidarCallback函数
    ros::Subscriber lidar_sub = n.subscribe("/scan", 10, &LidarCallback);

    // 进入ROS事件循环,等待回调函数被调用
    ros::spin();
    // 程序不会到达这里,除非ros::spin()返回
}

//记得修改CMakeLists文件

ROS中的IMU消息

在ROS中IMU的消息包格式如下:

这是一个用于保存惯性测量单元(IMU)数据的消息
加速度应该以 m/s^2 为单位(而不是 g),旋转速度应该以 rad/sec 为单位
如果已知测量的协方差,应该填写进去(如果你只知道每个测量的方差,例如,从数据手册中,只需将它们放在对角线上)
所有元素都为零的协方差矩阵将被解释为“协方差未知”,使用数据时必须假设一个协方差或从其他来源获取
如果你没有某个数据元素的估计值(例如,你的IMU不提供方向估计),请将相关协方差矩阵的第0个元素设置为 -1
如果你在解释这个消息,请检查每个协方差矩阵的第一个元素是否有 -1 的值,并忽略相关的估计值。

Header header // 消息头,包含时间戳和坐标系信息

geometry_msgs/Quaternion orientation // IMU测量的方向,使用四元数表示,四元数包含一个实部和三个虚部
float64[9] orientation_covariance # 方向测量的协方差矩阵,行主序排列,分别对应于x、y、z轴

geometry_msgs/Vector3 angular_velocity // IMU测量的角速度,单位是弧度/秒(rad/sec),表示设备绕x、y、z轴的旋转速度
float64[9] angular_velocity_covariance # 角速度测量的协方差矩阵,行主序排列,分别对应于x、y、z轴

geometry_msgs/Vector3 linear_acceleration // IMU测量的线加速度,单位是米/秒平方(m/s^2),表示设备沿x、y、z轴的加速度,不包括重力加速度
float64[9] linear_acceleration_covariance # 线加速度测量的协方差矩阵,行主序排列,分别对应于x、y、z轴

在ROS中IMU的话题消息分为三种:

  1. imu/data_raw (sensor_msgs/Imu)

    • 描述:这个话题发布原始的IMU数据,包括加速度计(测量线性加速度)和陀螺仪(测量角速度)的输出。这些数据通常没有经过任何融合或校正处理,直接反映了传感器的原始读数。

    • 消息类型sensor_msgs/Imu,包含四元数姿态、角速度和线加速度等信息。这些数据通常以未校准或未融合的形式提供。

  2. imu/data (sensor_msgs/Imu)

    • 描述:这个话题发布经过处理的IMU数据,通常是将加速度计和陀螺仪的原始数据通过传感器融合算法(如卡尔曼滤波器或互补滤波器)处理后得到的四元数姿态描述。这意味着数据已经被校准和融合,提供了更准确的姿态估计。

    • 消息类型sensor_msgs/Imu,除了包含imu/data_raw中的信息外,还可能包含融合后的姿态估计,使得姿态数据更加稳定和可靠。

  3. imu/mag (sensor_msgs/MagneticField)

    • 描述:如果IMU设备包含磁力计,这个话题用于发布磁强计测量的磁场数据。磁力计可以提供关于地球磁场方向的信息,这对于确定机器人的航向非常有用。

    • 消息类型sensor_msgs/MagneticField,包含磁场的三个分量(X、Y、Z轴)和磁场强度的大小。

我们一般订阅的是第二个话题,代码如下:

#include "ros/ros.h" // 包含ROS的头文件,用于访问ROS的功能
#include "sensor_msgs/Imu.h" // 包含IMU消息类型的头文件
#include "tf/tf.h" // 包含tf转换库的头文件,用于四元数和欧拉角之间的转换

// IMU 回调函数,当接收到IMU数据时被调用
void IMUCallback(const sensor_msgs::Imu msg) {
    // 检测消息包中四元数数据是否存在,如果不存在则返回
    if(msg.orientation_covariance[0] < 0)
        return;
    // 将四元数数据从IMU消息中提取出来
    tf::Quaternion quaternion(
        msg.orientation.x,
        msg.orientation.y,
        msg.orientation.z,
        msg.orientation.w
    );
    // 声明欧拉角变量
    double roll, pitch, yaw;
    // 将四元数转换为RPY(Roll, Pitch, Yaw)欧拉角
    tf::Matrix3x3(quaternion).getRPY(roll, pitch, yaw);
    // 将欧拉角从弧度转换为角度
    roll = roll * 180 / M_PI;
    pitch = pitch * 180 / M_PI;
    yaw = yaw * 180 / M_PI;
    // 打印欧拉角的值
    ROS_INFO("滚转= %.0f 俯仰= %.0f 朝向= %.0f", roll, pitch, yaw);
}

int main(int argc, char **argv) {
    setlocale(LC_ALL, ""); // 设置程序的区域设置,这里设置为默认区域设置
    ros::init(argc,argv, "demo_imu_data"); // 初始化ROS节点,节点名为"demo_imu_data"

    ros::NodeHandle n; // 创建ROS节点句柄
    // 订阅IMU的数据话题,队列大小为100,回调函数为IMUCallback
    ros::Subscriber sub = n.subscribe("imu/data", 100, IMUCallback);
    ros::spin(); // 进入ROS事件循环,等待回调函数被调用

    return 0; // 返回0,表示程序正常退出
}

ROS中的消息包

在ROS中消息包分类型大致分为两种,其中一种为std_msgs(标准消息包),std_msgs 包提供了一些基本的数据类型,这些类型常用于构建更复杂的消息。

基础类型

  • Bool:布尔类型,只有两个值:truefalse

  • Byte:字节类型,范围从 -128 到 127。

  • Char:字符类型,范围从 -128 到 127。

  • String:字符串类型,用于存储字符序列。

  • Int8, Int16, Int32, Int64:有符号整数类型,分别占用1、2、4、8字节。

  • UInt8, UInt16, UInt32, UInt64:无符号整数类型,分别占用1、2、4、8字节。

  • Float32, Float64:浮点数类型,分别占用4字节和8字节,用于表示实数。

数组类型

  • ByteMultiArray:字节类型数组,用于存储一系列字节。

  • Int8MultiArray, Int16MultiArray, Int32MultiArray, Int64MultiArray:有符号整数类型数组,分别存储一系列1、2、4、8字节的有符号整数。

  • UInt8MultiArray, UInt16MultiArray, UInt32MultiArray, UInt64MultiArray:无符号整数类型数组,分别存储一系列1、2、4、8字节的无符号整数。

  • Float32MultiArray, Float64MultiArray:浮点数类型数组,分别存储一系列4字节和8字节的浮点数。

结构体类型

  • ColorRGBA:颜色类型,包含红色、绿色、蓝色和透明度四个分量,每个分量的范围是0到1。

  • Duration:持续时间类型,用于表示两个时间点之间的时间间隔,以纳秒为单位。

  • Time:时间类型,用于表示特定的时间点,以纳秒为单位自Unix纪元(1970年1月1日)以来的秒数。

  • UInt8MultiArray:无符号8位整数类型数组,用于存储一系列1字节的无符号整数。

  • UInt16MultiArray:无符号16位整数类型数组,用于存储一系列2字节的无符号整数。

  • UInt32MultiArray:无符号32位整数类型数组,用于存储一系列4字节的无符号整数。

  • UInt64MultiArray:无符号64位整数类型数组,用于存储一系列8字节的无符号整数。

  • MultiArrayLayout:多维数组布局类型,用于描述多维数组的维度和布局。

  • MultiArrayDimension:多维数组维度类型,用于描述多维数组中的一个维度。

另一种消息包则为common_msgs(常规消息包):

actionlib_msgs

行为消息包:用于实现和调用ROS动作(Action)库。动作库是一种基于目标的接口,允许客户端发送一个目标(例如到达某个位置)并接收关于任务执行情况的反馈。

  • GoalID:唯一标识一个动作目标。

  • GoalStatus:描述动作目标的当前状态。

  • GoalStatusArray:包含多个GoalStatus,用于报告多个动作目标的状态。

diagnostic_msgs

诊断消息包:用于机器人系统的诊断和监控。它提供了一些消息类型来报告硬件状态、性能指标和其他诊断信息。

  • DiagnosticArray:包含一组诊断状态,用于报告多个组件的健康状态。

  • DiagnosticStatus:描述单个组件的诊断状态。

geometry_msgs

几何消息包:包含了描述几何形状和转换的简单消息类型。

  • PointPoint32PosePoseStampedQuaternion:描述点、姿态和四元数。

  • TransformTransformStamped:描述坐标变换。

  • TwistTwistStampedAccelAccelStamped:描述线速度、角速度和加速度。

导航消息包:包含了用于机器人导航和路径规划的消息类型。

  • Odometry:描述机器人的里程计数据。

  • Path:描述一系列路径点。

  • GridCells:描述一组网格单元,用于表示占据或自由空间。

sensor_msgs

传感器消息包:包含了各种传感器数据的消息类型。

  • ImageCompressedImageCameraInfo:描述图像和相机参数。

  • LaserScanMultiEchoLaserScan:描述激光雷达数据。

  • Imu:描述惯性测量单元(IMU)数据。

shape_msgs

形状消息包:包含了描述几何形状的消息类型。

  • SolidPrimitive:描述基本几何形状,如盒子、球体、圆柱体等。

  • MeshMeshTriangle:描述网格形状。

stereo_msgs

双目视觉消息包:包含了用于双目视觉系统的消息类型。

  • DisparityImage:描述双目相机的视差图像。

  • Point2f:描述二维浮点数点。

trajectory_msgs

运动轨迹消息包:包含了描述机器人运动轨迹的消息类型。

  • JointTrajectoryJointTrajectoryPoint:描述关节轨迹。

  • MultiDOFJointTrajectoryMultiDOFJointTrajectoryPoint:描述多自由度关节轨迹。

visualization_msgs

图形显示消息包:包含了用于可视化数据的消息类型。

  • MarkerMarkerArray:用于在RViz等可视化工具中绘制各种标记。

  • InteractiveMarkerInteractiveMarkerInitInteractiveMarkerFeedback:用于创建可交互的标记。

接下来介绍自定义消息包类型:

在ROS中创建自定义消息包,可以通过以下步骤使用catkin_create_pkg命令:

  1. 创建ROS包: 在工作空间的src目录下,使用catkin_create_pkg命令创建一个新的ROS包,并指定依赖项。例如,创建一个名为my_msgs的包,依赖于roscppstd_msgsmessage_generationmessage_runtime

    cd ~/catkin_ws/src
    catkin_create_pkg my_msgs roscpp std_msgs message_generation message_runtime

    这里my_msgs是包名,roscppstd_msgsmessage_generationmessage_runtime是依赖包。message_generation用于编译时生成消息,message_runtime用于运行时解析消息。

  2. 创建消息文件: 在新创建的包目录下,新建一个msg文件夹,并在其中创建自定义的消息文件。例如,创建一个名为Some.msg的消息文件:

    mkdir ~/catkin_ws/src/my_msgs/msg
    touch ~/catkin_ws/src/my_msgs/msg/Some.msg

    Some.msg文件中定义消息类型,例如:

    string name
    int32 data
    string test
  3. 设置编译规则: 在CMakeLists.txt文件中添加消息文件和依赖的其他消息包。例如:

    add_message_files(
      FILES
      Some.msg
    )
    
    generate_messages(
      DEPENDENCIES
      std_msgs
    )

    并在catkin_package()中添加message_runtime作为依赖:

    catkin_package(
      CATKIN_DEPENDS message_runtime
    )
    //以下为一个示例
    catkin_package(
    #  INCLUDE_DIRS include
    #  LIBRARIES wpr_simulation
    #  CATKIN_DEPENDS controller_manager joint_state_controller robot_state_publisher roscpp rospy std_msgs
    #  DEPENDS system_lib
    )
    确保message_runtime在其中即可
  4. 设置package.xml: 在package.xml文件中添加message_generationmessage_runtime作为依赖项:

  5. 编译: 返回工作空间的根目录,运行catkin_make来编译你的工作空间:

    bash
    cd ~/catkin_ws
    catkin_make
  6. 使用消息类型: 编译完成后,你可以在代码中包含自定义消息的头文件,并使用它:

    #include <my_msgs/Some.h>

    然后,你可以创建和发送自定义消息:

    my_msgs::Some msg;
    msg.name = "abc";
    msg.data = 123;
    msg.test = "@#$";

ROS中的栅格地图

在ROS中,栅格地图是一种重要的数据结构,用于表示环境的空间布局,常用于机器人的导航和路径规划。以下是关于ROS中栅格地图的一些详细介绍:

1. 栅格地图的基本概念

栅格地图将环境划分为一个个小格子(像素),每个格子包含了关于该区域的信息,如障碍物的存在与否。在ROS中,栅格地图的数据类型是nav_msgs::OccupancyGrid,它包含了地图的元数据和格子数据。

2. 栅格地图的消息格式

  • std_msgs/Header header

    • 包含地图的时间戳(timestamp)和坐标系ID(frame_id)。时间戳通常不是很重要,而frame_id一般固定使用“map”。

Info

  • nav_msgs/MapMetaData info

    • time map_load_time:加载地图的时间。

    • float32 resolution:地图的分辨率,单位是米/像素(m/cell),这个参数非常重要,它直接关系到地图的清晰度。例如,0.05表示一个像素代表5厘米。

    • uint32 width:地图的宽度,单位是像素数(cells)。

    • uint32 height:地图的高度,单位是像素数(cells)。

    • geometry_msgs/Pose origin:地图左下角的格子对应的物理世界的坐标。

      • geometry_msgs/Point position:包含x、y、z坐标,表示地图原点在物理世界中的位置。

      • geometry_msgs/Quaternion orientation:四元数,表示地图原点的方向。

Data

  • int8[] data

    • 一个整型数组,按照行优先顺序存储,表示每个格子的占据状态。数组中的每个值对应地图中的一个像素(格子)。

    • 值的范围为[-1, 100],其中:

      • 0:表示空白区域(自由空间)。

      • 100:表示被占据(有障碍物)。

      • -1:表示未知区域。

3. 栅格地图的值

  • 格子的值通常为int8类型,范围从-1到100。

    • 0:表示空白区域。

    • 100:表示被占据(有障碍物)。

    • -1:表示未知区域。

4. 地图的发布和订阅

  • 发布栅格地图:通过nav_msgs::OccupancyGrid消息类型在/map话题上发布地图数据。

  • 订阅栅格地图:使用RViz等可视化工具订阅/map话题,显示栅格地图。

5. 地图的保存和读取

  • map_server:ROS中的map_server包提供了map_savermap_server两个节点,用于保存和读取栅格地图。

    • map_saver:将栅格地图保存到磁盘。

    • map_server:从磁盘读取栅格地图,并提供相关服务。

6. 栅格地图的应用

  • 在机器人导航中,栅格地图可以用于路径规划、避障等任务。通过结合激光雷达(LIDAR)和SLAM(Simultaneous Localization and Mapping)技术,机器人可以实时构建和更新栅格地图。

7. 栅格地图的图层

  • 在一些高级的栅格地图处理包(如grid_map)中,可以有多个图层,每个图层可以表示不同的信息,如高度、颜色等。

以下为代码展示:

//  catkin_create_pkg map_pkg rospy roscpp nav_msgs

#include <iostream> // 包含标准输入输出流库,用于输入输出操作
#include <ros/ros.h> // 包含ROS的头文件,用于访问ROS的功能
#include <nav_msgs/OccupancyGrid.h> // 包含导航消息包中的OccupancyGrid消息类型的头文件

int main(int argc, char** argv) {
    // 初始化ROS节点,节点名为"demo_map_pub"
    ros::init(argc, argv, "demo_map_pub");

    // 创建ROS节点句柄
    ros::NodeHandle n;
    // 创建一个发布者,发布到"/map"话题,消息类型为nav_msgs::OccupancyGrid,队列大小为10
    ros::Publisher pub = n.advertise<nav_msgs::OccupancyGrid>("/map", 10);

    // 设置循环频率为1Hz
    ros::Rate r(1);
    while (ros::ok()) {
        // 创建一个OccupancyGrid消息实例
        nav_msgs::OccupancyGrid msg;
        // 设置消息头
        msg.header.frame_id = "map"; // 设置frame_id为"map"
        msg.header.stamp = ros::Time::now(); // 设置时间戳为当前时间

        // 设置地图描述信息
        msg.info.origin.position.x = 0; // 地图原点的x坐标
        msg.info.origin.position.y = 0; // 地图原点的y坐标
        msg.info.resolution = 1.0; // 地图分辨率,每个格子代表1米
        msg.info.width = 4; // 地图宽度,4个格子
        msg.info.height = 2; // 地图高度,2个格子

        // 设置地图数据
        msg.data.resize(4*2); // 调整数据数组大小以匹配地图的宽度和高度
        msg.data[0] = 100; // (0,0)位置的格子被占据
        msg.data[1] = 100; // (1,0)位置的格子被占据
        msg.data[2] = 0; // (0,1)位置的格子是自由空间
        msg.data[3] = -1; // (1,1)位置的格子未知

        // 发布地图消息
        pub.publish(msg);
        // 根据设置的频率休眠,以保持循环频率为1Hz
        r.sleep();
    }
    
    // 程序退出
    return 0;
}


//记得为节点添加编译规则
//运行你的节点->运行rviz
/* 
在rviz中点击add添加坐标轴标识(Axes),该标识为世界坐标系的原点
然后添加map 并且将map的话题名称(Topic)修改为/amp
*/

初识SLAM

SLAM,即Simultaneous Localization and Mapping(同步定位与地图构建),是一种在未知环境中,使机器人或自动驾驶车辆等移动设备能够在运动过程中同时进行自身定位和环境地图构建的技术。SLAM技术最早由Smith、Self和Cheeseman于1988年提出,被广泛认为是实现全自主移动机器人的关键技术之一。

SLAM技术的核心在于,它允许设备在没有先验环境信息的情况下,通过搭载的传感器(如激光雷达、摄像头等)观测并定位自身在三维空间中的位置和姿态,同时基于这些位置信息进行增量式的三维地图构建,实现同时定位和地图构建的目的。这个过程涉及到特征提取、三维重建、位置确定等多个步骤,并且需要实时进行,以适应环境的变化。

剩下的看视频:什么是SLAM


Hector_Mapping

hector_mapping是一个SLAM(Simultaneous Localization and Mapping,即同时定位与建图)方法,它可以在没有里程计(odometry)的情况下使用,也可以在传感器或平台展示俯仰/横滚运动的场合下使用。

以下为Hector_Mapping的API接口介绍

订阅的话题(Subscribed Topics)

  • scan (sensor_msgs/LaserScan)
    SLAM系统使用的激光扫描数据。

  • syscommand (std_msgs/String)
    系统命令。如果字符串等于"reset",则地图和机器人姿态会被重置到初始状态。

发布的话题(Published Topics)

  • map_metadata (nav_msgs/MapMetaData)
    从这个主题获取地图数据,它是latched(保持)的,并且会定期更新。

  • map (nav_msgs/OccupancyGrid)
    从这个主题获取地图数据,它是latched的,并且会定期更新。

  • slam_out_pose (geometry_msgs/PoseStamped)
    估计的机器人姿态,不包含协方差信息。

  • poseupdate (geometry_msgs/PoseWithCovarianceStamped)
    估计的机器人姿态,包含高斯估计的不确定性(协方差)。

提供的服务(Services)

  • dynamic_map (nav_msgs/GetMap)
    调用此服务以获取地图数据。

  • reset_map (std_srvs/Trigger)
    调用此服务以重置地图,hector将从头开始创建一个全新的地图。注意,这不会影响机器人的姿态,它将从最后记录的姿态开始。

  • pause_mapping (std_srvs/SetBool)
    调用此服务以停止或开始处理激光扫描。

  • restart_mapping_with_new_pose (hector_mapping/ResetMapping)
    调用此服务以重置地图、机器人的姿态,并在暂停后继续建图。

参数(Parameters)

  • ~base_frame (string, default: "base_link")
    机器人的基座帧名称,用于定位和转换激光扫描数据。

  • ~map_frame (string, default: "map_link")
    地图帧的名称。

  • ~odom_frame (string, default: "odom")
    里程计帧的名称。

  • ~map_resolution (double, default: 0.025)
    地图解析度,单位为米,这是网格单元边长。

  • ~map_size (int, default: 1024)
    地图的大小(每个轴上的单元格数量)。地图是正方形的,总共有(map_size * map_size)个网格单元。

  • ~map_start_x (double, default: 0.5)
    地图帧原点在x轴上的位置,相对于网格地图的范围是[0.0, 1.0]。0.5表示在中间。

  • ~map_start_y (double, default: 0.5)
    地图帧原点在y轴上的位置,相对于网格地图的范围是[0.0, 1.0]。0.5表示在中间。

  • ~map_update_distance_thresh (double, default: 0.4)
    执行地图更新的距离阈值,单位为米。平台必须移动这个距离或经历由map_update_angle_thresh参数描述的角度变化,才会触发地图更新。

  • ~map_update_angle_thresh (double, default: 0.9)
    执行地图更新的角度阈值,单位为弧度。平台必须经历这个角度变化或移动由map_update_distance_thresh参数指定的距离,才会触发地图更新。

  • ~map_pub_period (double, default: 2.0)
    地图发布周期,单位为秒。

  • ~map_multi_res_levels (int, default: 3)
    地图多分辨率网格级别的数量。

  • ~update_factor_free (double, default: 0.4)
    在[0.0, 1.0]范围内,空闲单元格更新的修改因子。0.5表示无变化。

  • ~update_factor_occupied (double, default: 0.9)
    在[0.0, 1.0]范围内,占用单元格更新的修改因子。0.5表示无变化。

  • ~laser_min_dist (double, default: 0.4)
    系统使用的激光扫描端点的最小距离,单位为米。比这个值更近的扫描端点将被忽略。

  • ~laser_max_dist (double, default: 30.0)
    系统使用的激光扫描端点的最大距离,单位为米。比这个值更远的扫描端点将被忽略。

  • ~laser_z_min_value (double, default: -1.0)
    系统使用的激光扫描端点的最小高度,相对于激光扫描器框架,单位为米。比这个值更低的扫描端点将被忽略。

  • ~laser_z_max_value (double, default: 1.0)
    系统使用的激光扫描端点的最大高度,相对于激光扫描器框架,单位为米。比这个值更高的扫描端点将被忽略。

  • ~pub_map_odom_transform (bool, default: true)
    确定是否由系统发布map->odom变换。

  • ~output_timing (bool, default: false)
    通过ROS_INFO输出每个激光扫描处理的计时信息。

  • ~scan_subscriber_queue_size (int, default: 5)
    扫描订阅者的队列大小。如果以高于实时速度回放日志文件到hector_mapping,应该设置为高值(例如50)。

  • ~pub_map_scanmatch_transform (bool, default: true)
    确定是否将scanmatcher到map的变换发布到tf。框架名称由tf_map_scanmatch_transform_frame_name参数确定。

  • ~tf_map_scanmatch_transform_frame_name (string, default: "scanmatcher_frame")
    发布scanmatcher到map变换时使用的框架名称。

需要的tf变换(Required tf Transforms)

  • <the frame attached to incoming scans> → base_frame
    通常是一个固定值,由robot_state_publisher或tf static_transform_publisher定期广播。

提供的tf变换(Provided tf Transforms)

  • map → odom
    机器人在地图帧中的姿态估计(仅当参数"pub_map_odom_transform"为true时提供)。

这些API和参数使得hector_mapping能够灵活地适应不同的机器人平台和环境,实现有效的SLAM。

功能实现:

首先执行: roslaunch wpr_simulation wpb_stage_slam.launch 启动仿真

接着执行:rosrun hector_mapping hector_mapping运行slam节点

最后执行:rosrun rviz rviz 打开rviz界面

在rviz界面中:

  • 添加机器人模型

  • 添加激光雷达的扫描测距点

并修改如下参数(订阅scan话题并且修改测距点大小)

  • 添加map地图

设置地图的话题名称为map

执行 rosrun wpr_simulation keyboard_vel_ctrl 使用键盘控制机器人并完成建图

  • 将rviz的显示配置保存为rviz文件,再次运行时打开对应的rviz文件即可

File->save Config As

新建launch文件一键执行 :

<launch>

  <!-- 载入 机器人 和 SLAM 的仿真场景 -->
  <include file="$(find wpr_simulation)/launch/wpb_stage_slam.launch"/>

  <!-- Hector SLAM -->
  <node pkg="hector_mapping" type="hector_mapping" name="hector_mapping">
    <param name="tf_map_scanmatch_transform_frame_name" value="base_footprint" />
  </node>

  <!-- Rviz 显示 -->
  <node pkg="rviz" type="rviz" name="rviz" args="-d $(find wpr_simulation)/rviz/slam.rviz"/>

  <!-- 运动控制 -->
  <node pkg="rqt_robot_steering" type="rqt_robot_steering" name="rqt_robot_steering"/>

</launch>

Gmapping

Gmapping是ROS(Robot Operating System)社区中广泛使用的SLAM(Simultaneous Localization and Mapping,即同时定位与地图构建)算法之一。它基于激光雷达数据进行地图创建和机器人定位,使用格子贝叶斯滤波器(Grid-based FastSLAM)算法,具有较高的地图精度和实时性

订阅的Topic(订阅主题)

  • tftf/tfMessage):用于关联激光、基座和里程计所需的帧变换信息。

  • scansensor_msgs/LaserScan):用于创建地图的激光扫描数据。

发布的Topic(发布主题)

  • map_metadatanav_msgs/MapMetaData):通过这个主题获取地图数据,它是latched(锁定)的,并且会定期更新。

  • mapnav_msgs/OccupancyGrid):通过这个主题获取地图数据,同样是latched的,并且会定期更新。

  • ~entropystd_msgs/Float64):机器人姿态分布的熵估计值(值越高表示不确定性越大)。这个参数在1.1.0版本中新增。

服务(Services)

  • dynamic_mapnav_msgs/GetMap):通过这个服务获取地图数据。

参数(Parameters)

以下是一些重要的参数,用于配置slam_gmapping的行为:

  • ~inverted_laser(字符串,默认:"false"):(在1.1.1版本中移除;使用变换数据代替)激光扫描器是正向(顺时针)还是反向(逆时针)?

  • ~throttle_scans(整数,默认:1):每处理这么多扫描中的一个(设置更高的数字可以跳过更多的扫描)。

  • ~base_frame(字符串,默认:"base_link"):与移动基座相连的帧。

  • ~map_frame(字符串,默认:"map"):与地图相连的帧。

  • ~odom_frame(字符串,默认:"odom"):与里程计系统相连的帧。

  • ~map_update_interval(浮点数,默认:5.0):地图更新的时间间隔(秒)。降低这个数值可以更频繁地更新占用网格,但会增加计算负担。

  • ~maxUrange(浮点数,默认:80.0):激光的最大可用范围。超过这个值的激光束会被裁剪。

  • ~sigma~kernelSize~lstep~astep~iterations~lsigma~ogain~lskip~minimumScore~srr~srt~str~stt~linearUpdate~angularUpdate~temporalUpdate~resampleThreshold~particles~xmin~ymin~xmax~ymax~delta~llsamplerange~llsamplestep~lasamplerange~lasamplestep~transform_publish_period~occ_thresh:这些参数用于调整算法的性能和行为,包括激光匹配、优化步骤、扫描匹配器迭代、粒子滤波器参数等。

所需的tf变换(Required tf Transforms)

  • <the frame attached to incoming scans> → base_link:通常由robot_state_publishertf static_transform_publisher定期广播的固定值。

  • base_link → odom:通常由里程计系统提供(例如,移动基座的驱动程序)。

提供的tf变换(Provided tf Transforms)

  • map → odom:机器人在地图帧内的姿态当前估计值。

slam_gmapping通过这些组件和参数,实现了机器人的定位和地图构建功能。


功能实现:

执行如下代码:

roslaunch wpr_simulation wpb_stage_robocup.launch  #地图的仿真环境
rosrun gmapping slam_gmapping #运行Gmapping
rosrun rviz rviz #运行rviz

与hector_mapping相同操作

  • 添加机器人模型

  • 添加LaserScan显示激光雷达的测距点

  • 添加map地图数据

执行 rosrun wpr_simulation keyboard_vel_ctrl 使用键盘控制机器人并完成建图

建图的完成后保存地图

执行 rosrun map server map saver 会在当前执行的目录下保存地图数据

执行 rosrun map_server mapserver map.yaml 即可加载保存后的地图

Navigation 导航系统

ROS中的Navigation导航系统是一个集成了路径规划、避障和定位功能的高级软件包,它允许自主机器人在复杂环境中规划并执行从起点到终点的路径。

Navigation导航的架构图如下:

一开始就直接看总体架构图想必很难看懂,下面我们来一一拆解:

首先,我们将Navigation的架构图简化到只保留输入量和输出量,可以看出,Navigation的输人是开发者设置的导航目标点坐标,输出是机器人的运动控制指令。只需要让机器人的主控节点订阅主题“cmd_vel”里的速度指令,然后向导最务"move basesimple/goal”提交导航目标点。Navigation会自动向主题“cmd_vel"发送速度值,驱动机器人到达导航目标。

接下来我们来了解全局规划器

在ROS中,地图的创建通常使用Hector Mapping或Gmapping方法来创建的。新建好的原始地图并不能直接进行导航,需要先将其转换为代价地图(cost_map),价地图意思是机器人在地图里移动是要付出“代价”的,这个“代价”有显性的,也有隐性的。显性的,如行走的距离,对于同样距离的目标,机器人行走距离越远,对时间和电能的消耗就越多,这是最明显的“代价”。隐性的,比如过于靠近障碍物,机器人稍微有控制偏差就会撞到障碍物,存在一个风险概率,这是隐性的“代价”。还有一些机器人体型比较长,比如仓储物流中的重型自动导引运输车(AGV),拐弯和调头比较困难。对它们来说,规划的路径转折太多也是“代价”,导航线路越顺滑代价就越小。

由上图可以了解到,在Navigation系统里,可以看到,全局代价地图是通过map_server提供的全局地图和激光雷达侦测到的当前机器人周围的障得物分布融合后生成的。map_server提供的全局地图代表的是之前用SLAM创建的地图,时间过去比较久现在可能已经有变化了。这种变化有很多,或许只是路上多了个行人,也可能曾经开着的门关上了。远处的地图变化对机器人影响比较小可以暂时先不考虑,但是近处的变化会对机器人的当前行动产生直接影响,需要用激光雷达实时扫描,这就是为何全局代价地图会融合两者的信息来生成。

上图中左边为全局地图,右边为全局代价地图,可以看到在全局代价地图里,在障碍物的边缘会膨胀出一层淡蓝色的渐变区城。这代表着机器人可能与障得物发生碰撞的隐性“代价”。越靠近障碍物,与障碍物碰撞的风险越大,于是颜色越深,隐性“代价”越大。

代价地图的参数设置

<!-- 定义一个名为move_base的节点,它属于move_base包,类型为move_base -->
<node pkg="move_base" type="move_base" name="move_base">
    <!-- 加载全局代价地图(costmap)的通用参数 -->
    <rosparam file="$(find wpb_home_tutorials)/nav_lidar/costmap_common_params.yaml" command="load" ns="global_costmap" />
    <!-- 加载局部代价地图的通用参数 -->
    <rosparam file="$(find wpb_home_tutorials)/nav_lidar/costmap_common_params.yaml" command="load" ns="local_costmap" />
    <!-- 加载全局代价地图的特定参数 -->
    <rosparam file="$(find wpb_home_tutorials)/nav_lidar/global_costmap_params.yaml" command="load" />
    <!-- 加载局部代价地图的特定参数 -->
    <rosparam file="$(find wpb_home_tutorials)/nav_lidar/local_costmap_params.yaml" command="load" />
    <!-- 设置全局路径规划器为global_planner/GlobalPlanner -->
    <param name="base_global_planner" value="global_planner/GlobalPlanner" /> 
    <!-- 设置局部路径规划器为wpbh_local_planner/WpbhLocalPlanner -->
    <param name="base_local_planner" value="wpbh_local_planner/WpbhLocalPlanner" />
</node>
参数前四条既为代价地图的参数

costmap_common_params.yaml文件的作用下面有解释主要影响的是生成代价地图的形状,该文件被引用两次,一个是给全局代价地图的参数,另一个是给局部代价地图的参数,通过修改costmap_common_params.yaml即可对两个代价地图的参数进行修改,避免分发给全局规划器与局部规划器参数不一致从而导致地图形状不一致,导致路径规划和执行过程中的潜在的冲突:

robot_radius: 0.25  # 机器人半径,用于确定机器人在代价地图中的占用空间,单位是米。
inflation_radius: 0.5  # 膨胀半径,用于在检测到障碍物时,将障碍物的边界扩展一定的距离,单位是米。
obstacle_range: 1.0  # 障碍物范围,指定了代价地图中障碍物的最大影响距离,单位是米。
raytrace_range: 6.0  # 射线追踪范围,指定了代价地图中自由空间的最大影响距离,单位是米。
observation_sources: base_lidar  # 观测源,指定了哪些传感器数据源被用于更新代价地图,这里是激光雷达。

base_lidar: {  # 基础激光雷达的配置参数
    data_type: LaserScan,  # 数据类型,指定传感器数据的类型,这里是激光扫描数据。
    topic: /scan,  # 主题,指定传感器数据发布到的ROS主题,这里是"/scan"。
    marking: true,  # 标记,一个布尔值,设置为true表示激光雷达的数据将用于标记障碍物。
    clearing: true  # 清除,一个布尔值,设置为true表示激光雷达的数据将用于清除代价地图中的已知自由空间。
}

global_costmap_params.yaml主要用于描述全局代价地图的计算范围和频率以及恢复行为:

global_costmap:  # 全局代价地图的配置
  global_frame: map  # 指定全局代价地图使用的坐标系名称,通常是"map",表示地图坐标系
  robot_base_frame: base_footprint  # 指定机器人基座的坐标系名称,通常是"base_footprint"
  static_map: true  # 指定是否使用静态地图数据,true表示使用预先加载的地图
  update_frequency: 1.0  # 指定全局代价地图更新的频率,单位是赫兹,这里设置为1.0 Hz
  publish_frequency: 1.0  # 指定全局代价地图发布到ROS的频率,单位是赫兹,这里设置为1.0 Hz
  transform_tolerance: 1.0  # 指定坐标变换的容忍度,单位是秒,用于处理坐标系之间的变换延迟

recovery_behaviors:  # 定义当全局代价地图无法找到路径时的恢复行为
  - name: 'conservative_reset'  # 恢复行为的名称:保守重置
    type: 'clear_costmap_recovery/ClearCostmapRecovery'  # 恢复行为的类型:清除代价地图
  - name: 'rotate_recovery'  # 恢复行为的名称:旋转恢复
    type: 'rotate_recovery/RotateRecovery'  # 恢复行为的类型:旋转恢复
  - name: 'aggressive_reset'  # 恢复行为的名称:激进重置
    type: 'clear_costmap_recovery/ClearCostmapRecovery'  # 恢复行为的类型:清除代价地图

conservative_reset:  # 保守重置行为的配置
  reset_distance: 2.0  # 指定重置障碍物的距离,这里是2.0米
  layer_names: ["obstacle_layer"]  # 指定需要重置的层名称,这里是"obstacle_layer"

aggressive_reset:  # 激进重置行为的配置
  reset_distance: 0.0  # 指定重置障碍物的距离,这里是0.0米,意味着清除所有障碍物
  layer_names: ["obstacle_layer"]  # 指定需要重置的层名称,这里是"obstacle_layer"

lacal_costmap_params.yaml主要用于描述局部代价地图的计算范围和频率:

local_costmap:  # 定义局部代价地图的配置
  global_frame: odom  # 局部代价地图使用的全局坐标系名称,通常是odom,表示里程计坐标系
  robot_base_frame: base_footprint  # 机器人基座的坐标系名称,通常是base_footprint
  static_map: false  # 是否使用静态地图数据,false表示不使用,局部代价地图将动态更新
  rolling_window: true  # 是否使用滚动窗口,true表示局部代价地图将只维护机器人周围的一个固定大小的区域
  width: 3.0  # 滚动窗口的宽度,单位是米
  height: 3.0  # 滚动窗口的高度,单位是米
  update_frequency: 10.0  # 局部代价地图更新的频率,单位是赫兹,10.0 Hz表示每秒钟更新10次
  publish_frequency: 10.0  # 局部代价地图发布到ROS的频率,单位是赫兹,10.0 Hz表示每秒钟发布10次
  transform_tolerance: 1.0  # 坐标变换的容忍度,单位是秒,用于处理坐标系之间的变换延迟

有了代价地图,如何得到导航的路线?在Navigation系统里,这个通常由全局规划器global_planner来生成。全局规划器的任务就是从外部获得航的目标点,然后在全局地图里找出“代价最小”的那条路线,这条路线就是最终得到的导航路线。在ROS中预制了三种全局路径规划器,如下:

可以通过设置move_base节点的参数来选择规划器,下面给出参考:

<node pkg="move base"type="move base"name="move base">
  <param name="base global planner"value="global planner/GlobalPlanner"/>
<!-- 该规划器默认使用Dijkstra算法 --> 

你也可以自己编写路径规划算法,自定义全局规划器。

了解了全局规划器,我们来进一步了解局部规划器

全局规划器生成的全局路线,依据的是很久之前SLAM创建的地图,它并没有考虑之后出现的环境变化。所以需要一个能够随机应变的处理机制去应对可能出现的突发情况,这个机制的实现就是局部规划器(local_planner)。

局部规划器的工作就是从全局规划器获得导航路线,根据这个路线向机器人发送速度,一边按照全局路线的总体方针行走,另一边根据遇到的突发情况,做出一些必要的修正,确保机器器人能够顺利到达目标点。为了达到随机应变的效果,局部规划器利用激光雷达获得的当前障碍物数据,又做了一个小范围的代价地图,称为局部代价地图(local_costmap),如下图所示。

在真实的环境里,并不是只有机器人在移动,在商场生有行人,在公路上有汽车,在工业环境中地有其他正在移作业的无人搬运车(Auomated Guided Vehicle,AGv),这些不停运动的物体,在全局形价地图中基本是看不到的,建图的时候无法预测这些障碍物的出现。所以就需要一个小范围(至少在澈光雷达探测距离内)的局部代价地图,并在机器人移动过程中,让这个局部代价地图跟着机器人位置移动,始终围绕在机器人周围,如图69所示,以弥补全局代价地图所缺失的实时障碍物的信息。

局部规划器有许多种,常用的为DWA规划器TEB规划器

AMCL

AMCL直名翻译过来的中文名为“蒙特卡罗粒子滤波定位算法”,在机器人开始移动之前,AMCL会在机器人周围“嘭”的一声分裂出一堆的分身,这分身按照一个概率分布在地图里。在Rviz窗口中会看到机器人周围很多绿色的小箭头,这些小箭头就是AMCL撒到地图上的“分身”,该分身的作用就是确定机器人的具体位置。

当机器人移动的时候,所有分身会用统一的步伐行进,这个统一步伐来自底盘电机码盘里程计。当电机码盘里程计提示机器人往前直行的时候,所有分身都会同时往前直行;当电机码盘里程计提示机器人往左转的时候,所有分身同时往左转。机器人的分身在移动的过程中,会不停地用激光雷达扫描身边障碍物和全局地图进行比对,以判断自己处于正确的位置。随着机器人的移动,这些定位错误的分身一个接一个消失,最后剩下的最有可能是机器人位置的真身。在这过程中,通过Rz可以看到导航中的机器人周围的绿色箭头会逐渐收拢,最终聚合到机器人脚下,与机器人真实的位置合为一体。

recovery_behaviors机制

reovery behaviors被激活,通常是出现在机器人面前被大体积的障得物挡住导航路线,一些局部规划器(pmer))如果参数设置不合理,就会导致规划不出能绕过障碍的局部路线,于是不得不向reoverybehaviors求助。

在大部分版本的ROS中,recovery_behaviors里默认行为通常会设置为rotate-recovery,也就是让机器人原地旋转,试图用激光雷达将附近的障碍物扫描完整。为何原地旋转就能完全扫描附近的障碍物?因为这是考虑到,大部分的机器人在激光雷达的周围,通常都有一些支撑结构会挡住它的扫描视野,也就导致机器人的激光雷达在视野上存在一些盲区。reovery behaviors觉得找不到路线只是因为局部规划器(local_pamner)的视野被挡住了,能够绕过原对物的路线就藏在激光雷达的视野盲区里,可能转一两圈就能解决问题。毕竟让全局现划器(global_pamner)重新现划全局路径太耗时了,尽量在局部现划器里解决问题。

按照recovery behavios设计的机制,当转圈也解决不了问题的时候,求助全局规划器(global_pamner)按照最新扫描后的地图,重新规划一条新的全局导航路线。

reovery behaviors的状态图如下图所示

翻译成中文如下:

机器人无法正常导航时,会进入Conservative Reset状态,也就是清除全局地图和局部地图中一定范围(以机器人为中心 3米以外的范围)的障碍物信息后重新规划导航路线,若是还规划不出新的路线,则进入Clearing Rotation状态,也就是上述所说的消除雷达盲区的信息,还是不能规划导航路线,进入Aggressive Reset 状态,该状态会对更大范围(以机器人为中心 1.84米以外的范围)的障碍物信息进行清除,还是找不到路线,进入Rotation Reset状态,再次旋转一圈,避免雷达盲区干扰, 还是不行就会放弃导航任务。


评论
你无需删除空行,直接评论以获取最佳展示效果
引用到评论