Skip to content

使用区域参数覆盖进行导航(Zone Parameter Filter)

本教程演示如何在机器人进入地图标记区域时自动更改 Nav2 服务器参数。典型场景包括室外路面上被冰雪覆盖的路段,或者与人共享的工作单元:机器人仍可在这些区域行驶,但应更加谨慎——降低最大速度、延长后退(backup)恢复的仿真时间等。Keepout Filter(禁入过滤器)会完全封锁此类区域,Speed Filter(速度过滤器)仅更改速度限制,而本文档将要启用和使用的 ZoneParameterFilter 代价地图过滤器插件则可以更改任意节点上的任意参数。当机器人进入某区域时,过滤器会应用为该区域配置的参数设定值(setpoint);当机器人返回正常区域时,则恢复声明的正常默认值。

假设 ROS 2、Gazebo 和 TurtleBot 4 软件包已安装或在本地构建,同时请确保 Nav2 项目也已按构建说明在本地完成构建。

如概念中所述,任何代价地图过滤器(Costmap Filter)(包括 Zone Parameter Filter)都会从过滤器掩码(filter mask)文件中读取标记数据。过滤器掩码本质上是常规的 Nav2 二维地图,以 PGM、PNG 或 BMP 栅格文件形式提供,元数据保存在 YAML 文件中。使用禁区(Keepout Zones)导航教程的第一步详细解释了掩码像素的亮度如何根据地图 mode 转换为 OccupancyGrid 值,若此前未制作过过滤器掩码,请先参阅该教程。

在 Zone Parameter Filter 中,掩码单元的 OccupancyGrid 值就是机器人位于该单元时需要激活的状态 id。值 0 表示正常状态,不进行任何覆盖;值 1 及以上则选择过滤器配置中声明的状态(参见步骤 3)。由于这里需要精确的整数值,请使用 raw 地图模式,在该模式下 OccupancyGrid 值等于像素亮度:

  • 用等于其状态 id 的亮度绘制每个区域:第一个区域为 1,第二个区域为 2,依此类推。
  • 其余所有像素填充为黑色(亮度 0,即正常状态)。

制作掩码时,先从 Nav2 仓库复制世界仿真所用的主地图 depot.pgm 为新的 depot_zones.pgm 文件,然后在栅格图形编辑器(如 GIMP)中编辑:先在需要覆盖的地图区域上绘制各个区域,再将所有剩余像素填充为黑色。

注意:在 raw 模式下,亮度为 101 或更高的像素会转换为未知值(-1)。在未知单元上,过滤器会保持当前状态并打印限频警告,因此零散的浅色像素会延迟恢复到正常状态的切换。请将每个像素保持在 0 或已声明的状态 id。当机器人完全离开掩码覆盖范围时,过滤器会重置为正常状态。

与所有其他地图一样,过滤器掩码也需要有自己的 YAML 元数据文件。将 depot.yaml 复制为 depot_zones.yaml,将 image 字段指向新掩码并设置 raw 模式:

image: depot_zones.pgm
mode: raw

保存 depot_zones.yaml,新的过滤器掩码即可使用。

2. 配置 Costmap Filter Info Publisher Server

Section titled “2. 配置 Costmap Filter Info Publisher Server”

每个代价地图过滤器都会读取 nav2_msgs/CostmapFilterInfo 消息中传入的元信息(如过滤器类型或数据转换系数)。这些消息由 Costmap Filter Info Publisher Server 发布,该服务器作为生命周期节点(lifecycle node)运行。nav2_msgs/CostmapFilterInfo 消息与 OccupancyGrid 过滤器掩码话题成对出现,因此除了 Costmap Filter Info Publisher Server 之外,还需启动一个新的 Map Server 实例来发布过滤器掩码。两个服务器的参数详见地图服务器配置页面。params_file 示例如下:

costmap_filter_info_server:
ros__parameters:
use_sim_time: true
type: 4
filter_info_topic: "/costmap_filter_info"
mask_topic: "/zone_filter_mask"
base: 0.0
multiplier: 1.0
filter_mask_server:
ros__parameters:
use_sim_time: true
frame_id: "map"
topic_name: "/zone_filter_mask"
yaml_filename: "depot_zones.yaml"

需要注意以下几点:

  • Zone Parameter Filter 的代价地图过滤器 type 应设置为 4。
  • 过滤器掩码话题名称需与 Costmap Filter Info Publisher Server 的 mask_topic 参数和 Map Server 的 topic_name 参数保持一致。
  • Zone Parameter Filter 不使用 base 和 multiplier:掩码值直接作为状态 id 使用,不进行线性转换。请保持 0.0 和 1.0 不变,其他值会在启动时产生警告。

两个服务器可通过如下独立 launch 文件作为生命周期节点运行(navigation2_tutorials 中的 costmap filters demo 对其他过滤器使用的就是相同的形式):

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description() -> LaunchDescription:
params_file = LaunchConfiguration('params_file')
mask_yaml_file = LaunchConfiguration('mask')
declare_params_file_cmd = DeclareLaunchArgument(
'params_file',
description='Full path to the ROS 2 parameters file for both servers')
declare_mask_yaml_file_cmd = DeclareLaunchArgument(
'mask',
description='Full path to filter mask yaml file to load')
start_filter_mask_server_cmd = Node(
package='nav2_map_server',
executable='map_server',
name='filter_mask_server',
output='screen',
parameters=[params_file, {'yaml_filename': mask_yaml_file}])
start_costmap_filter_info_server_cmd = Node(
package='nav2_map_server',
executable='costmap_filter_info_server',
name='costmap_filter_info_server',
output='screen',
parameters=[params_file])
start_lifecycle_manager_cmd = Node(
package='nav2_lifecycle_manager',
executable='lifecycle_manager',
name='lifecycle_manager_costmap_filters',
output='screen',
parameters=[{'use_sim_time': True},
{'autostart': True},
{'node_names': ['filter_mask_server',
'costmap_filter_info_server']}])
ld = LaunchDescription()
ld.add_action(declare_params_file_cmd)
ld.add_action(declare_mask_yaml_file_cmd)
ld.add_action(start_filter_mask_server_cmd)
ld.add_action(start_costmap_filter_info_server_cmd)
ld.add_action(start_lifecycle_manager_cmd)
return ld

代价地图过滤器(Costmap Filter)是 Costmap2D 插件。在 nav2_params.yaml 的 filters 参数中添加 zone_params 即可启用 ZoneParameterFilter 插件。Zone Parameter Filter 不改变代价地图的值,它只跟踪机器人当前所在的掩码单元,并在状态变化时发出参数更新。在 global_costmap 中配置一个实例即可;若在 local_costmap 中再添加一个实例,会将每次更新发送两次。

过滤器配置由三部分组成:状态(states)、设定值(setpoints)和正常默认值(nominal defaults)。

states 列出状态名称。每个状态声明一个 id,即选择该状态的掩码单元值:

zone_params:
plugin: "nav2_costmap_2d::ZoneParameterFilter"
filter_info_topic: "/costmap_filter_info"
states: ["snow_zone", "work_zone"]
snow_zone:
id: 1
work_zone:
id: 2

有效 id 范围为 1 到 255,0 保留给正常状态。Map Server 发布的掩码值最大为 100,因此实际使用中 id 应控制在此范围内。掩码中出现的每个非零值都必须声明对应状态,否则过滤器会将未声明的正值视为配置错误并抛出异常。

每个状态声明其 setpoints,即该区域激活时要覆盖的参数。一个设定值指定目标 node、该节点上的 parameter 以及要设置的 value。值可以是任意参数类型:double、integer、boolean、string 或 list。node 或 parameter 为空、或缺少 value 的设定值会在启动时被报告并跳过:

zone_params:
...
snow_zone:
id: 1
setpoints: ["slow_fwd", "long_backup"]
slow_fwd:
node: "controller_server"
parameter: "FollowPath.max_vel_x"
value: 0.15
long_backup:
node: "behavior_server"
parameter: "backup.simulate_ahead_time"
value: 2.5

nominal_defaults 以相同的 node/parameter/value 形式声明基线值。当机器人返回 0 区域或离开掩码时,过滤器会恢复这些值。每个条目位于 nominal_defaults 命名空间中。由于 nominal_defaults 本身是列表名,条目键以点号形式书写(如 nominal_defaults.fwd_speed:)。请为状态涉及的每个参数声明一个正常条目:若某状态设定值没有匹配的条目,过滤器会在启动时发出警告,且重置时不会恢复该参数:

zone_params:
...
nominal_defaults: ["fwd_speed", "backup_time"]
nominal_defaults.fwd_speed:
node: "controller_server"
parameter: "FollowPath.max_vel_x"
value: 0.26
nominal_defaults.backup_time:
node: "behavior_server"
parameter: "backup.simulate_ahead_time"
value: 2.0

最终的配置如下:

global_costmap:
global_costmap:
ros__parameters:
...
plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
filters: ["zone_params"]
...
zone_params:
plugin: "nav2_costmap_2d::ZoneParameterFilter"
enabled: True
filter_info_topic: "/costmap_filter_info"
states: ["snow_zone", "work_zone"]
snow_zone:
id: 1
setpoints: ["slow_fwd", "long_backup"]
slow_fwd:
node: "controller_server"
parameter: "FollowPath.max_vel_x"
value: 0.15
long_backup:
node: "behavior_server"
parameter: "backup.simulate_ahead_time"
value: 2.5
work_zone:
id: 2
setpoints: ["crawl_fwd"]
crawl_fwd:
node: "controller_server"
parameter: "FollowPath.max_vel_x"
value: 0.10
nominal_defaults: ["fwd_speed", "backup_time"]
nominal_defaults.fwd_speed:
node: "controller_server"
parameter: "FollowPath.max_vel_x"
value: 0.26
nominal_defaults.backup_time:
node: "behavior_server"
parameter: "backup.simulate_ahead_time"
value: 2.0

在两个区域之间切换时,过滤器首先将前一状态设置但新状态未设置的参数重置回正常默认值,然后应用新状态的设定值,并按目标节点分批执行。更新以异步方式发出,不会阻塞代价地图更新循环。若更新失败或被目标节点拒绝,过滤器会抛出异常而非静默继续。每次状态变化还会以 std_msgs/UInt8 消息形式发布到由 state_event_topic 参数(默认值:zone_filter_state)指定的话题上。

ZoneParameterFilter 支持的完整参数列表请参阅区域参数过滤器配置页面。

使用修改后的参数文件运行 Nav2:

Terminal window
ros2 launch nav2_bringup tb4_simulation_launch.py params_file:=/path/to/nav2_params.yaml

然后在第二个终端中启动步骤 2 的两个过滤器服务器:

Terminal window
ros2 launch /path/to/zone_filter_info.launch.py params_file:=/path/to/zone_filter_server_params.yaml mask:=/path/to/depot_zones.yaml

检查代价地图日志中的 ZoneParameterFilter: Received filter info 和 ZoneParameterFilter: Received filter mask 行:两者都出现后,过滤器便立即开始工作。

回显状态事件话题。默认的 zone_filter_state 名称会相对于代价地图的父命名空间解析,因此在本文使用的单机器人设置中,它出现在根命名空间下:

Terminal window
ros2 topic echo /zone_filter_state

在 RViz 中设置一个 Nav2 目标,使路径穿过已绘制的区域。当机器人进入 snow_zone 区域时,话题显示 data: 1 并应用设定值。由于更新通过目标节点的参数服务传输,它们在切换后稍后才生效,并非在切换瞬间立即完成:

Terminal window
$ ros2 param get /controller_server FollowPath.max_vel_x
Double value is: 0.15

当机器人驶入黑色区域时,话题显示 data: 0 并恢复正常默认值:

Terminal window
$ ros2 param get /controller_server FollowPath.max_vel_x
Double value is: 0.26

代价地图日志也会记录这些切换:

ZoneParameterFilter: Entered state 1 (reset 0 N-only parameter(s); applied 2 parameter(s) across 2 node(s)).
ZoneParameterFilter: Entered state 0 (reset to nominal).