feat(camera): add manual exposure, gain, and white balance options for RealSense cameras

The RealSense camera integration lacked sensor-level controls, causing
issues like unstable lighting from auto-exposure hunting. This adds
optional `exposure`, `gain`, and `white_balance` fields to
RealSenseCameraConfig that disable the corresponding auto modes and
apply fixed values when set.
This commit is contained in:
Lev Kozlov
2026-03-22 23:08:20 +09:00
committed by Steven Palma
parent 228cb5ddb9
commit 1c4eb45d9e
2 changed files with 66 additions and 0 deletions
@@ -191,6 +191,7 @@ class RealSenseCamera(Camera):
) from e
self._configure_capture_settings()
self._configure_sensor_options()
self._start_read_thread()
# NOTE(Steven/Caroline): Enforcing at least one second of warmup as RS cameras need a bit of time before the first read. If we don't wait, the first read from the warmup will raise.
@@ -339,6 +340,59 @@ class RealSenseCamera(Camera):
self.new_frame_event.clear()
return self._async_read(timeout_ms=10000, read_depth=read_depth)
def _get_color_sensor(self) -> "rs.sensor":
"""Returns the color sensor from the active pipeline profile."""
if self.rs_profile is None:
raise RuntimeError(f"{self}: rs_profile must be initialized before use.")
device = self.rs_profile.get_device()
for sensor in device.query_sensors():
if sensor.get_info(rs.camera_info.name) == "RGB Camera":
return sensor
raise RuntimeError(f"{self}: no RGB Camera sensor found on device.")
def _configure_sensor_options(self) -> None:
"""Applies manual sensor options (exposure, gain, white balance) to the color sensor.
When exposure or gain is set, auto-exposure is disabled first. When white_balance
is set, auto white balance is disabled first. Skipped entirely if no options are set.
"""
config = self.config
if config.exposure is None and config.gain is None and config.white_balance is None:
return
color_sensor = self._get_color_sensor()
if (config.exposure is not None or config.gain is not None) and color_sensor.supports(
rs.option.enable_auto_exposure
):
color_sensor.set_option(rs.option.enable_auto_exposure, 0)
logger.info(f"{self} auto-exposure disabled.")
if config.exposure is not None:
if color_sensor.supports(rs.option.exposure):
color_sensor.set_option(rs.option.exposure, config.exposure)
logger.info(f"{self} exposure set to {config.exposure}.")
else:
logger.warning(f"{self} sensor does not support manual exposure.")
if config.gain is not None:
if color_sensor.supports(rs.option.gain):
color_sensor.set_option(rs.option.gain, config.gain)
logger.info(f"{self} gain set to {config.gain}.")
else:
logger.warning(f"{self} sensor does not support manual gain.")
if config.white_balance is not None:
if color_sensor.supports(rs.option.enable_auto_white_balance):
color_sensor.set_option(rs.option.enable_auto_white_balance, 0)
logger.info(f"{self} auto white balance disabled.")
if color_sensor.supports(rs.option.white_balance):
color_sensor.set_option(rs.option.white_balance, config.white_balance)
logger.info(f"{self} white balance set to {config.white_balance}.")
else:
logger.warning(f"{self} sensor does not support manual white balance.")
@check_if_not_connected
def read_depth(self, timeout_ms: int = 200) -> NDArray[Any]:
"""
@@ -46,6 +46,15 @@ class RealSenseCameraConfig(CameraConfig):
use_depth: Whether to enable depth stream. Defaults to False.
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 (auto-exposure).
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 (auto).
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 (auto).
Note:
- Either name or serial_number must be specified.
@@ -61,6 +70,9 @@ class RealSenseCameraConfig(CameraConfig):
use_depth: bool = False
rotation: Cv2Rotation = Cv2Rotation.NO_ROTATION
warmup_s: int = 1
exposure: int | None = None
gain: int | None = None
white_balance: int | None = None
def __post_init__(self) -> None:
self.color_mode = ColorMode(self.color_mode)