Saving rendered images and 3D re-projection

Saving rendered images and 3D re-projection#

This guide accompanied with the run_usd_camera.py script in the IsaacLab/scripts/tutorials/04_sensors directory.

Code for 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()

Saving the images to file#

To save camera outputs, we use the save_images_to_file() utility. It writes a batch of images as a PNG file and does not depend on the renderer backend. The script creates the output folder once, before the simulation loop:

    # 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)

While stepping the simulator, the outputs of the camera at camera_index are saved once per data type and frame: 8-bit color outputs (the RGB image and the colorized segmentations) as PNG files, and floating-point outputs, such as depth and normals, as NumPy .npy files.

            # 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())

Projection into 3D Space#

We include utilities to project the depth image into 3D Space. The re-projection operations are done using PyTorch operations which allows faster computation.

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)

Alternately, we can use the isaaclab.sensors.camera.utils.create_pointcloud_from_depth() function to create a point cloud from the depth image and transform it to the world frame.

            # 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,
            )

The resulting point cloud can be visualized using VisualizationMarkers. This makes it easy to visualize the point cloud in the 3D space.

            # 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)

Executing the script#

To run the accompanying script, execute the following command:

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

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

The simulation should start, and you can observe different objects falling down. An output folder will be created in the IsaacLab/scripts/tutorials/04_sensors directory, where the images will be saved as PNG files. Additionally, you should see the point cloud in the 3D space drawn on the viewport.

To stop the simulation, close the window, or use Ctrl+C in the terminal.