Preview shows a bright, well-exposed picture; you call getsnapshot and the saved frame comes back nearly black. The camera is fine — the grab ran too early. A preview window has been running for seconds, so the driver's auto-exposure has converged and the source properties you set are already applied. A single getsnapshot on a freshly configured object starts the pipeline cold and hands you one of the first frames, taken while the sensor is still climbing from its power-up exposure and while a property set one line earlier is still in flight.
This guide is for anyone grabbing single frames from a webcam-class USB camera in code. You will rank the likely causes, apply a set-read-back-settle-grab sequence, budget the settle time against your frame rate, and recognize the cases where the dark frame is not a timing problem at all.
Why the preview is bright and the snapshot is dark
preview() runs the pipeline continuously. The driver's auto-exposure measures every frame and steers exposure and gain toward a mid-gray target, which takes several frame periods in good light and more in a dim room. The preview also gives the driver time to apply the source properties you set before it.
getsnapshot() does no such waiting. It starts acquisition if the object is not running, takes one frame and returns. Call it immediately after videoinput and set, and you are grabbing frame one or two of a pipeline that has just switched on: the brightness request may not have reached the sensor yet, and auto-exposure still sits near its power-up value.
Both processes are clocked in frame periods, not in code time. set() returns when the driver accepts the request, not when the sensor's exposure actually changes, and an auto-exposure loop acts once per frame. Your grab races both and loses. That is also why the fault hides in demos: a script that opens preview before snapping always grabs a warm pipeline.
The same cold-start signature appears on machine-vision cameras. First camera frame dark: exposure settling or light timing? separates settling from a genuine light problem with the same first-frame test.
Rank the causes before you change code
Work down the list; each cause comes with a test that takes seconds.
- Auto-exposure not settled — the most common cause. Test: grab, wait one second, grab again. A bright second frame confirms settling.
- Property applied asynchronously — set() queues the brightness request and returns at once; the sensor's exposure changes a few frames later. Test: read the value back from the same source object, as the fix below shows.
- Wrong source object or wrong device — Brightness lives on the source, not on the video object, and device 2 is whichever camera the driver enumerates second. Test: preview once and confirm it shows the scene you think; set properties on getselectedsource(cam).
- A control above your code — Windows 11 keeps per-camera settings outside your application, and camera utilities can hold exposure at the driver level. Test: if the readback shows your value took and every frame stays dark, look there.
- Genuine lack of light — test by aiming the camera at a lit wall. If it still snaps dark, the scene is not the problem; the exposure path is.
A display can lie during these tests. A grayscale stream pushed through a color path, or an image shown through an unsuitable colormap, renders darker than the data is. Judge frames by mean(double(frame(:))), never by the rendered window.
The fix: set, read back, settle, then grab
The sequence in five steps:
- Open with an explicit format. videoinput('winvideo', 2, 'RGB24_640x480') pins the mode; an unspecified format lets the driver pick a default that may not be the one your preview used.
- Get the source that owns the property. src = getselectedsource(cam).
- Set, then read back. set(src, 'Brightness', 240), then applied = get(src, 'Brightness'). applied must return 240; anything else means the driver refused or clamped the value.
- Start the camera and settle it. start(cam), then discard at least 10 frames — better, loop until the mean gray level is stable.
- Keep a settled frame, then stop. The last frame the settle loop retrieved is the one you save.
cam = videoinput('winvideo', 2, 'RGB24_640x480'); % explicit mode; device 2 as in the failing case
src = getselectedsource(cam); % the source object owns Brightness
set(src, 'Brightness', 240);
applied = get(src, 'Brightness'); % double readback, must return 240
start(cam); % run the pipeline continuously
means = zeros(1, 45); % double, mean gray per frame, 45-frame cap
frame = []; % uint8 HxWx3, the frame we keep
settled = false; % logical
for k = 1:45
frame = getdata(cam, 1); % blocks until one frame arrives
means(k) = mean(double(frame(:))); % 0-255
if k >= 10 && abs(means(k) - means(k-1)) < 2 % settle criterion: change under 2 counts
settled = true;
break
end
end
stop(cam);
% Test: applied == 240 proves the driver took the value; settled == true proves
% means(k) sits within 2 counts of means(k-1), so frame is settled by construction.
fprintf('applied = %g, settled = %g, frames used = %d, mean = %.1f\n', ...
applied, settled, k, means(k));
Read the two flags as the diagnosis. If applied is not 240, the driver refused or clamped: preview briefly with the value set, read it again, and if it still disagrees, set the property on the other source or drop it and control the light. If settled stays false through the 45-frame cap, auto-exposure is still climbing, which means a dim scene — go to manual exposure or add light rather than waiting longer. If you prefer to keep getsnapshot, hold the pipeline open across the settle budget and call it after; getdata inside the loop is cleaner because the kept frame is, by construction, a settled frame.
Settle budget for a 30 fps USB camera at 640 × 480
The inputs, and where each comes from:
- Frame rate 30 fps — the typical rate of a 640 × 480 RGB24 USB webcam mode. The format string comes from the failing case; the rate is this example's typical input.
- Minimum settle 10 frames — the conservative default: an auto-exposure loop acts once per frame and needs several frame periods to converge even in good light.
- Dim-room settle 30 frames — low light forces exposure to climb over more frames.
- Stability threshold under 2 counts of 255 — the criterion in the loop above.
- Hard cap 45 frames — the timeout that keeps the loop from hanging.
The rule in words: settle time in seconds equals frames discarded divided by frame rate in frames per second.
The arithmetic, written out:
- One frame period: 1 / 30 = 0.033 s, about 33 ms.
- Minimum settle: 10 / 30 = 0.33 s.
- Typical exit in normal light: 20 / 30 = 0.67 s.
- Dim room: 30 / 30 = 1.0 s.
- Cap: 45 / 30 = 1.5 s.
The result is a settle budget of 0.33-1.5 s, covering bright scenes through dim rooms. The decision: run the settle loop before every kept grab after opening the camera or changing a source property, and treat 1.5 s as the threshold where you stop trusting auto-exposure — past the cap, set exposure manually or add light. At 15 fps, a common alternative mode, every figure doubles: the default costs 0.67 s and the cap 3.0 s.
When the dark snapshot is not a timing problem
If the mean stays near zero after a full settle, sequencing is not your fault. Check these in order:
- Units. Some drivers expose an exposure control in normalized or logarithmic units rather than microseconds; query the range before setting a value and never treat a slider number as a time. Querying before setting transfers directly to industrial cameras: Auto exposure limit units in microseconds: query before you set.
- Device identity. Camera 2 is enumeration order, not a label on the hardware; one preview tells you which camera you are commanding.
- A control above the driver. Operating-system camera settings and vendor utilities override application requests; the tell is a correct readback with dark frames.
- Lighting. Under auto-exposure in normal room light, a settled frame lands near mid-gray, typically a mean of 100-150 counts. Far below that band with a correct readback means the scene needs light, not settings.
Prevention is four habits: read back every property after setting it; open with an explicit format; settle at least 10 frames after any open or exposure change; assert the mean gray band before a frame is accepted. Log frames-used from the settle loop as well — a rising trend across runs is a scene getting dimmer, caught before it fails a production run.
A grey image at runtime is a different fault with a different fix: Camera works in setup but grey image at runtime covers the stream-level causes.
The same rule on a machine-vision camera
Outgrow the webcam driver and the ordering rule gets stricter and better documented. GenICam cameras on GigE Vision and USB3 Vision expose exposure as a camera feature with real units — ExposureTime in microseconds on most models — set before acquisition, read back, and applied at a frame boundary. The sequence becomes: set exposure, read it back, start the camera, discard the first frames, then trust the image. Set manual exposure before streaming: initialization order walks that order, and Camera trigger mode source readback mismatches: find the real value shows why the readback step matters when a value reads back as something other than what you set.
Two follow-on reads for the same pipeline: Trigger call versus exposure time: understand capture timing separates the moment you command a frame from the moment the sensor is exposed, and Runtime camera parameter control: exposure and white balance live adjusts those features without stopping the stream — the preview equivalent, in real units.
The area scan cameras category is where these cameras live; the camera finder filters by interface and shutter, and the configurator walks camera, lens and light in the same order as this fix.
The LS-CMR-0166 prints 30 fps at its full 4000 × 3000 resolution, with a 12 MP 1/1.7" rolling-shutter sensor. At 30 fps, the ten-frame default settle costs 0.33 s, exactly the arithmetic above.
- Sensor format
- 1/1.7"
- Interface
- USB3
- Resolution
- 12 MP
- Shutter
- Rolling
For stations already wired for GigE, the MV-CU020-90GC is a compact option: a 2.5 MP color camera with a 1/1.7" sensor, C-mount and a global shutter.
- Sensor format
- 1/1.7"
- Lens mount
- C-mount
- Interface
- GigE
- Resolution
- Up to 3 MP
For webcam-style compactness with real camera features, board-level cameras are the drop-in path. And if a manual exposure comes out darker than the auto-exposure it replaced, Manual exposure darker than auto exposure: why AE beats your gain explains the gain trade you are missing.
Pre-flight checks
Run down this list before a frame is trusted:
- Readback equals the value set, checked on the same source object.
- Explicit format at open, and the preview and the grab run the same mode.
- At least 10 frames discarded after any open or exposure change; the 45-frame cap is the trigger to switch to manual exposure.
- Mean gray inside the expected band before a frame is accepted.
- Frames-used logged per grab; a rising trend flags a dimming scene.
- A regression test that grabs twice one second apart and fails when the first frame's mean sits more than 20 counts below the second's — the 20-count margin is a declared test criterion, tunable per scene.
If the camera in this sequence is a webcam you have outgrown, start the configurator at the camera step, or contact us with your scene and frame rate and we will help you choose the camera, lens and light together. The systems guides collect the acquisition-timing articles in one place.
Questions
How many frames should I throw away after changing brightness or exposure?
Start with 10 frames under normal room light, which costs 0.33 s at 30 fps and 0.67 s at 15 fps. In a dim room the auto-exposure has more exposure changes to walk through, so budget 30 frames, 1.0 s at 30 fps. The reliable method does not count frames at all: keep grabbing until the mean gray level moves less than 2 counts between consecutive frames, with a 45-frame cap and a manual-exposure fallback.
Does Brightness 240 set the exposure to a fixed time?
No. Brightness on a webcam-class source is a driver-side request, and each driver maps it differently onto sensor exposure, gain or processing. The value 240 is not microseconds or any other unit of exposure time. Treat it as a setpoint: read it back to confirm the driver accepted it, then judge the result by the frame's mean gray level rather than by the number you set.
Why is the second getsnapshot bright when the first one is dark?
By the second call the pipeline has been running for a while: the driver has applied your properties and the auto-exposure has converged, so the frame comes out right. That asymmetry, a cold first grab and warm later grabs, is the signature of a settling problem. It also explains why a demo script that previews before snapping never shows the fault.
How do I set a real exposure time instead of a brightness slider?
On a GenICam camera the exposure feature takes microseconds, set before acquisition and read back after. On webcam-class sources some drivers expose exposure in normalized or logarithmic units; query the range rather than assuming one. One caution: a manual exposure set shorter than the scene needs comes out darker than auto-exposure, because AE raises gain as well as exposure time to hit its target.
Can the operating system darken the camera even when my code is right?
Yes. Windows 11 keeps per-camera settings that sit above anything an application sets, and camera vendor utilities can hold exposure at the driver level. The tell is a readback that shows your value took effect while every frame stays dark. Check the operating system's camera settings and any vendor camera utility before rewriting your acquisition code.

