Cameras & sensorsHow-to

Query the option range before setting exposure or gain

Check support, read min, max and step from the option range, then clamp exposure and gain into it — so one control script works on every camera you own.

LumaSpec Vision10 min read Share on LinkedIn

On this page 8 sections
  1. 01Which exposure and gain controls does your camera actually support?
  2. 02How do you read the option range before writing a value?
  3. 03What units do exposure and gain use?
  4. 04How do you clamp a requested exposure or gain so it fits every camera?
  5. 05What does a worked example with real numbers look like?
  6. 06Why does a hard-coded value fail on the next camera model?
  7. 07How do you verify that the set actually took?
  8. 08Questions

A control script that types exposure and gain straight into the code works on exactly one camera: the one it was tuned on. Move the same recipe to the next sensor and the write either throws, clamps silently, or does nothing. The image goes dark with no error to explain it. The fix costs three calls. Ask the camera whether it supports the option, read the option range for its minimum, maximum and step, then clamp the requested value into that window before you set it. This guide shows the pattern in code, the unit traps for exposure and gain, a worked example with the arithmetic in full, and the read-back that proves the value landed.

Which exposure and gain controls does your camera actually support?

Ask before you write. Every camera API exposes a supports check per option, and on a typical industrial area scan camera exposure and gain return true while gamma and contrast often return false. One false answer ends the matter a line before the crash.

The supports call takes an option handle and returns a boolean, so you can enumerate the whole control set in a loop at startup. On a typical industrial area scan camera the answers look like this: ExposureTime true, Gain true, BlackLevel usually true, Gamma sometimes. The auto variants — auto exposure and auto gain — are separate options with their own answers. Check each control you intend to write; two models in the same series can disagree on gamma, so infer nothing from the sensor family.

One precondition comes before any manual write: switch the auto variant off, or the camera keeps overwriting your value every frame. The same discipline runs through every camera control guide on the cameras topic page. When the query itself returns an error instead of an answer, that is an option-compatibility problem between sensor and driver rather than a missing feature — camera control query error: diagnose option compatibility walks that diagnosis.

How do you read the option range before writing a value?

One call gives you the whole legal window: get_option_range — or the feature node's Min, Max and Inc values in GenICam — returns the smallest permitted value, the largest and the step. Any write outside that window is the root of most 'the setting did nothing' tickets.

A range read returns min, max, step and default. In a GenICam stack the same numbers sit on the feature node as Min, Max and Inc — Inc on integer features, Min and Max on floats. Read them once per sensor at startup and log them next to the serial number. That log becomes the per-model reference your recipes lean on.

The shape of a real answer: one published GenICam interface for an industrial camera lists ExposureTime as a float from 66 µs to 18000 µs with a 5000 µs default, and Gain as a float from 0 dB to 18 dB. Those are that camera's numbers, not yours. That is precisely why the call at startup, not any written list, is the source of truth for your fleet.

What units do exposure and gain use?

Exposure is microseconds — the GenICam feature ExposureTime carries µs, and one published industrial interface lists 66 to 18000 µs with a 5000 µs default — while gain is usually decibels, 0 to 18 dB on that same interface, and unitless raw counts on plenty of other stacks.

Exposure is settled by the GenICam standard: the SFNC name ExposureTime carries microseconds, so 40000 means 40 ms. The same convention runs through GigE Vision and USB3 Vision cameras. The trap is reading a raw register instead, where counts of sensor-clock ticks are common — auto exposure limit units in microseconds: query before you set is devoted to that single mistake.

Gain has no settled unit. Decibels are the common GenICam choice, but plenty of SDKs expose raw analog gain counts, and the two scales neither start nor end in the same place. Decibels convert cleanly to amplification: every 6 dB doubles the signal, because 20·log10(2) ≈ 6.02 dB, so an 18 dB ceiling is about ×8. The range object is the only place the true scale is written down — read it, then decide.

How do you clamp a requested exposure or gain so it fits every camera?

Clamp, snap, write: compute chosen = max(min, min(max, requested)) on every camera, snap to the step on integer features, and the same five steps then work on a 2.5 MP color camera and a 20 MP mono camera without touching the recipe.

Run these steps for every scalar control:

  1. Switch off the auto variant of the control (auto exposure, auto gain), or the camera rewrites your value each frame.
  2. Call supports on the option. A false answer means there is nothing to set: log the recipe value and continue.
  3. Call get_option_range and keep min, max, step and default.
  4. Clamp and snap: chosen = max(min, min(max, requested)); for an integer feature with step s, snap with chosen = min + round((requested − min) / s) × s.
  5. Write with set_option, read the same option back, and accept the write only when the readback matches within half a step.
chosen = max(min, min(max, requested)) chosen = min + round((requested - min) / step) * step

This is the whole portability trick: the recipe keeps asking for 40000 µs, and every camera gets a legal write without editing the recipe per model. The five calls are identical across the area scan cameras line — a 20 MP USB3 mono camera and a 2.5 MP GigE color camera take exactly the same sequence:

Area scan camerasHikrobot MV-CS200-10UM 20 MP Area Scan Camera, USB3.0, IMX183, 2nd Gen., MonoMV-CS200-10UM
Sensor format
1"
Lens mount
C-mount
$759.00View part
Area scan camerasHikrobot MV-CU020-90GC 2.5 MP Area Scan Camera, GigE, GMAX4002, ColorMV-CU020-90GC
Sensor format
1/1.7"
Lens mount
C-mount
$309.00View part

What does a worked example with real numbers look like?

Ask the example device for 40000 µs and the clamp writes 18000 µs: max(66, min(18000, 40000)) = 18000, and a gain request of 20 against a 0–18 dB window writes 18 dB — two lines of arithmetic, shown in full below.

The code is short because the calls do the work. Sensor, OptionHandle and OptionRange stand in for your SDK's types; only the three call names — supports, get_option_range, set_option — are the pattern itself.

// Sensor, OptionHandle and OptionRange stand in for your SDK's types.
// kExposure and kGain are the option handles for the ExposureTime and
// Gain features; name them as your SDK prints them.

double request_and_clamp(Sensor& sensor, OptionHandle opt, double requested) {
    if (!sensor.supports(opt))
        return requested;                             // no such control here
    OptionRange range = sensor.get_option_range(opt); // min, max, step, default
    double chosen = std::max(range.min, std::min(range.max, requested));
    sensor.set_option(opt, chosen);
    return chosen;
}

// Test against the example device: ExposureTime 66..18000 us, Gain 0..18 dB.
double exposure_applied = request_and_clamp(cam, kExposure, 40000.0);
double gain_applied     = request_and_clamp(cam, kGain, 20.0);
assert(exposure_applied == 18000.0);  // 40000 clamped to the 18000 us ceiling
assert(gain_applied == 18.0);         // 20 clamped to the 18 dB ceiling

If you write in Python against GenICam, the same sequence reads almost identically — setting camera exposure time through a GenICam API in Python shows the feature-node form.

Follow the first request through the clamp: the recipe asks for 40000 µs, min is 66 µs and max is 18000 µs, so chosen = max(66, min(18000, 40000)) = 18000 µs. The gain request of 20 meets a 0–18 dB window: max(0, min(18, 20)) = 18 dB. Eighteen decibels is roughly ×8 signal amplification, so that ceiling is the noise-heavy end of the dial — add light first, because one extra stop of illumination buys the same brightness as +6 dB of gain without multiplying sensor noise.

A clamped exposure also has to fit inside the frame period of the camera it lands on. Take the LS-CMR-0166, which runs 30 fps at 4000 × 3000:

Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
Resolution
12 MP
Shutter
Rolling
$560.00View partBuild a system with it

One frame period is 1/30 s ≈ 33.3 ms, so an 18 ms exposure leaves 33.3 − 18 = 15.3 ms for readout and holds the full frame rate. Push the recipe the other way, toward the 66 µs floor for fast-moving parts, and the margin only grows.

Why does a hard-coded value fail on the next camera model?

A hard-coded 40000 µs outlives the camera it was tuned on. On a sensor capped at 18000 µs it either throws, clamps silently, or saturates the driver, and you usually discover which one from a dark image, not from a log entry.

Three failure modes exist, and only one announces itself. Some SDKs throw and hand you the range in the message. Some clamp silently: the call returns success and the sensor runs at its own limit. Some accept the number and saturate downstream — the driver or the auto pipeline pins at a value the sensor cannot reach. All three look identical from the desk: a recipe that worked yesterday does nothing today.

The defense is structural. Store requested values in the recipe, never applied values, and clamp at runtime with the queried window; log the window at startup next to the serial number so the limits sheet builds itself. Enumeration controls fail the same way with a different error — a symbolic name the camera does not list — and GenICam enumeration invalid value: check symbolics before you set applies the identical discipline there. Depth and 3D sensors add one wrinkle: their exposure ceilings sit lower because the laser line or structured-light flash carries its own timing budget, so a value lifted from a 2D recipe clamps almost every time.

How do you verify that the set actually took?

Read the value back and shoot a test frame: the readback must equal the clamped number within half a step, and the mean level of a grey target should double when you double the exposure — 60 becomes roughly 120 in 8-bit — before gain moves at all.

Verification is two quick checks. First, read the option back: the readback must equal the clamped number within half a step, because a silent clamp shows up right there. Second, shoot a test frame of a grey target: doubling exposure from 5 ms to 10 ms should roughly double the mean pixel level — 60 to about 120 in 8-bit — with gain untouched, and linear response confirms the value is live.

Watch the ceiling as you raise gain: a mean approaching 255 in 8-bit is clipping, not sensitivity, and the fix is less gain or more light, never more exposure. When auto exposure is still in the path, the applied value lands in frame metadata rather than your variable — read exposure time from frame metadata under auto exposure shows where to look.

If you are choosing the camera this script will drive, the camera finder filters by interface, resolution and shutter, and the camera configurator builds a matched camera, lens and light set; the camera topic page collects the rest of the control guides, and /contact/ is the line to us when a recipe needs a second pair of eyes.

Questions

Does the range query add latency to the image path?

No, if you do it once. A range read is a control-channel transaction — microseconds to a few milliseconds over GigE or USB3 — and it never touches the frame stream. Query each option once at startup and cache the window against the camera's serial number; re-query only when a sensor is swapped. The per-frame path then contains nothing but the write.

Can I change exposure and gain while the camera is streaming?

Usually yes: ExposureTime and Gain are runtime-writable on most GigE Vision and USB3 Vision cameras, and the new value takes effect at a frame boundary so no image tears. Two conditions apply — the auto variants must be off first, and the effective exposure ceiling while streaming is the frame period, because an exposure longer than the period cannot complete every frame.

What if my SDK offers no supports or range call at all?

Fall back to the documented register map: read the feature's limits from the camera's registers, or write a value and read back what the sensor reports. If read-back is missing too, drive exposure through the documented sensor-register path — [set exposure via sensor registers when the SDK has no API](learn:set-exposure-via-sensor-registers-when-the-sdk-has-no-api) covers that route and its pitfalls.

Why does a gain of 20 overload one camera and do nothing on another?

Because decibels are not normalized between sensors: 18 dB on one camera is a ×8 analog amplification, while another sensor might span 0–24 dB or a 0–48 raw-count scale over a different analog range. The queried window, not the unit, is the contract — a gain of 20 is legal on one camera and physically impossible on another.

Does the same pattern apply to white balance, gamma and black level?

Yes — every scalar control uses the identical supports, range, clamp, write, read-back sequence. Expect two quirks: gamma is often a lookup-table node rather than a sensor register, so supports can return false while a gamma LUT still exists, and black level is typically an integer feature with a step, so snap the requested value to the step before writing.