同一张白纸搬到正午阳光、钨丝灯和蓝色遮阳棚下,传感器记录到的红、绿、蓝比例会完全不同。人眼会不自觉地适应光源,仍然觉得它是白纸;相机得到的 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.70G gain = 0.52 / 0.52 = 1.00B gain = 0.52 / 0.30 ≈ 1.73应用以后,三个通道都接近 0.52。这不是在把所有物体变灰,而是在用同一组比例校正整幅图像;红色物体仍然比灰卡更红,蓝色物体仍然更蓝。
手动白平衡实验室
这里不猜光源:增益是已知输入。选一个示意光源,再故意拨动三个增益,观察中性卡怎样离开灰轴。
当前对角矩阵为 diag(0.70, 1.00, 1.73)。这里只负责应用增益,不包含任何自动估计。
组件里的“暖光”“冷光”只是预先给好的教学数据,不是在根据画面自动判断光源。你可以故意把某个增益拉偏,观察中性卡怎样立刻离开灰轴。
三个增益就是一个对角矩阵
在线性 RGB 中,白平衡可以写成:
展开以后就是三次独立乘法:
对角线以外全是零,表示红色输出不需要读取绿色与蓝色输入。后面讲到 CCM 时,一般的 矩阵会允许三个通道互相混合;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 通常已经经过非线性编码。在编码值上直接乘白平衡增益,效果不等价于:
- 把编码值解码回线性光;
- 在线性光中应用通道增益;
- 再编码回输出格式。
所以,在 RAW ISP 中,WB 通常位于 Gamma 或输出传递函数之前。对于已经编码的图片,必须先弄清数据的传递函数,不能仅凭数组类型是 float 就认定它是线性的。
黑电平要先减,再应用增益
RAW 的“零光照”通常不对应数字零。传感器与模拟前端会保留一个黑电平偏置,正确的通道处理应当近似写成:
如果先乘增益再减原来的黑电平:
两者相差:
当红、绿、蓝使用不同增益时,残留偏置也不同。错误在亮部可能不显眼,却会让接近黑色的区域带上颜色。
先减黑电平,还是先乘增益?
拖动暗部 RAW 值。顺序错误会把本应被减掉的传感器偏置也留进结果,增益越大,暗部偏色越明显。
两式之差是 black × (gain − 1)。不同通道使用不同增益时,原本中性的黑位就会被染色。
生产管线还可能给每个通道、每一行甚至不同模拟增益设置不同黑电平。本文用单一标量说明顺序,但代码里不能想当然地把所有像素都减同一个常数。
Bayer 域白平衡:在马赛克上乘增益
Bayer RAW 的每个感光位置只保存一个通道。以 RGGB 为例,一个 单元是:
R GG BBayer 域 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 中,两种顺序可以严格等价;真实管线里不能默认它们永远等价。
假设 是一个只在各自通道内部做固定线性加权的 Debayer, 是对角白平衡矩阵。对红色平面来说,先把所有红样本乘 再加权,与先加权再把结果乘 相同:
但这个等式依赖几个条件:
- Debayer 的权重不受输入数值影响;
- 不使用其他通道修正当前通道;
- 中途没有裁剪、量化或整数溢出;
- Bayer Pattern、边界处理与浮点精度完全一致。
边缘感知算法可能根据局部梯度选择方向;高质量算法也可能利用通道差或色度一致性。当增益改变了用于判断的数值,插值路径就可能变化。跨通道矩阵与对角增益通常也不满足交换律。
白平衡应该放在 Debayer 前还是后?
两条路径使用同一组 RAW 样本和增益。逐通道线性插值可以与对角增益交换;跨通道步骤会让顺序产生差异。
“跨通道修正”是用来说明非交换性的简化模型,不代表某个特定商业 Debayer。真实算法还可能包含方向选择、降噪、裁剪和整数舍入。
在组件中选择“逐通道线性”,两条路径的最大误差会接近浮点计算误差;切换“跨通道修正”,顺序差异就会出现。这个简化模型不是在复刻某个商业算法,而是在隔离导致非交换的关键条件。
为什么许多 RAW 管线在 Bayer 域做 WB
Bayer 域 WB 有几个直接优势:
- 每个感光位置只处理一个数,内存与带宽较小;
- 增益可以在较高位深或浮点中尽早应用;
- Debayer 得到的输入已经具有较合理的通道比例;
- 后续算法不必一直携带相机光源下的强烈偏色。
但“更早”并不意味着可以不顾顺序。至少要先正确完成黑电平处理;坏点修复、镜头阴影校正、降噪与 WB 的相对位置也要结合算法设计。比如先放大蓝通道会同时放大其噪声,可能影响依赖梯度阈值的坏点或降噪判断。
白平衡会同时放大噪声
假设蓝通道的信号由 变成:
叠加在蓝通道上的噪声也会近似乘上 。若蓝通道本来信号弱、需要较大的增益,校正后的暗部蓝噪声通常会更明显。
这不是白平衡“制造了错误颜色”,而是它把原本就存在的微小信号和噪声一起放大了。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。
增益同时放大信号、噪声和高光
蓝通道需要更大增益时,暗部噪声与高光峰值都会一起上升。切换立即裁剪,看看哪部分信息无法再回来。
条形轨道以 1.0 为显示上限。浮点模式仍保留真实数值;裁剪模式把越界值压到 1.0,之后再降低曝光也无法辨认原来的差异。
浮点管线可以暂时保留大于 1.0 的中间值,再由曝光归一化、tone mapping 或高光重建决定怎样压回输出范围。但如果传感器采样时就已经达到物理饱和,后面保留浮点也不会凭空恢复细节。
归一化最大增益的代价
为了避免增益超过 1.0,有时会把整组 gain 除以其中最大值。例如:
原增益: (1.20, 1.00, 1.90)归一化: (0.63, 0.53, 1.00)三通道比例没有改变,所以白平衡色彩方向保持一致;但整体图像变暗了。随后如果再统一提亮,能否避免裁剪取决于管线保留了多少 headroom,而不是归一化这个动作本身创造了额外动态范围。
白平衡与曝光不是一件事
曝光或统一数字增益近似对所有通道乘同一个数:
白平衡关心的是三个通道之间的比例:
两者都会改变画面亮度,但目的不同。一个稳健的实现应明确:WB 阶段负责相对比例,曝光或 tone mapping 阶段负责整体明暗。否则同一份增益在两套软件里可能得到不同亮度,即使它们的色度方向一致。
白平衡与 CCM 能合并吗
WB 是对角矩阵 ,CCM 是一般矩阵 。如果二者都在线性域、其间没有非线性处理,那么可以合并:
合并能少做一次矩阵运算,但工程中仍常保留独立参数:
- WB 会随拍摄光源或相机设置变化;
- CCM 可能按光源选择或在两套标定矩阵间插值;
- 分开记录更容易检查错误来自增益还是通道混合;
- Bayer 域 WB 位于 Debayer 前,本来就无法简单与后面的 RGB CCM 合成一次逐像素矩阵。
另外,矩阵乘法的顺序不能颠倒。一般来说 ;把 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、延后不必要的裁剪。 做到这些,“乘三个数字”才真正成为一项可验证、可复现的颜色处理。