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 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.