4005 字
20 分钟
白平衡是怎么校正光源颜色的?从 Bayer 域增益到 RGB 域增益

同一张白纸搬到正午阳光、钨丝灯和蓝色遮阳棚下,传感器记录到的红、绿、蓝比例会完全不同。人眼会不自觉地适应光源,仍然觉得它是白纸;相机得到的 RAW 数字却不会替我们完成这种适应。

白平衡(White Balance,WB)要做的,就是根据一组已知的通道增益,重新调整红、绿、蓝的比例,让选定的中性物体回到中性。

这听起来只是“乘三个数字”,但落到 RAW 管线里马上会出现几个工程问题:增益是在 Bayer 马赛克上乘,还是等 Debayer 之后再乘?黑电平应该放在哪一步?蓝通道乘了两倍以后,高光和噪声怎么办?

本文只讨论已知白平衡增益怎样应用。增益可以来自相机元数据、人工设置或灰卡标定,但我们不讨论灰世界、白点搜索、色温估计和学习模型等 AWB(自动白平衡)算法。

白色为什么不再是白色#

传感器记录的值可以粗略理解为三者共同作用的结果:光源发出了什么光,物体反射了什么光,传感器的红、绿、蓝滤光片又分别响应了多少。

因此,“物体本来是灰色”并不意味着相机一定得到相等的三个数字。暖光下的一块中性卡可能测成:

R = 0.74, G = 0.52, B = 0.30

如果希望它回到以绿色为基准的中性值 0.52,一组直观的增益是:

R gain = 0.52 / 0.74 ≈ 0.70
G gain = 0.52 / 0.52 = 1.00
B gain = 0.52 / 0.30 ≈ 1.73

应用以后,三个通道都接近 0.52。这不是在把所有物体变灰,而是在用同一组比例校正整幅图像;红色物体仍然比灰卡更红,蓝色物体仍然更蓝。

手动白平衡实验室

这里不猜光源:增益是已知输入。选一个示意光源,再故意拨动三个增益,观察中性卡怎样离开灰轴。

校正后场景显示预览
中性卡
暖色物体
冷色物体
光源下测得的中性卡0.740 · 0.520 · 0.300
应用增益后的中性卡0.518 · 0.520 · 0.519
示意光源(不是 AWB 预测)

当前对角矩阵为 diag(0.70, 1.00, 1.73)。这里只负责应用增益,不包含任何自动估计。

组件里的“暖光”“冷光”只是预先给好的教学数据,不是在根据画面自动判断光源。你可以故意把某个增益拉偏,观察中性卡怎样立刻离开灰轴。

三个增益就是一个对角矩阵#

在线性 RGB 中,白平衡可以写成:

[RGB]=[gR000gG000gB][RGB]\begin{bmatrix} R'\\G'\\B' \end{bmatrix} = \begin{bmatrix} g_R&0&0\\ 0&g_G&0\\ 0&0&g_B \end{bmatrix} \begin{bmatrix} R\\G\\B \end{bmatrix}

展开以后就是三次独立乘法:

R=gRR,G=gGG,B=gBBR'=g_RR,\qquad G'=g_GG,\qquad B'=g_BB

对角线以外全是零,表示红色输出不需要读取绿色与蓝色输入。后面讲到 CCM 时,一般的 3×33\times3 矩阵会允许三个通道互相混合;WB 则是其中最简单的一类。

工程中经常把绿色增益归一化成 1.0,因为绿色样本多,也适合作为相对比例的基准。不过这只是表示方式,不是物理定律。把所有增益同时乘上一个常数不会改变白平衡后的色彩比例,却会改变整体亮度和高光余量。

Gain 和 neutral value 不要用反#

有的元数据给出的是可直接相乘的 gain;有的字段表达的是中性物体在相机空间中的比例。例如一个 neutral value 是:

(1.43, 1.00, 0.58)

对应的相对增益往往要取倒数再归一化,而不是直接把这三个数字乘到图像上:

neutral = (1.43, 1.00, 0.58)
gains = tuple(1.0 / value for value in neutral)
green_scale = gains[1]
gains = tuple(value / green_scale for value in gains)
print(gains) # 约为 (0.70, 1.00, 1.72)

字段的确切含义要以文件格式和相机元数据规范为准。“三个看起来很像增益的数字”并不能保证可以直接使用。

白平衡必须在线性光域进行#

如果一个传感器值从 0.2 增加到 0.4 代表接收到的光大约翻倍,那么它处在线性光域。WB 的通道比例模型依赖这种线性关系。

普通 JPEG 或屏幕上的 sRGB 通常已经经过非线性编码。在编码值上直接乘白平衡增益,效果不等价于:

  1. 把编码值解码回线性光;
  2. 在线性光中应用通道增益;
  3. 再编码回输出格式。

所以,在 RAW ISP 中,WB 通常位于 Gamma 或输出传递函数之前。对于已经编码的图片,必须先弄清数据的传递函数,不能仅凭数组类型是 float 就认定它是线性的。

黑电平要先减,再应用增益#

RAW 的“零光照”通常不对应数字零。传感器与模拟前端会保留一个黑电平偏置,正确的通道处理应当近似写成:

y=(rawblack)×gainy=(raw-black)\times gain

如果先乘增益再减原来的黑电平:

ywrong=raw×gainblacky_{wrong}=raw\times gain-black

两者相差:

ywrongy=black×(gain1)y_{wrong}-y=black\times(gain-1)

当红、绿、蓝使用不同增益时,残留偏置也不同。错误在亮部可能不显眼,却会让接近黑色的区域带上颜色。

先减黑电平,还是先乘增益?

拖动暗部 RAW 值。顺序错误会把本应被减掉的传感器偏置也留进结果,增益越大,暗部偏色越明显。

正确:(0.1600.060) × 1.80 = 0.180错误:0.160 × 1.800.060 = 0.228
正确结果0.1800
错误结果0.2280
残留偏置0.0480

两式之差是 black × (gain − 1)。不同通道使用不同增益时,原本中性的黑位就会被染色。

生产管线还可能给每个通道、每一行甚至不同模拟增益设置不同黑电平。本文用单一标量说明顺序,但代码里不能想当然地把所有像素都减同一个常数。

Bayer 域白平衡:在马赛克上乘增益#

Bayer RAW 的每个感光位置只保存一个通道。以 RGGB 为例,一个 2×22\times2 单元是:

R G
G B

Bayer 域 WB 不需要先凑出完整 RGB。程序只要根据像素坐标判断当前位置属于哪种滤光片,再乘对应的增益:

import numpy as np
def apply_wb_rggb(raw, red_gain, green_gain, blue_gain):
"""对已减黑电平的 RGGB RAW 应用白平衡。"""
output = raw.astype(np.float32).copy()
output[0::2, 0::2] *= red_gain
output[0::2, 1::2] *= green_gain
output[1::2, 0::2] *= green_gain
output[1::2, 1::2] *= blue_gain
return output

两个绿色位置通常使用同一个增益,但它们不是可以随意互换的“空位”。某些传感器或校准流程可能分别处理两个绿色平面,以修正通道不平衡;若元数据明确提供了不同参数,就要按规范使用。

更通用的写法是先为特定 CFA Pattern 建立三个 mask:

def apply_wb_bayer(raw, red_mask, green_mask, blue_mask, gains):
corrected = raw.astype(np.float32).copy()
corrected[red_mask] *= gains[0]
corrected[green_mask] *= gains[1]
corrected[blue_mask] *= gains[2]
return corrected

这能把“像素属于哪个通道”与“怎样应用增益”分开,也更容易支持 RGGB、BGGR、GRBG 和 GBRG。

RGB 域白平衡:Debayer 后再乘#

Debayer 之后,每个输出像素都有 R、G、B 三个分量。此时使用数组广播即可:

def apply_wb_rgb(image, gains):
"""image 的形状为 (height, width, 3),并且仍在线性光域。"""
image = image.astype(np.float32)
gains = np.asarray(gains, dtype=np.float32)
return image * gains[None, None, :]

这种实现更直观,也适合输入本来就是线性 RGB 的处理流程。代价是数据量已经从每个位置一个数变成三个数,而且 Debayer 在运行时看到的是尚未校正光源比例的样本。

Bayer 域和 RGB 域一定等价吗#

先给结论:在理想的、线性的、逐通道 Debayer 中,两种顺序可以严格等价;真实管线里不能默认它们永远等价。

假设 DD 是一个只在各自通道内部做固定线性加权的 Debayer,WW 是对角白平衡矩阵。对红色平面来说,先把所有红样本乘 gRg_R 再加权,与先加权再把结果乘 gRg_R 相同:

D(Wx)=W(Dx)D(Wx)=W(Dx)

但这个等式依赖几个条件:

  • Debayer 的权重不受输入数值影响;
  • 不使用其他通道修正当前通道;
  • 中途没有裁剪、量化或整数溢出;
  • Bayer Pattern、边界处理与浮点精度完全一致。

边缘感知算法可能根据局部梯度选择方向;高质量算法也可能利用通道差或色度一致性。当增益改变了用于判断的数值,插值路径就可能变化。跨通道矩阵与对角增益通常也不满足交换律。

白平衡应该放在 Debayer 前还是后?

两条路径使用同一组 RAW 样本和增益。逐通道线性插值可以与对角增益交换;跨通道步骤会让顺序产生差异。

Bayer WB → Debayer
Debayer → RGB WB
最大通道差0.00000
插值模型逐通道
结论可交换
教学版 Debayer

“跨通道修正”是用来说明非交换性的简化模型,不代表某个特定商业 Debayer。真实算法还可能包含方向选择、降噪、裁剪和整数舍入。

在组件中选择“逐通道线性”,两条路径的最大误差会接近浮点计算误差;切换“跨通道修正”,顺序差异就会出现。这个简化模型不是在复刻某个商业算法,而是在隔离导致非交换的关键条件。

为什么许多 RAW 管线在 Bayer 域做 WB#

Bayer 域 WB 有几个直接优势:

  • 每个感光位置只处理一个数,内存与带宽较小;
  • 增益可以在较高位深或浮点中尽早应用;
  • Debayer 得到的输入已经具有较合理的通道比例;
  • 后续算法不必一直携带相机光源下的强烈偏色。

但“更早”并不意味着可以不顾顺序。至少要先正确完成黑电平处理;坏点修复、镜头阴影校正、降噪与 WB 的相对位置也要结合算法设计。比如先放大蓝通道会同时放大其噪声,可能影响依赖梯度阈值的坏点或降噪判断。

白平衡会同时放大噪声#

假设蓝通道的信号由 BB 变成:

B=gBBB'=g_BB

叠加在蓝通道上的噪声也会近似乘上 gBg_B。若蓝通道本来信号弱、需要较大的增益,校正后的暗部蓝噪声通常会更明显。

这不是白平衡“制造了错误颜色”,而是它把原本就存在的微小信号和噪声一起放大了。WB 不能恢复传感器没有采集到的信息;后续降噪只能在保留细节与抑制波动之间取舍。

高光为什么会在 WB 后变色#

未校正 RAW 中的三个通道都小于 white level,不代表乘增益后仍然安全。假设一个高光像素是:

RAW: (0.72, 0.68, 0.58)
Gain: (1.20, 1.00, 1.90)
Result:(0.86, 0.68, 1.10)

蓝通道超过了 1.0。如果立刻裁剪,结果变成 (0.86, 0.68, 1.00),蓝通道超过上限的程度已经丢失。另一个原本应得到 1.40 的像素也会被压成同一个 1.00

增益同时放大信号、噪声和高光

蓝通道需要更大增益时,暗部噪声与高光峰值都会一起上升。切换立即裁剪,看看哪部分信息无法再回来。

R
0.78
G
0.62
B
1.18
越界通道1 / 3
R 噪声幅度0.031
B 噪声幅度0.048

条形轨道以 1.0 为显示上限。浮点模式仍保留真实数值;裁剪模式把越界值压到 1.0,之后再降低曝光也无法辨认原来的差异。

中间结果

浮点管线可以暂时保留大于 1.0 的中间值,再由曝光归一化、tone mapping 或高光重建决定怎样压回输出范围。但如果传感器采样时就已经达到物理饱和,后面保留浮点也不会凭空恢复细节。

归一化最大增益的代价#

为了避免增益超过 1.0,有时会把整组 gain 除以其中最大值。例如:

原增益: (1.20, 1.00, 1.90)
归一化: (0.63, 0.53, 1.00)

三通道比例没有改变,所以白平衡色彩方向保持一致;但整体图像变暗了。随后如果再统一提亮,能否避免裁剪取决于管线保留了多少 headroom,而不是归一化这个动作本身创造了额外动态范围。

白平衡与曝光不是一件事#

曝光或统一数字增益近似对所有通道乘同一个数:

(R,G,B)k(R,G,B)(R,G,B)\longrightarrow k(R,G,B)

白平衡关心的是三个通道之间的比例:

(R,G,B)(gRR,gGG,gBB)(R,G,B)\longrightarrow(g_RR,g_GG,g_BB)

两者都会改变画面亮度,但目的不同。一个稳健的实现应明确:WB 阶段负责相对比例,曝光或 tone mapping 阶段负责整体明暗。否则同一份增益在两套软件里可能得到不同亮度,即使它们的色度方向一致。

白平衡与 CCM 能合并吗#

WB 是对角矩阵 WW,CCM 是一般矩阵 MM。如果二者都在线性域、其间没有非线性处理,那么可以合并:

RGBout=M(WRGBin)=(MW)RGBinRGB_{out}=M(W\,RGB_{in})=(MW)RGB_{in}

合并能少做一次矩阵运算,但工程中仍常保留独立参数:

  • WB 会随拍摄光源或相机设置变化;
  • CCM 可能按光源选择或在两套标定矩阵间插值;
  • 分开记录更容易检查错误来自增益还是通道混合;
  • Bayer 域 WB 位于 Debayer 前,本来就无法简单与后面的 RGB CCM 合成一次逐像素矩阵。

另外,矩阵乘法的顺序不能颠倒。一般来说 MWWMMW\ne WM;把 WB 放到 CCM 后面再使用原来的参数,结果不会相同。

一个较完整的 Bayer 域实现#

下面把黑电平、浮点转换、白平衡和可选归一化放在一起。代码仍是教学版,不包含坏点、镜头阴影和饱和像素重建:

import numpy as np
def prepare_rggb(
raw,
black_level,
white_level,
gains,
normalize_gains=False,
):
"""准备 RGGB RAW,不在函数末尾裁剪高光。"""
raw = raw.astype(np.float32)
signal_range = float(white_level - black_level)
if signal_range <= 0:
raise ValueError("white_level 必须大于 black_level")
linear = (raw - black_level) / signal_range
linear = np.maximum(linear, 0.0)
gains = np.asarray(gains, dtype=np.float32)
14 collapsed lines
if gains.shape != (3,):
raise ValueError("gains 必须是 R、G、B 三个数")
if np.any(gains <= 0):
raise ValueError("白平衡增益必须大于 0")
if normalize_gains:
gains = gains / np.max(gains)
output = linear.copy()
output[0::2, 0::2] *= gains[0]
output[0::2, 1::2] *= gains[1]
output[1::2, 0::2] *= gains[1]
output[1::2, 1::2] *= gains[2]
return output

注意函数最后没有 np.clip(output, 0, 1)。是否裁剪应由后续阶段决定。另一方面,减黑电平后的小负值在这里被限制为零,是一种明确的策略;如果后续降噪或校准需要保留负值,也可以延后处理。

最容易踩的坑#

1. Bayer Pattern 或裁剪偏移错了#

原始图是 RGGB,但裁掉一行以后有效区域可能从 GRBG 开始。如果仍按原来的坐标奇偶应用增益,红增益会乘到绿色位置,画面出现明显网格或整体偏色。

2. 在整数数组上直接原地相乘#

整数类型可能截断小数,超过范围还可能溢出或饱和。先转换到足够精度的浮点或明确设计的定点格式,再应用增益。

3. 把 neutral value 当成 gain#

先确认字段语义、通道顺序和是否已经归一化。不要只凭数值“看起来像一组 RGB 比例”。

4. 忘记先减黑电平#

这会把传感器偏置也按通道放大,问题主要藏在暗部。

5. 在 Gamma 编码值上直接乘#

WB 的线性比例关系不再成立,饱和度和亮度都会以不期望的方式变化。

6. 过早裁剪#

乘增益后的越界值可能仍能在后续曝光或 tone mapping 中利用。裁剪一旦发生,通道比例就不可逆地改变。

7. 假设 Debayer 与 WB 永远可交换#

先确认 Debayer 是否真正逐通道线性,以及中间有没有方向判断、降噪、裁剪或量化。

怎样验证一套 WB 实现#

不要只拿一张“看起来正常”的照片判断。至少准备以下测试:

  • 单位增益 (1, 1, 1) 应保持输入不变;
  • 一个已减黑电平的中性块,校正后 R、G、B 应接近目标比例;
  • 四种 Bayer Pattern 和奇偶尺寸裁剪都要测试;
  • 分别输入纯 R、纯 G、纯 B 的合成马赛克,确认 mask 没有错位;
  • 检查接近黑电平的像素,防止残留彩色偏置;
  • 检查接近 white level 的高光,记录越界发生在哪个阶段;
  • 对逐通道线性 Debayer,比对“Bayer WB”和“RGB WB”的数值误差;
  • 对真实 Debayer,不预设两条路径相同,而是测量差值和观察伪影。

回到最初的问题:白平衡的核心确实只是三个通道增益,但这三个数字必须在正确的数据含义、正确的 CFA 位置和正确的处理顺序里使用。

Bayer 域 WB 是在每个感光位置上,根据滤光片类型乘增益;RGB 域 WB 是在 Debayer 后,对完整像素的三个通道分别缩放。在理想线性逐通道插值下,它们可以得到相同结果;一旦算法跨通道、自适应,或中途发生裁剪与量化,顺序就成为管线定义的一部分。

记住四个检查点:先减黑电平、确认线性域、核对 Bayer Pattern、延后不必要的裁剪。 做到这些,“乘三个数字”才真正成为一项可验证、可复现的颜色处理。

白平衡是怎么校正光源颜色的?从 Bayer 域增益到 RGB 域增益
https://cloudsir.top/posts/how-white-balance-works/
作者
CloudSir
发布于
2026-08-24
许可协议
CC BY-NC-SA 4.0
CCM 是怎么校正颜色的?从 3×3 矩阵看懂 RGB 通道混合
Gamma 矫正到底矫正了什么?从线性光走到屏幕亮度