From 0e13a51adf06d79a78cecdbbfafb9e5703c4f221 Mon Sep 17 00:00:00 2001 From: Kelly Guo Date: Wed, 7 Oct 2026 17:40:40 -0700 Subject: [PATCH 1/4] Add image normals data type parameter --- .../fix-camera-normals-default-data-type.fixed.rst | 1 + source/isaaclab/isaaclab/envs/mdp/observations.py | 4 +++- source/isaaclab/test/envs/test_camera_image_mdp.py | 9 +++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 source/isaaclab/changelog.d/fix-camera-normals-default-data-type.fixed.rst diff --git a/source/isaaclab/changelog.d/fix-camera-normals-default-data-type.fixed.rst b/source/isaaclab/changelog.d/fix-camera-normals-default-data-type.fixed.rst new file mode 100644 index 000000000000..cfee2f18d36f --- /dev/null +++ b/source/isaaclab/changelog.d/fix-camera-normals-default-data-type.fixed.rst @@ -0,0 +1 @@ +* Fixed ``image_normals`` raising ``KeyError: 'data_type'`` when constructed through the observation manager. diff --git a/source/isaaclab/isaaclab/envs/mdp/observations.py b/source/isaaclab/isaaclab/envs/mdp/observations.py index b5379ae2fd97..c4e8fdb7d1b0 100644 --- a/source/isaaclab/isaaclab/envs/mdp/observations.py +++ b/source/isaaclab/isaaclab/envs/mdp/observations.py @@ -527,6 +527,7 @@ class image_normals(CameraImageBase): Args: sensor_cfg: The camera sensor to read. Defaults to SceneEntityCfg("tiled_camera"). + data_type: The surface-normal camera data type. Defaults to "normals". normalize: Whether to normalize the image. Defaults to True. channel_first: Whether to return ``(num_envs, C, H, W)``. Defaults to False. frame_stack: Number of recent frames to stack along the channel axis. Defaults to 1. @@ -545,11 +546,12 @@ def __call__( self, env: ManagerBasedEnv, sensor_cfg: SceneEntityCfg = SceneEntityCfg("tiled_camera"), + data_type: str = "normals", normalize: bool = True, channel_first: bool = False, frame_stack: int = 1, ) -> torch.Tensor: - images = _read_camera_output(env, sensor_cfg, "normals") + images = _read_camera_output(env, sensor_cfg, data_type) return self._frames(images, normalize_normals if normalize else None) diff --git a/source/isaaclab/test/envs/test_camera_image_mdp.py b/source/isaaclab/test/envs/test_camera_image_mdp.py index 8bae4046d9d2..bf3ad24562ca 100644 --- a/source/isaaclab/test/envs/test_camera_image_mdp.py +++ b/source/isaaclab/test/envs/test_camera_image_mdp.py @@ -23,6 +23,7 @@ from isaaclab.envs.mdp.observations import ( image_depth, image_features, + image_normals, image_rgb, image_segmentation, stacked_image, @@ -144,6 +145,14 @@ def test_buffer_on_cuda(self): class TestModalityTerms: """Per-modality reading, validation and normalization.""" + def test_normals_uses_default_data_type(self): + """Normals resolve their default data type through the observation term signature.""" + normals = torch.linspace(-1.0, 1.0, NUM_ENVS * HEIGHT * WIDTH * CHANNELS).reshape( + NUM_ENVS, HEIGHT, WIDTH, CHANNELS + ) + _, observe = _make_term(image_normals, _make_env({"normals": normals})) + torch.testing.assert_close(observe(), (normals + 1.0) * 0.5) + def test_rgb_drops_alpha_and_accepts_constant_mean(self): """Albedo's unused alpha channel is dropped and a constant mean replaces the per-image mean.""" albedo = torch.randint(0, 255, (NUM_ENVS, HEIGHT, WIDTH, 4), dtype=torch.uint8) From 3afa065f134948a0573845db15d154556654430d Mon Sep 17 00:00:00 2001 From: Kelly Guo Date: Wed, 7 Oct 2026 18:44:24 -0700 Subject: [PATCH 2/4] Preserve image normals positional arguments --- source/isaaclab/isaaclab/envs/mdp/observations.py | 5 +++-- source/isaaclab/test/envs/test_camera_image_mdp.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/source/isaaclab/isaaclab/envs/mdp/observations.py b/source/isaaclab/isaaclab/envs/mdp/observations.py index c4e8fdb7d1b0..8394eeb380be 100644 --- a/source/isaaclab/isaaclab/envs/mdp/observations.py +++ b/source/isaaclab/isaaclab/envs/mdp/observations.py @@ -527,10 +527,10 @@ class image_normals(CameraImageBase): Args: sensor_cfg: The camera sensor to read. Defaults to SceneEntityCfg("tiled_camera"). - data_type: The surface-normal camera data type. Defaults to "normals". normalize: Whether to normalize the image. Defaults to True. channel_first: Whether to return ``(num_envs, C, H, W)``. Defaults to False. frame_stack: Number of recent frames to stack along the channel axis. Defaults to 1. + data_type: The surface-normal camera data type. Defaults to "normals". Returns: The normals image. Shape is ``(num_envs, H, W, 3 * frame_stack)``, or channel-first. @@ -546,10 +546,11 @@ def __call__( self, env: ManagerBasedEnv, sensor_cfg: SceneEntityCfg = SceneEntityCfg("tiled_camera"), - data_type: str = "normals", normalize: bool = True, channel_first: bool = False, frame_stack: int = 1, + *, + data_type: str = "normals", ) -> torch.Tensor: images = _read_camera_output(env, sensor_cfg, data_type) return self._frames(images, normalize_normals if normalize else None) diff --git a/source/isaaclab/test/envs/test_camera_image_mdp.py b/source/isaaclab/test/envs/test_camera_image_mdp.py index bf3ad24562ca..e9faba2ffb2c 100644 --- a/source/isaaclab/test/envs/test_camera_image_mdp.py +++ b/source/isaaclab/test/envs/test_camera_image_mdp.py @@ -153,6 +153,15 @@ def test_normals_uses_default_data_type(self): _, observe = _make_term(image_normals, _make_env({"normals": normals})) torch.testing.assert_close(observe(), (normals + 1.0) * 0.5) + def test_normals_preserves_positional_normalize(self): + """The positional normalize argument keeps its pre-data-type meaning.""" + normals = torch.linspace(-1.0, 1.0, NUM_ENVS * HEIGHT * WIDTH * CHANNELS).reshape( + NUM_ENVS, HEIGHT, WIDTH, CHANNELS + ) + env = _make_env({"normals": normals}) + term, _ = _make_term(image_normals, env) + torch.testing.assert_close(term(env, SENSOR_CFG, False), normals) + def test_rgb_drops_alpha_and_accepts_constant_mean(self): """Albedo's unused alpha channel is dropped and a constant mean replaces the per-image mean.""" albedo = torch.randint(0, 255, (NUM_ENVS, HEIGHT, WIDTH, 4), dtype=torch.uint8) From b349fa01a0fecd7b963adaae91e1884453ef6592 Mon Sep 17 00:00:00 2001 From: Antoine RICHARD Date: Tue, 13 Oct 2026 14:21:50 +0200 Subject: [PATCH 3/4] Remove unused CameraImageBase default data type Since #8127 the base class reads data_type from the term parameters, which the manager fills from each subclass's __call__ signature. The default_data_type attribute was no longer read and suggested a fallback that does not exist, which is how image_normals ended up without a data_type argument. --- ...x-camera-normals-default-data-type.removed.rst | 2 ++ source/isaaclab/isaaclab/envs/mdp/observations.py | 15 +++------------ 2 files changed, 5 insertions(+), 12 deletions(-) create mode 100644 source/isaaclab/changelog.d/fix-camera-normals-default-data-type.removed.rst diff --git a/source/isaaclab/changelog.d/fix-camera-normals-default-data-type.removed.rst b/source/isaaclab/changelog.d/fix-camera-normals-default-data-type.removed.rst new file mode 100644 index 000000000000..8d469b946bb0 --- /dev/null +++ b/source/isaaclab/changelog.d/fix-camera-normals-default-data-type.removed.rst @@ -0,0 +1,2 @@ +* Removed the unused ``CameraImageBase.default_data_type`` class attribute. Custom camera image terms + declare their default in the ``data_type`` argument of ``__call__`` instead. diff --git a/source/isaaclab/isaaclab/envs/mdp/observations.py b/source/isaaclab/isaaclab/envs/mdp/observations.py index 8394eeb380be..4bf57b21d96d 100644 --- a/source/isaaclab/isaaclab/envs/mdp/observations.py +++ b/source/isaaclab/isaaclab/envs/mdp/observations.py @@ -390,12 +390,11 @@ class CameraImageBase(ManagerTermBase): """Base for camera image terms: validates the data type and owns the frame stack. Subclasses read their sensor output and pass it with a normalizer from - :mod:`isaaclab.utils.images` to :attr:`_frames`. + :mod:`isaaclab.utils.images` to :attr:`_frames`. Each subclass declares a + ``data_type`` argument with a default in ``__call__``, which the manager + resolves into the term parameters before construction. """ - default_data_type: str - """Legacy data-type alias. Defaults are resolved from the subclass's ``__call__`` signature.""" - def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedEnv): super().__init__(cfg, env) data_type = cfg.params["data_type"] @@ -440,8 +439,6 @@ class image_rgb(CameraImageBase): The image, or ``out`` when supplied. Shape is ``(num_envs, H, W, 3 * frame_stack)``, or channel-first. """ - default_data_type = "rgb" - @staticmethod def _accepts(data_type: str) -> bool: return is_rgb_like(data_type) @@ -491,8 +488,6 @@ class image_depth(CameraImageBase): or channel-first. """ - default_data_type = "distance_to_image_plane" - @staticmethod def _accepts(data_type: str) -> bool: return is_depth_like(data_type) @@ -536,8 +531,6 @@ class image_normals(CameraImageBase): The normals image. Shape is ``(num_envs, H, W, 3 * frame_stack)``, or channel-first. """ - default_data_type = "normals" - @staticmethod def _accepts(data_type: str) -> bool: return is_normals_like(data_type) @@ -573,8 +566,6 @@ class image_segmentation(CameraImageBase): The segmentation image. Shape is ``(num_envs, H, W, C * frame_stack)``, or channel-first. """ - default_data_type = "semantic_segmentation" - @staticmethod def _accepts(data_type: str) -> bool: return "segmentation" in data_type From 260c9e4bec0b7bb6156c095d0ef67d03704e22e5 Mon Sep 17 00:00:00 2001 From: Antoine RICHARD Date: Tue, 13 Oct 2026 14:21:50 +0200 Subject: [PATCH 4/4] Test default data type resolution for every camera image term Replace the normals-only regression and the positional-call test with a sweep over CameraImageBase subclasses, so a new image term that omits data_type from __call__ fails here. Keep the normals normalization check, which has no other coverage. --- .../test/envs/test_camera_image_mdp.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/source/isaaclab/test/envs/test_camera_image_mdp.py b/source/isaaclab/test/envs/test_camera_image_mdp.py index e9faba2ffb2c..4627d0c3a04c 100644 --- a/source/isaaclab/test/envs/test_camera_image_mdp.py +++ b/source/isaaclab/test/envs/test_camera_image_mdp.py @@ -21,6 +21,7 @@ pytestmark = pytest.mark.unit from isaaclab.envs.mdp.observations import ( + CameraImageBase, image_depth, image_features, image_normals, @@ -145,23 +146,23 @@ def test_buffer_on_cuda(self): class TestModalityTerms: """Per-modality reading, validation and normalization.""" - def test_normals_uses_default_data_type(self): - """Normals resolve their default data type through the observation term signature.""" + @pytest.mark.parametrize("term_cls", CameraImageBase.__subclasses__(), ids=lambda cls: cls.__name__) + def test_constructs_without_data_type(self, term_cls): + """Every camera image term declares a default data type, so its config may omit it.""" + outputs = { + name: _random_rgb() for name in ("rgb", "distance_to_image_plane", "normals", "semantic_segmentation") + } + _, observe = _make_term(term_cls, _make_env(outputs), normalize=False) + assert observe().shape == (NUM_ENVS, HEIGHT, WIDTH, CHANNELS) + + def test_normals_map_to_unit_range(self): + """Normalized normals are mapped from ``[-1, 1]`` to ``[0, 1]``.""" normals = torch.linspace(-1.0, 1.0, NUM_ENVS * HEIGHT * WIDTH * CHANNELS).reshape( NUM_ENVS, HEIGHT, WIDTH, CHANNELS ) _, observe = _make_term(image_normals, _make_env({"normals": normals})) torch.testing.assert_close(observe(), (normals + 1.0) * 0.5) - def test_normals_preserves_positional_normalize(self): - """The positional normalize argument keeps its pre-data-type meaning.""" - normals = torch.linspace(-1.0, 1.0, NUM_ENVS * HEIGHT * WIDTH * CHANNELS).reshape( - NUM_ENVS, HEIGHT, WIDTH, CHANNELS - ) - env = _make_env({"normals": normals}) - term, _ = _make_term(image_normals, env) - torch.testing.assert_close(term(env, SENSOR_CFG, False), normals) - def test_rgb_drops_alpha_and_accepts_constant_mean(self): """Albedo's unused alpha channel is dropped and a constant mean replaces the per-image mean.""" albedo = torch.randint(0, 255, (NUM_ENVS, HEIGHT, WIDTH, 4), dtype=torch.uint8)