Find How Many/What Cameras You Should Train With#
Currently in Isaac Lab, there are several camera types; USD Cameras (standard), Tiled Cameras,
and Ray Caster cameras. These camera types differ in functionality and performance. The benchmark_cameras.py
script can be used to understand the difference in cameras types, as well to characterize their relative performance
at different parameters such as camera quantity, image dimensions, and data types.
This utility is provided so that one easily can find the camera type/parameters that are the most performant while meeting the requirements of the user’s scenario. This utility also helps estimate the maximum number of cameras one can realistically run, assuming that one wants to maximize the number of environments while minimizing step time.
This utility can inject cameras into an existing task from the gym registry,
which can be useful for benchmarking cameras in a specific scenario. Also,
if you install pynvml, you can let this utility automatically find the maximum
numbers of cameras that can run in your task environment up to a
certain specified system resource utilization threshold (without training; taking zero actions
at each timestep).
This guide accompanies the benchmark_cameras.py script in the scripts/benchmarks
directory.
Code for benchmark_cameras.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 might help you determine how many cameras your system can realistically run
8at different desired settings.
9
10You can supply different task environments to inject cameras into, or just test a sample scene.
11Additionally, you can automatically find the maximum amount of cameras you can run a task with
12through the auto-tune functionality.
13
14.. code-block:: bash
15
16 # Usage with GUI
17 uv run python scripts/benchmarks/benchmark_cameras.py -h
18
19 # Usage with headless
20 uv run python scripts/benchmarks/benchmark_cameras.py -h
21
22"""
23
24"""Launch Isaac Sim Simulator first."""
25
26import argparse
27from collections.abc import Callable
28from dataclasses import MISSING
29
30from isaaclab.app import AppLauncher
31
32# parse the arguments
33args_cli = argparse.Namespace()
34
35parser = argparse.ArgumentParser(description="This script can help you benchmark how many cameras you could run.")
36
37"""
38The following arguments only need to be supplied for when one wishes
39to try injecting cameras into their environment, and automatically determining
40the maximum camera count.
41"""
42parser.add_argument(
43 "--task",
44 type=str,
45 default=None,
46 required=False,
47 help="Supply this argument to spawn cameras within an known manager-based task environment.",
48)
49
50parser.add_argument(
51 "--autotune",
52 default=False,
53 action="store_true",
54 help=(
55 "Autotuning is only supported for provided task environments."
56 " Supply this argument to increase the number of environments until a desired threshold is reached."
57 "Install pynvml in your environment; ./isaaclab.sh -m pip install pynvml"
58 ),
59)
60
61parser.add_argument(
62 "--task_num_cameras_per_env",
63 type=int,
64 default=1,
65 help="The number of cameras per environment to use when using a known task.",
66)
67
68parser.add_argument(
69 "--use_fabric", action="store_true", default=False, help="Enable fabric and use USD I/O operations."
70)
71
72parser.add_argument(
73 "--autotune_max_percentage_util",
74 nargs="+",
75 type=float,
76 default=[100.0, 80.0, 80.0, 80.0],
77 required=False,
78 help=(
79 "The system utilization percentage thresholds to reach before an autotune is finished. "
80 "If any one of these limits are hit, the autotune stops."
81 "Thresholds are, in order, maximum CPU percentage utilization,"
82 "maximum RAM percentage utilization, maximum GPU compute percent utilization, "
83 "amd maximum GPU memory utilization."
84 ),
85)
86
87parser.add_argument(
88 "--autotune_max_camera_count", type=int, default=4096, help="The maximum amount of cameras allowed in an autotune."
89)
90
91parser.add_argument(
92 "--autotune_camera_count_interval",
93 type=int,
94 default=25,
95 help=(
96 "The number of cameras to try to add to the environment if the current camera count"
97 " falls within permitted system resource utilization limits."
98 ),
99)
100
101"""
102The following arguments are shared for when injecting cameras into a task environment,
103as well as when creating cameras independent of a task environment.
104"""
105
106parser.add_argument(
107 "--num_tiled_cameras",
108 type=int,
109 default=0,
110 required=False,
111 help="Number of tiled cameras to create. For autotuning, this is how many cameras to start with.",
112)
113
114parser.add_argument(
115 "--num_standard_cameras",
116 type=int,
117 default=0,
118 required=False,
119 help="Number of standard cameras to create. For autotuning, this is how many cameras to start with.",
120)
121
122parser.add_argument(
123 "--num_ray_caster_cameras",
124 type=int,
125 default=0,
126 required=False,
127 help="Number of ray caster cameras to create. For autotuning, this is how many cameras to start with.",
128)
129
130parser.add_argument(
131 "--tiled_camera_data_types",
132 nargs="+",
133 type=str,
134 default=["rgb", "depth"],
135 help="The data types rendered by the tiled camera",
136)
137
138parser.add_argument(
139 "--standard_camera_data_types",
140 nargs="+",
141 type=str,
142 default=["rgb", "distance_to_image_plane", "distance_to_camera"],
143 help="The data types rendered by the standard camera",
144)
145
146parser.add_argument(
147 "--ray_caster_camera_data_types",
148 nargs="+",
149 type=str,
150 default=["distance_to_image_plane"],
151 help="The data types rendered by the ray caster camera.",
152)
153
154parser.add_argument(
155 "--ray_caster_visible_mesh_prim_paths",
156 nargs="+",
157 type=str,
158 default=["/World/ground"],
159 help="WARNING: Ray Caster can currently only cast against a single, static, object",
160)
161
162parser.add_argument(
163 "--convert_depth_to_camera_to_image_plane",
164 action="store_true",
165 default=True,
166 help=(
167 "Enable undistorting from perspective view (distance to camera data_type)"
168 "to orthogonal view (distance to plane data_type) for depth."
169 "This is currently needed to create undisorted depth images/point cloud."
170 ),
171)
172
173parser.add_argument(
174 "--keep_raw_depth",
175 dest="convert_depth_to_camera_to_image_plane",
176 action="store_false",
177 help=(
178 "Disable undistorting from perspective view (distance to camera)"
179 "to orthogonal view (distance to plane data_type) for depth."
180 ),
181)
182
183parser.add_argument(
184 "--height",
185 type=int,
186 default=120,
187 required=False,
188 help="Height in pixels of cameras",
189)
190
191parser.add_argument(
192 "--width",
193 type=int,
194 default=140,
195 required=False,
196 help="Width in pixels of cameras",
197)
198
199parser.add_argument(
200 "--warm_start_length",
201 type=int,
202 default=3,
203 required=False,
204 help=(
205 "Number of steps to run the sim before starting benchmark."
206 "Needed to avoid blank images at the start of the simulation."
207 ),
208)
209
210parser.add_argument(
211 "--experiment_length",
212 type=int,
213 default=15,
214 required=False,
215 help="Number of steps to average over",
216)
217
218# This argument is only used when a task is not provided.
219parser.add_argument(
220 "--num_objects",
221 type=int,
222 default=10,
223 required=False,
224 help="Number of objects to spawn into the scene when not using a known task.",
225)
226
227# Benchmark arguments
228parser.add_argument(
229 "--benchmark_formatter",
230 type=str,
231 default="omniperf",
232 choices=["json", "osmo", "omniperf", "summary"],
233 help="Benchmark output formatter, defaults omniperf",
234)
235parser.add_argument("--output_path", type=str, default=".", help="Path to output benchmark results.")
236
237
238AppLauncher.add_app_launcher_args(parser)
239args_cli = parser.parse_args()
240args_cli.enable_cameras = True
241
242if args_cli.autotune:
243 import pynvml
244
245if len(args_cli.ray_caster_visible_mesh_prim_paths) > 1:
246 print("[WARNING]: Ray Casting is only currently supported for a single, static object")
247# launch omniverse app
248app_launcher = AppLauncher(args_cli)
249simulation_app = app_launcher.app
250
251"""Rest everything follows."""
252
253import random
254import time
255
256import gymnasium as gym
257import numpy as np
258import psutil
259import torch
260
261import isaaclab.sim as sim_utils
262from isaaclab.assets import RigidObject, RigidObjectCfg
263from isaaclab.benchmark import BaseIsaacLabBenchmark, DictMeasurement, SingleMeasurement
264from isaaclab.scene.interactive_scene import InteractiveScene
265from isaaclab.sensors import (
266 Camera,
267 CameraCfg,
268 RayCasterCamera,
269 RayCasterCameraCfg,
270 patterns,
271)
272from isaaclab.utils.math import orthogonalize_perspective_depth, unproject_depth
273
274from isaaclab_tasks.utils import parse_env_cfg
275
276"""
277Camera Creation
278"""
279
280
281def _get_camera_class_name(camera_cfg: type[CameraCfg]) -> str:
282 """Return the configured camera sensor class name."""
283 class_type_field = camera_cfg.__dataclass_fields__["class_type"]
284 if class_type_field.default is not MISSING:
285 class_type = class_type_field.default
286 elif class_type_field.default_factory is not MISSING:
287 class_type = class_type_field.default_factory()
288 else:
289 raise AttributeError(f"{camera_cfg.__name__} has no default class_type.")
290
291 if hasattr(class_type, "__name__"):
292 return class_type.__name__
293 return str(class_type).rsplit(":", maxsplit=1)[-1]
294
295
296def create_camera_base(
297 camera_cfg: type[CameraCfg],
298 num_cams: int,
299 data_types: list[str],
300 height: int,
301 width: int,
302 prim_path: str | None = None,
303 instantiate: bool = True,
304) -> Camera | CameraCfg | None:
305 """Generalized function to create a camera or tiled camera sensor."""
306 # If valid camera settings are provided, create the camera
307 if num_cams <= 0 or len(data_types) <= 0 or height <= 0 or width <= 0:
308 return None
309
310 name = _get_camera_class_name(camera_cfg)
311 cfg = camera_cfg(
312 prim_path=prim_path if prim_path is not None else f"/World/{name}_.*/{name}",
313 update_period=0,
314 height=height,
315 width=width,
316 data_types=data_types,
317 spawn=sim_utils.PinholeCameraCfg(
318 focal_length=24, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 1e4)
319 ),
320 )
321 if instantiate:
322 # Create the necessary prims
323 for idx in range(num_cams):
324 sim_utils.create_prim(f"/World/{name}_{idx:02d}", "Xform")
325 return cfg.class_type(cfg=cfg)
326
327 return cfg
328
329
330def create_tiled_cameras(
331 num_cams: int = 2, data_types: list[str] | None = None, height: int = 100, width: int = 120
332) -> Camera | None:
333 if data_types is None:
334 data_types = ["rgb", "depth"]
335 """Defines the camera sensor to add to the scene."""
336 return create_camera_base(
337 camera_cfg=CameraCfg,
338 num_cams=num_cams,
339 data_types=data_types,
340 height=height,
341 width=width,
342 )
343
344
345def create_cameras(
346 num_cams: int = 2, data_types: list[str] | None = None, height: int = 100, width: int = 120
347) -> Camera | None:
348 """Defines the Standard cameras."""
349 if data_types is None:
350 data_types = ["rgb", "depth"]
351 return create_camera_base(
352 camera_cfg=CameraCfg, num_cams=num_cams, data_types=data_types, height=height, width=width
353 )
354
355
356def create_ray_caster_cameras(
357 num_cams: int = 2,
358 data_types: list[str] = ["distance_to_image_plane"],
359 mesh_prim_paths: list[str] = ["/World/ground"],
360 height: int = 100,
361 width: int = 120,
362 prim_path: str = "/World/RayCasterCamera_.*/RayCaster",
363 instantiate: bool = True,
364) -> RayCasterCamera | RayCasterCameraCfg | None:
365 """Create the raycaster cameras; different configuration than Standard/Tiled camera"""
366 for idx in range(num_cams):
367 sim_utils.create_prim(f"/World/RayCasterCamera_{idx:02d}/RayCaster", "Xform")
368
369 if num_cams > 0 and len(data_types) > 0 and height > 0 and width > 0:
370 cam_cfg = RayCasterCameraCfg(
371 prim_path=prim_path,
372 mesh_prim_paths=mesh_prim_paths,
373 update_period=0,
374 offset=RayCasterCameraCfg.OffsetCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)),
375 data_types=data_types,
376 debug_vis=False,
377 pattern_cfg=patterns.PinholeCameraPatternCfg(
378 focal_length=24.0,
379 horizontal_aperture=20.955,
380 height=480,
381 width=640,
382 ),
383 )
384 if instantiate:
385 return RayCasterCamera(cfg=cam_cfg)
386 else:
387 return cam_cfg
388
389 else:
390 return None
391
392
393def create_tiled_camera_cfg(prim_path: str) -> CameraCfg:
394 """Grab a simple camera config for injecting into task environments."""
395 return create_camera_base(
396 CameraCfg,
397 num_cams=args_cli.num_tiled_cameras,
398 data_types=args_cli.tiled_camera_data_types,
399 width=args_cli.width,
400 height=args_cli.height,
401 prim_path="{ENV_REGEX_NS}/" + prim_path,
402 instantiate=False,
403 )
404
405
406def create_standard_camera_cfg(prim_path: str) -> CameraCfg:
407 """Grab a simple standard camera config for injecting into task environments."""
408 return create_camera_base(
409 CameraCfg,
410 num_cams=args_cli.num_standard_cameras,
411 data_types=args_cli.standard_camera_data_types,
412 width=args_cli.width,
413 height=args_cli.height,
414 prim_path="{ENV_REGEX_NS}/" + prim_path,
415 instantiate=False,
416 )
417
418
419def create_ray_caster_camera_cfg(prim_path: str) -> RayCasterCameraCfg:
420 """Grab a simple ray caster config for injecting into task environments."""
421 return create_ray_caster_cameras(
422 num_cams=args_cli.num_ray_caster_cameras,
423 data_types=args_cli.ray_caster_camera_data_types,
424 width=args_cli.width,
425 height=args_cli.height,
426 prim_path="{ENV_REGEX_NS}/" + prim_path,
427 )
428
429
430"""
431Scene Creation
432"""
433
434
435def design_scene(
436 num_tiled_cams: int = 2,
437 num_standard_cams: int = 0,
438 num_ray_caster_cams: int = 0,
439 tiled_camera_data_types: list[str] | None = None,
440 standard_camera_data_types: list[str] | None = None,
441 ray_caster_camera_data_types: list[str] | None = None,
442 height: int = 100,
443 width: int = 200,
444 num_objects: int = 20,
445 mesh_prim_paths: list[str] = ["/World/ground"],
446) -> dict:
447 """Design the scene."""
448 if tiled_camera_data_types is None:
449 tiled_camera_data_types = ["rgb"]
450 if standard_camera_data_types is None:
451 standard_camera_data_types = ["rgb"]
452 if ray_caster_camera_data_types is None:
453 ray_caster_camera_data_types = ["distance_to_image_plane"]
454
455 # Populate scene
456 # -- Ground-plane
457 cfg = sim_utils.GroundPlaneCfg()
458 cfg.func("/World/ground", cfg)
459 # -- Lights
460 cfg = sim_utils.DistantLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75))
461 cfg.func("/World/Light", cfg)
462
463 # Create a dictionary for the scene entities
464 scene_entities = {}
465
466 # Xform to hold objects
467 sim_utils.create_prim("/World/Objects", "Xform")
468 # Random objects
469 for i in range(num_objects):
470 # sample random position
471 position = np.random.rand(3) - np.asarray([0.05, 0.05, -1.0])
472 position *= np.asarray([1.5, 1.5, 0.5])
473 # sample random color
474 color = (random.random(), random.random(), random.random())
475 # choose random prim type
476 prim_type = random.choice(["Cube", "Cone", "Cylinder"])
477 common_properties = {
478 "rigid_props": sim_utils.RigidBodyPropertiesCfg(),
479 "mass_props": sim_utils.MassPropertiesCfg(mass=5.0),
480 "collision_props": sim_utils.CollisionPropertiesCfg(),
481 "visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=color, metallic=0.5),
482 "semantic_tags": [("class", prim_type)],
483 }
484 if prim_type == "Cube":
485 shape_cfg = sim_utils.CuboidCfg(size=(0.25, 0.25, 0.25), **common_properties)
486 elif prim_type == "Cone":
487 shape_cfg = sim_utils.ConeCfg(radius=0.1, height=0.25, **common_properties)
488 elif prim_type == "Cylinder":
489 shape_cfg = sim_utils.CylinderCfg(radius=0.25, height=0.25, **common_properties)
490 # Rigid Object
491 obj_cfg = RigidObjectCfg(
492 prim_path=f"/World/Objects/Obj_{i:02d}",
493 spawn=shape_cfg,
494 init_state=RigidObjectCfg.InitialStateCfg(pos=position),
495 )
496 scene_entities[f"rigid_object{i}"] = RigidObject(cfg=obj_cfg)
497
498 # Sensors
499 standard_camera = create_cameras(
500 num_cams=num_standard_cams, data_types=standard_camera_data_types, height=height, width=width
501 )
502 tiled_camera = create_tiled_cameras(
503 num_cams=num_tiled_cams, data_types=tiled_camera_data_types, height=height, width=width
504 )
505 ray_caster_camera = create_ray_caster_cameras(
506 num_cams=num_ray_caster_cams,
507 data_types=ray_caster_camera_data_types,
508 mesh_prim_paths=mesh_prim_paths,
509 height=height,
510 width=width,
511 )
512 # return the scene information
513 if tiled_camera is not None:
514 scene_entities["tiled_camera"] = tiled_camera
515 if standard_camera is not None:
516 scene_entities["standard_camera"] = standard_camera
517 if ray_caster_camera is not None:
518 scene_entities["ray_caster_camera"] = ray_caster_camera
519 return scene_entities
520
521
522def inject_cameras_into_task(
523 task: str,
524 num_cams: int,
525 camera_name_prefix: str,
526 camera_creation_callable: Callable,
527 num_cameras_per_env: int = 1,
528) -> gym.Env:
529 """Loads the task, sticks cameras into the config, and creates the environment."""
530 cfg = parse_env_cfg(task, device=args_cli.device, use_fabric=args_cli.use_fabric)
531 scene_cfg = cfg.scene
532
533 num_envs = int(num_cams / num_cameras_per_env)
534 scene_cfg.num_envs = num_envs
535
536 for idx in range(num_cameras_per_env):
537 suffix = "" if idx == 0 else str(idx)
538 name = camera_name_prefix + suffix
539 setattr(scene_cfg, name, camera_creation_callable(name))
540 cfg.scene = scene_cfg
541 env = gym.make(task, cfg=cfg)
542 return env
543
544
545"""
546System diagnosis
547"""
548
549
550def get_utilization_percentages(reset: bool = False, max_values: list[float] = [0.0, 0.0, 0.0, 0.0]) -> list[float]:
551 """Get the maximum CPU, RAM, GPU utilization (processing), and
552 GPU memory usage percentages since the last time reset was true."""
553 if reset:
554 max_values[:] = [0, 0, 0, 0] # Reset the max values
555
556 # CPU utilization
557 cpu_usage = psutil.cpu_percent(interval=0.1)
558 max_values[0] = max(max_values[0], cpu_usage)
559
560 # RAM utilization
561 memory_info = psutil.virtual_memory()
562 ram_usage = memory_info.percent
563 max_values[1] = max(max_values[1], ram_usage)
564
565 # GPU utilization using pynvml
566 if torch.cuda.is_available():
567 if args_cli.autotune:
568 pynvml.nvmlInit() # Initialize NVML
569 for i in range(torch.cuda.device_count()):
570 handle = pynvml.nvmlDeviceGetHandleByIndex(i)
571
572 # GPU Utilization
573 gpu_utilization = pynvml.nvmlDeviceGetUtilizationRates(handle)
574 gpu_processing_utilization_percent = gpu_utilization.gpu # GPU core utilization
575 max_values[2] = max(max_values[2], gpu_processing_utilization_percent)
576
577 # GPU Memory Usage
578 memory_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
579 gpu_memory_total = memory_info.total
580 gpu_memory_used = memory_info.used
581 gpu_memory_utilization_percent = (gpu_memory_used / gpu_memory_total) * 100
582 max_values[3] = max(max_values[3], gpu_memory_utilization_percent)
583
584 pynvml.nvmlShutdown() # Shutdown NVML after usage
585 else:
586 gpu_processing_utilization_percent = None
587 gpu_memory_utilization_percent = None
588 return max_values
589
590
591"""
592Experiment
593"""
594
595
596def run_simulator(
597 sim: sim_utils.SimulationContext | None,
598 scene_entities: dict | InteractiveScene,
599 warm_start_length: int = 10,
600 experiment_length: int = 100,
601 tiled_camera_data_types: list[str] | None = None,
602 standard_camera_data_types: list[str] | None = None,
603 ray_caster_camera_data_types: list[str] | None = None,
604 depth_predicate: Callable = lambda x: "to" in x or x == "depth",
605 perspective_depth_predicate: Callable = lambda x: x == "distance_to_camera",
606 convert_depth_to_camera_to_image_plane: bool = True,
607 max_cameras_per_env: int = 1,
608 env: gym.Env | None = None,
609) -> dict:
610 """Run the simulator with all cameras, and return timing analytics. Visualize if desired."""
611
612 if tiled_camera_data_types is None:
613 tiled_camera_data_types = ["rgb"]
614 if standard_camera_data_types is None:
615 standard_camera_data_types = ["rgb"]
616 if ray_caster_camera_data_types is None:
617 ray_caster_camera_data_types = ["distance_to_image_plane"]
618
619 # Initialize camera lists
620 tiled_cameras = []
621 standard_cameras = []
622 ray_caster_cameras = []
623
624 # Dynamically extract cameras from the scene entities up to max_cameras_per_env
625 for i in range(max_cameras_per_env):
626 # Extract tiled cameras
627 tiled_camera_key = f"tiled_camera{i}" if i > 0 else "tiled_camera"
628 standard_camera_key = f"standard_camera{i}" if i > 0 else "standard_camera"
629 ray_caster_camera_key = f"ray_caster_camera{i}" if i > 0 else "ray_caster_camera"
630
631 try: # if instead you checked ... if key is in scene_entities... # errors out always even if key present
632 tiled_cameras.append(scene_entities[tiled_camera_key])
633 standard_cameras.append(scene_entities[standard_camera_key])
634 ray_caster_cameras.append(scene_entities[ray_caster_camera_key])
635 except KeyError:
636 break
637
638 # Initialize camera counts
639 camera_lists = [tiled_cameras, standard_cameras, ray_caster_cameras]
640 camera_data_types = [tiled_camera_data_types, standard_camera_data_types, ray_caster_camera_data_types]
641 labels = ["tiled", "standard", "ray_caster"]
642
643 if sim is not None:
644 # Set camera world poses
645 for camera_list in camera_lists:
646 for camera in camera_list:
647 num_cameras = camera.data.intrinsic_matrices.size(0)
648 positions = torch.tensor([[2.5, 2.5, 2.5]], device=sim.device).repeat(num_cameras, 1)
649 targets = torch.tensor([[0.0, 0.0, 0.0]], device=sim.device).repeat(num_cameras, 1)
650 camera.set_world_poses_from_view(positions, targets)
651
652 # Initialize timing variables
653 timestep = 0
654 total_time = 0.0
655 valid_timesteps = 0
656 sim_step_time = 0.0
657
658 while simulation_app.is_running() and timestep < experiment_length:
659 print(f"On timestep {timestep} of {experiment_length}, with warm start of {warm_start_length}")
660 get_utilization_percentages()
661
662 # Measure the total simulation step time
663 step_start_time = time.time()
664
665 if sim is not None:
666 sim.step()
667
668 if env is not None:
669 with torch.inference_mode():
670 # compute zero actions
671 actions = torch.zeros(env.action_space.shape, device=env.unwrapped.device)
672 # apply actions
673 env.step(actions)
674
675 # Update cameras and process vision data within the simulation step
676 clouds = {}
677 images = {}
678 depth_images = {}
679
680 # Loop through all camera lists and their data_types
681 for camera_list, data_types, label in zip(camera_lists, camera_data_types, labels):
682 for cam_idx, camera in enumerate(camera_list):
683 if env is None: # No env, need to step cams manually
684 # Only update the camera if it hasn't been updated as part of scene_entities.update ...
685 camera.update(dt=sim.get_physics_dt())
686
687 for data_type in data_types:
688 data_label = f"{label}_{cam_idx}_{data_type}"
689
690 if depth_predicate(data_type): # is a depth image, want to create cloud
691 depth = camera.data.output[data_type]
692 depth_images[data_label + "_raw"] = depth
693 if perspective_depth_predicate(data_type) and convert_depth_to_camera_to_image_plane:
694 depth = orthogonalize_perspective_depth(
695 camera.data.output[data_type], camera.data.intrinsic_matrices
696 )
697 depth_images[data_label + "_undistorted"] = depth
698
699 pointcloud = unproject_depth(depth=depth, intrinsics=camera.data.intrinsic_matrices)
700 clouds[data_label] = pointcloud
701 else: # rgb image, just save it
702 image = camera.data.output[data_type]
703 images[data_label] = image
704
705 # End timing for the step
706 step_end_time = time.time()
707 sim_step_time += step_end_time - step_start_time
708
709 if timestep > warm_start_length:
710 get_utilization_percentages(reset=True)
711 total_time += step_end_time - step_start_time
712 valid_timesteps += 1
713
714 timestep += 1
715
716 # Calculate average timings
717 if valid_timesteps > 0:
718 avg_timestep_duration = total_time / valid_timesteps
719 avg_sim_step_duration = sim_step_time / experiment_length
720 else:
721 avg_timestep_duration = 0.0
722 avg_sim_step_duration = 0.0
723
724 # Package timing analytics in a dictionary
725 timing_analytics = {
726 "average_timestep_duration": avg_timestep_duration,
727 "average_sim_step_duration": avg_sim_step_duration,
728 "total_simulation_time": sim_step_time,
729 "total_experiment_duration": sim_step_time,
730 }
731
732 system_utilization_analytics = get_utilization_percentages()
733
734 print("--- Benchmark Results ---")
735 print(f"Average timestep duration: {avg_timestep_duration:.6f} seconds")
736 print(f"Average simulation step duration: {avg_sim_step_duration:.6f} seconds")
737 print(f"Total simulation time: {sim_step_time:.6f} seconds")
738 print("\nSystem Utilization Statistics:")
739 print(
740 f"| CPU:{system_utilization_analytics[0]}% | "
741 f"RAM:{system_utilization_analytics[1]}% | "
742 f"GPU Compute:{system_utilization_analytics[2]}% | "
743 f" GPU Memory: {system_utilization_analytics[3]:.2f}% |"
744 )
745
746 return {"timing_analytics": timing_analytics, "system_utilization_analytics": system_utilization_analytics}
747
748
749def main():
750 """Main function."""
751 # Load simulation context
752 if args_cli.num_tiled_cameras + args_cli.num_standard_cameras + args_cli.num_ray_caster_cameras <= 0:
753 raise ValueError("You must select at least one camera.")
754 if (
755 (args_cli.num_tiled_cameras > 0 and args_cli.num_standard_cameras > 0)
756 or (args_cli.num_ray_caster_cameras > 0 and args_cli.num_standard_cameras > 0)
757 or (args_cli.num_ray_caster_cameras > 0 and args_cli.num_tiled_cameras > 0)
758 ):
759 print("[WARNING]: You have elected to use more than one camera type.")
760 print("[WARNING]: For a benchmark to be meaningful, use ONLY ONE camera type at a time.")
761 print(
762 "[WARNING]: For example, if num_tiled_cameras=100, for a meaningful benchmark,"
763 "num_standard_cameras should be 0, and num_ray_caster_cameras should be 0"
764 )
765 raise ValueError("Benchmark one camera at a time.")
766
767 # Determine which camera type is being used
768 camera_type = "tiled"
769 num_cameras = args_cli.num_tiled_cameras
770 if args_cli.num_standard_cameras > 0:
771 camera_type = "standard"
772 num_cameras = args_cli.num_standard_cameras
773 elif args_cli.num_ray_caster_cameras > 0:
774 camera_type = "ray_caster"
775 num_cameras = args_cli.num_ray_caster_cameras
776
777 # Create the benchmark
778 formatter_type = args_cli.benchmark_formatter
779 benchmark = BaseIsaacLabBenchmark(
780 benchmark_name="benchmark_cameras",
781 formatter_type=formatter_type,
782 output_path=args_cli.output_path,
783 use_recorders=True,
784 frametime_recorders=formatter_type in ("summary", "omniperf"),
785 output_prefix="benchmark_cameras",
786 workflow_metadata={
787 "metadata": [
788 {"name": "task", "data": args_cli.task},
789 {"name": "camera_type", "data": camera_type},
790 {"name": "num_cameras", "data": num_cameras},
791 {"name": "height", "data": args_cli.height},
792 {"name": "width", "data": args_cli.width},
793 {"name": "experiment_length", "data": args_cli.experiment_length},
794 {"name": "autotune", "data": args_cli.autotune},
795 ]
796 },
797 )
798
799 print("[INFO]: Designing the scene")
800 final_analysis = None
801
802 if args_cli.task is None:
803 print("[INFO]: No task environment provided, creating random scene.")
804 sim_cfg = sim_utils.SimulationCfg(device=args_cli.device)
805 sim = sim_utils.SimulationContext(sim_cfg)
806 # Set main camera
807 sim.set_camera_view([2.5, 2.5, 2.5], [0.0, 0.0, 0.0])
808 scene_entities = design_scene(
809 num_tiled_cams=args_cli.num_tiled_cameras,
810 num_standard_cams=args_cli.num_standard_cameras,
811 num_ray_caster_cams=args_cli.num_ray_caster_cameras,
812 tiled_camera_data_types=args_cli.tiled_camera_data_types,
813 standard_camera_data_types=args_cli.standard_camera_data_types,
814 ray_caster_camera_data_types=args_cli.ray_caster_camera_data_types,
815 height=args_cli.height,
816 width=args_cli.width,
817 num_objects=args_cli.num_objects,
818 mesh_prim_paths=args_cli.ray_caster_visible_mesh_prim_paths,
819 )
820 # Play simulator
821 sim.reset()
822 # Now we are ready!
823 print("[INFO]: Setup complete...")
824 # Run simulator
825 final_analysis = run_simulator(
826 sim=sim,
827 scene_entities=scene_entities,
828 warm_start_length=args_cli.warm_start_length,
829 experiment_length=args_cli.experiment_length,
830 tiled_camera_data_types=args_cli.tiled_camera_data_types,
831 standard_camera_data_types=args_cli.standard_camera_data_types,
832 ray_caster_camera_data_types=args_cli.ray_caster_camera_data_types,
833 convert_depth_to_camera_to_image_plane=args_cli.convert_depth_to_camera_to_image_plane,
834 )
835 else:
836 print("[INFO]: Using known task environment, injecting cameras.")
837 autotune_iter = 0
838 max_sys_util_thresh = [0.0, 0.0, 0.0]
839 max_num_cams = max(args_cli.num_tiled_cameras, args_cli.num_standard_cameras, args_cli.num_ray_caster_cameras)
840 cur_num_cams = max_num_cams
841 cur_sys_util = max_sys_util_thresh
842 interval = args_cli.autotune_camera_count_interval
843
844 if args_cli.autotune:
845 max_sys_util_thresh = args_cli.autotune_max_percentage_util
846 max_num_cams = args_cli.autotune_max_camera_count
847 print("[INFO]: Auto tuning until any of the following threshold are met")
848 print(f"|CPU: {max_sys_util_thresh[0]}% | RAM {max_sys_util_thresh[1]}% | GPU: {max_sys_util_thresh[2]}% |")
849 print(f"[INFO]: Maximum number of cameras allowed: {max_num_cams}")
850 # Determine which camera is being tested...
851 tiled_camera_cfg = create_tiled_camera_cfg("tiled_camera")
852 standard_camera_cfg = create_standard_camera_cfg("standard_camera")
853 ray_caster_camera_cfg = create_ray_caster_camera_cfg("ray_caster_camera")
854 camera_name_prefix = ""
855 camera_creation_callable = None
856 num_cams = 0
857 if tiled_camera_cfg is not None:
858 camera_name_prefix = "tiled_camera"
859 camera_creation_callable = create_tiled_camera_cfg
860 num_cams = args_cli.num_tiled_cameras
861 elif standard_camera_cfg is not None:
862 camera_name_prefix = "standard_camera"
863 camera_creation_callable = create_standard_camera_cfg
864 num_cams = args_cli.num_standard_cameras
865 elif ray_caster_camera_cfg is not None:
866 camera_name_prefix = "ray_caster_camera"
867 camera_creation_callable = create_ray_caster_camera_cfg
868 num_cams = args_cli.num_ray_caster_cameras
869
870 while (
871 all(cur <= max_thresh for cur, max_thresh in zip(cur_sys_util, max_sys_util_thresh))
872 and cur_num_cams <= max_num_cams
873 ):
874 cur_num_cams = num_cams + interval * autotune_iter
875 autotune_iter += 1
876
877 env = inject_cameras_into_task(
878 task=args_cli.task,
879 num_cams=cur_num_cams,
880 camera_name_prefix=camera_name_prefix,
881 camera_creation_callable=camera_creation_callable,
882 num_cameras_per_env=args_cli.task_num_cameras_per_env,
883 )
884 env.reset()
885 print(f"Testing with {cur_num_cams} {camera_name_prefix}")
886 analysis = run_simulator(
887 sim=None,
888 scene_entities=env.unwrapped.scene,
889 warm_start_length=args_cli.warm_start_length,
890 experiment_length=args_cli.experiment_length,
891 tiled_camera_data_types=args_cli.tiled_camera_data_types,
892 standard_camera_data_types=args_cli.standard_camera_data_types,
893 ray_caster_camera_data_types=args_cli.ray_caster_camera_data_types,
894 convert_depth_to_camera_to_image_plane=args_cli.convert_depth_to_camera_to_image_plane,
895 max_cameras_per_env=args_cli.task_num_cameras_per_env,
896 env=env,
897 )
898
899 cur_sys_util = analysis["system_utilization_analytics"]
900 final_analysis = analysis
901 print("Triggering reset...")
902 env.close()
903 sim_utils.create_new_stage()
904 print("[INFO]: DONE! Feel free to CTRL + C Me ")
905 print(f"[INFO]: If you've made it this far, you can likely simulate {cur_num_cams} {camera_name_prefix}")
906 print("Keep in mind, this is without any training running on the GPU.")
907 print("Set lower utilization thresholds to account for training.")
908
909 if not args_cli.autotune:
910 print("[WARNING]: GPU Util Statistics only correct while autotuning, ignore above.")
911
912 # Log benchmark measurements
913 if final_analysis is not None:
914 timing = final_analysis["timing_analytics"]
915 sys_util = final_analysis["system_utilization_analytics"]
916
917 # Log timing measurements
918 benchmark.add_measurement(
919 "runtime",
920 measurement=SingleMeasurement(
921 name="Average Timestep Duration", value=timing["average_timestep_duration"] * 1000, unit="ms"
922 ),
923 )
924 benchmark.add_measurement(
925 "runtime",
926 measurement=SingleMeasurement(
927 name="Average Simulation Step Duration", value=timing["average_sim_step_duration"] * 1000, unit="ms"
928 ),
929 )
930 benchmark.add_measurement(
931 "runtime",
932 measurement=SingleMeasurement(
933 name="Total Simulation Time", value=timing["total_simulation_time"] * 1000, unit="ms"
934 ),
935 )
936
937 # Log system utilization
938 benchmark.add_measurement(
939 "runtime",
940 measurement=DictMeasurement(
941 name="System Utilization",
942 value={
943 "cpu_percent": sys_util[0],
944 "ram_percent": sys_util[1],
945 "gpu_compute_percent": sys_util[2],
946 "gpu_memory_percent": sys_util[3],
947 },
948 ),
949 )
950
951 # Finalize benchmark
952 benchmark.update_manual_recorders()
953 benchmark.finalize()
954
955
956if __name__ == "__main__":
957 # run the main function
958 main()
959 # close sim app
960 simulation_app.close()
Possible Parameters#
First, run
python scripts/benchmarks/benchmark_cameras.py -h
to see all possible parameters you can vary with this utility.
See the command line parameters related to autotune for more information about
automatically determining maximum camera count.
Compare Performance in Task Environments and Automatically Determine Task Max Camera Count#
Currently, tiled cameras are the most performant camera that can handle multiple dynamic objects.
For example, to see how your system could handle 100 tiled cameras in the cartpole environment, with 2 cameras per environment (so 50 environments total) only in RGB mode, run
python scripts/benchmarks/benchmark_cameras.py --task Isaac-Cartpole --num_tiled_cameras 100 --task_num_cameras_per_env 2 --tiled_camera_data_types rgb
If you have pynvml installed, (python -m pip install pynvml), you can also
find the maximum number of cameras that you could run in the specified environment up to
a certain performance threshold (specified by max CPU utilization percent, max RAM utilization percent,
max GPU compute percent, and max GPU memory percent). For example, to find the maximum number of cameras
you can run with cartpole, you could run:
python scripts/benchmarks/benchmark_cameras.py --task Isaac-Cartpole --num_tiled_cameras 100 --task_num_cameras_per_env 2 --tiled_camera_data_types rgb --autotune --autotune_max_percentage_util 100 80 50 50
Autotune may lead to the program crashing, which means that it tried to run too many cameras at once. However, the max percentage utilization parameter is meant to prevent this from happening.
The output of the benchmark doesn’t include the overhead of training the network, so consider decreasing the maximum utilization percentages to account for this overhead. The final output camera count is for all cameras, so to get the total number of environments, divide the output camera count by the number of cameras per environment.
Compare Camera Type and Performance (Without a Specified Task)#
This tool can also asses performance without a task environment. For example, to view 100 random objects with 2 standard cameras, one could run
python scripts/benchmarks/benchmark_cameras.py --height 100 --width 100 --num_standard_cameras 2 --standard_camera_data_types instance_segmentation normals --num_objects 100 --experiment_length 100
If your system cannot handle this due to performance reasons, then the process will be killed.
It’s recommended to monitor CPU/RAM utilization and GPU utilization while running this script, to get
an idea of how many resources rendering the desired camera requires. In Ubuntu, you can use tools like htop and nvtop
to live monitor resources while running this script, and in Windows, you can use the Task Manager.
If your system has a hard time handling the desired cameras, you can try the following
Switch to headless mode (omit
--viz, or pass--viz noneif a config selects visualizers)Ensure you are using the GPU pipeline not CPU!
If you aren’t using Tiled Cameras, switch to Tiled Cameras
Decrease camera resolution
Decrease how many data_types there are for each camera.
Decrease the number of cameras
Decrease the number of objects in the scene
If your system is able to handle the amount of cameras, then the time statistics will be printed to the terminal. After the simulations stops it can be closed with CTRL+C.