System designHow-to

Set camera exposure and white balance with Media Foundation

QueryInterface a Media Foundation capture source for IAMCameraControl and IAMVideoProcAmp, and set camera exposure and white balance with the manual flag.

LumaSpec Vision9 min read Share on LinkedIn

On this page 12 sections
  1. 01What you need
  2. 02The two interfaces and the properties each one owns
  3. 03Step 1: query the capture source itself
  4. 04Step 2: read the range before you write anything
  5. 05Step 3: set exposure with the manual flag
  6. 06Step 4: white balance — auto first, then lock
  7. 07Step 5: prove the setting stuck
  8. 08Common mistakes
  9. 09When your camera is not a Windows capture device
  10. 10Hardware for this path
  11. 11Before you ship
  12. 12Questions

Your Media Foundation capture app already opens the device, negotiates resolutions and pulls frames, and now the image needs a fixed exposure and a locked white balance — and the Media Foundation capture API has no property for either. The answer is still in the operating system: the source object Media Foundation hands you also exposes the two DirectShow control interfaces, IAMCameraControl and IAMVideoProcAmp. This walkthrough shows how to query them from your IMFMediaSource, probe what the driver actually supports, set exposure and white balance deliberately, and prove each setting stuck with a read-back and a captured frame.

What you need

  • A working capture pipeline with an IMFMediaSource* in hand — the source you activated from the device enumerator. Enumeration and streaming stay exactly as they are.
  • The Windows SDK headers: both interfaces are declared in strmif.h, which <dshow.h> pulls in. No DirectShow graph, no filters, no property page, no extra library.
  • A device whose driver implements the interfaces. UVC webcams and UVC-class USB modules normally do, on Windows 7 through Windows 11.
  • A way to dump frames, so the final check runs on pixels rather than on return codes.

The whole control path costs one QueryInterface per interface and three method calls per property: GetRange, Get, Set.

The two interfaces and the properties each one owns

IAMCameraControl owns the settings that live on the camera itself: Exposure, Focus, Iris, Pan, Tilt, Roll and Zoom. IAMVideoProcAmp owns the picture processing in the driver: brightness, contrast, hue, saturation, gamma, white balance, gain and backlight compensation. Exposure sits under CameraControl; white balance sits under VideoProcAmp — the split surprises people hunting for a white-balance call on a camera interface. The IAMCameraControl interface documentation lists its properties and flags, and the Get method reference shows the read-back call.

Both interfaces share one three-call pattern. GetRange reports minimum, maximum, step, default and which flags the property supports. Get reports the current value and flag. Set writes a value with a flag. If you know industrial cameras, this maps onto the same auto-versus-manual split as automatic iris, exposure and gain control in machine vision.

Step 1: query the capture source itself

  1. Start from the IMFMediaSource* you already hold after activating the device.
  2. Query it for both interfaces and record which ones answered.
  3. Treat E_NOINTERFACE as a capability fact, not an error: the driver exposes no such controls on this device.
// pSource: IMFMediaSource*, already activated for your capture device.
IAMCameraControl* pCam  = nullptr;   // camera-side: exposure, focus, iris, pan, tilt, roll, zoom
IAMVideoProcAmp*  pProc = nullptr;   // driver-side: white balance, gain, brightness, ...

HRESULT hrCam  = pSource->QueryInterface(IID_IAMCameraControl, (void**)&pCam);
HRESULT hrProc = pSource->QueryInterface(IID_IAMVideoProcAmp,  (void**)&pProc);

bool hasCameraControl = SUCCEEDED(hrCam);   // false: driver hides the camera-side properties
bool hasProcAmp       = SUCCEEDED(hrProc);  // false: no processing-side properties either

The two booleans are the start of your capability table; keep them beside the device handle and rebuild them after every reconnect. The interfaces live on the source object: the same query on a session, a topology node or your own wrapper class returns E_NOINTERFACE even on a fully supported camera.

Step 2: read the range before you write anything

Support is per property, not per interface. One driver exposes exposure but no white balance; another offers white balance with an auto flag only. GetRange tells you, and E_PROP_ID_UNSUPPORTED means the property is absent — a normal answer, not a failure. Probe every property you plan to surface in your UI, once at start-up:

long minValue = 0, maxValue = 0, stepSize = 0, defaultValue = 0, capsFlags = 0;

HRESULT hrExp = pCam->GetRange(CameraControl_Exposure,
                               &minValue, &maxValue, &stepSize, &defaultValue, &capsFlags);
// E_PROP_ID_UNSUPPORTED: no exposure control on this device.
// capsFlags reports which of CameraControl_Flags_Auto / CameraControl_Flags_Manual work.

Run the identical call for VideoProcAmp_WhiteBalance and store the five numbers per property; they are the only legal inputs for every later Set. The check-before-set habit is the same one that prevents invalid-value errors in GenICam — see the guide on GenICam enumeration invalid values — and its Windows-side twin, the case of a camera exposure property not found.

Step 3: set exposure with the manual flag

The unit documented for CameraControl_Exposure is log base 2 seconds: a value v means an exposure time of 2^v seconds. stepSize is commonly 1, so every step doubles or halves the exposure. Set the manual flag explicitly — without it the driver keeps its auto exposure running and overrides you:

long exposureValue = defaultValue;    // start where GetRange said the driver starts
HRESULT hrSet = pCam->Set(CameraControl_Exposure, exposureValue, CameraControl_Flags_Manual);
t = 2^v seconds (v = CameraControl_Exposure value) frame_period = 1000 / target_fps ms frame rate holds while t < frame_period

Worked example, with an illustrative span a typical webcam reports: min -13, max 0, step 1, default -6, both flags supported. At a 30 fps target the frame period is 1000 ms / 30 = 33.3 ms.

value v exposure t = 2^v light vs -6 verdict at 30 fps
-6 15.6 ms (2^-6 s = 1/64 s) ×1 fits: 33.3 − 15.6 = 17.7 ms spare
-5 31.25 ms ×2 fits, barely: 33.3 − 31.25 = 2.1 ms spare
-4 62.5 ms ×4 fails: 62.5 ms > 33.3 ms, frame rate falls to about 16 fps

The decision at a 30 fps target: stop at -5 and add light or gain beyond that, because -4 costs half the frame rate. Keep the exposure on a mains half-cycle — 10 ms at 50 Hz, 8.33 ms at 60 Hz — or mains-driven LED and fluorescent lighting will band every frame.

Step 4: white balance — auto first, then lock

The unit documented for VideoProcAmp_WhiteBalance is Kelvin; an illustrative module reports min 2800, max 6500, step 10, both flags. A Set with the auto flag makes the driver pick the value and ignore yours. The reliable route to a locked number is auto, settle, read, re-set with the manual flag:

// 1. let the driver find its own value on a stable scene
HRESULT hrAuto = pProc->Set(VideoProcAmp_WhiteBalance, 0, VideoProcAmp_Flags_Auto);

// 2. after the scene settles for a second or two, read the result back
long wbNow = 0, wbFlags = 0;
HRESULT hrGet = pProc->Get(VideoProcAmp_WhiteBalance, &wbNow, &wbFlags);

// 3. lock that exact value: manual flag, value from Get, not from your head
HRESULT hrLock = SUCCEEDED(hrGet)
    ? pProc->Set(VideoProcAmp_WhiteBalance, wbNow, VideoProcAmp_Flags_Manual)
    : hrGet;

With one camera this is a nicety; with four cameras on one reject line it is the difference between consistent and drifting rejects. Lock each unit against the same neutral target, then run the multi-camera white balance consistency test before trusting any of them.

Step 5: prove the setting stuck

An S_OK from Set means the driver accepted the call, not that the pixels changed. Close the loop twice. First, read back:

long valueNow = 0, flagNow = 0;
bool exposureStuck = SUCCEEDED(pCam->Get(CameraControl_Exposure, &valueNow, &flagNow))
                     && (flagNow & CameraControl_Flags_Manual) != 0
                     && (valueNow == exposureValue);

Second, capture. Two frames one exposure step apart should differ by close to a factor of 2 in mean brightness over a flat patch; a locked white balance should hold a neutral patch near gray. Many drivers also report the applied exposure in each frame's metadata, which turns the once-per-change check into a per-frame assertion — the workflow for reading exposure time from frame metadata covers it. If a driver accepts Set but ignores it while streaming, stop the source, set, and restart; the same initialization order as setting manual exposure before streaming applies.

Common mistakes

  • Set before GetRange. Out-of-range values fail outright or clamp silently; the range read is what makes Set safe to call.
  • Value without a flag. Passing 0 for the flag leaves the mode undefined and auto adjustment wins; name the flag on every call.
  • Carrying one device's numbers to another. The next model returns a different span and step; re-probe per device, the same discipline as setting and reading camera parameters inside a capture loop.
  • Exposure against a fixed frame rate. t must stay under the frame period; the interaction with triggering is laid out in trigger call versus exposure time.
  • Querying the wrong object. Session and topology queries fail with E_NOINTERFACE even when the camera supports everything; query the source.
  • Forgetting the reconnect. A re-added device is a new source object: fresh QueryInterface, fresh GetRange, fresh capability table.

When your camera is not a Windows capture device

GigE Vision, USB3 Vision and CoaXPress cameras never register as Windows capture sources, so Media Foundation never enumerates them and none of the code above applies. They are driven through their SDK against the GenICam feature map: exposure is a floating-point time in microseconds, white balance is per-channel gain or a one-shot auto, and each feature carries its own minimum, maximum and increment that the SDK reads for you. The setting-exposure-time-through-GenICam-in-Python walkthrough and the runtime camera parameter control guide cover that path, and industrial camera image quality settings explain how exposure, gain and white balance trade against each other once you get there.

The decision by device class: a UVC webcam or module follows this walkthrough; an industrial camera follows GenICam from the start. If the camera choice itself is still open, the area scan cameras category and the camera finder are the starting points, and the camera configurator pairs the sensor with lens and light.

Hardware for this path

The Media Foundation route suits compact, build-in capture hardware: a board-level camera module on your own carrier, inside your own housing, enumerating over USB3. A USB3 module that implements the USB video class appears in Media Foundation like any webcam, and everything above applies unchanged; the module's data sheet states its USB device class, and that one line decides the path. The catalog's compact options live in the board-level cameras category:

Board-level cameras3.1 MP USB3 board-level camera, IMX252, 1/1.8" global shutter, 120 fps (LS-BCM-0003)LS-BCM-0003
Sensor format
1/1.8"
Lens mount
C-mount
$690.00View part
Board-level camerasHikrobot MV-CB060-10UM-B 6 MP Board Level Camera, USB3, IMX178, Mono, Bare Board, V4MV-CB060-10UM-B
Sensor format
1/1.8"
Interface
USB3
$239.00View part

The LS-BCM-0003 pairs a 1/1.8" sensor with 3.1 MP global shutter at 120 fps over USB3 and a C-mount front. The MV-CB060-10UM-B ships as a bare board: 6 MP on an IMX178 mono sensor, USB3. Either mounts where a consumer webcam cannot, which is usually the point of building the capture end yourself.

Before you ship

  • Capability table per device: one row per property with min, max, step, default and flags from GetRange.
  • Every Set paired with a Get read-back and a flag comparison, both logged.
  • Exposure checked against the frame period and against a mains half-cycle: 10 ms at 50 Hz, 8.33 ms at 60 Hz.
  • White balance locked from a settled auto value and logged with each saved frame.
  • Two-frame exposure test on record: one step apart, mean brightness ratio close to 2.
  • A reconnect handler that re-queries both interfaces and rebuilds the table.

When the hardware side of the project is still open, tell us the scene, the part and the interface budget through the contact page — expert support comes with every order — or start from the camera finder and the surrounding guides under the systems topic.

Questions

Do these DirectShow interfaces still work on Windows 10 and 11?

Yes, for capture sources whose drivers expose them. Microsoft lists IAMCameraControl and IAMVideoProcAmp as superseded by the newer media stack, but that stack adds no per-camera exposure or white-balance property, so the QueryInterface route remains the working path. Keep the capability probe in place: a Windows or driver update can change what a device reports, and the probe turns that into a logged fact instead of a mystery.

Why does GetRange return E_PROP_ID_UNSUPPORTED for white balance?

Drivers implement only the properties their hardware and firmware expose; many modules ship without white balance, iris or zoom. Treat E_PROP_ID_UNSUPPORTED as a capability report rather than a failure: probe each property once at start-up, store which ones answered, and hide the matching controls in your UI. A missing property is normal. A Set that fails on a property GetRange accepted is the real bug.

What unit is the exposure value in?

Log base 2 seconds per the DirectShow documentation: a value of -6 means 2^-6 seconds, about 15.6 ms, and each step of 1 doubles or halves the exposure. Read the span with GetRange anyway, because some drivers report their own scaled values. Confirm once by capturing two frames one step apart: the mean brightness ratio should come out close to 2.

Can I control a GigE or USB3 Vision industrial camera this way?

No. GigE Vision, USB3 Vision and CoaXPress cameras do not register as Windows capture sources, so Media Foundation never enumerates them. They are driven through their GenICam feature map, where exposure is a floating-point time in microseconds and white balance is per-channel gain or a one-shot auto. If the application must stay in Media Foundation, the hardware choice is a UVC-class module, not an industrial camera.

How do I stop auto white balance from drifting during a run?

Run auto on a stable scene, read the settled value with Get, then Set it back with the manual flag. Add two things that are easy to miss: re-run the lock in your device-lost handler, because a re-enumerated source starts fresh, and log the locked value with every saved frame so a disputed color measurement traces back to the exact setting that produced it.