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.