ROS学习笔记1.Mapping
为了执行自主导航,机器人必须拥有环境地图。机器人将使用此地图进行许多操作,例如规划轨迹、避开障碍物等。
您可以为机器人提供预先构建的环境地图(在极少数情况下,您已经拥有具有正确格式的地图),也可以自己构建地图。第二种选择是最常见的。
在 Rviz 中可视化地图
对于制图,您基本上需要使用 RViz 的 2 个display:
- LaserScan Display
- Map Display
- (可选)Robot Model(在地图上显示机器人模型)
a) 执行以下命令以启动 slam_gmapping 节点。我们需要运行此节点才能可视化地图。
roslaunch turtlebot_navigation_gazebo gmapping_demo.launch
b) 执行以下命令来启动 Rviz。
rosrun rviz rviz
c) 按照以下步骤将 LaserScan 显示器添加到 RViz。
可视化 LaserScan
1.- 单击“ Displays ”下的“Add”按钮并选择 LaserScan 显示。
2.- 在激光扫描显示属性中,输入激光发布其数据的主题名称。
3.- 在“Global Options”中,将Fixed Frame更改为map。
4.- 要在 Rviz 中查看机器人,你可以选择添加 RobotModel 显示。这将在屏幕上显示机器人的当前情况。你还可以通过在 Rviz 中添加 TF 显示来尝试显示机器人的所有参考坐标系。
5.- 构建的激光“地图”会随着时间的推移而消失,因为 Rviz 只能缓冲有限数量的激光扫描。
可视化 Map
- 单击“添加”按钮并添加地图显示。
- 在地图显示属性中,将主题设置为 /map。
保存 RViz 配置
- 转到 RViz 屏幕的左上角,打开文件菜单。选择将配置另存为选项。
- 输入您想要的配置文件名称,然后按保存。
用保存的配置打开RViz
rviz -d ~/catkin_ws/src/myconfig.rviz
移动小车
a) 执行以下命令以开始在环境中移动机器人。
roslaunch turtlebot_teleop keyboard_teleop.launch
b) 在整个房间内移动,直到创建出环境的完整地图。
SLAM
同步定位和地图构建 (SLAM)。这个名称定义了机器人问题,即构建未知环境的地图,同时跟踪机器人在正在构建的地图上的位置。这基本上就是地图构建正在解决的问题。
The gmapping package
gmapping ROS 包是特定 SLAM 算法(称为 gmapping)的实现。这意味着,有人已经实现了 gmapping 算法供您在 ROS 中使用,而无需自己编写代码。因此,如果您使用 ROS 导航堆栈,您只需要知道(并且不必担心)如何为您的特定机器人配置 gmapping。
gmapping 包包含一个名为 slam_gmapping 的 ROS 节点,它允许您使用移动机器人在环境中移动时提供的激光和姿势数据创建 2D 地图。该节点基本上从激光和机器人的变换中读取数据,并将其转换为占据栅格地图 OGM(Occupancy Grid Map)。
以上操作流程:
- 使用之前创建的配置启动文件 (gmapping_demo.launch) 启动 Kobuki 机器人的 gmapping 包。
- 该启动文件启动了一个 slam_gmapping 节点(来自 gmapping 包)。然后,您将机器人移动到房间内。
- 然后,slam_gmapping 节点订阅了the Laser (/kobuki/laser/scan) 和the Transform 主题 (/tf),以获取所需的数据,并构建了地图。
- 生成的地图在整个过程中发布到 /map 主题中,这就是您可以看到使用 Rviz 构建地图的过程的原因(因为 Rviz 只是可视化主题)。
/map topic使用 nav_msgs/OccupancyGrid 消息类型,因为它是 OGM。占用率表示为 {0, 100} 范围内的整数。0 表示完全空闲,100 表示完全占用,特殊值 -1 表示完全未知。
保存地图
ROS Navigation Stack中可用的另一个包是 map_server 包。此包提供 map_saver 节点,允许我们从 ROS 服务访问地图数据并将其保存到文件中。
当您请求 map_saver 保存当前地图时,地图数据将保存到两个文件中:一个是 YAML 文件,其中包含地图元数据和图像名称;另一个是图像本身,其中包含占用网格地图的编码数据。
保存地图的命令
rosrun map_server map_saver -f name_of_map
该命令将从地图主题中获取地图数据,并将其写入2个文件,name_of_map.pgm和name_of_map.yaml。
生成的 YAML 文件将包含以下 6 个字段:
- image:包含生成地图图像的文件的名称。
- resolution:地图的分辨率(以米/像素为单位)。
- origin:地图左下角像素的坐标。此坐标以 2D(x,y)表示。第三个值表示旋转。如果没有旋转,则值为 0。
- occupied_thresh:值大于此值的像素将被视为完全占用区域。
- free_thresh:值小于此值的像素将被视为完全空闲区域。
- negate:反转地图的颜色。默认情况下,白色表示完全空闲,黑色表示完全占用。
概率栅格地图 PGM (Probability Grid Map)
- PGM 是基于概率的方法表示环境信息,它是 OGM 的一种扩展或特例。
- 在 PGM 中,每个栅格保存的不是简单的二值占据信息,而是一个概率值,表示该栅格被占用的可能性。这个概率可以基于传感器数据的融合和不确定性进行更新和计算。
- PGM 更关注对不确定性的建模,适用于复杂或动态环境下的地图构建。
提供地图
除了 map_saver 节点,map_server 包还提供 map_server 节点。此节点从磁盘读取地图文件,并通过 ROS 服务将地图提供给任何其他请求它的节点。
许多节点向 map_server 请求机器人正在移动的当前地图。例如,此请求由 move_base 节点执行,以便从地图中获取数据并使用它执行路径规划,或由定位节点执行,以便确定机器人在地图中的位置。
获取地图需要调用的服务是:
- static_map (nav_msgs/GetMap):通过此服务提供地图占用数据。
除了通过上述服务请求地图外,还有两个锁定主题可以连接,以便获取带有地图的 ROS 消息。此节点写入地图数据的主题是:
- map_metadata(nav_msgs/MapMetaData):通过此主题提供地图元数据。
- map(nav_msgs/OccupancyGrid):通过此主题提供地图占用数据。
启动map_server 节点的命令
要启动 map_server 节点以提供给定地图文件的地图信息,请使用以下命令:
rosrun map_server map_server map_file.yaml
(map_file.yaml是先前创建的地图文件)
Echo of the /map_metadata topic:
Echo of the /map topic:
创建一个package来启动这个节点
a) 创建一个名为 provide_map 的包,在此包内,创建一个启动文件,该文件将启动 map_server 节点以提供您创建的地图。
cd ~/catkin_ws/src #创建软件包 catkin_create_pkg provide_map rospy std_msgs nav_msgs cd ~/catkin_ws #编译 catkin_make #更新源 source devel/setup.bash cd ~/catkin_ws/src/provide_map mkdir launch cd launch #创建启动文件 touch provide_map.launch
b)编辑provide_map.launch内容
<launch> <arg name="map_file" default="$(find provide_map)/maps/mymap.yaml"/> <!-- 启动 map_server 节点 --> <node name="map_server" pkg="map_server" type="map_server" output="screen" args="$(arg map_file)"/> </launch>
c) 执行您的启动文件,并通过访问正确的主题来检查是否正在提供地图。
roslaunch provide_map provide_map.launch
获取地图数据
rosservice call /static_map "{}"
创建一个Service Client 来调用这个服务。
a) 创建一个名为 get_map_data 的新包。添加 rospy 作为依赖项。
cd ~/catkin_ws/src catkin_create_pkg get_map_data rospy std_msgs nav_msgs cd ~/catkin_ws catkin_make source devel/setup.bash
b) 在此包内,创建一个名为 call_map_service.py 的文件。在此文件中,编写服务客户端的代码。此服务客户端将调用 /static_map 服务以获取地图数据,然后在屏幕上打印地图的尺寸和分辨率。
cd ~/catkin_ws/src/get_map_data mkdir scripts cd scripts touch call_map_service.py chmod +x call_map_service.py
#!/usr/bin/env python import rospy from nav_msgs.srv import GetMap def call_map_service(): rospy.init_node('map_service_client') # Wait for the service to be available rospy.wait_for_service('/static_map') try: # Create a service proxy for calling the service static_map_service = rospy.ServiceProxy('/static_map', GetMap) # Call the service response = static_map_service() # Extract map information from the response map_data = response.map resolution = map_data.info.resolution width = map_data.info.width height = map_data.info.height rospy.loginfo(f"Map resolution: {resolution}") rospy.loginfo(f"Map width: {width} cells") rospy.loginfo(f"Map height: {height} cells") rospy.loginfo(f"Map dimensions: {width * resolution} meters x {height * resolution} meters") except rospy.ServiceException as e: rospy.logerr(f"Service call failed: {e}") if __name__ == "__main__": call_map_service()
c) 为您的服务客户端创建一个启动文件,并测试它是否正常工作。
cd ~/catkin_ws/src/get_map_data mkdir launch cd launch touch call_map_service.launch
编辑启动文件
<launch> <!-- Launch the map service client --> <node name="call_map_service" pkg="get_map_data" type="call_map_service.py" output="screen"/> </launch>
测试启动文件
roslaunch get_map_data call_map_service.launch
您创建的地图是静态地图。这意味着地图将始终保持创建时的状态。因此,当您创建地图时,它将在执行映射过程的确切时刻捕获环境。如果出于任何原因,环境将来发生变化,这些变化将不会出现在地图上,因此它将不再有效(或与实际环境不符)。
您创建的地图是 2D 地图。这意味着,地图上出现的障碍物没有高度。因此,例如,如果您尝试使用此地图使用无人机导航,它将无效。
生成3D映射的软件包
硬件要求
配置对于构建正确的地图非常重要。如果机器人配置不当,您将无法获得良好的环境地图。如果没有良好的环境地图,您将无法正确导航。
因此,为了构建正确的地图,您需要满足以下两个要求:
- 提供良好的激光数据
- 提供良好的里程表数据
然后,slam_gmapping 节点将尝试将每个传入的激光读数转换为里程表框架。
Transforms
首先,我们将定义两个框架(坐标框架),一个位于激光中心,另一个位于机器人中心。对于导航,重要的是将机器人的中心放置在机器人的旋转中心。我们将激光框架命名为 base_laser,将机器人框架命名为 base_link。
这里您可以看到它的外观图:
例如,对于本例中使用的 Kobuki 机器人,框架如下所示:
现在,我们需要定义 base_laser 和 base_link 之间的关系(就位置和方向而言)。例如,我们知道 base_laser 框架在 y 轴上的距离为 20 厘米,在 x 轴上的距离为 10 厘米(参考 base_link 框架)。然后我们必须将这种关系提供给机器人。在 ROS 中,激光和机器人底座之间的这种关系称为激光和机器人之间的 TRANSFORM。
为了使 slam_gmapping 节点正常工作,您需要提供 2 个转换:
- 连接到激光的框架 -> base_link:通常是一个固定值,由 robot_state_publisher 或 tf static_transform_publisher 定期广播。
- base_link -> odom:通常由 Odometry 系统提供
由于机器人需要能够随时访问这些信息,我们将这些信息发布到变换树中。变换树就像一个数据库,我们可以在其中找到有关机器人不同框架(元素)之间所有变换的信息。
您可以随时使用以下命令可视化正在运行的系统的变换树:
此命令将生成一个 pdf 文件,其中包含带有系统变换树的图表。
rosrun tf2_tools view_frames.py
现在,让我们想象一下,您刚刚将激光器安装在机器人上,因此激光器和机器人底座之间的变换尚未设置。您能做什么?发布变换基本上有两种方式:
- 使用static_transform_publisher
- 使用transform broadcaster
在本课程中,我们将使用 static_transform_publisher,因为它是最快的方法。static_transform_publisher 是一个现成的节点,允许我们直接使用命令行发布变换。命令的结构如下:
# rosrun tf static_transform_publisher x y z yaw pitch roll frame_id child_frame_id period_in_ms rosrun tf static_transform_publisher 0 0 0 0 0 0 map odom 100
其中:
- x、y、z 是以米为单位的偏移量
- yaw、pitch、roll 是可用于将变换置于任何方向的旋转(以弧度为单位的值)
- period_in_ms 指定发送变换的频率
您还可以创建一个启动文件来启动上述命令,并通过以下方式指定不同的值:
<launch> <node pkg="tf" type="static_transform_publisher" name="name_of_node" args="0 0 0 0 0 0 map odom 100"> </node> </launch>
为 slam_gmapping 节点创建启动文件
创建此启动文件的主要任务是正确设置 slam_gmapping 节点的参数。此节点具有高度可配置性,并且有许多参数可供您更改以提高映射性能。这些参数将从 ROS 参数服务器读取,可以在启动文件本身或单独的参数文件(YAML 文件)中设置。如果您不设置某些参数,它将只采用默认值。您可以在此处查看 slam_gmapping 节点可用的完整参数列表:http://wiki.ros.org/gmapping
常规参数
- base_frame(默认值:“base_link”):表示连接到移动基座的框架的名称。
- map_frame(默认值:“map”):表示连接到地图的框架的名称。
- odom_frame(默认值:“odom”):表示连接到里程计系统的框架的名称。
- map_update_interval(默认值:5.0):设置等待更新地图的时间(以秒为单位)。
激光参数
- maxRange(浮点数):设置激光的最大范围。将此值设置为略高于实际传感器的最大范围的值。
- maxUrange(默认值:80.0):设置激光的最大可用范围。激光束将被裁剪为此值。
- minimumScore(默认值:0.0):设置认为激光读数良好的最低分数。
初始地图尺寸和分辨率
- xmin(默认值:-100.0):初始地图大小
- ymin(默认值:-100.0):初始地图大小
- xmax(默认值:100.0):初始地图大小
- ymax(默认值:100.0):初始地图大小
- delta(默认值:0.05):设置地图的分辨率
其他参数
- linearUpdate(默认值:1.0):设置机器人处理激光读数时必须移动的线性距离。
- angularUpdate(默认值:0.5):设置机器人处理激光读数时必须移动的角度距离。
- temporaryUpdate(默认值:-1.0):设置激光读数之间等待的时间(以秒为单位)。如果此值设置为 -1.0,则此功能关闭。
- granulars(默认值:30):过滤器中的粒子数
在 gmapping_demo.launch 文件中,如您所见,参数加载在启动文件本身中。因此,您直接在启动文件中更改了参数。但这不是加载参数的唯一方法。事实上,参数通常是从外部文件加载的。包含参数的文件通常是 YAML 文件。
因此,您也可以将所有参数写入 YAML 文件中,然后通过在 <node> 标记内添加以下行,将此文件(和参数)加载到启动文件中:
<rosparam file="$(find my_mapping_launcher)/params/gmapping_params.yaml" command="load" />
这与直接通过启动文件加载参数的结果完全相同。正如您所看到的,这是一种更简洁的方法。
a) 在以上练习中创建的包内创建一个名为 params 的新目录。
b) 创建一个名为 gmapping_params.yaml 的 YAML 文件,并写入要设置的所有参数。
# Gmapping 配置参数 # 以下是用于调整 gmapping 节点的参数 # 最大测距范围。机器人在生成地图时会考虑到这一范围的障碍物。 maxUrange: 10.0 # 最大距离。机器人在生成地图时能够检测到的最大距离。 maxRange: 12.0 # 高斯噪声的标准差,用于调整激光扫描的噪声。 sigma: 0.05 # 角度步长,用于控制激光扫描的角度分辨率。 lstep: 0.05 # 角度步进,用于控制每次角度扫描的步长。 astepp: 0.05 # 迭代次数。每次地图更新时,gmapping 将执行这些迭代以优化地图。 iterations: 5 # 重采样间隔,用于控制激光扫描数据的重采样频率。 resample_interval: 1 # 地图更新的最大允许偏移量。此值控制 gmapping 更新地图的频率。 delta: 0.1 # 地图更新间隔,单位为秒。控制地图的更新频率。 map_update_interval: 15.0
c) 删除启动文件中正在加载的所有参数,并加载刚刚创建的 YAML 文件。
<launch> <!-- 定义启动参数 --> <!-- 激光扫描数据的主题,默认值为 "kobuki/laser/scan" --> <arg name="scan_topic" default="kobuki/laser/scan" /> <!-- 机器人的基础框架名称,默认值为 "base_footprint" --> <arg name="base_frame" default="base_footprint"/> <!-- 里程计框架的名称,默认值为 "odom" --> <arg name="odom_frame" default="odom"/> <!-- 启动 gmapping 节点 --> <node pkg="gmapping" type="slam_gmapping" name="slam_gmapping" output="screen"> <!-- 从参数服务器中加载 YAML 配置文件 --> <rosparam file="$(find my_mapping_launcher)/params/gmapping_params.yaml" command="load" /> <!-- 基本参数配置 --> <param name="base_frame" value="$(arg base_frame)"/> <param name="odom_frame" value="$(arg odom_frame)"/> <!-- 一些额外参数 --> <param name="kernelSize" value="1"/> <!-- 内核大小,用于地图生成的平滑处理 --> <param name="lsigma" value="0.075"/> <!-- 激光测量的高斯噪声的标准差 --> <param name="ogain" value="3.0"/> <!-- 地图优化的增益 --> <param name="lskip" value="0"/> <!-- 激光数据跳过的数量 --> <param name="minimumScore" value="200"/> <!-- 最小评分,用于保留激光扫描的数据 --> <param name="srr" value="0.01"/> <!-- 机器人移动的标准差(前进) --> <param name="srt" value="0.02"/> <!-- 机器人转动的标准差(前进) --> <param name="str" value="0.01"/> <!-- 机器人移动的标准差(转向) --> <param name="stt" value="0.02"/> <!-- 机器人转动的标准差(转向) --> <param name="linearUpdate" value="0.5"/> <!-- 线性更新阈值 --> <param name="angularUpdate" value="0.436"/> <!-- 角度更新阈值 --> <param name="temporalUpdate" value="-1.0"/> <!-- 时间更新阈值 --> <param name="resampleThreshold" value="0.5"/> <!-- 重采样阈值 --> <param name="particles" value="80"/> <!-- 粒子数目 --> <!-- 地图边界设置,起始时设置较小的范围有助于减少内存消耗 --> <param name="xmin" value="-1.0"/> <param name="ymin" value="-1.0"/> <param name="xmax" value="1.0"/> <param name="ymax" value="1.0"/> <!-- 设置地图的分辨率 --> <param name="llsamplerange" value="0.01"/> <!-- 局部线性采样范围 --> <param name="llsamplestep" value="0.01"/> <!-- 局部线性采样步长 --> <param name="lasamplerange" value="0.005"/> <!-- 激光采样范围 --> <param name="lasamplestep" value="0.005"/> <!-- 激光采样步长 --> <!-- 激光扫描数据的主题重映射 --> <remap from="scan" to="$(arg scan_topic)"/> </node> </launch>
d) 再次启动节点并检查一切是否正常。
使用记录的数据(bag)构建地图
为了使用bag文件构建地图,您必须遵循以下 2 个步骤(分为子步骤)
1. 创建包文件
为了创建适合地图绘制的包文件,您需要按照以下步骤操作:
a) 首先,启动键盘遥控装置以开始移动机器人:
roslaunch pkg_name keyboard_teleop_launch_file.launch
b) 确保机器人正在发布其激光数据和 tfs。
rostopic list
c) 开始记录扫描和变换(请注意,扫描主题可能因机器人而异)
rosbag record -O mylaserdata /laser_topic /tf_topic
这将开始在当前目录中写入一个名为 mylaserdata.bag 的文件。
d) 驱动机器人四处移动。一般建议:
尽量限制快速旋转,因为它们对扫描匹配器来说最难。这有助于降低速度。
2.建图
可视化机器人用激光“看到”的东西;如果激光看不到它,它就不会在地图上。
循环闭合是最难的部分;当闭合循环时,一定要再驱动 5-10 米,以便在循环的开始和结束之间获得足够的重叠。
f) 终止 rosbag 实例,并记下创建的文件的名称。一旦我们有了 bag 文件,我们就可以开始构建地图了,为了进行查看,需要按照以下步骤操作:
a) 启动 slam_gmapping 节点,它将接收激光扫描(在本例中为 /kobuki/laser/scan 主题)并生成地图:
rosrun gmapping slam_gmapping scan:=kobuki/laser/scan
b)播放bag文件,向slam_gmapping节点提供数据:
rosbag play mylaserdata
c) 使用map_saver节点保存创建的地图,如前面内容所示:
rosrun map_server map_saver -f map_name
总结
导航所需的第一个东西是环境地图。如果没有地图,就无法导航。此外,必须正确构建此地图,以便它准确地表示您想要导航的环境。
为了创建环境地图,ROS 提供了 slam_gmapping 节点(gmapping 包),它是 SLAM(模拟定位和映射)算法的实现。基本上,此节点将机器人的激光和里程表读数作为输入(以便从环境中获取数据),并创建 2D 地图。
此地图是环境的占用表示,因此它提供有关地图中每个像素占用的信息。地图完全构建后,您可以将其保存到文件中。
还请记住,创建地图时只需要 slam_gmapping 节点(映射器)。一旦创建并保存了地图,这个节点就不再需要了,因此必须将其终止。此时,您应该启动 map_server 节点,该节点将向其他节点提供您刚刚创建的地图。