Your color stream comes out too dark, the exposure call throws an exception, and the values you set seem to do nothing. On a depth camera that generates its color image from the stereo module, all three symptoms share one cause: there is no separate RGB sensor, so there is no RGB exposure control to call. The exposure you need belongs to the depth sensor at index 0 in the device's sensor list. This guide shows you how to confirm which architecture your camera uses, set the shared exposure from code, check the result on live streams, and decide when one shared exposure can no longer serve the job — and a dedicated color camera becomes the better fix.
Why one exposure serves color and depth
An active stereo depth camera points two monochrome imagers at the scene, projects a pattern onto it, and computes depth from the disparity between the two views. On designs with no separate color imager, the color texture you receive is generated inside that depth module. One physical sensor, one exposure time, two processed streams — there is nothing for a color exposure control to act on.
That architecture explains the exact failure you are seeing. The device's sensor list holds the stereo module at entry 0. A second entry may enumerate for the color stream, but it owns no options of its own: set_option validates the requested option against that sensor's supported list and throws before any value reaches hardware — the VALIDATE_OPTION failure inside rs2_set_option. Code ported from a camera that does carry a dedicated color imager expects entry 1 to accept RS2_OPTION_EXPOSURE. On a derived-color camera it never will.
The tell takes one call: on a camera with a dedicated color imager, entry 1 of query_sensors() accepts RS2_OPTION_EXPOSURE directly; on a derived-color camera, only entry 0 does.
Set the shared exposure on sensor index 0
You need the device streaming and your stream configuration started; everything else happens through the sensor list.
-
Enumerate the sensors. query_sensors() returns the device's sensor list, and the stereo/depth sensor is entry 0. If entry 1 exists and accepts exposure options, your camera has a dedicated color imager, and the per-sensor pattern in Set auto exposure per stereo camera in a depth rig applies instead.
-
Park auto exposure where the pipeline needs it. Auto mode retunes the shared exposure every frame, which shifts your model's input distribution. For a fixed response, disable it on entry 0; for a scene with stable light, enable it there and let the module hold the histogram.
-
Set the exposure value on the same sensor, in microseconds. Start at the module default of 33000 µs and step in doublings or halves — each step is one stop:
double exposure_us = 33000; // module default, µs
auto depthSensor = profile.get_device().query_sensors()[0]; // stereo module
depthSensor.set_option(RS2_OPTION_ENABLE_AUTO_EXPOSURE, 0); // manual mode
depthSensor.set_option(RS2_OPTION_EXPOSURE, exposure_us); // drives color and depth
- Read the direction of the control. Raising the value brightens the color stream; lowering it darkens color and depth together, because both come from the same capture.
Verify the change
Sweep the value live before you hard-code anything. In the viewer application that ships with the SDK, enable both streams and change exposure under Stereo Module > Controls in the options side-panel. From your own code, the same runtime tuning approach works: change the value, grab a frame, look, repeat.
Three checks:
- Brightness moves as expected. Going from 10000 µs to 33000 µs multiplies captured light by 33000 / 10000 = 3.3, about log2(3.3) ≈ 1.7 stops — plainly visible on the gray histogram of the color stream.
- Depth stays healthy. No new saturated bands on shiny surfaces, and the frame rate you configured still holds.
- The fps counter agrees with the math. Crossing about 33 ms of exposure caps an exposure-limited stream near 30 fps; if you need more, the next section is the budget to run.
Common mistakes
- Routing exposure to entry 1. The call throws at validation; entry 0 owns the option.
- Treating the value as milliseconds. 33000 means 33 ms; the unit is µs, so 33000 interpreted as milliseconds would be a 33-second exposure.
- Chasing brightness past the default. If 33000 µs still reads dark, the scene needs light, not more exposure.
Worked example: recovering a robot-cell scene from a 450 µs guess
The case: a robot-guidance cell where a trained model reads the color stream. Ported code set manual exposure to 450 µs, the image came back effectively black, and raising the value as far as 10000 µs still left it too dark. The module's default exposure is 33000 µs.
Inputs and the rule:
- Manual value set by the ported code: 450 µs.
- Brightest value tried before diagnosing: 10000 µs.
- Module default: 33000 µs.
- Rule: captured light scales with exposure time, one stop is a doubling, so brightness follows the ratio of the two times.
Arithmetic:
- The 450 µs guess sat 33000 / 450 = 73.3× below the default: log2(73.3) ≈ 6.2 stops dark. A model sees an image six stops under its training distribution as black.
- The 10000 µs attempt gained 10000 / 450 = 22.2×, about +4.5 stops — visibly brighter, yet still 33000 / 10000 = 3.3× short of default: log2(3.3) ≈ 1.7 stops. Indoors, 1.7 stops under reads as too dark.
- Result: no value on the color path was ever going to stick, because that path owns no exposure. The working value lives on sensor entry 0.
Decision: set entry 0 to 33000 µs with auto exposure off, then trim by stops — 16500 µs is −1 stop if highlights clip. If the cell still reads dark at the default, add light; beyond 33 ms the exposure starts costing frame rate, and the next section puts numbers on that.
What the shared exposure costs: frame rate, smear and depth
Frame rate first. The fps ceiling from exposure alone is 1/t: at 33000 µs that is 1 / 0.033 s ≈ 30 fps, at 16500 µs about 61 fps, at 10000 µs about 100 fps. Doubling exposure halves the ceiling.
Motion smear is speed × time: at 33 ms, every 1 m/s of part motion draws a 33 mm streak; at 10 ms, 10 mm. Run the budget on a concrete case: a part moving at 0.5 m/s with a 5 mm smear budget allows t_max = 5 mm ÷ 500 mm/s = 10 ms, i.e. 10000 µs — still 1.7 stops short of default brightness. When brightness and smear cannot both be met on one sensor, add light, apply gain where the sensor exposes a gain option, or split color onto its own camera.
Depth quality: the same imagers capture the projector pattern, so overexposure can saturate shiny surfaces, wash out pattern contrast and open depth holes; short of saturation, longer exposure strengthens the signal. After each change, scan the confidence image for flagged pixels before trusting the cloud.
Decision rule: a static scene tunes around the 33000 µs default and stops there. A moving scene computes t_max from the smear budget first; if the brightness you need exceeds t_max, the fix is light, gain or architecture — not more exposure.
When one exposure stops being enough
A trained model wants a fixed color response; moving parts want short exposures. One sensor cannot deliver both, and there are two ways out, in order of cost:
-
A depth camera with a dedicated color imager. Color then enumerates as its own sensor with its own exposure options, and the entry 0 / entry 1 split works the way ported code expects.
-
A separate 2D color camera registered to the depth output. Exposure lives wholly on the 2D camera — on a standard area scan it is a plain camera option set through GenICam, as in setting exposure time through a GenICam API in Python. The work is registration: fix the extrinsics with aligning depth to a third RGB camera and keep the streams mapped with stereo camera RGB calibration.
For the color role, browse the area scan cameras or the camera finder. A compact 2.5 MP GigE color model such as the MV-CU020-90GC runs a global shutter, so color frames stay sharp while the belt moves:
- Sensor format
- 1/1.7"
- Lens mount
- C-mount
- Interface
- GigE
- Resolution
- Up to 3 MP
Depth camera families for larger jobs
The bench rig is not the only shape this problem takes. The shop's 3D sensors range spans the other depth architectures: laser profilers that sweep a profile line across the part, projected structured-light and laser galvanometer stereo cameras for dense surface work, ToF sensing cameras, and speckle structured-light models built for logistics volume measurement:
- Type
- Stereo / structured light
- Type
- Stereo / structured light
- Type
- Depth / ToF camera
On any of these families, the integration question stays the same: where does the color stream enumerate in the SDK? That one answer decides where your exposure code goes.
The decision in one pass
- Derived color — no exposure options on sensor entry 1: set RS2_OPTION_EXPOSURE on entry 0, in µs, starting at 33000.
- Color too dark at 10000 µs: you are 1.7 stops under the default, not at a ceiling — go to 33000 µs.
- Static scene: tune around the default in stops; auto exposure on entry 0 can hold it for you.
- Moving scene: compute t_max = smear budget ÷ speed first; add light or gain when brightness exceeds it.
- Fixed color response plus fast depth: a dedicated color imager, or a registered 2D color camera.
To shortlist hardware for either role, start the camera configurator; for scene-specific advice, the contact page reaches the applications team. More depth-camera fixes sit on the 3D vision topic page.
Questions
Why does the color image from my stereo depth camera look grayscale?
First check which stream you are viewing: the infrared feed looks like a washed-out color image until you switch streams in the viewer, and that mix-up is more common than a sensor fault. On a derived-color design the texture comes from monochrome imagers, so treat it as geometry texture. True color needs a dedicated color imager in the depth camera or a separate color camera registered to the depth output.
What happens if I call set_option on sensor index 1 anyway?
The call throws before any value reaches the hardware, because the SDK validates the option against that sensor's supported list first, and a stereo-module color entry has no exposure options to match. Nothing is half-applied and nothing needs resetting afterwards. Route both the auto-exposure enable and the exposure value to the sensor at entry 0 and the calls go through.
Can color and depth run at different exposures on one of these cameras?
No. One exposure serves both streams, so a fixed color response for a trained model and a short exposure for moving parts cannot coexist on the same sensor. Pick a depth camera whose architecture includes a dedicated color imager, or register a separate 2D color camera to the depth output and expose each stream on its own terms.
Does a longer shared exposure hurt depth quality?
It can. The same imagers capture the projected pattern, so an exposure long enough to saturate shiny surfaces washes out pattern contrast and opens depth holes. Short of saturation, more exposure generally strengthens the pattern signal. Read the confidence map after every exposure change and mask flagged pixels instead of applying a fixed rule.
Which exposure value should I ship as the default?
Start from the module default of 33000 µs and tune in stops for your lighting. Keep auto exposure off in production, because a moving exposure keeps shifting the input distribution your model was trained on. Record the tuned value with the model version, and re-tune whenever the lighting in the cell changes.



