向机器人添加传感器#

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

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

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

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

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

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

代码#

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

add_sensors_on_robot.py 的代码
  1# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
  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    ./isaaclab.sh -p source/standalone/tutorials/04_sensors/add_sensors_on_robot.py
 19
 20"""
 21
 22"""Launch Isaac Sim Simulator first."""
 23
 24import argparse
 25
 26from omni.isaac.lab.app import AppLauncher
 27
 28# add argparse arguments
 29parser = argparse.ArgumentParser(description="Tutorial on adding sensors on a robot.")
 30parser.add_argument("--num_envs", type=int, default=2, help="Number of environments to spawn.")
 31# append AppLauncher cli args
 32AppLauncher.add_app_launcher_args(parser)
 33# parse the arguments
 34args_cli = parser.parse_args()
 35
 36# launch omniverse app
 37app_launcher = AppLauncher(args_cli)
 38simulation_app = app_launcher.app
 39
 40"""Rest everything follows."""
 41
 42import torch
 43
 44import omni.isaac.lab.sim as sim_utils
 45from omni.isaac.lab.assets import ArticulationCfg, AssetBaseCfg
 46from omni.isaac.lab.scene import InteractiveScene, InteractiveSceneCfg
 47from omni.isaac.lab.sensors import CameraCfg, ContactSensorCfg, RayCasterCfg, patterns
 48from omni.isaac.lab.utils import configclass
 49
 50##
 51# Pre-defined configs
 52##
 53from omni.isaac.lab_assets.anymal import ANYMAL_C_CFG  # isort: skip
 54
 55
 56@configclass
 57class SensorsSceneCfg(InteractiveSceneCfg):
 58    """Design the scene with sensors on the robot."""
 59
 60    # ground plane
 61    ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg())
 62
 63    # lights
 64    dome_light = AssetBaseCfg(
 65        prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75))
 66    )
 67
 68    # robot
 69    robot: ArticulationCfg = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
 70
 71    # sensors
 72    camera = CameraCfg(
 73        prim_path="{ENV_REGEX_NS}/Robot/base/front_cam",
 74        update_period=0.1,
 75        height=480,
 76        width=640,
 77        data_types=["rgb", "distance_to_image_plane"],
 78        spawn=sim_utils.PinholeCameraCfg(
 79            focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 1.0e5)
 80        ),
 81        offset=CameraCfg.OffsetCfg(pos=(0.510, 0.0, 0.015), rot=(0.5, -0.5, 0.5, -0.5), convention="ros"),
 82    )
 83    height_scanner = RayCasterCfg(
 84        prim_path="{ENV_REGEX_NS}/Robot/base",
 85        update_period=0.02,
 86        offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)),
 87        attach_yaw_only=True,
 88        pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=[1.6, 1.0]),
 89        debug_vis=True,
 90        mesh_prim_paths=["/World/defaultGroundPlane"],
 91    )
 92    contact_forces = ContactSensorCfg(
 93        prim_path="{ENV_REGEX_NS}/Robot/.*_FOOT", update_period=0.0, history_length=6, debug_vis=True
 94    )
 95
 96
 97def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene):
 98    """Run the simulator."""
 99    # Define simulation stepping
100    sim_dt = sim.get_physics_dt()
101    sim_time = 0.0
102    count = 0
103
104    # Simulate physics
105    while simulation_app.is_running():
106        # Reset
107        if count % 500 == 0:
108            # reset counter
109            count = 0
110            # reset the scene entities
111            # root state
112            # we offset the root state by the origin since the states are written in simulation world frame
113            # if this is not done, then the robots will be spawned at the (0, 0, 0) of the simulation world
114            root_state = scene["robot"].data.default_root_state.clone()
115            root_state[:, :3] += scene.env_origins
116            scene["robot"].write_root_state_to_sim(root_state)
117            # set joint positions with some noise
118            joint_pos, joint_vel = (
119                scene["robot"].data.default_joint_pos.clone(),
120                scene["robot"].data.default_joint_vel.clone(),
121            )
122            joint_pos += torch.rand_like(joint_pos) * 0.1
123            scene["robot"].write_joint_state_to_sim(joint_pos, joint_vel)
124            # clear internal buffers
125            scene.reset()
126            print("[INFO]: Resetting robot state...")
127        # Apply default actions to the robot
128        # -- generate actions/commands
129        targets = scene["robot"].data.default_joint_pos
130        # -- apply action to the robot
131        scene["robot"].set_joint_position_target(targets)
132        # -- write data to sim
133        scene.write_data_to_sim()
134        # perform step
135        sim.step()
136        # update sim-time
137        sim_time += sim_dt
138        count += 1
139        # update buffers
140        scene.update(sim_dt)
141
142        # print information from the sensors
143        print("-------------------------------")
144        print(scene["camera"])
145        print("Received shape of rgb   image: ", scene["camera"].data.output["rgb"].shape)
146        print("Received shape of depth image: ", scene["camera"].data.output["distance_to_image_plane"].shape)
147        print("-------------------------------")
148        print(scene["height_scanner"])
149        print("Received max height value: ", torch.max(scene["height_scanner"].data.ray_hits_w[..., -1]).item())
150        print("-------------------------------")
151        print(scene["contact_forces"])
152        print("Received max contact force of: ", torch.max(scene["contact_forces"].data.net_forces_w).item())
153
154
155def main():
156    """Main function."""
157
158    # Initialize the simulation context
159    sim_cfg = sim_utils.SimulationCfg(dt=0.005)
160    sim = sim_utils.SimulationContext(sim_cfg)
161    # Set main camera
162    sim.set_camera_view(eye=[3.5, 3.5, 3.5], target=[0.0, 0.0, 0.0])
163    # design scene
164    scene_cfg = SensorsSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0)
165    scene = InteractiveScene(scene_cfg)
166    # Play the simulator
167    sim.reset()
168    # Now we are ready!
169    print("[INFO]: Setup complete...")
170    # Run the simulator
171    run_simulator(sim, scene)
172
173
174if __name__ == "__main__":
175    # run the main function
176    main()
177    # close sim app
178    simulation_app.close()

代码解释#

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

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

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

相机传感器#

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

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

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

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

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

为了将 RGB-D 相机传感器附加到机器人的头部,我们指定相对于机器人基准框架的偏移量。偏移量是相对于基准框架的平移和旋转指定的,以及指定偏移量的 convnetion

接下来,我们展示本教程中使用的相机传感器的配置。我们将更新周期设置为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"),
    )

高度扫描传感器#

高度扫描器被实现为使用 NVIDIA Warp 光线投射内核的虚拟传感器。通过 sensors.RayCasterCfg ,我们可以指定要投射的光线模式和要对其进行投射的网格。由于它们是虚拟传感器,因此在场景中没有为其创建对应的 prim。而是将其附加到场景中的一个 prim 上,该 prim 用于指定传感器的位置。

对于本教程,基于光线集的高度扫描器附加到机器人的基准框架。使用 pattern 属性指定光线的模式。对于均匀网格模式,我们使用 GridPatternCfg 指定模式。由于我们只关心高度信息,因此我们不需要考虑机器人的滚动和俯仰。因此,我们将 attach_yaw_only 设置为 true。

对于高度扫描器,可以将光线击中网格的点可视化。通过设置 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)),
        attach_yaw_only=True,
        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""RF_FOOT" 。我们传递一个正则表达式 " .*_FOOT" 来简化 prim 路径的指定。该正则表达式与以 "_FOOT" 结尾的所有 prims 匹配。

将更新周期设置为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[..., -1]).item())
        print("-------------------------------")
        print(scene["contact_forces"])
        print("Received max contact force of: ", torch.max(scene["contact_forces"].data.net_forces_w).item())

代码执行#

既然我们已经查看了代码,让我们运行脚本并查看结果:

./isaaclab.sh -p source/standalone/tutorials/04_sensors/add_sensors_on_robot.py --num_envs 2

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

add_sensors_on_robot.py 的结果

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

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

# Frame Transformer
./isaaclab.sh -p source/standalone/tutorials/04_sensors/run_frame_transformer.py

# Ray Caster
./isaaclab.sh -p source/standalone/tutorials/04_sensors/run_ray_caster.py

# Ray Caster Camera
./isaaclab.sh -p source/standalone/tutorials/04_sensors/run_ray_caster_camera.py

# USD Camera
./isaaclab.sh -p source/standalone/tutorials/04_sensors/run_usd_camera.py