保存渲染图像和 3D 重投影

保存渲染图像和 3D 重投影#

通过 scripts/tutorials/04_sensors/run_usd_camera.py 保存相机图像,并将深度反投影为三维点。

run_usd_camera.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 shows how to use the camera sensor from the Isaac Lab framework.
  8
  9Instead of using the simulator or OpenGL convention for the camera, we use the robotics or ROS convention.
 10
 11.. code-block:: bash
 12
 13    # Usage with GUI
 14    uv run python scripts/tutorials/04_sensors/run_usd_camera.py --viz kit
 15
 16    # Usage with no visualizer
 17    uv run python scripts/tutorials/04_sensors/run_usd_camera.py
 18
 19"""
 20
 21"""Parse the command-line arguments first."""
 22
 23import argparse
 24from typing import TYPE_CHECKING
 25
 26from isaaclab.app import add_launcher_args, launch_simulation
 27
 28# add argparse arguments
 29parser = argparse.ArgumentParser(description="This script demonstrates how to use the camera sensor.")
 30parser.add_argument(
 31    "--draw",
 32    action="store_true",
 33    default=False,
 34    help="Draw the pointcloud from camera at index specified by ``--camera_id``.",
 35)
 36parser.add_argument(
 37    "--save",
 38    action="store_true",
 39    default=False,
 40    help="Save the data from camera at index specified by ``--camera_id``.",
 41)
 42parser.add_argument(
 43    "--camera_id",
 44    type=int,
 45    choices={0, 1},
 46    default=0,
 47    help=(
 48        "The camera ID to use for displaying points or saving the camera data. Default is 0."
 49        " The viewport will always initialize with the perspective of camera 0."
 50    ),
 51)
 52# append simulation launcher cli args
 53add_launcher_args(parser)
 54# parse the arguments
 55args_cli = parser.parse_args()
 56# Camera sensors require the rendering extensions in headless and viewport-free launches.
 57args_cli.enable_cameras = True
 58
 59"""Rest everything follows."""
 60
 61import os
 62import random
 63
 64import numpy as np
 65import torch
 66from isaaclab_physx.renderers import IsaacRtxRendererCfg
 67
 68import isaaclab.sim as sim_utils
 69from isaaclab.assets import RigidObject, RigidObjectCfg
 70from isaaclab.markers import VisualizationMarkers
 71from isaaclab.markers.config import RAY_CASTER_MARKER_CFG
 72from isaaclab.sensors.camera import CameraCfg
 73from isaaclab.sensors.camera.utils import create_pointcloud_from_depth, save_images_to_file
 74from isaaclab.utils import instantiate, replace
 75
 76if TYPE_CHECKING:
 77    from isaaclab.sensors.camera import Camera
 78
 79
 80def define_sensor() -> "Camera":
 81    """Defines the camera sensor to add to the scene."""
 82    # Setup camera sensor
 83    # In contrast to the ray-cast camera, we spawn the prim at these locations.
 84    # This means the camera sensor will be attached to these prims.
 85    sim_utils.create_prim("/World/Origin_00", "Xform")
 86    sim_utils.create_prim("/World/Origin_01", "Xform")
 87    camera_cfg = CameraCfg(
 88        prim_path="/World/Origin_[^/]+/CameraSensor",
 89        update_period=0,
 90        height=480,
 91        width=640,
 92        data_types=[
 93            "rgb",
 94            "distance_to_image_plane",
 95            "normals",
 96            "semantic_segmentation",
 97            "instance_segmentation",
 98            "instance_id_segmentation_fast",
 99        ],
100        renderer_cfg=IsaacRtxRendererCfg(
101            colorize_semantic_segmentation=True,
102            colorize_instance_id_segmentation=True,
103            colorize_instance_segmentation=True,
104        ),
105        spawn=sim_utils.PinholeCameraCfg(
106            focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 1.0e5)
107        ),
108    )
109    # Create camera
110    camera = instantiate(camera_cfg)
111
112    return camera
113
114
115def design_scene() -> dict:
116    """Design the scene."""
117    # Populate scene
118    # -- Ground-plane
119    cfg = sim_utils.GroundPlaneCfg()
120    cfg.func("/World/defaultGroundPlane", cfg)
121    # -- Lights
122    cfg = sim_utils.DistantLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75))
123    cfg.func("/World/Light", cfg)
124
125    # Create a dictionary for the scene entities
126    scene_entities = {}
127
128    # Xform to hold objects
129    sim_utils.create_prim("/World/Objects", "Xform")
130    # Random objects
131    for i in range(8):
132        # sample random position
133        position = np.random.rand(3) - np.asarray([0.05, 0.05, -1.0])
134        position *= np.asarray([1.5, 1.5, 0.5])
135        # sample random color
136        color = (random.random(), random.random(), random.random())
137        # choose random prim type
138        prim_type = random.choice(["Cube", "Cone", "Cylinder"])
139        common_properties = {
140            "rigid_props": sim_utils.UsdPhysicsRigidBodyCfg(),
141            "mass_props": sim_utils.MassCfg(mass=5.0),
142            "collision_props": sim_utils.UsdPhysicsCollisionCfg(),
143            "visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=color, metallic=0.5),
144            "semantic_tags": [("class", prim_type)],
145        }
146        if prim_type == "Cube":
147            shape_cfg = sim_utils.CuboidCfg(size=(0.25, 0.25, 0.25), **common_properties)
148        elif prim_type == "Cone":
149            shape_cfg = sim_utils.ConeCfg(radius=0.1, height=0.25, **common_properties)
150        elif prim_type == "Cylinder":
151            shape_cfg = sim_utils.CylinderCfg(radius=0.25, height=0.25, **common_properties)
152        # Rigid Object
153        obj_cfg = RigidObjectCfg(
154            prim_path=f"/World/Objects/Obj_{i:02d}",
155            spawn=shape_cfg,
156            init_state=RigidObjectCfg.InitialStateCfg(pos=position),
157        )
158        scene_entities[f"rigid_object{i}"] = RigidObject(cfg=obj_cfg)
159
160    # Sensors
161    camera = define_sensor()
162
163    # return the scene information
164    scene_entities["camera"] = camera
165    return scene_entities
166
167
168def run_simulator(sim: sim_utils.SimulationContext, scene_entities: dict):
169    """Run the simulator."""
170    # extract entities for simplified notation
171    camera: Camera = scene_entities["camera"]
172
173    # Create the output directory
174    output_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "output", "camera")
175    os.makedirs(output_dir, exist_ok=True)
176
177    # Camera positions, targets, orientations
178    camera_positions = torch.tensor([[2.5, 2.5, 2.5], [-2.5, -2.5, 2.5]], device=sim.device)
179    camera_targets = torch.tensor([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]], device=sim.device)
180    # These orientations are in ROS-convention, and will position the cameras to view the origin
181    camera_orientations = torch.tensor(  # noqa: F841
182        [[-0.1759, 0.3399, 0.8205, -0.4247], [-0.4247, 0.8205, -0.3399, 0.1759]], device=sim.device
183    )
184
185    # Set pose: There are two ways to set the pose of the camera.
186    # -- Option-1: Set pose using view
187    camera.set_world_poses_from_view(camera_positions, camera_targets)
188    # -- Option-2: Set pose using ROS
189    # camera.set_world_poses(camera_positions, camera_orientations, convention="ros")
190
191    # Index of the camera to use for visualization and saving
192    camera_index = args_cli.camera_id
193
194    # Create the markers for the --draw option outside of the simulation loop
195    if sim.get_setting("/isaaclab/has_gui") and args_cli.draw:
196        cfg = replace(RAY_CASTER_MARKER_CFG, prim_path="/Visuals/CameraPointCloud")
197        cfg.markers["hit"].radius = 0.002
198        pc_markers = VisualizationMarkers(cfg)
199
200    # Simulate physics
201    while sim.is_running():
202        # Step simulation
203        sim.step()
204        # Update camera data
205        camera.update(dt=sim.get_physics_dt())
206
207        # Print camera info
208        print(camera)
209        if "rgb" in camera.data.output.keys():
210            print("Received shape of rgb image        : ", camera.data.output["rgb"].shape)
211        if "distance_to_image_plane" in camera.data.output.keys():
212            print("Received shape of depth image      : ", camera.data.output["distance_to_image_plane"].shape)
213        if "normals" in camera.data.output.keys():
214            print("Received shape of normals          : ", camera.data.output["normals"].shape)
215        if "semantic_segmentation" in camera.data.output.keys():
216            print("Received shape of semantic segm.   : ", camera.data.output["semantic_segmentation"].shape)
217        if "instance_segmentation" in camera.data.output.keys():
218            print("Received shape of instance segm.   : ", camera.data.output["instance_segmentation"].shape)
219        if "instance_id_segmentation_fast" in camera.data.output.keys():
220            print("Received shape of instance id segm.: ", camera.data.output["instance_id_segmentation_fast"].shape)
221        print("-------------------------------")
222
223        # Extract camera data
224        if args_cli.save:
225            # Save the camera outputs at camera_index: 8-bit color images as PNG, other data (depth, normals) as NumPy
226            for key, data in camera.data.output.items():
227                file_stem = os.path.join(output_dir, f"{key}_{camera.frame[camera_index]}")
228                if data.torch.dtype == torch.uint8:
229                    save_images_to_file(data.torch[camera_index : camera_index + 1].float() / 255.0, f"{file_stem}.png")
230                else:
231                    np.save(f"{file_stem}.npy", data.torch[camera_index].cpu().numpy())
232
233        # Draw pointcloud if there is a GUI and --draw has been passed
234        if (
235            sim.get_setting("/isaaclab/has_gui")
236            and args_cli.draw
237            and "distance_to_image_plane" in camera.data.output.keys()
238        ):
239            # Derive pointcloud from camera at camera_index
240            pointcloud = create_pointcloud_from_depth(
241                intrinsic_matrix=camera.data.intrinsic_matrices[camera_index],
242                depth=camera.data.output["distance_to_image_plane"][camera_index],
243                position=camera.data.pos_w[camera_index],
244                orientation=camera.data.quat_w_ros[camera_index],
245                device=sim.device,
246            )
247
248            # In the first few steps, things are still being instanced and Camera.data
249            # can be empty. If we attempt to visualize an empty pointcloud it will crash
250            # the sim, so we check that the pointcloud is not empty.
251            if pointcloud.size()[0] > 0:
252                pc_markers.visualize(translations=pointcloud)
253
254
255def main():
256    """Main function."""
257    # Configure the simulation
258    sim_cfg = sim_utils.SimulationCfg(device=args_cli.device)
259    # Launch the simulator runtime that the configuration needs
260    with launch_simulation(sim_cfg, args_cli):
261        # Initialize the simulation context
262        sim = sim_utils.SimulationContext(sim_cfg)
263        # Set main camera
264        sim.set_camera_view([2.5, 2.5, 2.5], [0.0, 0.0, 0.0])
265        # Design scene
266        scene_entities = design_scene()
267        # Play simulator
268        sim.reset()
269        # Now we are ready!
270        print("[INFO]: Setup complete...")
271        # Run simulator
272        run_simulator(sim, scene_entities)
273
274
275if __name__ == "__main__":
276    # run the main function
277    main()

将图像保存到文件#

使用 save_images_to_file() 工具保存相机输出。它将一批图像写入 PNG 文件,不依赖渲染后端。脚本在仿真循环开始前一次性创建输出目录:

    # Create the output directory
    output_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "output", "camera")
    os.makedirs(output_dir, exist_ok=True)

推进仿真器时,对 camera_index 指定的相机,按每种数据类型、每帧保存一次输出:8 位彩色输出(RGB 图像和彩色分割图)保存为 PNG;深度和法线等浮点输出保存为 NumPy .npy 文件。

            # Save the camera outputs at camera_index: 8-bit color images as PNG, other data (depth, normals) as NumPy
            for key, data in camera.data.output.items():
                file_stem = os.path.join(output_dir, f"{key}_{camera.frame[camera_index]}")
                if data.torch.dtype == torch.uint8:
                    save_images_to_file(data.torch[camera_index : camera_index + 1].float() / 255.0, f"{file_stem}.png")
                else:
                    np.save(f"{file_stem}.npy", data.torch[camera_index].cpu().numpy())

投影到 3D 空间#

我们包含实用程序来将深度图像投影到 3D 空间。重新投影操作使用 PyTorch 操作来进行,这可以加快计算速度。

from isaaclab.utils.math import transform_points, unproject_depth

# Pointcloud in world frame
points_3d_cam = unproject_depth(
   camera.data.output["distance_to_image_plane"], camera.data.intrinsic_matrices
)

points_3d_world = transform_points(points_3d_cam, camera.data.pos_w, camera.data.quat_w_ros)

或者,我们可以使用 isaaclab.sensors.camera.utils.create_pointcloud_from_depth() 函数从深度图像创建点云,并将其转换为世界框架。

            # Derive pointcloud from camera at camera_index
            pointcloud = create_pointcloud_from_depth(
                intrinsic_matrix=camera.data.intrinsic_matrices[camera_index],
                depth=camera.data.output["distance_to_image_plane"][camera_index],
                position=camera.data.pos_w[camera_index],
                orientation=camera.data.quat_w_ros[camera_index],
                device=sim.device,
            )

生成的点云可通过 Isaac Lab 的 VisualizationMarkers 显示。配套教程使用 RAY_CASTER_MARKER_CFG,并调用 visualize() 在视口中绘制点。

            # In the first few steps, things are still being instanced and Camera.data
            # can be empty. If we attempt to visualize an empty pointcloud it will crash
            # the sim, so we check that the pointcloud is not empty.
            if pointcloud.size()[0] > 0:
                pc_markers.visualize(translations=pointcloud)

执行脚本#

要运行附带的脚本,请执行以下命令:

# Usage with saving and drawing
uv run python scripts/tutorials/04_sensors/run_usd_camera.py --save --draw --viz kit

# Usage with saving only (no visualizer)
uv run python scripts/tutorials/04_sensors/run_usd_camera.py --save

仿真开始后,可以观察不同物体下落。脚本会在 IsaacLab/scripts/tutorials/04_sensors 目录下创建输出文件夹,将图像保存为 PNG。同时,视口中应显示绘制在三维空间中的点云。

要停止仿真,关闭窗口,或在终端中使用 Ctrl+C 。