向机器人添加传感器#

尽管资产类别允许我们创建和仿真机器人的实体,但传感器有助于获取有关环境的信息。它们通常以比仿真更新频率更低的频率更新,并且有助于获取不同的本体感知和外部感知信息。例如,相机传感器可以用于获取环境的视觉信息,接触传感器可以用于获取机器人与环境的接触信息。

在本教程中,我们将看到如何向机器人添加不同的传感器。我们将在本教程中使用 ANYmal-C 机器人。ANYmal-C 机器人是一个有 12 个自由度的四足机器人。它有 4 条腿,每个腿有 3 个自由度。机器人具有以下传感器:

  • 机器人头部的相机传感器,提供 RGB-D 图像

  • 提供地形高度信息的高度扫描传感器

  • 机器人脚部的接触传感器,提供接触信息

我们将从之前关于 使用交互式场景 的教程中继续本教程,在那里我们了解了 scene.InteractiveScene 类。

代码#

本教程对应于 scripts/tutorials/04_sensors 目录中的 add_sensors_on_robot.py 脚本。

add_sensors_on_robot.py 的代码
  1# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
  2# All rights reserved.
  3#
  4# SPDX-License-Identifier: BSD-3-Clause
  5
  6"""
  7This script demonstrates how to add and simulate on-board sensors for a robot.
  8
  9We add the following sensors on the quadruped robot, ANYmal-C (ANYbotics):
 10
 11* USD-Camera: This is a camera sensor that is attached to the robot's base.
 12* Height Scanner: This is a height scanner sensor that is attached to the robot's base.
 13* Contact Sensor: This is a contact sensor that is attached to the robot's feet.
 14
 15.. code-block:: bash
 16
 17    # Usage
 18    uv run python scripts/tutorials/04_sensors/add_sensors_on_robot.py --viz kit
 19
 20"""
 21
 22"""Parse the command-line arguments first."""
 23
 24import argparse
 25from typing import TYPE_CHECKING
 26
 27from isaaclab.app import add_launcher_args, launch_simulation
 28
 29# add argparse arguments
 30parser = argparse.ArgumentParser(description="Tutorial on adding sensors on a robot.")
 31parser.add_argument("--num_envs", type=int, default=2, help="Number of environments to spawn.")
 32# append simulation launcher cli args
 33add_launcher_args(parser)
 34# parse the arguments
 35args_cli = parser.parse_args()
 36# The scene has a camera sensor, which requires the rendering extensions in headless launches.
 37args_cli.enable_cameras = True
 38
 39"""Rest everything follows."""
 40
 41import torch
 42
 43import isaaclab.sim as sim_utils
 44from isaaclab.assets import ArticulationCfg, AssetBaseCfg
 45from isaaclab.scene import InteractiveSceneCfg
 46from isaaclab.sensors import CameraCfg, ContactSensorCfg, RayCasterCfg, patterns
 47from isaaclab.utils import configclass, instantiate, replace
 48
 49if TYPE_CHECKING:
 50    from isaaclab.scene import InteractiveScene
 51
 52##
 53# Pre-defined configs
 54##
 55from isaaclab_assets.robots.anymal import ANYMAL_C_CFG  # isort: skip
 56
 57
 58@configclass
 59class SensorsSceneCfg(InteractiveSceneCfg):
 60    """Design the scene with sensors on the robot."""
 61
 62    # ground plane
 63    ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg())
 64
 65    # lights
 66    dome_light = AssetBaseCfg(
 67        prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75))
 68    )
 69
 70    # robot
 71    robot: ArticulationCfg = replace(ANYMAL_C_CFG, prim_path="{ENV_REGEX_NS}/Robot")
 72
 73    # sensors
 74    camera = CameraCfg(
 75        prim_path="{ENV_REGEX_NS}/Robot/base/front_cam",
 76        update_period=0.1,
 77        height=480,
 78        width=640,
 79        data_types=["rgb", "distance_to_image_plane"],
 80        spawn=sim_utils.PinholeCameraCfg(
 81            focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 1.0e5)
 82        ),
 83        offset=CameraCfg.OffsetCfg(pos=(0.510, 0.0, 0.015), rot=(0.5, -0.5, 0.5, -0.5), convention="ros"),
 84    )
 85    height_scanner = RayCasterCfg(
 86        prim_path="{ENV_REGEX_NS}/Robot/base",
 87        update_period=0.02,
 88        offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)),
 89        ray_alignment="yaw",
 90        pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=[1.6, 1.0]),
 91        debug_vis=True,
 92        mesh_prim_paths=["/World/defaultGroundPlane"],
 93    )
 94    contact_forces = ContactSensorCfg(
 95        prim_path="{ENV_REGEX_NS}/Robot/.*_FOOT", update_period=0.0, history_length=6, debug_vis=True
 96    )
 97
 98
 99def run_simulator(sim: sim_utils.SimulationContext, scene: "InteractiveScene"):
100    """Run the simulator."""
101    # Define simulation stepping
102    sim_dt = sim.get_physics_dt()
103    sim_time = 0.0
104    count = 0
105
106    # Simulate physics
107    while sim.is_running():
108        # Reset
109        if count % 500 == 0:
110            # reset counter
111            count = 0
112            # reset the scene entities
113            # root state
114            # we offset the root state by the origin since the states are written in simulation world frame
115            # if this is not done, then the robots will be spawned at the (0, 0, 0) of the simulation world
116            root_pose = scene["robot"].data.default_root_pose.torch.clone()
117            root_pose[:, :3] += scene.env_origins
118            scene["robot"].write_root_pose_to_sim_index(root_pose=root_pose)
119            root_vel = scene["robot"].data.default_root_vel.torch.clone()
120            scene["robot"].write_root_velocity_to_sim_index(root_velocity=root_vel)
121            # set joint positions with some noise
122            joint_pos, joint_vel = (
123                scene["robot"].data.default_joint_pos.torch.clone(),
124                scene["robot"].data.default_joint_vel.torch.clone(),
125            )
126            joint_pos += torch.rand_like(joint_pos) * 0.1
127            scene["robot"].write_joint_position_to_sim_index(position=joint_pos)
128            scene["robot"].write_joint_velocity_to_sim_index(velocity=joint_vel)
129            # clear internal buffers
130            scene.reset()
131            print("[INFO]: Resetting robot state...")
132        # Apply default actions to the robot
133        # -- generate actions/commands
134        targets = scene["robot"].data.default_joint_pos.torch
135        # -- apply action to the robot
136        scene["robot"].set_joint_position_target_index(target=targets)
137        # -- write data to sim
138        scene.write_data_to_sim()
139        # perform step
140        sim.step()
141        # update sim-time
142        sim_time += sim_dt
143        count += 1
144        # update buffers
145        scene.update(sim_dt)
146
147        # print information from the sensors
148        print("-------------------------------")
149        print(scene["camera"])
150        print("Received shape of rgb   image: ", scene["camera"].data.output["rgb"].shape)
151        print("Received shape of depth image: ", scene["camera"].data.output["distance_to_image_plane"].shape)
152        print("-------------------------------")
153        print(scene["height_scanner"])
154        print(
155            "Received max height value: ",
156            torch.max(scene["height_scanner"].data.ray_hits_w.torch[..., -1]).item(),
157        )
158        print("-------------------------------")
159        print(scene["contact_forces"])
160        print("Received max contact force of: ", torch.max(scene["contact_forces"].data.net_normal_forces_w).item())
161
162
163def main():
164    """Main function."""
165
166    # Configure the simulation
167    sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device)
168    # Launch the simulator runtime that the configuration needs
169    with launch_simulation(sim_cfg, args_cli):
170        # Initialize the simulation context
171        sim = sim_utils.SimulationContext(sim_cfg)
172        # Set main camera
173        sim.set_camera_view(eye=[3.5, 3.5, 3.5], target=[0.0, 0.0, 0.0])
174        # Design scene
175        scene_cfg = SensorsSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0)
176        scene = instantiate(scene_cfg)
177        # Play the simulator
178        sim.reset()
179        # Now we are ready!
180        print("[INFO]: Setup complete...")
181        # Run the simulator
182        run_simulator(sim, scene)
183
184
185if __name__ == "__main__":
186    # run the main function
187    main()

代码解释#

与之前的教程类似,我们添加资产到场景中时,传感器也是使用场景配置添加到场景中的。所有传感器都继承自 sensors.SensorBase 类,并通过其各自的配置类进行配置。每个传感器实例可以定义自己的更新周期,即传感器更新的频率。更新周期通过 sensors.SensorBaseCfg.update_period 属性以秒为单位指定。

根据指定的路径和传感器类型,传感器将附加到场景中的 prims 上。它们可能有一个关联的 prim 在场景中创建,或者它们可能附加到现有的 prim 上。例如,相机传感器有一个对应的 prim 在场景中创建,而对于接触传感器,激活接触报告是刚体 prim 上的一个属性。

接下来,我们介绍本教程中使用的不同传感器及其配置。有关更多描述,请查看 sensors 模块。

相机传感器#

使用 sensors.CameraCfg 定义相机。它基于 USD 相机传感器,并使用 Omniverse Replicator API 捕获不同的数据类型。由于它在场景中有一个对应的 prim,因此 prims 会在指定的 prim 路径上创建在场景中。

相机传感器的配置包括以下参数:

  • spawn: 要创建的 USD 相机类型。这可以是 PinholeCameraCfg 或 FisheyeCameraCfg 。

  • offset: 相机传感器相对于父 prim 的偏移量。

  • data_types: 要捕获的数据类型。这可以是 rgb 、 distance_to_image_plane 、 normals 等 USD 相机传感器支持的其他类型。

要将 RGB-D 相机传感器安装在机器人头部,需要设置相对于机器人基座坐标系的偏移。该偏移包含相对基座坐标系的平移和旋转,并通过 convention 指定所采用的坐标系约定。

接下来,我们展示本教程中使用的相机传感器的配置。我们将更新周期设置为 0.1 秒,这意味着相机传感器以 10Hz 进行更新。prim 路径表达式设置为 {ENV_REGEX_NS}/Robot/base/front_cam ,其中 {ENV_REGEX_NS} 是环境命名空间, "Robot" 是机器人名称, "base" 是相机附加到的 prim 的名称, "front_cam" 是与相机传感器关联的 prim 的名称。

    camera = CameraCfg(
        prim_path="{ENV_REGEX_NS}/Robot/base/front_cam",
        update_period=0.1,
        height=480,
        width=640,
        data_types=["rgb", "distance_to_image_plane"],
        spawn=sim_utils.PinholeCameraCfg(
            focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 1.0e5)
        ),
        offset=CameraCfg.OffsetCfg(pos=(0.510, 0.0, 0.015), rot=(0.5, -0.5, 0.5, -0.5), convention="ros"),
    )

即使不使用可视化器,相机传感器也需要仿真器的渲染扩展。当传给 launch_simulation() 的配置包含相机时,它会自动启用这些扩展。由于本脚本只传入仿真配置,而相机位于场景配置中,因此脚本会在解析参数后显式请求相机渲染:

args_cli = parser.parse_args()
# The scene has a camera sensor, which requires the rendering extensions in headless launches.
args_cli.enable_cameras = True

高度扫描传感器#

高度扫描器使用虚拟射线投射传感器实现,通过 sensors.RayCasterCfg 指定射线分布。基于 PhysX 的后端对配置的网格进行射线检测,而 Newton 对其当前场景的 BVH 进行检测,并忽略网格列表。默认情况下,spawn 会在 prim_path 创建一个普通 USD Xform,作为传感器的挂载坐标系。这一兼容行为已弃用;新配置应将 spawn 设为 None,并跟踪已有坐标系。

本教程将基于射线投射的高度扫描器挂在机器人基座坐标系下。射线模式由 pattern 属性指定;均匀网格模式使用 GridPatternCfg 。由于我们只关心高度信息,无需考虑机器人的滚转和俯仰,因此将 ray_alignment 设为 "yaw"。

对于高度扫描器,可以将光线击中网格的点可视化。通过设置 debug_vis 属性为 true 实现此功能。

高度扫描器的整体配置如下:

    height_scanner = RayCasterCfg(
        prim_path="{ENV_REGEX_NS}/Robot/base",
        update_period=0.02,
        offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)),
        ray_alignment="yaw",
        pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=[1.6, 1.0]),
        debug_vis=True,
        mesh_prim_paths=["/World/defaultGroundPlane"],
    )

接触传感器#

接触传感器包装 PhysX 接触报告 API 以获取机器人与环境的接触信息。由于它依赖于 PhysX,因此接触传感器希望启用机器人的刚体上的接触报告 API。可以通过在资产配置中将 activate_contact_sensors 设置为 true 来完成此操作。

通过 sensors.ContactSensorCfg ,可以指定要获取其接触信息的 prims。可以设置附加标志以获取有关接触的更多信息,例如过滤 prims 之间的接触空气时间、接触力等。

本教程将接触传感器附加到机器人的足部。四个足部名称分别为 "LF_FOOT" 、 "RF_FOOT" 、 "LH_FOOT" 和 "RH_FOOT" 。为简化 prim 路径设置,我们使用正则表达式 ".*_FOOT" ,匹配所有名称以 "_FOOT" 结尾的 prim。

将更新周期设置为 0 以使传感器以仿真相同的频率更新。此外,对于接触传感器,可以指定要存储的接触信息的历史长度。对于本教程,我们将历史长度设置为 6,这意味着存储最后 6 个仿真步骤的接触信息。

接触传感器的整体配置如下:

    contact_forces = ContactSensorCfg(
        prim_path="{ENV_REGEX_NS}/Robot/.*_FOOT", update_period=0.0, history_length=6, debug_vis=True
    )

运行仿真循环#

与使用资产时类似,仅当播放仿真时才会初始化传感器的缓冲区和物理句柄,即在创建场景后调用 sim.reset() 是重要的。

        # Play the simulator
        sim.reset()

此外,仿真循环类似于以前的教程。传感器作为场景更新的一部分进行更新,并且它们内部处理基于其更新周期的缓冲区的更新。

可以通过它们的 data 属性访问传感器的数据。作为示例,我们展示了如何访问本教程中创建的不同传感器的数据:

        # print information from the sensors
        print("-------------------------------")
        print(scene["camera"])
        print("Received shape of rgb   image: ", scene["camera"].data.output["rgb"].shape)
        print("Received shape of depth image: ", scene["camera"].data.output["distance_to_image_plane"].shape)
        print("-------------------------------")
        print(scene["height_scanner"])
        print(
            "Received max height value: ",
            torch.max(scene["height_scanner"].data.ray_hits_w.torch[..., -1]).item(),
        )
        print("-------------------------------")
        print(scene["contact_forces"])
        print("Received max contact force of: ", torch.max(scene["contact_forces"].data.net_normal_forces_w).item())

代码执行#

现在我们已经浏览了代码,让我们运行脚本并查看结果:

uv run python scripts/tutorials/04_sensors/add_sensors_on_robot.py --num_envs 2 --viz kit
./isaaclab.sh -p scripts/tutorials/04_sensors/add_sensors_on_robot.py --num_envs 2 --viz kit

此命令应该打开一个带有地面平面、灯光和两个四足机器人的场景。在机器人周围,您应该看到指示光线击中网格点的红色球。此外,您可以将视口切换到相机视图,以查看相机传感器捕获的 RGB 图像。请查看 这里 获取有关如何将视口切换到相机视图的更多信息。

add_sensors_on_robot.py 的结果

要停止仿真,您可以关闭窗口,或在终端中按 Ctrl+C 。

在本教程中,我们讨论了创建和使用不同的传感器, sensors 模块中还有许多其他传感器可用。我们在 scripts/tutorials/04_sensors 目录中包含了使用这些传感器的最低限度示例。为了完整起见,可以使用以下命令运行这些脚本:

# Frame Transformer
uv run python scripts/tutorials/04_sensors/run_frame_transformer.py --viz kit

# Ray Caster
uv run python scripts/tutorials/04_sensors/run_ray_caster.py --viz kit

# Ray Caster Camera
uv run python scripts/tutorials/04_sensors/run_ray_caster_camera.py --viz kit

# USD Camera
uv run python scripts/tutorials/04_sensors/run_usd_camera.py --viz kit
# Frame Transformer
./isaaclab.sh -p scripts/tutorials/04_sensors/run_frame_transformer.py --viz kit

# Ray Caster
./isaaclab.sh -p scripts/tutorials/04_sensors/run_ray_caster.py --viz kit

# Ray Caster Camera
./isaaclab.sh -p scripts/tutorials/04_sensors/run_ray_caster_camera.py --viz kit

# USD Camera
./isaaclab.sh -p scripts/tutorials/04_sensors/run_usd_camera.py --viz kit