ROS的二维导航功能包,简单来说,就是根据输入的里程计等传感器的信息流和机器人的全局位置,通过导航算法,计算得出安全可靠的机器人速度控制指令。但是,如何在特定的机器人上实现导航功能包的功能,却是一件较为复杂的工程。作为导航功能包使用的必要先决条件,机器人必须运行ROS,发布tf变换树,并发布使用ROS消息类型的传感器数据。同时,为了让机器人更好的完成导航任务,开发者还要根据机器人的外形尺寸和性能,配置导航功能包的一些参数。
尽管导航功能包设计得尽可能通用,但是仍然对机器人的硬件有以下三个要求:
(1)导航功能包仅对差分等轮式机器人有效,并且假设机器人可直接使用速度指令进行控制,速度指令的格式为:x方向速度、y方向速度、速度向量角度。
(2)导航功能包要求机器人必须安装有激光雷达等二维平面测距设备。
(3)导航功能包以正方型的机器人为模型进行开发,所以对于正方形或者圆形外形的机器人支持度较好,而对于其他外形的机器人来讲,虽然仍然可以正常使用,但是表现则很有可能不佳。
导航功能包的结构如上图所示,在自己的机器人平台上实现自主导航,简单来说,就是按照上图将需要的功能按照需求完成即可。其中白色的部分是ROS功能包已经完成的部分,不需要我们去实现,灰色的是可选的部分,也由ROS完成,在使用中根据需求使用,需要关注的重点部分是蓝色部分,这些需要我们根据输入输出的要求完成相应的功能。
首先,请确保你的机器人安装了ROS框架。
导航功能包要求机器人以tf树的形式发布各个相关参考系的变换关系。
导航功能包需要采集机器人的传感器信息,以达到实时避障的效果。这些传感器要求能够通过ROS发布
sensor_msgs/LaserScan或者sensor_msgs/PointCloud 格式的消息,也就是二维雷达信息或者三维点云数据。ROS社区已经支持大部分激光雷达、Kinect等设备的驱动,可以直接使用社区提供的驱动功能包发布满足要求的传感器信息。如果你使用的传感器没有ROS支持,或者你想使用自己的驱动,也可以自己将传感器信息封装成要求的格式。
导航功能包要求机器人发布nav_msgs/Odometry格式的里程计信息,同时在也要发布相应的tf变换。
导航功能包最终的输出是针对机器人geometry_msgs/Twist格式的控制指令,这就要求机器人控制节点具备解析控制指令中速度、角度的能力,并且最终通过这些指令控制机器人完成相应的运动目标。
地图并不是导航功能所必需的。
在满足以上条件的前提下,我们来针对导航功能进行一些配置。
首先,我们需要创建一个功能包,用来存储导航需要用到的所有的配置文件和launch启动文件。在创建功能包的时候,我们需要添加相关的所有依赖,包括机器人配置中使用到的功能包,当然不要忘记了move_base功能包,因为该包有很多我们后面需要用到的接口。找到合适的位置,输入以下命令来创建包:
catkin_create_pkg my_robot_name_2dnav move_base my_tf_configuration_depmy_odom_configuration_dep my_sensor_configuration_dep
现在,我们已经有了一个存储各种文件的工作空间,下一步,我们来创建一个机器人启动文件,用来启动机器人配置中所提到的所有硬件,并发布相应的消息和变换关系。
打开编辑器,输入以下格式的内容,并保存为my_robot_configuration.launch命名的文件:
- <launch>
- <nodepkg="sensor_node_pkg" type="sensor_node_type"name="sensor_node_name" output="screen">
- <paramname="sensor_param" value="param_value" />
- </node>
-
- <nodepkg="odom_node_pkg" type="odom_node_type"name="odom_node" output="screen">
- <paramname="odom_param" value="param_value" />
- </node>
-
- <nodepkg="transform_configuration_pkg"type="transform_configuration_type"name="transform_configuration_name" output="screen">
- <paramname="transform_configuration_param" value="param_value"/>
- </node>
- </launch>
让我们来详细的解读以上内容的含义:
- <launch>
- <nodepkg="sensor_node_pkg" type="sensor_node_type"name="sensor_node_name" output="screen">
- <paramname="sensor_param" value="param_value" />
这部分代码用来启动机器人的传感器,根据以上格式,修改你所使用到的传感器驱动包名称、类型、命名等信息,并且添加驱动包节点需要使用到的参数。当然,如果你需要使用多个传感器,可以使用相同的方法,启动多个传感器的驱动节点。
- <node pkg="odom_node_pkg"type="odom_node_type" name="odom_node"output="screen">
- <paramname="odom_param" value="param_value" />
- </node>
这部分代码用来启动机器人上的里程计,根据需要修改功能包名、类型、节点名、参数。
- <nodepkg="transform_configuration_pkg"type="transform_configuration_type"name="transform_configuration_name" output="screen">
- <paramname="transform_configuration_param" value="param_value"/>
- </node>
这部分代码需要启动机器人相关的坐标变换。
导航功能包使用两种代价地图存储周围环境中的障碍信息,一种用于全局路径规划,一种用于本地路径规划和实时避障。两种代价地图需要使用一些共同和独立的配置文件:通用配置文件,全局规划配置文件,本地规划配置文件。以下将详细讲解这三种配置文件:
(1)通用配置文件(Common Configuration (local_costmap) &(global_costmap))
代价地图用来存储周围环境的障碍信息,其中需要注明地图关注的机器人传感器消息,以便于地图信息进行更行。针对两种代价地图通用的配置选项,创建名为costmap_common_params.yaml的配置文件:
- obstacle_range: 2.5
- raytrace_range: 3.0
- footprint: [[x0, y0], [x1, y1], ... [xn, yn]]
- #robot_radius: ir_of_robot
- inflation_radius: 0.55
-
- observation_sources: laser_scan_sensor point_cloud_sensor
-
- laser_scan_sensor: {sensor_frame: frame_name, data_type: LaserScan, topic: topic_name, marking: true, clearing: true}
-
- point_cloud_sensor: {sensor_frame: frame_name, data_type: PointCloud, topic: topic_name, marking: true, clearing: true}
详细解析以上配置文件的内容:
- obstacle_range: 2.5
- raytrace_range: 3.0
这两个参数用来设置代价地图中障碍物的相关阈值。obstacle_range参数用来设置机器人检测障碍物的最大范围,设置为2.5意为在2.5米范围内检测到的障碍信息,才会在地图中进行更新。raytrace_range参数用来设置机器人检测自由空间的最大范围,设置为3.0意为在3米范围内,机器人将根据传感器的信息,清除范围内的自由空间。
- footprint: [[x0, y0], [x1, y1], ... [xn, yn]]
- #robot_radius: ir_of_robot
- inflation_radius: 0.55
这些参数用来设置机器人在二维地图上的占用面积,如果机器人外形是圆形,则需要设置机器人的外形半径。所有参数以机器人的中心作为坐标(0,0)点。inflation_radius参数是设置障碍物的膨胀参数,也就是机器人应该与障碍物保持的最小安全距离,这里设置为0.55意为为机器人规划的路径应该与机器人保持0.55米以上的安全距离。
observation_sources: laser_scan_sensorpoint_cloud_sensor
observation_sources参数列出了代价地图需要关注的所有传感器信息,每一个传感器信息都将在后边列出详细信息。
laser_scan_sensor: {sensor_frame: frame_name, data_type:LaserScan, topic: topic_name, marking: true, clearing: true}
以激光雷达为例,sensor_frame标识传感器的参考系名称,data_type表示激光数据或者点云数据使用的消息类型,topic_name表示传感器发布的话题名称,而marking和clearing参数用来表示是否需要使用传感器的实时信息来添加或清楚代价地图中的障碍物信息。
(2)全局规划配置文件(Global Configuration (global_costmap))
全局规划配置文件用来存储用于全局代价地图的配置参数,我们使用global_costmap_params.yaml来命名,内容如下:
- global_costmap:
- global_frame: /map
- robot_base_frame: base_link
- update_frequency: 5.0
- static_map:true
global_frame参数用来表示全局代价地图需要在那个参考系下运行,这里我们选择了map这个参考系。robot_base_frame参数表示代价地图可以参考的机器人本体的参考系。update_frequency参数绝地全局地图信息更新的频率,单位是Hz。static_map参数决定代价地图是否需要根据map_server提供的地图信息进行初始化,如果你不需要使用已有的地图或者map_server,最好将该参数设置为false。
(3)本地规划配置文件(Local Configuration (local_costmap))
本地规划配置文件用来存储用于本地代价地图的配置参数,命名为local_costmap_params.yaml,内容如下:
- local_costmap:
- global_frame: odom
- robot_base_frame: base_link
- update_frequency: 5.0
- publish_frequency: 2.0
- static_map:false
- rolling_window: true
- width: 6.0
- height: 6.0
- resolution:0.05
"global_frame", "robot_base_frame","update_frequency", 和 "static_map"参数的意义与全局规划配置文件中的参数相同。publish_frequency设置代价地图发布可视化信息的频率,单位是Hz。rolling_window参数是用来设置在机器人移动过程中是否需要滚动窗口,以保持机器人处于中心位置。"width," "height," 和"resolution" 设置设置代价地图长(米)、高(米)和分辨率(米/格)。分辨率可以设置的与静态地图不同,但是一般情况下两者是相同的。
本地规划器base_local_planner的主要作用是根据规划的全局路径,计算发布给机器人的速度指令。该规划器需要我们根据机器人的规格,配置一些相应的参数。我们创建名为base_local_planner_params.yaml的配置文件:
- TrajectoryPlannerROS:
- max_vel_x: 0.45
- min_vel_x: 0.1
- max_vel_theta: 1.0
- min_in_place_vel_theta: 0.4
-
- acc_lim_theta: 3.2
- acc_lim_x: 2.5
- acc_lim_y: 2.5
-
- holonomic_robot: true
该配置文件声明了机器人的本地规划采用Trajectory Rollout算法。第一段设置了机器人的速度阈值,第二段设置了机器人的加速度阈值。
到此为止,我们已经创建完毕所有需要用到的配置文件,接下来我们需要创建一个启动文件,来启动所有需要的功能。创建move_base.launch的文件:
- <launch>
- <masterauto="start"/>
-
- <!-- Runthe map server -->
- <nodename="map_server" pkg="map_server"type="map_server" args="$(find my_map_package)/my_map.pgm my_map_resolution"/>
-
- <!---Run AMCL -->
- <includefile="$(find amcl)/examples/amcl_omni.launch" />
-
- <nodepkg="move_base" type="move_base" respawn="false"name="move_base" output="screen">
- <rosparam file="$(find my_robot_name_2dnav)/costmap_common_params.yaml"command="load" ns="global_costmap" />
- <rosparam file="$(findmy_robot_name_2dnav)/costmap_common_params.yaml" command="load"ns="local_costmap" />
- <rosparam file="$(findmy_robot_name_2dnav)/local_costmap_params.yaml" command="load" />
- <rosparam file="$(findmy_robot_name_2dnav)/global_costmap_params.yaml" command="load"/>
- <rosparam file="$(findmy_robot_name_2dnav)/base_local_planner_params.yaml"command="load" />
- </node>
- </launch>
在该配置文件中,你需要修改的只有map-server输入的地图,以及如果使用差分驱动的机器人,需要修改"amcl_omni.launch"成"amcl_diff.launch" 。
AMCL有许多的参数设置,会影响机器人的定位效果,具体参考amcldocumentation。
现在,我们已经完成了所有需要的工作,最后一步,运行启动文件,开始导航之旅:
- roslaunch my_robot_configuration.launch
- roslaunch move_base.launch
现在导航功能包应该已经可以顺利运行了,但这绝对不是结束,因为你只能从终端里看到一端乱蹦的代码,如何使用更友好的方式进行机器人导航呢?如果你想使用UI界面,请参考rviz and navigationtutorial,如果你想使用代码,请参考Sending SimpleNavigation Goals 。
无论是 sensor_msgs/LaserScan,还是sensor_msgs/PointCloud ,都和ROS中tf帧信息等时间相关的消息一样,带标准格式的头信息。
#Standard metadata for higher-level flow data types
#sequence ID: consecutively increasing ID
uint32 seq
#Two-integer timestamp that is expressed as:
# * stamp.secs: seconds (stamp_secs) since epoch
# * stamp.nsecs: nanoseconds since stamp_secs
# time-handling sugar is provided by the client library
time stamp
#Frame this data is associated with
# 0: no frame
# 1: global frame
string frame_id
以上是标准头信息的主要部分。seq是消息的顺序标识,不需要手动设置,发布节点在发布消息时,会自动累加。stamp 是消息中与数据相关联的时间戳,例如激光数据中,时间戳对应激光数据的采集时间点。frame_id 是消息中与数据相关联的参考系id,例如在在激光数据中,frame_id对应激光数据采集的参考系。
针对激光雷达,ROS在sensor_msgs 包中定义了专用了数据结构来存储激光消息的相关信息,成为LaserScan。LaserScan消息的格式化定义,为虚拟的激光雷达数据采集提供了方便,在我们讨论如何使用他之前,来看看该消息的结构是什么样的:
#
# Laser scans angles are measured counter clockwise, with 0 facing forward
# (along the x-axis) of the device frame
#
Header header
float32 angle_min # start angle of the scan [rad]
float32 angle_max # end angle of the scan [rad]
float32 angle_increment # angular distance between measurements [rad]
float32 time_increment # time between measurements [seconds]
float32 scan_time # time between scans [seconds]
float32 range_min # minimum range value [m]
float32 range_max # maximum range value [m]
float32[] ranges # range data [m] (Note: values < range_min or > range_max should be discarded)
float32[] intensities # intensity data [device-specific units]
备注中已经详细介绍了每个参数的意义。
使用ROS发布LaserScan格式的激光消息非常简洁,下边是一个简单的例程:
#include <ros/ros.h>
#include <sensor_msgs/LaserScan.h>
int main(int argc, char** argv){
ros::init(argc, argv, "laser_scan_publisher");
ros::NodeHandle n;
ros::Publisher scan_pub = n.advertise<sensor_msgs::LaserScan>("scan", 50);
unsigned int num_readings = 100;
double laser_frequency = 40;
double ranges[num_readings];
double intensities[num_readings];
int count = 0;
ros::Rate r(1.0);
while(n.ok()){
//generate some fake data for our laser scan
for(unsigned int i = 0; i < num_readings; ++i){
ranges[i] = count;
intensities[i] = 100 + count;
}
ros::Time scan_time = ros::Time::now();
//populate the LaserScan message
sensor_msgs::LaserScan scan;
scan.header.stamp = scan_time;
scan.header.frame_id = "laser_frame";
scan.angle_min = -1.57;
scan.angle_max = 1.57;
scan.angle_increment = 3.14 / num_readings;
scan.time_increment = (1 / laser_frequency) / (num_readings);
scan.range_min = 0.0;
scan.range_max = 100.0;
scan.ranges.resize(num_readings);
scan.intensities.resize(num_readings);
for(unsigned int i = 0; i < num_readings; ++i){
scan.ranges[i] = ranges[i];
scan.intensities[i] = intensities[i];
}
scan_pub.publish(scan);
++count;
r.sleep();
}
}
我们将代码分解以便于分析:
#include <sensor_msgs/LaserScan.h>
首先我们需要先包含Laserscan的数据结构。
ros::Publisher scan_pub = n.advertise<sensor_msgs::LaserScan>("scan", 50);
创建一个发布者,以便于后边发布针对scan主题的Laserscan消息。
unsigned int num_readings = 100;
double laser_frequency = 40;
double ranges[num_readings];
double intensities[num_readings];
int count = 0;
ros::Rate r(1.0);
while(n.ok()){
//generate some fake data for our laser scan
for(unsigned int i = 0; i < num_readings; ++i){
ranges[i] = count;
intensities[i] = 100 + count;
}
ros::Time scan_time = ros::Time::now();
这里的例程中我们虚拟一些激光雷达的数据,如果使用真是的激光雷达,这部分数据需要从驱动中获取。
//populate the LaserScan message
sensor_msgs::LaserScan scan;
scan.header.stamp = scan_time;
scan.header.frame_id = "laser_frame";
scan.angle_min = -1.57;
scan.angle_max = 1.57;
scan.angle_increment = 3.14 / num_readings;
scan.time_increment = (1 / laser_frequency) / (num_readings);
scan.range_min = 0.0;
scan.range_max = 100.0;
scan.ranges.resize(num_readings);
scan.intensities.resize(num_readings);
for(unsigned int i = 0; i < num_readings; ++i){
scan.ranges[i] = ranges[i];
scan.intensities[i] = intensities[i];
}
创建scan_msgs::LaserScan数据类型的变量scan,把我们之前伪造的数据填入格式化的消息结构中。
scan_pub.publish(scan);
数据填充完毕后,通过前边定义的发布者,将数据发布。
3、如何发布点云数据
3.1、点云消息的结构
为存储和发布点云消息,ROS定义了sensor_msgs/PointCloud消息结构:
#This message holds a collection of 3d points, plus optional additional information about each point.
#Each Point32 should be interpreted as a 3d point in the frame given in the header
Header header
geometry_msgs/Point32[] points #Array of 3d points
ChannelFloat32[] channels #Each channel should have the same number of elements as points array, and the data in each channel should correspond 1:1 with each point
如上所示,点云消息的结构支持存储三维环境的点阵列,而且channels参数中,可以设置这些点云相关的数据,例如可以设置一个强度通道,存储每个点的数据强度,还可以设置一个系数通道,存储每个点的反射系数,等等。
3.2、通过代码发布点云数据
ROS发布点云数据同样简洁:
#include <ros/ros.h>
#include <sensor_msgs/PointCloud.h>
int main(int argc, char** argv){
ros::init(argc, argv, "point_cloud_publisher");
ros::NodeHandle n;
ros::Publisher cloud_pub = n.advertise<sensor_msgs::PointCloud>("cloud", 50);
unsigned int num_points = 100;
int count = 0;
ros::Rate r(1.0);
while(n.ok()){
sensor_msgs::PointCloud cloud;
cloud.header.stamp = ros::Time::now();
cloud.header.frame_id = "sensor_frame";
cloud.points.resize(num_points);
//we'll also add an intensity channel to the cloud
cloud.channels.resize(1);
cloud.channels[0].name = "intensities";
cloud.channels[0].values.resize(num_points);
//generate some fake data for our point cloud
for(unsigned int i = 0; i < num_points; ++i){
cloud.points[i].x = 1 + count;
cloud.points[i].y = 2 + count;
cloud.points[i].z = 3 + count;
cloud.channels[0].values[i] = 100 + count;
}
cloud_pub.publish(cloud);
++count;
r.sleep();
}
}
分解代码来分析:
#include <sensor_msgs/PointCloud.h>
首先也是要包含sensor_msgs/PointCloud消息结构。
ros::Publisher cloud_pub = n.advertise<sensor_msgs::PointCloud>("cloud", 50);
定义一个发布点云消息的发布者。
sensor_msgs::PointCloud cloud;
cloud.header.stamp = ros::Time::now();
cloud.header.frame_id = "sensor_frame";
为点云消息填充头信息,包括时间戳和相关的参考系id。
cloud.points.resize(num_points);
设置存储点云数据的空间大小。
//we'll also add an intensity channel to the cloud
cloud.channels.resize(1);
cloud.channels[0].name = "intensities";
cloud.channels[0].values.resize(num_points);
设置一个名为“intensity“的强度通道,并且设置存储每个点强度信息的空间大小。
//generate some fake data for our point cloud
for(unsigned int i = 0; i < num_points; ++i){
cloud.points[i].x = 1 + count;
cloud.points[i].y = 2 + count;
cloud.points[i].z = 3 + count;
cloud.channels[0].values[i] = 100 + count;
}
将我们伪造的数据填充到点云消息结构当中。
cloud_pub.publish(cloud);
最后,发布点云数据。
参考连接: http://wiki.ros.org/navigation/Tutorials/RobotSetup/Sensors
nav_msgs/Odometry消息包含有机器人在自由空间中的位置和速度估算值。
# This represents an estimate of a position and velocity in free space.
# The pose in this message should be specified in the coordinate frame given by header.frame_id.
# The twist in this message should be specified in the coordinate frame given by the child_frame_id
Header header
string child_frame_id
geometry_msgs/PoseWithCovariance pose
geometry_msgs/TwistWithCovariance twist
pose参数包含机器人在里程计参考系下的位置估算值,同时带有可选的估算协方差。twist参数包含机器人在子参考系(一般是机器人基础参考系)下的速度,同时带有可选的速度估算协方差。
参见 Transform Configuration教程。
这里使用一个简单的例程,实现 nav_msgs/Odometry消息的发布和tf变换,通过伪造的数据,实现机器人圆周运动。
首先需要将一些以来添加到manifest.xml中:
<depend package="tf"/>
<depend package="nav_msgs"/>
实现代码如下:
#include <ros/ros.h>
#include <tf/transform_broadcaster.h>
#include <nav_msgs/Odometry.h>
int main(int argc, char** argv){
ros::init(argc, argv, "odometry_publisher");
ros::NodeHandle n;
ros::Publisher odom_pub = n.advertise<nav_msgs::Odometry>("odom", 50);
tf::TransformBroadcaster odom_broadcaster;
double x = 0.0;
double y = 0.0;
double th = 0.0;
double vx = 0.1;
double vy = -0.1;
double vth = 0.1;
ros::Time current_time, last_time;
current_time = ros::Time::now();
last_time = ros::Time::now();
ros::Rate r(1.0);
while(n.ok()){
ros::spinOnce(); // check for incoming messages
current_time = ros::Time::now();
//compute odometry in a typical way given the velocities of the robot
double dt = (current_time - last_time).toSec();
double delta_x = (vx * cos(th) - vy * sin(th)) * dt;
double delta_y = (vx * sin(th) + vy * cos(th)) * dt;
double delta_th = vth * dt;
x += delta_x;
y += delta_y;
th += delta_th;
//since all odometry is 6DOF we'll need a quaternion created from yaw
geometry_msgs::Quaternion odom_quat = tf::createQuaternionMsgFromYaw(th);
//first, we'll publish the transform over tf
geometry_msgs::TransformStamped odom_trans;
odom_trans.header.stamp = current_time;
odom_trans.header.frame_id = "odom";
odom_trans.child_frame_id = "base_link";
odom_trans.transform.translation.x = x;
odom_trans.transform.translation.y = y;
odom_trans.transform.translation.z = 0.0;
odom_trans.transform.rotation = odom_quat;
//send the transform
odom_broadcaster.sendTransform(odom_trans);
//next, we'll publish the odometry message over ROS
nav_msgs::Odometry odom;
odom.header.stamp = current_time;
odom.header.frame_id = "odom";
//set the position
odom.pose.pose.position.x = x;
odom.pose.pose.position.y = y;
odom.pose.pose.position.z = 0.0;
odom.pose.pose.orientation = odom_quat;
//set the velocity
odom.child_frame_id = "base_link";
odom.twist.twist.linear.x = vx;
odom.twist.twist.linear.y = vy;
odom.twist.twist.angular.z = vth;
//publish the message
odom_pub.publish(odom);
last_time = current_time;
r.sleep();
}
}
下面来剖析代码进行分析:
#include <tf/transform_broadcaster.h>
#include <nav_msgs/Odometry.h>
我们需要实现“odom”参考系到“base_link”参考系的变换,以及nav_msgs/Odometry消息的发布,所以首先需要包含相关的头文件。
ros::Publisher odom_pub = n.advertise<nav_msgs::Odometry>("odom", 50);
tf::TransformBroadcaster odom_broadcaster;
定义一个消息发布者来发布“odom”消息,在定义一个tf广播,来发布tf变换信息。
double x = 0.0;
double y = 0.0;
double th = 0.0;
默认机器人的起始位置是odom参考系下的0点。
double vx = 0.1;
double vy = -0.1;
double vth = 0.1;
我们设置机器人的默认前进速度,让机器人的base_link参考系在odom参考系下以x轴方向0.1m/s,Y轴速度-0.1m/s,角速度0.1rad/s的状态移动,这种状态下,可以让机器人保持圆周运动。
ros::Rate r(1.0);
使用1Hz的频率发布odom消息,当然,在实际系统中,往往需要更快的速度进行发布。
//compute odometry in a typical way given the velocities of the robot
double dt = (current_time - last_time).toSec();
double delta_x = (vx * cos(th) - vy * sin(th)) * dt;
double delta_y = (vx * sin(th) + vy * cos(th)) * dt;
double delta_th = vth * dt;
x += delta_x;
y += delta_y;
th += delta_th;
使用我们设置的速度信息,来计算并更新里程计的信息,包括单位时间内机器人在x轴、y轴的坐标变化和角度的变化。在实际系统中,需要更具里程计的实际信息进行更新。
//since all odometry is 6DOF we'll need a quaternion created from yaw
geometry_msgs::Quaternion odom_quat = tf::createQuaternionMsgFromYaw(th);
为了兼容二维和三维的功能包,让消息结构更加通用,里程计的偏航角需要转换成四元数才能发布,辛运的是,ROS为我们提供了偏航角与四元数相互转换的功能。
//first, we'll publish the transform over tf
geometry_msgs::TransformStamped odom_trans;
odom_trans.header.stamp = current_time;
odom_trans.header.frame_id = "odom";
odom_trans.child_frame_id = "base_link";
创建一个tf发布需要使用的TransformStamped类型消息,然后根据消息结构填充当前的时间戳、参考系id、子参考系id,注意两个参考系的id必须要是“odom”和“base_link”。
odom_trans.transform.translation.x = x;
odom_trans.transform.translation.y = y;
odom_trans.transform.translation.z = 0.0;
odom_trans.transform.rotation = odom_quat;
填充里程计信息,然后发布tf变换的消息。
//next, we'll publish the odometry message over ROS
nav_msgs::Odometry odom;
odom.header.stamp = current_time;
别忘了,我们还要发布nav_msgs/Odometry消息,让导航包获取机器人的速度。创建消息变量,然后填充时间戳。
//set the position
odom.pose.pose.position.x = x;
odom.pose.pose.position.y = y;
odom.pose.pose.position.z = 0.0;
odom.pose.pose.orientation = odom_quat;
//set the velocity
odom.child_frame_id = "base_link";
odom.twist.twist.linear.x = vx;
odom.twist.twist.linear.y = vy;
odom.twist.twist.angular.z = vth;
填充机器人的位置、速度,然后发布消息。注意,我们发布的是机器人本体的信息,所以参考系需要填"base_link"。
参考链接:http://wiki.ros.org/navigation/Tutorials/RobotSetup/Odom
ROS中的很多软件包都需要机器人发布tf变换树,那么什么是tf变换树呢?抽象的来讲,一棵tf变换树定义了不同坐标系之间的平移与旋转变换关系。具体来说,我们假设有一个机器人,包括一个机器人移动平台和一个安装在平台之上的激光雷达,以这个机器人为例,定义两个坐标系,一个坐标系以机器人移动平台的中心为原点,称为base_link参考系,另一个坐标系以激光雷达的中心为原点,称为base_laser参考系。
假设在机器人运行过程中,激光雷达可以采集到距离前方障碍物的数据,这些数据当然是以激光雷达为原点的测量值,换句话说,也就是base_laser参考系下的测量值。现在,如果我们想使用这些数据帮助机器人完成避障功能,当然,由于激光雷达在机器人之上,直接使用这些数据不会产生太大的问题,但是激光雷达并不在机器人的中心之上,在极度要求较高的系统中,会始终存在一个雷达与机器人中心的偏差值。这个时候,如果我们采用一种坐标变换,将及激光数据从base_laser参考系变换到base_link参考下,问题不就解决了么。这里我们就需要定义这两个坐标系之间的变换关系。
为了定义这个变换关系,假设我们已知激光雷达安装的位置在机器人的中心点上方20cm,前方10cm处。这就根据这些数据,就足以定义这两个参考系之间的变换关系:当我们获取激光数据后,采用(x: 0.1m, y: 0.0m, z: 0.2m)的坐标变换,就可以将数据从base_laser参考系变换到base_link参考系了。当然,如果要方向变化,采用(x: -0.1m, y: 0.0m, z: -0.20m)的变换即可。
从上边的示例看来,参考系之间的坐标变换好像并不复杂,但是在复杂的系统中,存在的参考系可能远远大于两个,如果我们都使用这种手动的方式进行变换,估计很快你就会被繁杂的坐标关系搞蒙了。ROS提供的tf变换就是为解决这个问题而生的,tf功能包提供了存储、计算不同数据在不同参考系之间变换的功能,我们只需要告诉tf树这些参考系之间的变换公式即可,这颗tf树就可以树的数据结构,管理我们所需要的参考系变换。
还是以上边的示例为基础,为了定义和存储base_link和base_laser两个参考系之间的关系,我们需要将他们添加到tf树中。从树的概念上来讲,tf树中的每个节点都对应一个参考系,而节点之间的边对应于参考系之间的变换关系。tf就是使用这样的树结构,保证每两个参考系之间只有一种遍历方式,而且所有变换关系,都是母节点到子节点的变换。
为了定义上边示例中的参考系,我们需要定义两个节点,一个对应于base_link参考系,一个对应于base_laser参考系。为了创建两个节点之间的边,我们首先需要决定哪一个节点作为母节点,哪一个节点作为子节点,这一点在tf树中是非常重要的。这里我们选择base_link作为母节点,这样会方便后边为机器人添加更多的传感器作为子节点。所以,根据之前的分析,从base_link节点到base_laser节点的变换关系为(x: 0.1m, y: 0.0m, z: 0.2m)。设置完毕后,我们就可以通过调用tf库,直接完成base_laser参考系到base_link参考系的数据坐标变换了。
通过上边的分析,应该可以从理论上帮助你理解什么是tf,以及tf的功能了。在实际应用中,我们需要使用代码来完成这些理论,下边我们就来看看如何使用代码来调用tf的变换。
代码的总体思路分为两个部分:
(1)编写一个节点,广播两个参考系之间的tf变换关系
(2)编写另外一个节点,订阅tf树,然后从tf树中遍历到两个参考系之间的变换公式,然后通过公式计算数据的变换。
我们先来完成第一步。首先我们来创建一个功能包,用于后边程序的放置,这里需要依赖roscpp、tf、geometry_msgs。
$ cd %TOP_DIR_YOUR_CATKIN_WS%/src
$ catkin_create_pkg robot_setup_tf roscpp tf geometry_msgs
进入功能包,创建src/tf_broadcaster.cpp文件,来完成广播节点的代码:
#include <ros/ros.h>
#include <tf/transform_broadcaster.h>
int main(int argc, char** argv){
ros::init(argc, argv, "robot_tf_publisher");
ros::NodeHandle n;
ros::Rate r(100);
tf::TransformBroadcaster broadcaster;
while(n.ok()){
broadcaster.sendTransform(
tf::StampedTransform(
tf::Transform(tf::Quaternion(0, 0, 0, 1), tf::Vector3(0.1, 0.0, 0.2)),
ros::Time::now(),"base_link", "base_laser"));
r.sleep();
}
}
逐行分析如上代码:
#include <tf/transform_broadcaster.h>
因为后边会使用到tf::TransformBroadcaster类的实例,来完成tf树的广播,所以需要先包含相关的头文件。
tf::TransformBroadcaster broadcaster;
创建一个tf::TransformBroadcaster类的实例,用来广播 base_link → base_laser的变换关系。
broadcaster.sendTransform(
tf::StampedTransform(
tf::Transform(tf::Quaternion(0, 0, 0, 1), tf::Vector3(0.1, 0.0, 0.2)),
ros::Time::now(),"base_link", "base_laser"));
这部分是代码的关键所在。通过TransformBroadcaster 类来发布变换关系的接口,需要五个参数。首先是两个参考系之间的旋转变换,通过btQuaternion四元数来存储旋转变换的参数,因为我们用到的两个参考系没有发生旋转变换,所以倾斜角、滚动角、偏航角都是0。第二个参数是坐标的位移变换,我们用到的两个参考系在X轴和Z轴发生了位置,根据位移值填入到btVector3 向量中。第三个参数是时间戳,直接太难过ROS的API完成。第四个参数是母节点存储的参考系,即base_link,最后一个参数是子节点存储的参考系,即base_laser。
上一节讲解了如何使用代码编写一个广播tf变换的节点,这一节,我们编写一个订阅tf广播的节点,并且使用tf树中base_link到base_laser的变换关系,完成数据的坐标变换。在robot_setup_tf功能包中创建src/tf_listener.cpp文件,代码如下:
#include <ros/ros.h>
#include <geometry_msgs/PointStamped.h>
#include <tf/transform_listener.h>
void transformPoint(const tf::TransformListener& listener){
//we'll create a point in the base_laser frame that we'd like to transform to the base_link frame
geometry_msgs::PointStamped laser_point;
laser_point.header.frame_id = "base_laser";
//we'll just use the most recent transform available for our simple example
laser_point.header.stamp = ros::Time();
//just an arbitrary point in space
laser_point.point.x = 1.0;
laser_point.point.y = 0.2;
laser_point.point.z = 0.0;
try{
geometry_msgs::PointStamped base_point;
listener.transformPoint("base_link", laser_point, base_point);
ROS_INFO("base_laser: (%.2f, %.2f. %.2f) -----> base_link: (%.2f, %.2f, %.2f) at time %.2f",
laser_point.point.x, laser_point.point.y, laser_point.point.z,
base_point.point.x, base_point.point.y, base_point.point.z, base_point.header.stamp.toSec());
}
catch(tf::TransformException& ex){
ROS_ERROR("Received an exception trying to transform a point from \"base_laser\" to \"base_link\": %s", ex.what());
}
}
int main(int argc, char** argv){
ros::init(argc, argv, "robot_tf_listener");
ros::NodeHandle n;
tf::TransformListener listener(ros::Duration(10));
//we'll transform a point once every second
ros::Timer timer = n.createTimer(ros::Duration(1.0), boost::bind(&transformPoint, boost::ref(listener)));
ros::spin();
}
进行详细的分析:
#include <tf/transform_listener.h>
在后边的代码中,我们会使用到tf::TransformListener对象,该对象会自动订阅ROS中的tf消息,并且管理所有的变换关系数据。所以需要先包含相关的头文件。
void transformPoint(const tf::TransformListener& listener){
我们创建一个回调函数,每次收到tf消息时,都会自动调用该函数,上一节我们设置了发布tf消息的频率是1Hz,所以回调函数执行的频率也是1Hz。在回调函数中,我们需要完成数据从base_laser到base_link参考系的坐标变换。
//we'll create a point in the base_laser frame that we'd like to transform to the base_link frame
geometry_msgs::PointStamped laser_point;
laser_point.header.frame_id = "base_laser";
//we'll just use the most recent transform available for our simple example
laser_point.header.stamp = ros::Time();
//just an arbitrary point in space
laser_point.point.x = 1.0;
laser_point.point.y = 0.2;
laser_point.point.z = 0.0;
我们创建了一个geometry_msgs::PointStamped类型的虚拟点,该点的坐标为(1.0,0.2,0.0)。该类型包含标准的header消息结构,这样,我们可以就可以在消息中加入发布数据的时间戳和参考系的id。
try{
geometry_msgs::PointStamped base_point;
listener.transformPoint("base_link", laser_point, base_point);
ROS_INFO("base_laser: (%.2f, %.2f. %.2f) -----> base_link: (%.2f, %.2f, %.2f) at time %.2f",
laser_point.point.x, laser_point.point.y, laser_point.point.z,
base_point.point.x, base_point.point.y, base_point.point.z, base_point.header.stamp.toSec());
}
这里是代码的关键位置。我们已经在base_laser参考系下虚拟了一个数据点,那么怎样将该点的数据转换到base_base参考系下呢?使用TransformListener 对象中的transformPoint()函数即可,该函数包含三个参数:第一个参数是需要转换到的参考系id,当然是base_link了;第二个参数是需要转换的原始数据;第三个参数用来存储转换完成的数据。该函数执行完毕后,base_point就是我们转换完成的点坐标了!
catch(tf::TransformException& ex){
ROS_ERROR("Received an exception trying to transform a point from \"base_laser\" to \"base_link\": %s", ex.what());
}
为了保证代码的稳定性,我们也需要应对出错处理,例如当tf并没有发布需要的变换关系时,在执行transformPoint时就会出现错误。
到此为止,我们已经完成了代码编写的全部流程,接下来就是编译代码了!打开功能包的CMakeLists.txt文件,加入相应的编译选项:
add_executable(tf_broadcaster src/tf_broadcaster.cpp)
add_executable(tf_listener src/tf_listener.cpp)
target_link_libraries(tf_broadcaster ${catkin_LIBRARIES})
target_link_libraries(tf_listener ${catkin_LIBRARIES})
然后保存文件,经行编译:
$ cd %TOP_DIR_YOUR_CATKIN_WS%
$ catkin_make
终于来到最激动人心的一步,见证奇迹的时刻就要来临!
打开一个终端,运行roscore:
roscore
再打开一个终端,运行tf广播tf_broadcaster:
rosrun robot_setup_tf tf_broadcaster
还要再打开一个终端,运行tf监听tf_listener,将我们虚拟的点坐标,从base_laser转换到base_link参考系下:
rosrun robot_setup_tf tf_listener
如果一切正常,你应该已经可以在第三个终端中看到如下的数据了:
[ INFO] 1248138528.200823000: base_laser: (1.00, 0.20. 0.00) -----> base_link: (1.10, 0.20, 0.20) at time 1248138528.19
[ INFO] 1248138529.200820000: base_laser: (1.00, 0.20. 0.00) -----> base_link: (1.10, 0.20, 0.20) at time 1248138529.19
[ INFO] 1248138530.200821000: base_laser: (1.00, 0.20. 0.00) -----> base_link: (1.10, 0.20, 0.20) at time 1248138530.19
[ INFO] 1248138531.200835000: base_laser: (1.00, 0.20. 0.00) -----> base_link: (1.10, 0.20, 0.20) at time 1248138531.19
[ INFO] 1248138532.200849000: base_laser: (1.00, 0.20. 0.00) -----> base_link: (1.10, 0.20, 0.20) at time 1248138532.19
从上边的信息中,我们可以清晰的看到,我们虚拟的坐标点,成功转换到了base_link参考系下,坐标位置变换成了(1.10, 0.20, 0.20)。
到此为止,我们已经完成了例程的所有讲解,在你需要完整的真实系统中,请根据需求将PointStamped数据类型修改成你所使用传感器发布的实际消息对应类型。
参考链接:http://wiki.ros.org/navigation/Tutorials/RobotSetup/TF
Unified Robot Description Format,统一机器人描述格式,简称为URDF。ROS中的urdf功能包包含一个URDF的C++解析器,URDF文件使用XML格式描述机器人模型。
URDF由一些不同的功能包和组件组成,下图描述了这些组件之间的联系。
如下图所示,本节将创建一个如下图所示结构的机器人模型。
上图所示是一个树形机器人模型,我们先从机器人的整体结构出发,不考虑过多的细节,可以将机器人通过如下的URDF表示:
<robot name="test_robot">
<link name="link1" />
<link name="link2" />
<link name="link3" />
<link name="link4" />
<joint name="joint1" type="continuous">
<parent link="link1"/>
<child link="link2"/>
</joint>
<joint name="joint2" type="continuous">
<parent link="link1"/>
<child link="link3"/>
</joint>
<joint name="joint3" type="continuous">
<parent link="link3"/>
<child link="link4"/>
</joint>
</robot>
上边的URDF模型定义了机器人的4个环节(link),然后定义了三个关节(joint)来描述环节之间的关联。
ROS为用户提供了一个检查URDF语法的工具:
$ sudo apt-get install liburdfdom-tools
安装完毕后,执行检查:
check_urdf my_robot.urdf
如果一切正常,将会有如下显示:
robot name is: test_robot
---------- Successfully Parsed XML ---------------
root Link: link1 has 2 child(ren)
child(1): link2
child(2): link3
child(1): link4
在基础模型之上,我们为机器人添加尺寸大小。由于每个环节的参考系都位于该环节的底部,关节也是如此,所以在表示尺寸大小时,只需要描述其相对于连接的关节的相对位置关系即可。URDF中的<origin>域就是用来表示这种相对关系。
例如,joint2相对于连接的link1在x轴和y轴都有相对位移,而且在x轴上还有90度的旋转变换,所以表示成<origin>域的参数就如下所示:
<origin xyz="-2 5 0" rpy="0 0 1.57" />
为所有关节应用尺寸:
<robot name="test_robot">
<link name="link1" />
<link name="link2" />
<link name="link3" />
<link name="link4" />
<joint name="joint1" type="continuous">
<parent link="link1"/>
<child link="link2"/>
<origin xyz="5 3 0" rpy="0 0 0" />
</joint>
<joint name="joint2" type="continuous">
<parent link="link1"/>
<child link="link3"/>
<origin xyz="-2 5 0" rpy="0 0 1.57" />
</joint>
<joint name="joint3" type="continuous">
<parent link="link3"/>
<child link="link4"/>
<origin xyz="5 0 0" rpy="0 0 -1.57" />
</joint>
</robot>
再次使用check_urdf检查通过后继续下一步。
如果我们为机器人的关节添加旋转轴参数,那么该机器人模型就可以具备基本的运动学参数。
例如,joint2围绕正y轴旋转,可以表示成:
<axis xyz="0 1 0" />
同理,joint1的旋转轴是:
<axis xyz="-0.707 0.707 0" />
应用到我们的URDF中:
<robot name="test_robot">
<link name="link1" />
<link name="link2" />
<link name="link3" />
<link name="link4" />
<joint name="joint1" type="continuous">
<parent link="link1"/>
<child link="link2"/>
<origin xyz="5 3 0" rpy="0 0 0" />
<axis xyz="-0.9 0.15 0" />
</joint>
<joint name="joint2" type="continuous">
<parent link="link1"/>
<child link="link3"/>
<origin xyz="-2 5 0" rpy="0 0 1.57" />
<axis xyz="-0.707 0.707 0" />
</joint>
<joint name="joint3" type="continuous">
<parent link="link3"/>
<child link="link4"/>
<origin xyz="5 0 0" rpy="0 0 -1.57" />
<axis xyz="0.707 -0.707 0" />
</joint>
</robot>
便忘记使用check_urdf检查语法错误。
我们现在已经完成了一个简单的URDF模型创建,ROS提供了相应的工具可以让URDF图像化显示出来。
$ urdf_to_graphiz my_robot.urdf
然后打开生成的pdf文件,即可看到图形化的URDF:
参见:Example
上文已经提到了URDF语法检查工具check_urdf,在indigo版本的ROS中,执行如下命令即可:
rosrun urdfdom check_urdf /tmp/pr2.urdf
检查结果如下:
上文同样提到的URDF可视化工具urdf_to_graphiz,在indigo版本额ROS中属于liburdfdom-tools包中的一个工具,可以使用如下命令安装:
sudo apt-get install liburdfdom-tools
可视化工具的使用方法:
urdf_to_graphiz pr2.urdf
结果如下所示:
1、urdf功能包:http://wiki.ros.org/urdf
2、urdf语法规范:http://wiki.ros.org/urdf/XML
3、urdf教程:http://wiki.ros.org/urdf/Tutorials
4、urdf示例:http://wiki.ros.org/urdf/ExamplesURDF文件完成后,可以在rviz中显示机器人的模型,如果要在gazebo中进行物理环境仿真,还需要为URDF文件加入一些gazebo相关的标签。既然是仿真,那么机器人应该像真在真实环境中一样,可以通过传感器感知周围环境,还可以根据指令进行运动。
在gazebo中可以通过插入一些插件,来仿真机器人的传感器、执行器的特性,这些插件通过<gazebo>元素中的<plugin>标签描述,使用下边的格式加入到URDF文件中:
<gazebo>
<plugin name="differential_drive_controller" filename="libdiffdrive_plugin.so">
... plugin parameters ...
</plugin>
</gazebo>
当然,这些插件常常需要附着于机器人的某个部分,比如要加入一个激光传感器的插件,那么我们需要将这个插件与机器人模型中的激光传感器link绑定:
<gazebo reference="your_link_name">
<plugin name="your_link_laser_controller" filename="libgazebo_ros_laser.so">
... plugin parameters ...
</plugin>
</gazebo>
gazebo默认支持不少常用的设备,可以找到ros安装目录中的gazebo_plugins功能包,里边就是所有支持的插件。当然,你也可以自己编写需要的gazebo插件。
下边列出一些常用插件的使用方法:
很多机器人本体都采用差速驱动的方式,gazebo提供差速机器人的仿真插件,可以直接将下边的代码放到URDF文件中,修改相应的参数,指定运动控制需要订阅的主题,让机器人在gazebo中动起来。
<gazebo>
<plugin name="differential_drive_controller" filename="libgazebo_ros_diff_drive.so">
<alwaysOn>true</alwaysOn>
<updateRate>${update_rate}</updateRate>
<leftJoint>base_link_right_wheel_joint</leftJoint>
<rightJoint>base_link_left_wheel_joint</rightJoint>
<wheelSeparation>0.5380</wheelSeparation>
<wheelDiameter>0.2410</wheelDiameter>
<torque>20</torque>
<commandTopic>cmd_vel</commandTopic>
<odometryTopic>odom</odometryTopic>
<odometryFrame>odom</odometryFrame>
<robotBaseFrame>base_footprint</robotBaseFrame>
</plugin>
</gazebo>
在gazebo中也可以让机器人的摄像头看到虚拟的环境,将下边的代码放到URDF文件中,“reference”修改成机器人的摄像头link,还有一些分辨率、刷新率、发布的图像主题等等参数。把机器人模型在gazebo中运行起来后,用image_view或者rviz就可以看到机器人在虚拟环境中的视野了。
<!-- camera -->
<gazebo reference="camera_link">
<sensor type="camera" name="camera1">
<update_rate>30.0</update_rate>
<camera name="head">
<horizontal_fov>1.3962634</horizontal_fov>
<image>
<width>800</width>
<height>800</height>
<format>R8G8B8</format>
</image>
<clip>
<near>0.02</near>
<far>300</far>
</clip>
<noise>
<type>gaussian</type>
<!-- Noise is sampled independently per pixel on each frame.
That pixel's noise value is added to each of its color
channels, which at that point lie in the range [0,1]. -->
<mean>0.0</mean>
<stddev>0.007</stddev>
</noise>
</camera>
<plugin name="camera_controller" filename="libgazebo_ros_camera.so">
<alwaysOn>true</alwaysOn>
<updateRate>0.0</updateRate>
<cameraName>rrbot/camera1</cameraName>
<imageTopicName>image_raw</imageTopicName>
<cameraInfoTopicName>camera_info</cameraInfoTopicName>
<frameName>camera_link</frameName>
<hackBaseline>0.07</hackBaseline>
<distortionK1>0.0</distortionK1>
<distortionK2>0.0</distortionK2>
<distortionK3>0.0</distortionK3>
<distortionT1>0.0</distortionT1>
<distortionT2>0.0</distortionT2>
</plugin>
</sensor>
</gazebo>
我们来解释一下这个插件中的参数含义:
<gazebo reference="camera_link">
插件都是用来描述link、joint的,是一种虚无的属性描述,需要关联相应的实体,所以首先我们需要通过reference参数来定义关联的link或者joint。这里我们关联的是camera_link。
<sensor type="camera" name="camera1">
然后声明插件的类型,并为该插件取一个唯一的名称。
<update_rate>30.0</update_rate>
设置摄像头数据更新的最大频率。
<horizontal_fov>1.3962634</horizontal_fov>
<image>
<width>800</width>
<height>800</height>
<format>R8G8B8</format>
</image>
<clip>
<near>0.02</near>
<far>300</far>
</clip>
再来设置一些摄像头相关的参数,尽量让这些仿真的参数与实际使用的硬件相同。
<plugin name="camera_controller" filename="libgazebo_ros_camera.so">
关联摄像头插件,该插件已经在gazebo中实现,所以直接关联即可。
<cameraName>rrbot/camera1</cameraName>
<imageTopicName>image_raw</imageTopicName>
<cameraInfoTopicName>camera_info</cameraInfoTopicName>
<frameName>camera_link</frameName>
声明摄像头发布的消息话题,以及数据所在的参考系。
Kinect在机器人导航中用的也很多,为机器人的kinect link绑定这个kinect插件,我们就可以在gazebo中进行SLAM、导航了,对于一些初学者,就算没有实体机器人,也可以在仿真器中玩转机器人。
<gazebo>
<plugin name="${link_name}_controller" filename="libgazebo_ros_openni_kinect.so">
<baseline>0.2</baseline>
<alwaysOn>true</alwaysOn>
<updateRate>1.0</updateRate>
<cameraName>${camera_name}_ir</cameraName>
<imageTopicName>/${camera_name}/depth/image_raw</imageTopicName>
<cameraInfoTopicName>/${camera_name}/depth/camera_info</cameraInfoTopicName>
<depthImageTopicName>/${camera_name}/depth/image_raw</depthImageTopicName>
<depthImageInfoTopicName>/${camera_name}/depth/camera_info</depthImageInfoTopicName>
<pointCloudTopicName>/${camera_name}/depth/points</pointCloudTopicName>
<frameName>${frame_name}</frameName>
<pointCloudCutoff>0.5</pointCloudCutoff>
<distortionK1>0.00000001</distortionK1>
<distortionK2>0.00000001</distortionK2>
<distortionK3>0.00000001</distortionK3>
<distortionT1>0.00000001</distortionT1>
<distortionT2>0.00000001</distortionT2>
<CxPrime>0</CxPrime>
<Cx>0</Cx>
<Cy>0</Cy>
<focalLength>0</focalLength>
<hackBaseline>0</hackBaseline>
</plugin>
</gazebo>
有没有感叹过激光传感器太贵了!现在不要9999,不要999,只要把激光传感器的插件放到URDF模型中,上万的激光传感器在gazebo里随便用,妈妈再也不用担心玩机器人败家了。
<gazebo reference="hokuyo_link">
<sensor type="ray" name="head_hokuyo_sensor">
<pose>0 0 0 0 0 0</pose>
<visualize>false</visualize>
<update_rate>40</update_rate>
<ray>
<scan>
<horizontal>
<samples>720</samples>
<resolution>1</resolution>
<min_angle>-1.570796</min_angle>
<max_angle>1.570796</max_angle>
</horizontal>
</scan>
<range>
<min>0.10</min>
<max>30.0</max>
<resolution>0.01</resolution>
</range>
<noise>
<type>gaussian</type>
<!-- Noise parameters based on published spec for Hokuyo laser
achieving "+-30mm" accuracy at range < 10m. A mean of 0.0m and
stddev of 0.01m will put 99.7% of samples within 0.03m of the true
reading. -->
<mean>0.0</mean>
<stddev>0.01</stddev>
</noise>
</ray>
<plugin name="gazebo_ros_head_hokuyo_controller" filename="libgazebo_ros_laser.so">
<topicName>/rrbot/laser/scan</topicName>
<frameName>hokuyo_link</frameName>
</plugin>
</sensor>
</gazebo>
当然,实践是第一要义,这些插件用起来效果到底怎么样,还得你上手来试试。ROS中的很多机器人也提供了gazebo中仿真的模型和实验的案例,如果你还不太明白,直接运行这些已有的模型,看看别人写的代码,理解会更快。这里,推荐一个husky机器人的案例,可以在gazebo中SLAM和自主导航。
husky机器人的主页是http://wiki.ros.org/Robots/Husky,仿真案例在Demo Applications那里。
联系客服