You set an auto-exposure cap of 5000 µs, the call returns without an error, and the frames still report 30 ms. The first suspect is usually the lighting; it almost never is. In the case behind this checklist, the limit never reached the sensor's auto-exposure loop: the host library predates the fix, and the process ran under a different interpreter than the one that received the upgrade. What follows is the diagnostic sequence in order of probability, the per-frame telemetry that settles each test, and the blur arithmetic that tells you what the cap should be before you send it again.
The symptom: two plateaus, one cap
A PoE machine-vision camera was capped with initialControl.setAutoExposureLimit(maxExposureTimeUs=5000), and the same limit was re-sent on the control queue every 0.3 s while streaming at 15 fps. The per-frame exposure printout showed only two values, 29.995 ms and 4.996 ms, with nothing in between. Read those numbers before touching anything:
- 4.996 ms is your 5000 µs cap taking hold. The small shortfall is the sensor's line-time quantization, not a different limit.
- 29.995 ms is the roughly 30 ms ceiling the exposure loop runs to when nothing holds it down.
- At 15 fps the frame period is 1 ÷ 15 = 66.7 ms, so both values fit inside the period. Frame rate is not the constraint.
Auto exposure is swinging between a cap that occasionally binds and a default ceiling it escapes to. That is a state and version problem, not a light problem.
First test: read the exposure from the frames, not from your code
The only evidence that counts is the exposure each frame says it used. In that SDK the frame object carries getExposureTime(), a timedelta you print as total_seconds() × 1000 in milliseconds. On GenICam cameras the same evidence is chunk data embedded with each image, timestamp included.
Decision rule: under steady illumination, any frame reporting more than your cap means the sensor never adopted the limit — stop adjusting the light and fix the delivery path. Frames pinned at the cap mean the limit works, and your real problem is brightness, gain, or the blur budget itself.
Cause 1: the library in the process predates the fix
Host-side SDKs carry bugs like any library, and a limit the API accepts without error while the sensor never receives it is a known bug class. In this case the vendor shipped the fix in 2.25.0.0, with 2.25.1.0 the current release at the time; older copies returned success and dropped the call. Test from inside the process, never from a second shell:
import importlib.metadata as md
import sys
PACKAGE: str = "your-camera-sdk" # the pip name used to install it
print(sys.executable) # interpreter this process runs on
print(md.version(PACKAGE)) # SDK version this process imports
Run the file with your real launch command. A version older than the release that carries the fix is your answer: upgrade, then repeat the telemetry test.
Cause 2: you upgraded a different interpreter than the one you run
This is what actually solved the case. pip installs into one interpreter's site-packages; python and python3 can be two separate installations, each holding its own copy of the SDK. The upgrade went to python3, the app was started with python, and the fault persisted on hardware that had already been fixed. The same trap appears as a service unit pointed at another binary, a virtual environment that never gets activated, a notebook kernel, or a container image built weeks earlier. The sys.executable line above, run under the real launch command, settles it; the fix is to start the app from the upgraded environment, or to install the SDK into the one you start.
Two live write-ups cover the pattern in depth: pinning one library version everywhere in the build and matching a GenICam package to your interpreter.
Cause 3: the limit is applied once, then overwritten
The exposure loop re-initializes when streaming starts and when resolution, frame rate, or ISP settings change; a limit set only in initial control can be lost along that path. The case code re-sent the limit every 0.3 s, which is why the cap kept reappearing between excursions to the ceiling. Two better patterns exist: register the limit as a persistent startup setting on the device, or send it after the first frames arrive and re-send after every mode change. Expect a one-frame delay between a control write and its effect on the telemetry — setting exposure every frame measures it, and startup settings overwriting an upper exposure limit is the same failure seen from the device side.
Cause 4: the unit is microseconds
The parameter name carries the unit: maxExposureTimeUs takes microseconds, so 5000 means 5.000 ms. Write 5 and you have asked for 5 µs, a value no scene can reach, so the loop saturates at its ceiling and the symptom looks identical to a broken limit. The telemetry tells you which one you have: 4.996 ms reported against a 5000 µs cap is a working limit. Read the current limits back before you change them; querying exposure limits in microseconds shows the check.
Worked example: set the cap from the blur budget
The 5 ms cap in the case was a round number, not a requirement. Derive it instead. The example camera is the LS-CMR-0166, a 12 MP USB3 camera with 4000 × 3000 resolution at 30 fps.
- Sensor format
- 1/1.7"
- Interface
- USB3
- Resolution
- 12 MP
- Shutter
- Rolling
Over a 300 mm wide field of view, the object-side pixel size is 300 mm ÷ 4000 px = 0.075 mm/px. The part moves at 200 mm/s. Blur is speed × exposure:
- Allowed blur of 1 px: t_max = 1 × 0.075 mm ÷ 200 mm/s = 0.000375 s = 375 µs.
- The case's 5 ms cap: blur = 200 mm/s × 0.005 s = 1.0 mm, which is 1.0 ÷ 0.075 ≈ 13 px of smear. Unusable.
- Decision: cap at 400 µs. Blur = 200 mm/s × 0.0004 s = 0.08 mm ≈ 1 px, and the exposure occupies 400 µs of the 33.3 ms frame period at 30 fps, about 1.2 %.
Dropping from 5000 µs to 400 µs costs 5000 ÷ 400 = 12.5× the light, or log2(12.5) ≈ 3.6 stops. Opening the lens from f/4 to f/2 recovers 4× of that, two stops; the remaining 12.5 ÷ 4 ≈ 3× must come from the light source or from gain. At these speeds, shutter type matters as much as the cap: each row of a rolling shutter exposes at a slightly different instant, so fast parts shear even at a correct exposure time. The MV-CH120-11UC, a 12 MP USB3 camera with the IMX304 sensor and a global shutter, is the class to use.
- Sensor format
- 1.1"
- Lens mount
- C-mount
- Interface
- USB3
- Resolution
- 12 MP
The general method is in capping the maximum auto exposure time; when your smallest feature needs more pixels than this example, size the sensor first:
Verify, then lock it down
Re-run the telemetry for 60 s while the scene changes: lights on and off, target moved through the field. Every frame must report at or below the cap, plus the quantization you already know (4.996 ms against a 5.000 ms cap). Then keep the fix: the exact interpreter path in the service definition, the SDK version in the lockfile, and the two-line version print at startup. On GenICam cameras the exposure-auto mode and its upper limit live in the feature interface — setting exposure through a GenICam API in Python shows the call pattern — and writing sensor registers directly is the fallback when no API exposes the limit.
When the cap is the wrong fix
If the blur budget demands 400 µs but the image only reaches usable contrast at 5 ms, do not raise the cap — add photons. Raise the light output by the missing factor, move the light closer, or strobe the source and gate the camera inside the pulse: a 100 µs strobe freezes motion better than any continuous exposure cap. On smart cameras with integrated lighting, raising the light is the first move, because the exposure loop and the source are one device. With a constant scene, fixed manual exposure beats a capped loop for repeatability — why manual exposure looks darker than auto exposure explains the gain change you will see.
Checklist before you blame the lighting
- Print the per-frame exposure; find the cap value and the default ceiling in the numbers.
- Print
sys.executableand the SDK version from inside the process that talks to the camera. - Check the release notes for an auto-exposure fix newer than the version you print.
- Start the app under the environment you upgraded; pin interpreter and library version in deployment.
- Re-send the limit after any change to resolution, frame rate, or ISP settings.
- Confirm the unit suffix:
Usin a parameter name means microseconds. - Compute the cap as blur pixels × pixel size ÷ speed, then confirm it in 60 s of telemetry.
- If light is short, add output, open the aperture, or strobe — keep the cap.
When the camera side is settled, size the rest of the cell with the camera configurator, compare area scan cameras in the camera finder, or read more set-up pieces on the cameras topic page.
Questions
Why does the reported exposure jump between my cap and about 30 ms?
The two plateaus are the limit you sent and the ceiling the exposure loop runs to when nothing holds it down. The jump means the limit is not persistent in the sensor's state: each control write clamps exposure for a while, the state is then lost, and the loop climbs back to the ceiling. A persistent startup setting, a newer runtime library, or a re-send after every mode change stops the jumping.
How do I tell which Python interpreter is running my camera code?
Print sys.executable from inside the running process, not from a second shell. On many Linux installs python and python3 are different installations, and each holds its own copy of every package, so an upgrade under one leaves the other untouched. Printing the SDK version with importlib.metadata in the same process exposes the mismatch in two lines.
How short an exposure cap does a moving part need?
Set it from the blur budget: exposure no longer than allowed blur in pixels times object size per pixel, divided by speed. Example: 1 mm per pixel and a part at 250 mm/s allows 1 × 1 mm ÷ 250 mm/s = 0.004 s, so a 4 ms cap holds blur near one pixel. Double the speed and the cap must halve; halve the pixel size and it must halve too.
Does updating the camera SDK also update the camera firmware?
Often, yes: embedded-camera SDKs bundle matching device firmware and flash it when the device opens, which is why host and device versions must move together. The failure described here was host-side only, so starting the app under the interpreter that held the new library fixed it without any firmware change on the camera.
Should I cap auto exposure or switch to fixed manual exposure?
Keep a capped auto exposure when scene brightness changes part to part: the loop adapts and still respects your blur budget. Switch to fixed manual exposure when scene and lighting are constant — it removes the hunting and makes brightness repeatable for threshold tools. Leave the cap set either way, so any fallback to auto cannot reintroduce blur.

