Auto exposure on a depth camera averages the whole frame by default, so a bright conveyor behind the part decides the exposure your part gets. The fix is a four-number window that tells the sensor which pixels to judge. This guide walks the C++ path: wrap the depth sensor in the ROI-capable interface, write the bounds with set_region_of_interest before streaming starts, then read them back with get_region_of_interest. The final step proves exposure really follows the region, using a bench light and the camera's own exposure read-out. The type and the two calls are the documented names from the depth SDK the example targets; the pixel values are a worked example for a 1280 × 720 stream. The whole job is about twenty lines of code.
What you need
A depth camera that streams into your own C++ process. The 3D sensors range covers the main families:
- Type
- Depth / ToF camera
- Type
- Stereo / structured light
- Type
- Stereo / structured light
- Your camera's SDK, installed and able to open the device. The code below uses the ROI interface of the depth SDK from the worked example; another SDK exposes the same four bounds under its own names.
- A decided stream resolution, for example 1280 × 720. The bounds are pixels of that frame, so fix the resolution before you fix the numbers.
- A bench light you can aim, for the proof step.
Two cameras in a rig are two separate jobs, each sensor with its own window; the auto exposure per stereo camera in a depth rig guide works that case.
Find the depth sensor and wrap it
Your SDK enumerates the sensors on the device, and in this SDK the depth sensor carries the exposure controls for the depth frames. Wrap that one handle; the wrapper adds exactly two calls, get and set:
// depth_sensor: the sensor handle your SDK gives you for the depth module
rs2::roi_sensor ae(depth_sensor); // wrapper adds get/set of the AE window
If the wrapper rejects the handle, that camera or SDK build has no ROI feature for the exposure engine. There is no retry loop or hidden setting that changes this: control the scene light instead, or use a model and SDK version that expose the window.
Write the four bounds before streaming starts
Decide the resolution first, because the numbers are pixels: 420 is a third of the way into a 1280-wide frame and two-thirds into a 640-wide one. The worked example: a part projects between x = 470 and x = 810, y = 290 and y = 450 in a 1280 × 720 frame. Add a 50-pixel margin on every side so mounting drift cannot feed background into the statistics, which lands the window at 420-860 across and 240-500 down.
// Worked example: part at x 470-810, y 290-450, plus a 50 px margin
int min_x = 420; // left edge, pixels of the depth frame
int min_y = 240; // top edge
int max_x = 860; // right edge
int max_y = 500; // bottom edge
rs2::region_of_interest roi; // the SDK's four-bound struct
roi.min_x = min_x;
roi.min_y = min_y;
roi.max_x = max_x;
roi.max_y = max_y;
ae.set_region_of_interest(roi); // write while the stream is stopped
// only after this: your SDK's start / stream call
The four numbers obey three rules: every min strictly below its max, all four inside the configured frame, and the window equal to or smaller than the captured area. Writes outside the frame come back clamped or refused, which the read-back step catches.
Push the window while the device is open and the stream is stopped. Mid-stream writes are where builds disagree — applied a frame late, or dropped until the next start. If depth exposure comes up at defaults no matter what you write, suspect the order first; the depth auto exposure not applied at startup guide walks exactly that failure.
Read the bounds back and fail loudly
A get-then-set round trip — read the current window, write it straight back — proves the interface answers and aims nothing: it re-commits whatever window was already there, which is why it looks like a fix and changes nothing. The useful pattern is the opposite order. Write your four values, then read back and compare:
rs2::region_of_interest now = ae.get_region_of_interest();
if (now.min_x != min_x || now.min_y != min_y ||
now.max_x != max_x || now.max_y != max_y) {
throw std::runtime_error("AE ROI read-back mismatch");
}
Keep that comparison in the initialization path. A throw means the sensor edited your numbers: usually a bound outside the frame or a min that is not below its max. Clamp, rewrite, read back again. If it still mismatches, that firmware build does not keep a custom window; full-frame exposure plus scene control is the honest fallback.
Prove the exposure follows the window
- Stream and read the exposure time reported for the depth frames; the frame metadata under auto exposure guide shows where the value lives. Note the baseline — 8,000 µs on a dim bench scene, say.
- Light only the window: aim the lamp at a white card where the part sits. Auto exposure drives the window's mean brightness to a target, so quadrupling the light inside should cut exposure to roughly a quarter, 8,000 µs toward 2,000 µs.
- Light only outside the window and watch the value hold. If exposure follows light outside the region, the window is not applied — re-check ordering and read-back.
- Block the window with dark foam and expect the exposure to climb toward its ceiling.
Size and place the window: the numbers
For the worked example: (860 - 420) × (500 - 240) = 440 × 260 = 114,400 pixels, against a 1280 × 720 frame of 921,600 pixels. That is 114,400 / 921,600 ≈ 0.12, so the window judges about 12% of the frame and ignores the rest. Three values to hold on to:
- Aim for part plus margin. The 50-pixel border absorbs about ±50 pixels of mounting drift before background leaks into the average.
- Floor near 1% of the frame. On this stream that is 9,216 pixels, about a 96 × 96 window; below it, a few hot pixels dominate the mean and exposure swings frame to frame.
- Ceiling is the full frame. The default window is valid, and a custom window must sit equal to or smaller than the captured area.
Resolution changes move the numbers, so rewrite the bounds and read back after any switch. Keep the layers straight too: the AE window changes statistics, not pixels. Cropping the point cloud is a different call, covered in the XY crop on a depth point cloud guide, and the inspection ROI your tools measure is another setting again, mapped in 2D vs 3D camera ROI.
Mistakes that cost an afternoon
- Setting the window after start(). Mid-stream behavior is where builds disagree; writing before streaming behaves the same everywhere.
- The get-then-set round trip. It succeeds and aims nothing.
- min ≥ max, or a bound past the frame. The read-back mismatch is the check catching it before the line does.
- Window on the background. A dark conveyor behind a bright part drags exposure up until the part saturates; put the window on the part.
- Exposure with no ceiling in a dark scene. Cap the maximum exposure time so a dark corner cannot stretch frames past your frame-rate budget; the maximum exposure time cap guide sets that number.
- Trusting the window to persist. Rewrite it at every device open.
Bench checks before you ship
- The read-back test passes on a cold start from the production code path.
- Light inside the window drops exposure; light outside holds it flat.
- A resolution switch rewrites the bounds and the read-back still matches.
- A power cycle re-applies the window from your init path with no manual step.
- The exposure cap is set, so the darkest scene still meets your frame rate.
Pick the camera, then start the stream
Family follows task: ToF and structured light for volume, presence and bin work; laser profiling for cross-sections and seams. For cross-sections and seams, a 3D laser profile sensor is the depth source to build on:
- Type
- Laser profiler
Filter the rest of the range in the camera finder, or start at the camera step of the system configurator. The 3D vision topic page collects the other worked depth-camera guides, and the contact page is the route when a scene resists the bench checks.
Questions
Does the auto exposure ROI also apply to the RGB stream?
The window belongs to the sensor you wrapped, so a color camera keeps its own exposure engine and its own statistics. If you also want a window on color, wrap the color sensor the same way and write its bounds in its pixel space at its own resolution. Keep the two windows in separate variables and check each with its own read-back comparison.
What does a read-back mismatch after set_region_of_interest mean?
Usually the sensor edited your numbers: bounds outside the configured resolution, or min_x not smaller than max_x. Clamp the window to the frame, make sure every min sits strictly below its max, and write again. If the read-back still returns different values, that firmware build does not keep a custom window; use full-frame exposure and control the scene light instead.
Do I need to set the AE ROI again after every reboot?
Treat the window as session state and write it in your initialization path every time the device opens. One set call costs microseconds, removes any dependence on what the camera kept from its last session, and the read-back that follows doubles as a start-up self-test. Settings persisted inside the camera are a convenience your line should never need.
Can I move the AE ROI while frames are streaming?
Write the window before the stream starts; that order behaves the same everywhere. Mid-stream writes are where firmware builds differ: some apply the new bounds on the next stream, some drop them. To re-aim during a run, stop the stream, set the window, read it back, then start again. You lose a few frames and gain a deterministic result.



