Commit f09f8645 authored by Kelly Guo's avatar Kelly Guo Committed by David Hoeller

Fixes tiled rendering limitations and frame offset issue (#113)

# Description

This fix removes the limitations of tiled rendering, allowing for
non-square resolutions and non-perfect square number of
tiles/environments. In addition, for small resolutions (width or height
<265), DLAA denoiser is used by default as a workaround to avoid frame
offset issues. An additional `rerender_on_reset` flag is added to the
environment configs to allow for an extra `render` call in reset APIs to
make sure sensor observations are updated after reset.

## Type of change

- Bug fix (non-breaking change which fixes an issue)

## Checklist

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there
parent ac7fafd9
...@@ -60,21 +60,6 @@ environment. For example: ...@@ -60,21 +60,6 @@ environment. For example:
python source/standalone/workflows/rl_games/train.py --task=Isaac-Cartpole-RGB-Camera-Direct-v0 --headless --enable_cameras python source/standalone/workflows/rl_games/train.py --task=Isaac-Cartpole-RGB-Camera-Direct-v0 --headless --enable_cameras
.. warning::
There are currently a few limitations with tiled rendering:
* Number of cameras must be a perfect square
* Tile resolution must be a square
* Due to upsampling in the denoising process, image quality may appear different when running with different numbers of cameras.
To overcome this issue, we can use the DLAA denoiser at the cost of some performance.
.. code-block:: python
import omni.replicator.core as rep
rep.settings.set_render_rtx_realtime(antialiasing="DLAA")
Annotators and Data Types Annotators and Data Types
^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^
......
...@@ -57,6 +57,9 @@ exts."omni.renderer.core".present.enabled=false ...@@ -57,6 +57,9 @@ exts."omni.renderer.core".present.enabled=false
rtx.raytracing.cached.enabled = false rtx.raytracing.cached.enabled = false
rtx.ambientOcclusion.enabled = false rtx.ambientOcclusion.enabled = false
# Set the DLSS model
rtx.post.dlss.execMode = 0 # can be 0 (Performance), 1 (Balanced), 2 (Quality), or 3 (Auto)
# Avoids unnecessary GPU context initialization # Avoids unnecessary GPU context initialization
renderer.multiGpu.maxGpuCount=1 renderer.multiGpu.maxGpuCount=1
......
...@@ -206,8 +206,8 @@ app.runLoops.rendering_1.rateLimitUsePrecisionSleep = true ...@@ -206,8 +206,8 @@ app.runLoops.rendering_1.rateLimitUsePrecisionSleep = true
app.runLoops.rendering_1.syncToPresent = false app.runLoops.rendering_1.syncToPresent = false
app.runLoopsGlobal.syncToPresent = false app.runLoopsGlobal.syncToPresent = false
app.vsync = false app.vsync = false
exts.omni.kit.renderer.core.present.enabled = false exts."omni.kit.renderer.core".present.enabled = false
exts.omni.kit.renderer.core.present.presentAfterRendering = false exts."omni.kit.renderer.core".present.presentAfterRendering = false
persistent.app.viewport.defaults.tickRate = 120 persistent.app.viewport.defaults.tickRate = 120
rtx-transient.dlssg.enabled = false rtx-transient.dlssg.enabled = false
......
...@@ -57,6 +57,9 @@ exts."omni.renderer.core".present.enabled=false ...@@ -57,6 +57,9 @@ exts."omni.renderer.core".present.enabled=false
rtx.raytracing.cached.enabled = false rtx.raytracing.cached.enabled = false
rtx.ambientOcclusion.enabled = false rtx.ambientOcclusion.enabled = false
# Set the DLSS model
rtx.post.dlss.execMode = 0 # can be 0 (Performance), 1 (Balanced), 2 (Quality), or 3 (Auto)
# Avoids unnecessary GPU context initialization # Avoids unnecessary GPU context initialization
renderer.multiGpu.maxGpuCount=1 renderer.multiGpu.maxGpuCount=1
......
...@@ -529,6 +529,10 @@ class DirectRLEnv(gym.Env): ...@@ -529,6 +529,10 @@ class DirectRLEnv(gym.Env):
""" """
self.scene.reset(env_ids) self.scene.reset(env_ids)
# if sensors are added to the scene, make sure we render to reflect changes in reset
if len(env_ids) > 0 and self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset:
self.sim.render()
# apply events such as randomization for environments that need a reset # apply events such as randomization for environments that need a reset
if self.cfg.events: if self.cfg.events:
if "reset" in self.event_manager.available_modes: if "reset" in self.event_manager.available_modes:
......
...@@ -127,3 +127,14 @@ class DirectRLEnvCfg: ...@@ -127,3 +127,14 @@ class DirectRLEnvCfg:
Please refer to the :class:`omni.isaac.lab.utils.noise.NoiseModel` class for more details. Please refer to the :class:`omni.isaac.lab.utils.noise.NoiseModel` class for more details.
""" """
rerender_on_reset: bool = False
"""Whether a render step is performed again after at least one environment has been reset.
Defaults to False, which means no render step will be performed after reset.
* When this is False, data collected from sensors after performing reset will be stale and will not reflect the
latest states in simulation caused by the reset.
* When this is True, an extra render step will be performed to update the sensor data
to reflect the latest states from the reset. This comes at a cost of performance as an additional render
step will be performed after each time an environment is reset.
"""
...@@ -350,6 +350,9 @@ class ManagerBasedEnv: ...@@ -350,6 +350,9 @@ class ManagerBasedEnv:
""" """
# reset the internal buffers of the scene elements # reset the internal buffers of the scene elements
self.scene.reset(env_ids) self.scene.reset(env_ids)
# if sensors are added to the scene, make sure we render to reflect changes in reset
if len(env_ids) > 0 and self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset:
self.sim.render()
# apply events such as randomization for environments that need a reset # apply events such as randomization for environments that need a reset
if "reset" in self.event_manager.available_modes: if "reset" in self.event_manager.available_modes:
env_step_count = self._sim_step_counter // self.cfg.decimation env_step_count = self._sim_step_counter // self.cfg.decimation
......
...@@ -95,3 +95,14 @@ class ManagerBasedEnvCfg: ...@@ -95,3 +95,14 @@ class ManagerBasedEnvCfg:
Please refer to the :class:`omni.isaac.lab.managers.EventManager` class for more details. Please refer to the :class:`omni.isaac.lab.managers.EventManager` class for more details.
""" """
rerender_on_reset: bool = False
"""Whether a render step is performed again after at least one environment has been reset.
Defaults to False, which means no render step will be performed after reset.
* When this is False, data collected from sensors after performing reset will be stale and will not reflect the
latest states in simulation caused by the reset.
* When this is True, an extra render step will be performed to update the sensor data
to reflect the latest states from the reset. This comes at a cost of performance as an additional render
step will be performed after each time an environment is reset.
"""
...@@ -319,6 +319,9 @@ class ManagerBasedRLEnv(ManagerBasedEnv, gym.Env): ...@@ -319,6 +319,9 @@ class ManagerBasedRLEnv(ManagerBasedEnv, gym.Env):
self.curriculum_manager.compute(env_ids=env_ids) self.curriculum_manager.compute(env_ids=env_ids)
# reset the internal buffers of the scene elements # reset the internal buffers of the scene elements
self.scene.reset(env_ids) self.scene.reset(env_ids)
# if sensors are added to the scene, make sure we render to reflect changes in reset
if len(env_ids) > 0 and self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset:
self.sim.render()
# apply events such as randomizations for environments that need a reset # apply events such as randomizations for environments that need a reset
if "reset" in self.event_manager.available_modes: if "reset" in self.event_manager.available_modes:
env_step_count = self._sim_step_counter // self.cfg.decimation env_step_count = self._sim_step_counter // self.cfg.decimation
......
...@@ -398,6 +398,10 @@ class Camera(SensorBase): ...@@ -398,6 +398,10 @@ class Camera(SensorBase):
f" the number of environments ({self._num_envs})." f" the number of environments ({self._num_envs})."
) )
# WAR: use DLAA antialiasing to avoid frame offset issue at small resolutions
if self.cfg.width < 265 or self.cfg.height < 265:
rep.settings.set_render_rtx_realtime(antialiasing="DLAA")
# Create all env_ids buffer # Create all env_ids buffer
self._ALL_INDICES = torch.arange(self._view.count, device=self._device, dtype=torch.long) self._ALL_INDICES = torch.arange(self._view.count, device=self._device, dtype=torch.long)
# Create frame count buffer # Create frame count buffer
......
...@@ -189,6 +189,10 @@ class TiledCamera(Camera): ...@@ -189,6 +189,10 @@ class TiledCamera(Camera):
) )
self._render_product_paths = [rp.path] self._render_product_paths = [rp.path]
# WAR: use DLAA antialiasing to avoid frame offset issue at small resolutions
if self._tiling_grid_shape()[0] * self.cfg.width < 265 or self._tiling_grid_shape()[1] * self.cfg.height < 265:
rep.settings.set_render_rtx_realtime(antialiasing="DLAA")
# Define the annotators based on requested data types # Define the annotators based on requested data types
self._annotators = dict() self._annotators = dict()
for annotator_type in self.cfg.data_types: for annotator_type in self.cfg.data_types:
...@@ -377,7 +381,7 @@ class TiledCamera(Camera): ...@@ -377,7 +381,7 @@ class TiledCamera(Camera):
def _tiling_grid_shape(self) -> tuple[int, int]: def _tiling_grid_shape(self) -> tuple[int, int]:
"""Returns a tuple containing the tiling grid dimension.""" """Returns a tuple containing the tiling grid dimension."""
cols = round(math.sqrt(self._view.count)) cols = math.ceil(math.sqrt(self._view.count))
rows = math.ceil(self._view.count / cols) rows = math.ceil(self._view.count / cols)
return (cols, rows) return (cols, rows)
......
...@@ -518,6 +518,71 @@ class TestCamera(unittest.TestCase): ...@@ -518,6 +518,71 @@ class TestCamera(unittest.TestCase):
self.assertEqual(output["instance_segmentation_fast"].dtype, torch.int32) self.assertEqual(output["instance_segmentation_fast"].dtype, torch.int32)
self.assertEqual(output["instance_id_segmentation_fast"].dtype, torch.int32) self.assertEqual(output["instance_id_segmentation_fast"].dtype, torch.int32)
def test_camera_large_resolution_all_colorize(self):
"""Test camera resolution is correctly set for all types with colorization enabled."""
# Add all types
camera_cfg = copy.deepcopy(self.camera_cfg)
camera_cfg.data_types = [
"rgb",
"rgba",
"depth",
"distance_to_camera",
"distance_to_image_plane",
"normals",
"motion_vectors",
"semantic_segmentation",
"instance_segmentation_fast",
"instance_id_segmentation_fast",
]
camera_cfg.colorize_instance_id_segmentation = True
camera_cfg.colorize_instance_segmentation = True
camera_cfg.colorize_semantic_segmentation = True
camera_cfg.width = 512
camera_cfg.height = 512
# Create camera
camera = Camera(camera_cfg)
# Play sim
self.sim.reset()
# Simulate for a few steps
# note: This is a workaround to ensure that the textures are loaded.
# Check "Known Issues" section in the documentation for more details.
for _ in range(5):
self.sim.step()
camera.update(self.dt)
# expected sizes
hw_1c_shape = (1, camera_cfg.height, camera_cfg.width, 1)
hw_2c_shape = (1, camera_cfg.height, camera_cfg.width, 2)
hw_3c_shape = (1, camera_cfg.height, camera_cfg.width, 3)
hw_4c_shape = (1, camera_cfg.height, camera_cfg.width, 4)
# access image data and compare shapes
output = camera.data.output
self.assertEqual(output["rgb"].shape, hw_3c_shape)
self.assertEqual(output["rgba"].shape, hw_4c_shape)
self.assertEqual(output["depth"].shape, hw_1c_shape)
self.assertEqual(output["distance_to_camera"].shape, hw_1c_shape)
self.assertEqual(output["distance_to_image_plane"].shape, hw_1c_shape)
self.assertEqual(output["normals"].shape, hw_3c_shape)
self.assertEqual(output["motion_vectors"].shape, hw_2c_shape)
self.assertEqual(output["semantic_segmentation"].shape, hw_4c_shape)
self.assertEqual(output["instance_segmentation_fast"].shape, hw_4c_shape)
self.assertEqual(output["instance_id_segmentation_fast"].shape, hw_4c_shape)
# access image data and compare dtype
output = camera.data.output
self.assertEqual(output["rgb"].dtype, torch.uint8)
self.assertEqual(output["rgba"].dtype, torch.uint8)
self.assertEqual(output["depth"].dtype, torch.float)
self.assertEqual(output["distance_to_camera"].dtype, torch.float)
self.assertEqual(output["distance_to_image_plane"].dtype, torch.float)
self.assertEqual(output["normals"].dtype, torch.float)
self.assertEqual(output["motion_vectors"].dtype, torch.float)
self.assertEqual(output["semantic_segmentation"].dtype, torch.uint8)
self.assertEqual(output["instance_segmentation_fast"].dtype, torch.uint8)
self.assertEqual(output["instance_id_segmentation_fast"].dtype, torch.uint8)
def test_camera_resolution_rgb_only(self): def test_camera_resolution_rgb_only(self):
"""Test camera resolution is correctly set for RGB only.""" """Test camera resolution is correctly set for RGB only."""
# Add all types # Add all types
......
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Launch Isaac Sim Simulator first."""
import argparse
import sys
from omni.isaac.lab.app import AppLauncher, run_tests
# add argparse arguments
parser = argparse.ArgumentParser(
description=(
"Test Isaac-Cartpole-RGB-Camera-Direct-v0 environment with different resolutions and number of environments."
)
)
parser.add_argument("--save_images", action="store_true", default=False, help="Save out renders to file.")
parser.add_argument("unittest_args", nargs="*")
# parse the arguments
args_cli = parser.parse_args()
# set the sys.argv to the unittest_args
sys.argv[1:] = args_cli.unittest_args
# launch the simulator
app_launcher = AppLauncher(headless=True, enable_cameras=True)
simulation_app = app_launcher.app
"""Rest everything follows."""
import gymnasium as gym
import sys
import unittest
import omni.usd
from omni.isaac.lab.envs import DirectRLEnv, DirectRLEnvCfg, ManagerBasedRLEnv, ManagerBasedRLEnvCfg
from omni.isaac.lab.sensors import save_images_to_file
import omni.isaac.lab_tasks # noqa: F401
from omni.isaac.lab_tasks.utils.parse_cfg import parse_env_cfg
class TestTiledCameraCartpole(unittest.TestCase):
"""Test cases for all registered environments."""
@classmethod
def setUpClass(cls):
# acquire all Isaac environments names
cls.registered_tasks = list()
cls.registered_tasks.append("Isaac-Cartpole-RGB-Camera-Direct-v0")
print(">>> All registered environments:", cls.registered_tasks)
def test_tiled_resolutions_tiny(self):
"""Define settings for resolution and number of environments"""
num_envs = 1024
tile_widths = range(32, 48)
tile_heights = range(32, 48)
self._launch_tests(tile_widths, tile_heights, num_envs)
def test_tiled_resolutions_small(self):
"""Define settings for resolution and number of environments"""
num_envs = 300
tile_widths = range(128, 156)
tile_heights = range(128, 156)
self._launch_tests(tile_widths, tile_heights, num_envs)
def test_tiled_resolutions_medium(self):
"""Define settings for resolution and number of environments"""
num_envs = 64
tile_widths = range(320, 400, 20)
tile_heights = range(320, 400, 20)
self._launch_tests(tile_widths, tile_heights, num_envs)
def test_tiled_resolutions_large(self):
"""Define settings for resolution and number of environments"""
num_envs = 4
tile_widths = range(480, 640, 40)
tile_heights = range(480, 640, 40)
self._launch_tests(tile_widths, tile_heights, num_envs)
def test_tiled_resolutions_edge_cases(self):
"""Define settings for resolution and number of environments"""
num_envs = 1000
tile_widths = [12, 67, 93, 147]
tile_heights = [12, 67, 93, 147]
self._launch_tests(tile_widths, tile_heights, num_envs)
def test_tiled_num_envs_edge_cases(self):
"""Define settings for resolution and number of environments"""
num_envs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 53, 359, 733, 927]
tile_widths = [67, 93, 147]
tile_heights = [67, 93, 147]
for n_envs in num_envs:
self._launch_tests(tile_widths, tile_heights, n_envs)
"""
Helper functions.
"""
def _launch_tests(self, tile_widths: int, tile_heights: int, num_envs: int):
"""Run through different resolutions for tiled rendering"""
device = "cuda:0"
task_name = "Isaac-Cartpole-RGB-Camera-Direct-v0"
# iterate over all registered environments
for width in tile_widths:
for height in tile_heights:
with self.subTest(width=width, height=height):
# create a new stage
omni.usd.get_context().new_stage()
# parse configuration
env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg = parse_env_cfg(
task_name, device=device, num_envs=num_envs
)
env_cfg.tiled_camera.width = width
env_cfg.tiled_camera.height = height
print(f">>> Running test for resolution: {width} x {height}")
# check environment
self._run_environment(env_cfg)
# close the environment
print(f">>> Closing environment: {task_name}")
print("-" * 80)
def _run_environment(self, env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg):
"""Run environment and capture a rendered image."""
# create environment
env: ManagerBasedRLEnv | DirectRLEnv = gym.make("Isaac-Cartpole-RGB-Camera-Direct-v0", cfg=env_cfg)
# this flag is necessary to prevent a bug where the simulation gets stuck randomly when running the
# test on many environments.
env.sim.set_setting("/physics/cooking/ujitsoCollisionCooking", False)
# reset environment
obs, _ = env.reset()
# save image
if args_cli.save_images:
save_images_to_file(
obs["policy"] + 0.93,
f"output_{env.num_envs}_{env_cfg.tiled_camera.width}x{env_cfg.tiled_camera.height}.png",
)
# close the environment
env.close()
if __name__ == "__main__":
run_tests()
Changelog Changelog
--------- ---------
0.10.3 (2024-09-05)
~~~~~~~~~~~~~~~~~~~
Added
^^^^^
* Added environment config flag ``rerender_on_reset`` to allow updating sensor data after a reset.
0.10.2 (2024-08-23) 0.10.2 (2024-08-23)
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
......
...@@ -14,4 +14,5 @@ TESTS_TO_SKIP = [ ...@@ -14,4 +14,5 @@ TESTS_TO_SKIP = [
# lab_tasks # lab_tasks
"test_data_collector.py", # Failing "test_data_collector.py", # Failing
"test_record_video.py", # Failing "test_record_video.py", # Failing
"test_tiled_camera_env.py", # Need to improve the logic
] ]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment