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)
239# forward unrecognized args as Hydra-style task config overrides
240args_cli, hydra_overrides = parser.parse_known_args()
241args_cli.enable_cameras = True
242
243if args_cli.autotune:
244    import pynvml
245
246if len(args_cli.ray_caster_visible_mesh_prim_paths) > 1:
247    print("[WARNING]: Ray Casting is only currently supported for a single, static object")
248# launch omniverse app
249app_launcher = AppLauncher(args_cli)
250simulation_app = app_launcher.app
251
252"""Rest everything follows."""
253
254import random
255import time
256
257import gymnasium as gym
258import numpy as np
259import psutil
260import torch
261
262import isaaclab.sim as sim_utils
263from isaaclab.assets import RigidObject, RigidObjectCfg
264from isaaclab.benchmark import BaseIsaacLabBenchmark, DictMeasurement, SingleMeasurement
265from isaaclab.scene.interactive_scene import InteractiveScene
266from isaaclab.sensors import (
267    Camera,
268    CameraCfg,
269    RayCasterCamera,
270    RayCasterCameraCfg,
271    patterns,
272)
273from isaaclab.utils.math import orthogonalize_perspective_depth, unproject_depth
274
275from isaaclab_tasks.utils import parse_env_cfg
276
277"""
278Camera Creation
279"""
280
281
282def _get_camera_class_name(camera_cfg: type[CameraCfg]) -> str:
283    """Return the configured camera sensor class name."""
284    class_type_field = camera_cfg.__dataclass_fields__["class_type"]
285    if class_type_field.default is not MISSING:
286        class_type = class_type_field.default
287    elif class_type_field.default_factory is not MISSING:
288        class_type = class_type_field.default_factory()
289    else:
290        raise AttributeError(f"{camera_cfg.__name__} has no default class_type.")
291
292    if hasattr(class_type, "__name__"):
293        return class_type.__name__
294    return str(class_type).rsplit(":", maxsplit=1)[-1]
295
296
297def create_camera_base(
298    camera_cfg: type[CameraCfg],
299    num_cams: int,
300    data_types: list[str],
301    height: int,
302    width: int,
303    prim_path: str | None = None,
304    instantiate: bool = True,
305) -> Camera | CameraCfg | None:
306    """Generalized function to create a camera or tiled camera sensor."""
307    # If valid camera settings are provided, create the camera
308    if num_cams <= 0 or len(data_types) <= 0 or height <= 0 or width <= 0:
309        return None
310
311    name = _get_camera_class_name(camera_cfg)
312    cfg = camera_cfg(
313        prim_path=prim_path if prim_path is not None else f"/World/{name}_.*/{name}",
314        update_period=0,
315        height=height,
316        width=width,
317        data_types=data_types,
318        spawn=sim_utils.PinholeCameraCfg(
319            focal_length=24, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 1e4)
320        ),
321    )
322    if instantiate:
323        # Create the necessary prims
324        for idx in range(num_cams):
325            sim_utils.create_prim(f"/World/{name}_{idx:02d}", "Xform")
326        return cfg.class_type(cfg=cfg)
327
328    return cfg
329
330
331def create_tiled_cameras(
332    num_cams: int = 2, data_types: list[str] | None = None, height: int = 100, width: int = 120
333) -> Camera | None:
334    if data_types is None:
335        data_types = ["rgb", "depth"]
336    """Defines the camera sensor to add to the scene."""
337    return create_camera_base(
338        camera_cfg=CameraCfg,
339        num_cams=num_cams,
340        data_types=data_types,
341        height=height,
342        width=width,
343    )
344
345
346def create_cameras(
347    num_cams: int = 2, data_types: list[str] | None = None, height: int = 100, width: int = 120
348) -> Camera | None:
349    """Defines the Standard cameras."""
350    if data_types is None:
351        data_types = ["rgb", "depth"]
352    return create_camera_base(
353        camera_cfg=CameraCfg, num_cams=num_cams, data_types=data_types, height=height, width=width
354    )
355
356
357def create_ray_caster_cameras(
358    num_cams: int = 2,
359    data_types: list[str] = ["distance_to_image_plane"],
360    mesh_prim_paths: list[str] = ["/World/ground"],
361    height: int = 100,
362    width: int = 120,
363    prim_path: str = "/World/RayCasterCamera_.*/RayCaster",
364    instantiate: bool = True,
365) -> RayCasterCamera | RayCasterCameraCfg | None:
366    """Create the raycaster cameras; different configuration than Standard/Tiled camera"""
367    for idx in range(num_cams):
368        sim_utils.create_prim(f"/World/RayCasterCamera_{idx:02d}/RayCaster", "Xform")
369
370    if num_cams > 0 and len(data_types) > 0 and height > 0 and width > 0:
371        cam_cfg = RayCasterCameraCfg(
372            prim_path=prim_path,
373            mesh_prim_paths=mesh_prim_paths,
374            update_period=0,
375            offset=RayCasterCameraCfg.OffsetCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)),
376            data_types=data_types,
377            debug_vis=False,
378            pattern_cfg=patterns.PinholeCameraPatternCfg(
379                focal_length=24.0,
380                horizontal_aperture=20.955,
381                height=480,
382                width=640,
383            ),
384        )
385        if instantiate:
386            return RayCasterCamera(cfg=cam_cfg)
387        else:
388            return cam_cfg
389
390    else:
391        return None
392
393
394def create_tiled_camera_cfg(prim_path: str) -> CameraCfg:
395    """Grab a simple camera config for injecting into task environments."""
396    return create_camera_base(
397        CameraCfg,
398        num_cams=args_cli.num_tiled_cameras,
399        data_types=args_cli.tiled_camera_data_types,
400        width=args_cli.width,
401        height=args_cli.height,
402        prim_path="{ENV_REGEX_NS}/" + prim_path,
403        instantiate=False,
404    )
405
406
407def create_standard_camera_cfg(prim_path: str) -> CameraCfg:
408    """Grab a simple standard camera config for injecting into task environments."""
409    return create_camera_base(
410        CameraCfg,
411        num_cams=args_cli.num_standard_cameras,
412        data_types=args_cli.standard_camera_data_types,
413        width=args_cli.width,
414        height=args_cli.height,
415        prim_path="{ENV_REGEX_NS}/" + prim_path,
416        instantiate=False,
417    )
418
419
420def create_ray_caster_camera_cfg(prim_path: str) -> RayCasterCameraCfg:
421    """Grab a simple ray caster config for injecting into task environments."""
422    return create_ray_caster_cameras(
423        num_cams=args_cli.num_ray_caster_cameras,
424        data_types=args_cli.ray_caster_camera_data_types,
425        width=args_cli.width,
426        height=args_cli.height,
427        prim_path="{ENV_REGEX_NS}/" + prim_path,
428    )
429
430
431"""
432Scene Creation
433"""
434
435
436def design_scene(
437    num_tiled_cams: int = 2,
438    num_standard_cams: int = 0,
439    num_ray_caster_cams: int = 0,
440    tiled_camera_data_types: list[str] | None = None,
441    standard_camera_data_types: list[str] | None = None,
442    ray_caster_camera_data_types: list[str] | None = None,
443    height: int = 100,
444    width: int = 200,
445    num_objects: int = 20,
446    mesh_prim_paths: list[str] = ["/World/ground"],
447) -> dict:
448    """Design the scene."""
449    if tiled_camera_data_types is None:
450        tiled_camera_data_types = ["rgb"]
451    if standard_camera_data_types is None:
452        standard_camera_data_types = ["rgb"]
453    if ray_caster_camera_data_types is None:
454        ray_caster_camera_data_types = ["distance_to_image_plane"]
455
456    # Populate scene
457    # -- Ground-plane
458    cfg = sim_utils.GroundPlaneCfg()
459    cfg.func("/World/ground", cfg)
460    # -- Lights
461    cfg = sim_utils.DistantLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75))
462    cfg.func("/World/Light", cfg)
463
464    # Create a dictionary for the scene entities
465    scene_entities = {}
466
467    # Xform to hold objects
468    sim_utils.create_prim("/World/Objects", "Xform")
469    # Random objects
470    for i in range(num_objects):
471        # sample random position
472        position = np.random.rand(3) - np.asarray([0.05, 0.05, -1.0])
473        position *= np.asarray([1.5, 1.5, 0.5])
474        # sample random color
475        color = (random.random(), random.random(), random.random())
476        # choose random prim type
477        prim_type = random.choice(["Cube", "Cone", "Cylinder"])
478        common_properties = {
479            "rigid_props": sim_utils.RigidBodyPropertiesCfg(),
480            "mass_props": sim_utils.MassPropertiesCfg(mass=5.0),
481            "collision_props": sim_utils.CollisionPropertiesCfg(),
482            "visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=color, metallic=0.5),
483            "semantic_tags": [("class", prim_type)],
484        }
485        if prim_type == "Cube":
486            shape_cfg = sim_utils.CuboidCfg(size=(0.25, 0.25, 0.25), **common_properties)
487        elif prim_type == "Cone":
488            shape_cfg = sim_utils.ConeCfg(radius=0.1, height=0.25, **common_properties)
489        elif prim_type == "Cylinder":
490            shape_cfg = sim_utils.CylinderCfg(radius=0.25, height=0.25, **common_properties)
491        # Rigid Object
492        obj_cfg = RigidObjectCfg(
493            prim_path=f"/World/Objects/Obj_{i:02d}",
494            spawn=shape_cfg,
495            init_state=RigidObjectCfg.InitialStateCfg(pos=position),
496        )
497        scene_entities[f"rigid_object{i}"] = RigidObject(cfg=obj_cfg)
498
499    # Sensors
500    standard_camera = create_cameras(
501        num_cams=num_standard_cams, data_types=standard_camera_data_types, height=height, width=width
502    )
503    tiled_camera = create_tiled_cameras(
504        num_cams=num_tiled_cams, data_types=tiled_camera_data_types, height=height, width=width
505    )
506    ray_caster_camera = create_ray_caster_cameras(
507        num_cams=num_ray_caster_cams,
508        data_types=ray_caster_camera_data_types,
509        mesh_prim_paths=mesh_prim_paths,
510        height=height,
511        width=width,
512    )
513    # return the scene information
514    if tiled_camera is not None:
515        scene_entities["tiled_camera"] = tiled_camera
516    if standard_camera is not None:
517        scene_entities["standard_camera"] = standard_camera
518    if ray_caster_camera is not None:
519        scene_entities["ray_caster_camera"] = ray_caster_camera
520    return scene_entities
521
522
523def inject_cameras_into_task(
524    task: str,
525    num_cams: int,
526    camera_name_prefix: str,
527    camera_creation_callable: Callable,
528    num_cameras_per_env: int = 1,
529) -> gym.Env:
530    """Loads the task, sticks cameras into the config, and creates the environment."""
531    cfg = parse_env_cfg(task, device=args_cli.device, use_fabric=args_cli.use_fabric, overrides=hydra_overrides)
532    scene_cfg = cfg.scene
533
534    num_envs = int(num_cams / num_cameras_per_env)
535    scene_cfg.num_envs = num_envs
536
537    for idx in range(num_cameras_per_env):
538        suffix = "" if idx == 0 else str(idx)
539        name = camera_name_prefix + suffix
540        setattr(scene_cfg, name, camera_creation_callable(name))
541    cfg.scene = scene_cfg
542    env = gym.make(task, cfg=cfg)
543    return env
544
545
546"""
547System diagnosis
548"""
549
550
551def get_utilization_percentages(reset: bool = False, max_values: list[float] = [0.0, 0.0, 0.0, 0.0]) -> list[float]:
552    """Get the maximum CPU, RAM, GPU utilization (processing), and
553    GPU memory usage percentages since the last time reset was true."""
554    if reset:
555        max_values[:] = [0, 0, 0, 0]  # Reset the max values
556
557    # CPU utilization
558    cpu_usage = psutil.cpu_percent(interval=0.1)
559    max_values[0] = max(max_values[0], cpu_usage)
560
561    # RAM utilization
562    memory_info = psutil.virtual_memory()
563    ram_usage = memory_info.percent
564    max_values[1] = max(max_values[1], ram_usage)
565
566    # GPU utilization using pynvml
567    if torch.cuda.is_available():
568        if args_cli.autotune:
569            pynvml.nvmlInit()  # Initialize NVML
570            for i in range(torch.cuda.device_count()):
571                handle = pynvml.nvmlDeviceGetHandleByIndex(i)
572
573                # GPU Utilization
574                gpu_utilization = pynvml.nvmlDeviceGetUtilizationRates(handle)
575                gpu_processing_utilization_percent = gpu_utilization.gpu  # GPU core utilization
576                max_values[2] = max(max_values[2], gpu_processing_utilization_percent)
577
578                # GPU Memory Usage
579                memory_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
580                gpu_memory_total = memory_info.total
581                gpu_memory_used = memory_info.used
582                gpu_memory_utilization_percent = (gpu_memory_used / gpu_memory_total) * 100
583                max_values[3] = max(max_values[3], gpu_memory_utilization_percent)
584
585            pynvml.nvmlShutdown()  # Shutdown NVML after usage
586    else:
587        gpu_processing_utilization_percent = None
588        gpu_memory_utilization_percent = None
589    return max_values
590
591
592"""
593Experiment
594"""
595
596
597def run_simulator(
598    sim: sim_utils.SimulationContext | None,
599    scene_entities: dict | InteractiveScene,
600    warm_start_length: int = 10,
601    experiment_length: int = 100,
602    tiled_camera_data_types: list[str] | None = None,
603    standard_camera_data_types: list[str] | None = None,
604    ray_caster_camera_data_types: list[str] | None = None,
605    depth_predicate: Callable = lambda x: "to" in x or x == "depth",
606    perspective_depth_predicate: Callable = lambda x: x == "distance_to_camera",
607    convert_depth_to_camera_to_image_plane: bool = True,
608    max_cameras_per_env: int = 1,
609    env: gym.Env | None = None,
610) -> dict:
611    """Run the simulator with all cameras, and return timing analytics. Visualize if desired."""
612
613    if tiled_camera_data_types is None:
614        tiled_camera_data_types = ["rgb"]
615    if standard_camera_data_types is None:
616        standard_camera_data_types = ["rgb"]
617    if ray_caster_camera_data_types is None:
618        ray_caster_camera_data_types = ["distance_to_image_plane"]
619
620    # Initialize camera lists
621    tiled_cameras = []
622    standard_cameras = []
623    ray_caster_cameras = []
624
625    # Dynamically extract cameras from the scene entities up to max_cameras_per_env
626    for i in range(max_cameras_per_env):
627        # Extract tiled cameras
628        tiled_camera_key = f"tiled_camera{i}" if i > 0 else "tiled_camera"
629        standard_camera_key = f"standard_camera{i}" if i > 0 else "standard_camera"
630        ray_caster_camera_key = f"ray_caster_camera{i}" if i > 0 else "ray_caster_camera"
631
632        try:  # if instead you checked ... if key is in scene_entities... # errors out always even if key present
633            tiled_cameras.append(scene_entities[tiled_camera_key])
634            standard_cameras.append(scene_entities[standard_camera_key])
635            ray_caster_cameras.append(scene_entities[ray_caster_camera_key])
636        except KeyError:
637            break
638
639    # Initialize camera counts
640    camera_lists = [tiled_cameras, standard_cameras, ray_caster_cameras]
641    camera_data_types = [tiled_camera_data_types, standard_camera_data_types, ray_caster_camera_data_types]
642    labels = ["tiled", "standard", "ray_caster"]
643
644    if sim is not None:
645        # Set camera world poses
646        for camera_list in camera_lists:
647            for camera in camera_list:
648                num_cameras = camera.data.intrinsic_matrices.size(0)
649                positions = torch.tensor([[2.5, 2.5, 2.5]], device=sim.device).repeat(num_cameras, 1)
650                targets = torch.tensor([[0.0, 0.0, 0.0]], device=sim.device).repeat(num_cameras, 1)
651                camera.set_world_poses_from_view(positions, targets)
652
653    # Initialize timing variables
654    timestep = 0
655    total_time = 0.0
656    valid_timesteps = 0
657    sim_step_time = 0.0
658
659    while simulation_app.is_running() and timestep < experiment_length:
660        print(f"On timestep {timestep} of {experiment_length}, with warm start of {warm_start_length}")
661        get_utilization_percentages()
662
663        # Measure the total simulation step time
664        step_start_time = time.time()
665
666        if sim is not None:
667            sim.step()
668
669        if env is not None:
670            with torch.inference_mode():
671                # compute zero actions
672                actions = torch.zeros(env.action_space.shape, device=env.unwrapped.device)
673                # apply actions
674                env.step(actions)
675
676        # Update cameras and process vision data within the simulation step
677        clouds = {}
678        images = {}
679        depth_images = {}
680
681        # Loop through all camera lists and their data_types
682        for camera_list, data_types, label in zip(camera_lists, camera_data_types, labels):
683            for cam_idx, camera in enumerate(camera_list):
684                if env is None:  # No env, need to step cams manually
685                    # Only update the camera if it hasn't been updated as part of scene_entities.update ...
686                    camera.update(dt=sim.get_physics_dt())
687
688                for data_type in data_types:
689                    data_label = f"{label}_{cam_idx}_{data_type}"
690
691                    if depth_predicate(data_type):  # is a depth image, want to create cloud
692                        depth = camera.data.output[data_type]
693                        depth_images[data_label + "_raw"] = depth
694                        if perspective_depth_predicate(data_type) and convert_depth_to_camera_to_image_plane:
695                            depth = orthogonalize_perspective_depth(
696                                camera.data.output[data_type], camera.data.intrinsic_matrices
697                            )
698                            depth_images[data_label + "_undistorted"] = depth
699
700                        pointcloud = unproject_depth(depth=depth, intrinsics=camera.data.intrinsic_matrices)
701                        clouds[data_label] = pointcloud
702                    else:  # rgb image, just save it
703                        image = camera.data.output[data_type]
704                        images[data_label] = image
705
706        # End timing for the step
707        step_end_time = time.time()
708        sim_step_time += step_end_time - step_start_time
709
710        if timestep > warm_start_length:
711            get_utilization_percentages(reset=True)
712            total_time += step_end_time - step_start_time
713            valid_timesteps += 1
714
715        timestep += 1
716
717    # Calculate average timings
718    if valid_timesteps > 0:
719        avg_timestep_duration = total_time / valid_timesteps
720        avg_sim_step_duration = sim_step_time / experiment_length
721    else:
722        avg_timestep_duration = 0.0
723        avg_sim_step_duration = 0.0
724
725    # Package timing analytics in a dictionary
726    timing_analytics = {
727        "average_timestep_duration": avg_timestep_duration,
728        "average_sim_step_duration": avg_sim_step_duration,
729        "total_simulation_time": sim_step_time,
730        "total_experiment_duration": sim_step_time,
731    }
732
733    system_utilization_analytics = get_utilization_percentages()
734
735    print("--- Benchmark Results ---")
736    print(f"Average timestep duration: {avg_timestep_duration:.6f} seconds")
737    print(f"Average simulation step duration: {avg_sim_step_duration:.6f} seconds")
738    print(f"Total simulation time: {sim_step_time:.6f} seconds")
739    print("\nSystem Utilization Statistics:")
740    print(
741        f"| CPU:{system_utilization_analytics[0]}% | "
742        f"RAM:{system_utilization_analytics[1]}% | "
743        f"GPU Compute:{system_utilization_analytics[2]}% | "
744        f" GPU Memory: {system_utilization_analytics[3]:.2f}% |"
745    )
746
747    return {"timing_analytics": timing_analytics, "system_utilization_analytics": system_utilization_analytics}
748
749
750def main():
751    """Main function."""
752    # Load simulation context
753    if args_cli.num_tiled_cameras + args_cli.num_standard_cameras + args_cli.num_ray_caster_cameras <= 0:
754        raise ValueError("You must select at least one camera.")
755    if (
756        (args_cli.num_tiled_cameras > 0 and args_cli.num_standard_cameras > 0)
757        or (args_cli.num_ray_caster_cameras > 0 and args_cli.num_standard_cameras > 0)
758        or (args_cli.num_ray_caster_cameras > 0 and args_cli.num_tiled_cameras > 0)
759    ):
760        print("[WARNING]: You have elected to use more than one camera type.")
761        print("[WARNING]: For a benchmark to be meaningful, use ONLY ONE camera type at a time.")
762        print(
763            "[WARNING]: For example, if num_tiled_cameras=100, for a meaningful benchmark,"
764            "num_standard_cameras should be 0, and num_ray_caster_cameras should be 0"
765        )
766        raise ValueError("Benchmark one camera at a time.")
767
768    # Determine which camera type is being used
769    camera_type = "tiled"
770    num_cameras = args_cli.num_tiled_cameras
771    if args_cli.num_standard_cameras > 0:
772        camera_type = "standard"
773        num_cameras = args_cli.num_standard_cameras
774    elif args_cli.num_ray_caster_cameras > 0:
775        camera_type = "ray_caster"
776        num_cameras = args_cli.num_ray_caster_cameras
777
778    # Create the benchmark
779    formatter_type = args_cli.benchmark_formatter
780    benchmark = BaseIsaacLabBenchmark(
781        benchmark_name="benchmark_cameras",
782        formatter_type=formatter_type,
783        output_path=args_cli.output_path,
784        use_recorders=True,
785        frametime_recorders=formatter_type in ("summary", "omniperf"),
786        output_prefix="benchmark_cameras",
787        workflow_metadata={
788            "metadata": [
789                {"name": "task", "data": args_cli.task},
790                {"name": "camera_type", "data": camera_type},
791                {"name": "num_cameras", "data": num_cameras},
792                {"name": "height", "data": args_cli.height},
793                {"name": "width", "data": args_cli.width},
794                {"name": "experiment_length", "data": args_cli.experiment_length},
795                {"name": "autotune", "data": args_cli.autotune},
796            ]
797        },
798    )
799
800    print("[INFO]: Designing the scene")
801    final_analysis = None
802
803    if args_cli.task is None:
804        print("[INFO]: No task environment provided, creating random scene.")
805        sim_cfg = sim_utils.SimulationCfg(device=args_cli.device)
806        sim = sim_utils.SimulationContext(sim_cfg)
807        # Set main camera
808        sim.set_camera_view([2.5, 2.5, 2.5], [0.0, 0.0, 0.0])
809        scene_entities = design_scene(
810            num_tiled_cams=args_cli.num_tiled_cameras,
811            num_standard_cams=args_cli.num_standard_cameras,
812            num_ray_caster_cams=args_cli.num_ray_caster_cameras,
813            tiled_camera_data_types=args_cli.tiled_camera_data_types,
814            standard_camera_data_types=args_cli.standard_camera_data_types,
815            ray_caster_camera_data_types=args_cli.ray_caster_camera_data_types,
816            height=args_cli.height,
817            width=args_cli.width,
818            num_objects=args_cli.num_objects,
819            mesh_prim_paths=args_cli.ray_caster_visible_mesh_prim_paths,
820        )
821        # Play simulator
822        sim.reset()
823        # Now we are ready!
824        print("[INFO]: Setup complete...")
825        # Run simulator
826        final_analysis = run_simulator(
827            sim=sim,
828            scene_entities=scene_entities,
829            warm_start_length=args_cli.warm_start_length,
830            experiment_length=args_cli.experiment_length,
831            tiled_camera_data_types=args_cli.tiled_camera_data_types,
832            standard_camera_data_types=args_cli.standard_camera_data_types,
833            ray_caster_camera_data_types=args_cli.ray_caster_camera_data_types,
834            convert_depth_to_camera_to_image_plane=args_cli.convert_depth_to_camera_to_image_plane,
835        )
836    else:
837        print("[INFO]: Using known task environment, injecting cameras.")
838        autotune_iter = 0
839        max_sys_util_thresh = [0.0, 0.0, 0.0]
840        max_num_cams = max(args_cli.num_tiled_cameras, args_cli.num_standard_cameras, args_cli.num_ray_caster_cameras)
841        cur_num_cams = max_num_cams
842        cur_sys_util = max_sys_util_thresh
843        interval = args_cli.autotune_camera_count_interval
844
845        if args_cli.autotune:
846            max_sys_util_thresh = args_cli.autotune_max_percentage_util
847            max_num_cams = args_cli.autotune_max_camera_count
848            print("[INFO]: Auto tuning until any of the following threshold are met")
849            print(f"|CPU: {max_sys_util_thresh[0]}% | RAM {max_sys_util_thresh[1]}% | GPU: {max_sys_util_thresh[2]}% |")
850            print(f"[INFO]: Maximum number of cameras allowed: {max_num_cams}")
851        # Determine which camera is being tested...
852        tiled_camera_cfg = create_tiled_camera_cfg("tiled_camera")
853        standard_camera_cfg = create_standard_camera_cfg("standard_camera")
854        ray_caster_camera_cfg = create_ray_caster_camera_cfg("ray_caster_camera")
855        camera_name_prefix = ""
856        camera_creation_callable = None
857        num_cams = 0
858        if tiled_camera_cfg is not None:
859            camera_name_prefix = "tiled_camera"
860            camera_creation_callable = create_tiled_camera_cfg
861            num_cams = args_cli.num_tiled_cameras
862        elif standard_camera_cfg is not None:
863            camera_name_prefix = "standard_camera"
864            camera_creation_callable = create_standard_camera_cfg
865            num_cams = args_cli.num_standard_cameras
866        elif ray_caster_camera_cfg is not None:
867            camera_name_prefix = "ray_caster_camera"
868            camera_creation_callable = create_ray_caster_camera_cfg
869            num_cams = args_cli.num_ray_caster_cameras
870
871        while (
872            all(cur <= max_thresh for cur, max_thresh in zip(cur_sys_util, max_sys_util_thresh))
873            and cur_num_cams <= max_num_cams
874        ):
875            cur_num_cams = num_cams + interval * autotune_iter
876            autotune_iter += 1
877
878            env = inject_cameras_into_task(
879                task=args_cli.task,
880                num_cams=cur_num_cams,
881                camera_name_prefix=camera_name_prefix,
882                camera_creation_callable=camera_creation_callable,
883                num_cameras_per_env=args_cli.task_num_cameras_per_env,
884            )
885            env.reset()
886            print(f"Testing with {cur_num_cams} {camera_name_prefix}")
887            analysis = run_simulator(
888                sim=None,
889                scene_entities=env.unwrapped.scene,
890                warm_start_length=args_cli.warm_start_length,
891                experiment_length=args_cli.experiment_length,
892                tiled_camera_data_types=args_cli.tiled_camera_data_types,
893                standard_camera_data_types=args_cli.standard_camera_data_types,
894                ray_caster_camera_data_types=args_cli.ray_caster_camera_data_types,
895                convert_depth_to_camera_to_image_plane=args_cli.convert_depth_to_camera_to_image_plane,
896                max_cameras_per_env=args_cli.task_num_cameras_per_env,
897                env=env,
898            )
899
900            cur_sys_util = analysis["system_utilization_analytics"]
901            final_analysis = analysis
902            print("Triggering reset...")
903            env.close()
904            sim_utils.create_new_stage()
905        print("[INFO]: DONE! Feel free to CTRL + C Me ")
906        print(f"[INFO]: If you've made it this far, you can likely simulate {cur_num_cams} {camera_name_prefix}")
907        print("Keep in mind, this is without any training running on the GPU.")
908        print("Set lower utilization thresholds to account for training.")
909
910        if not args_cli.autotune:
911            print("[WARNING]: GPU Util Statistics only correct while autotuning, ignore above.")
912
913    # Log benchmark measurements
914    if final_analysis is not None:
915        timing = final_analysis["timing_analytics"]
916        sys_util = final_analysis["system_utilization_analytics"]
917
918        # Log timing measurements
919        benchmark.add_measurement(
920            "runtime",
921            measurement=SingleMeasurement(
922                name="Average Timestep Duration", value=timing["average_timestep_duration"] * 1000, unit="ms"
923            ),
924        )
925        benchmark.add_measurement(
926            "runtime",
927            measurement=SingleMeasurement(
928                name="Average Simulation Step Duration", value=timing["average_sim_step_duration"] * 1000, unit="ms"
929            ),
930        )
931        benchmark.add_measurement(
932            "runtime",
933            measurement=SingleMeasurement(
934                name="Total Simulation Time", value=timing["total_simulation_time"] * 1000, unit="ms"
935            ),
936        )
937
938        # Log system utilization
939        benchmark.add_measurement(
940            "runtime",
941            measurement=DictMeasurement(
942                name="System Utilization",
943                value={
944                    "cpu_percent": sys_util[0],
945                    "ram_percent": sys_util[1],
946                    "gpu_compute_percent": sys_util[2],
947                    "gpu_memory_percent": sys_util[3],
948                },
949            ),
950        )
951
952    # Finalize benchmark
953    benchmark.update_manual_recorders()
954    benchmark.finalize()
955
956
957if __name__ == "__main__":
958    # run the main function
959    main()
960    # close sim app
961    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 none if 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.