setManualWhiteBalance() produces green cast on ZED X One 4K

Environment:

  • Camera: ZED X One 4K (GMSL2)
  • Platform: Jetson Orin NX
  • JetPack: 6.2.1 (L4T R36.4.7)
  • ZED Link driver: stereolabs-zedlink-duo 1.3.0
  • Library: zedx-one-capture (commit 7585028, Argus Capture Version 1.1.0)
  • OS: Ubuntu 22.04

We’ve been integrating the ZED X One 4K into our ROS2 camera pipeline and found that setManualWhiteBalance() produces a strong green color cast at every color temperature (2800K through 8000K). Auto white balance setAutomaticWhiteBalance(1) and the preset AWB modes (incandescent, daylight, etc.) all work correctly — only the manual color temperature path is broken.

Reproduction:

  1. Open camera with ArgusBayerCapture::openCamera()
  2. Call setManualWhiteBalance(5000) (or any value 2800-12000)
  3. Capture frames — all have a strong green tint
  4. Call setAutomaticWhiteBalance(1) — colors are correct

We wrote a sweep test that captures images at every color temp and reads back the actual RGGB gains from ICaptureMetadata::getAwbGains() This let us compare what the auto modes produce vs what setManualWhiteBalance()
computes.

Auto WB gains (correct — good image):
R=1.776 Ge=1.0 Go=1.0 B=2.095

Manual 5000K gains from stock code (green image):
R=1.672 Ge=1.190 Go=1.190 B=1.169

The auto mode always has green=1.0 with R and B above it. The manual computation has all three channels elevated,
with green not pinned to 1.0.

Root cause analysis of estimageRGGBGainFromColorTemperature_v2():

We traced through the code in ArgusBayerCapture.cpp and found three issues:

  1. Stray yD overwrite (line ~2027)

The CIE Daylight yD is computed correctly, then unconditionally overwritten by a Blackbody 10-degree observer fit
that should be commented out (the corresponding xD line above it IS commented):

yD = -3 \* xD \* xD + 2.87 \* xD - 0.275;  // CIE Daylight (correct)

// Fit for Blackbody using CIE standard observer function at 10 degrees
//xD = -1.98883e9/(T*T*T) + 1.45155e6/(T*T) + 0.364774e3/T + 0.231136;
yD = -2.35563*xD*xD + 2.39688*xD - 0.196035;  // ← Overwrites yD!
  1. Gains are not inverted

The function computes the illuminant’s RGB color via CIE chromaticity → XYZ → RGB, then uses those values to derive
Bayer gains. But white balance correction gains should be the inverse of the illuminant color — you want to boost
channels that are weak under the given illuminant, not channels that are strong. The original scaling pipeline
(*255/127, (x-1.0)/factor) produces gains proportional to the illuminant, which is backwards.

  1. Green channel Bayer compensation missing

In an RGGB Bayer pattern, there are 2 green pixels per 4. The per-pixel green gain needs to be halved relative to R
and B to avoid over-representing green in the demosaiced output. The auto AWB modes handle this internally (green is
always 1.0 in the readback), but the manual computation doesn’t account for it.

Our fix:

We replaced the gain scaling pipeline with:

// WB correction: inverse of illuminant, green halved for RGGB Bayer
r = static_cast(1.0 / RGB\[0\]);
g_even = static_cast(0.5 / RGB\[1\]);
g_odd = static_cast(0.5 / RGB\[1\]);
b = static_cast(1.0 / RGB\[2\]);

// Normalize so minimum gain = 1.0
float min_gain = std::min({r, g_even, b});
r /= min_gain;
g_even /= min_gain;
g_odd /= min_gain;
b /= min_gain;

Results after fix (manual 5500K in ~5500K room lighting):
R=1.789 Ge=1.0 Go=1.0 B=2.402

Compare to auto: R=1.776, Ge=1.0, Go=1.0, B=2.095 — the direction and magnitude are now correct. Images look proper
with natural colors across the sweep.

I’m no camera expert, so I’d like validation that this is a proper fix, and not misguided AI gains tuning, but if this passes the test I can open a pr to my open issue with my fixes from which includes a standalone sweep tool

Hi,

Thanks for the very detailed report.

If possible, could you open a PR so we can review it on our side?

Best regards