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