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/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 b5379ae2fd97..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) @@ -530,13 +525,12 @@ class image_normals(CameraImageBase): 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. """ - default_data_type = "normals" - @staticmethod def _accepts(data_type: str) -> bool: return is_normals_like(data_type) @@ -548,8 +542,10 @@ def __call__( normalize: bool = True, channel_first: bool = False, frame_stack: int = 1, + *, + data_type: str = "normals", ) -> 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) @@ -570,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 diff --git a/source/isaaclab/test/envs/test_camera_image_mdp.py b/source/isaaclab/test/envs/test_camera_image_mdp.py index 8bae4046d9d2..4627d0c3a04c 100644 --- a/source/isaaclab/test/envs/test_camera_image_mdp.py +++ b/source/isaaclab/test/envs/test_camera_image_mdp.py @@ -21,8 +21,10 @@ pytestmark = pytest.mark.unit from isaaclab.envs.mdp.observations import ( + CameraImageBase, image_depth, image_features, + image_normals, image_rgb, image_segmentation, stacked_image, @@ -144,6 +146,23 @@ def test_buffer_on_cuda(self): class TestModalityTerms: """Per-modality reading, validation and normalization.""" + @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_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)