From 0f30ad34243606af922ce4b4f77852d988b6ecf6 Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Thu, 23 Jul 2026 20:37:20 +0200 Subject: [PATCH] refactor(camera): apply feedback --- docs/source/cameras.mdx | 3 ++ .../cameras/realsense/camera_realsense.py | 53 ++++++++++++------- .../realsense/configuration_realsense.py | 13 ++--- tests/cameras/test_realsense.py | 43 +++++++++++++-- 4 files changed, 82 insertions(+), 30 deletions(-) diff --git a/docs/source/cameras.mdx b/docs/source/cameras.mdx index e5a1f9032..82d6c8355 100644 --- a/docs/source/cameras.mdx +++ b/docs/source/cameras.mdx @@ -164,6 +164,9 @@ includes the range reported by the sensor. Requesting an unsupported control als Omitted controls leave the sensor's existing automatic or manual setting unchanged. These options require `use_rgb=True`. +On the RealSense D405, the color stream is provided by the Stereo Module, so changing manual +exposure or gain also affects the depth stream. + diff --git a/src/lerobot/cameras/realsense/camera_realsense.py b/src/lerobot/cameras/realsense/camera_realsense.py index a701a7450..f0e58ef4b 100644 --- a/src/lerobot/cameras/realsense/camera_realsense.py +++ b/src/lerobot/cameras/realsense/camera_realsense.py @@ -134,6 +134,9 @@ class RealSenseCamera(Camera): self.use_rgb = config.use_rgb self.use_depth = config.use_depth self.warmup_s = config.warmup_s + self.exposure: int | None = config.exposure + self.gain: int | None = config.gain + self.white_balance: int | None = config.white_balance self.rs_pipeline: rs.pipeline | None = None self.rs_profile: rs.pipeline_profile | None = None @@ -175,7 +178,8 @@ class RealSenseCamera(Camera): Raises: DeviceAlreadyConnectedError: If the camera is already connected. - ValueError: If the configuration is invalid (e.g., missing serial/name, name not unique). + ValueError: If the configuration is invalid, a requested sensor option is unsupported, + or a requested sensor value is invalid. ConnectionError: If the camera is found but fails to start the pipeline or no RealSense devices are detected at all. RuntimeError: If the pipeline starts but fails to apply requested settings. """ @@ -400,16 +404,15 @@ class RealSenseCamera(Camera): value is invalid. Invalid-value errors include the option name, requested value, and supported range when available. """ - config = self.config - if config.exposure is None and config.gain is None and config.white_balance is None: + if self.exposure is None and self.gain is None and self.white_balance is None: return color_sensor = self._get_color_sensor() requested_options = ( - (rs.option.exposure, config.exposure, "exposure"), - (rs.option.gain, config.gain, "gain"), - (rs.option.white_balance, config.white_balance, "white balance"), + (rs.option.exposure, self.exposure, "exposure"), + (rs.option.gain, self.gain, "gain"), + (rs.option.white_balance, self.white_balance, "white balance"), ) unsupported_options = [ label @@ -421,30 +424,40 @@ class RealSenseCamera(Camera): f"{self}: color sensor does not support requested manual options: {unsupported_options}." ) - if (config.exposure is not None or config.gain is not None) and color_sensor.supports( - rs.option.enable_auto_exposure - ): - self._set_sensor_option(color_sensor, rs.option.enable_auto_exposure, 0, "auto-exposure") - logger.info(f"{self} auto-exposure disabled.") + manual_exposure_requested = self.exposure is not None or self.gain is not None + if manual_exposure_requested: + if color_sensor.supports(rs.option.enable_auto_exposure): + self._set_sensor_option(color_sensor, rs.option.enable_auto_exposure, 0, "auto-exposure") + logger.info(f"{self} auto-exposure disabled.") + else: + logger.warning( + f"{self} sensor does not support disabling auto-exposure; " + "applying manual exposure/gain directly." + ) - if config.exposure is not None: - self._set_sensor_option(color_sensor, rs.option.exposure, config.exposure, "exposure") - logger.info(f"{self} exposure set to {config.exposure}.") + if self.exposure is not None: + self._set_sensor_option(color_sensor, rs.option.exposure, self.exposure, "exposure") + logger.info(f"{self} exposure set to {self.exposure}.") - if config.gain is not None: - self._set_sensor_option(color_sensor, rs.option.gain, config.gain, "gain") - logger.info(f"{self} gain set to {config.gain}.") + if self.gain is not None: + self._set_sensor_option(color_sensor, rs.option.gain, self.gain, "gain") + logger.info(f"{self} gain set to {self.gain}.") - if config.white_balance is not None: + if self.white_balance is not None: if color_sensor.supports(rs.option.enable_auto_white_balance): self._set_sensor_option( color_sensor, rs.option.enable_auto_white_balance, 0, "auto white balance" ) logger.info(f"{self} auto white balance disabled.") + else: + logger.warning( + f"{self} sensor does not support disabling auto white balance; " + "applying manual white balance directly." + ) self._set_sensor_option( - color_sensor, rs.option.white_balance, config.white_balance, "white balance" + color_sensor, rs.option.white_balance, self.white_balance, "white balance" ) - logger.info(f"{self} white balance set to {config.white_balance}.") + logger.info(f"{self} white balance set to {self.white_balance}.") @check_if_not_connected def read_depth(self, timeout_ms: int = 200) -> NDArray[Any]: diff --git a/src/lerobot/cameras/realsense/configuration_realsense.py b/src/lerobot/cameras/realsense/configuration_realsense.py index e2f5b6b76..c4f1c0e83 100644 --- a/src/lerobot/cameras/realsense/configuration_realsense.py +++ b/src/lerobot/cameras/realsense/configuration_realsense.py @@ -47,14 +47,15 @@ class RealSenseCameraConfig(CameraConfig): rotation: Image rotation setting (0°, 90°, 180°, or 270°). Defaults to no rotation. warmup_s: Time reading frames before returning from connect (in seconds) exposure: Manual exposure value for the color sensor. When set, auto-exposure is - disabled and this fixed value is used. Valid range depends on the camera model - (e.g., 1-10000 for D400 series). Defaults to None (leave unchanged). + disabled and this fixed value is used. Valid ranges are camera-model specific + and reported if the value is rejected. Defaults to None (leave unchanged). gain: Manual gain value for the color sensor. When set, auto-exposure is disabled - and this fixed gain is used. Valid range depends on the camera model - (e.g., 16-248 for D400 series). Defaults to None (leave unchanged). + and this fixed gain is used, which also freezes exposure at its current value + when no exposure is configured. Valid ranges are camera-model specific and + reported if the value is rejected. Defaults to None (leave unchanged). white_balance: Manual white balance value for the color sensor. When set, auto - white balance is disabled and this fixed value is used. Valid range depends on - the camera model (e.g., 2800-6500 for D400 series). Defaults to None + white balance is disabled and this fixed value is used. Valid ranges are + camera-model specific and reported if the value is rejected. Defaults to None (leave unchanged). Note: diff --git a/tests/cameras/test_realsense.py b/tests/cameras/test_realsense.py index ffcc2402d..928a2b3c3 100644 --- a/tests/cameras/test_realsense.py +++ b/tests/cameras/test_realsense.py @@ -347,14 +347,21 @@ def test_configure_sensor_options_raises_when_requested_option_is_unsupported(co sensor.set_option.assert_not_called() -def test_configure_sensor_options_only_exposure_disables_auto_exposure(): +@pytest.mark.parametrize( + ("config_field", "option", "value"), + [ + ("exposure", rs.option.exposure, 120), + ("gain", rs.option.gain, 64), + ], +) +def test_configure_sensor_options_exposure_or_gain_disables_auto_exposure(config_field, option, value): """white_balance=None should not touch auto white balance.""" - config = RealSenseCameraConfig(serial_number_or_name="042", exposure=120) + config = RealSenseCameraConfig(serial_number_or_name="042", **{config_field: value}) camera = RealSenseCamera(config) sensor = _make_mock_sensor( "RGB Camera", - supported_options={rs.option.enable_auto_exposure, rs.option.exposure}, + supported_options={rs.option.enable_auto_exposure, option}, ) _attach_mock_color_sensor(camera, sensor) @@ -362,12 +369,40 @@ def test_configure_sensor_options_only_exposure_disables_auto_exposure(): calls = [call.args for call in sensor.set_option.call_args_list] assert (rs.option.enable_auto_exposure, 0) in calls - assert (rs.option.exposure, 120) in calls + assert (option, value) in calls for opt, _ in calls: assert opt != rs.option.enable_auto_white_balance assert opt != rs.option.white_balance +def test_configure_sensor_options_warns_when_auto_exposure_control_is_unsupported(caplog): + config = RealSenseCameraConfig(serial_number_or_name="042", exposure=120) + camera = RealSenseCamera(config) + + sensor = _make_mock_sensor("RGB Camera", supported_options={rs.option.exposure}) + _attach_mock_color_sensor(camera, sensor) + + with caplog.at_level("WARNING"): + camera._configure_sensor_options() + + sensor.set_option.assert_called_once_with(rs.option.exposure, 120) + assert "does not support disabling auto-exposure" in caplog.text + + +def test_configure_sensor_options_warns_when_auto_white_balance_control_is_unsupported(caplog): + config = RealSenseCameraConfig(serial_number_or_name="042", white_balance=4600) + camera = RealSenseCamera(config) + + sensor = _make_mock_sensor("RGB Camera", supported_options={rs.option.white_balance}) + _attach_mock_color_sensor(camera, sensor) + + with caplog.at_level("WARNING"): + camera._configure_sensor_options() + + sensor.set_option.assert_called_once_with(rs.option.white_balance, 4600) + assert "does not support disabling auto white balance" in caplog.text + + def test_configure_sensor_options_out_of_range_raises_value_error(): """set_option errors should be re-raised as ValueError with range diagnostics.""" config = RealSenseCameraConfig(serial_number_or_name="042", exposure=999999)