3. 白平衡问题定义:偏色现象、校正目标与两大经典假设

好,咱们正式开始进入白平衡算法的核心地带。在做自动白平衡之前,你得先搞清楚一个问题:我们到底在解决什么问题?

说白了,就是照片拍出来颜色不对。明明是一张白纸,在暖色灯光下拍出来发黄,在阴天拍出来发蓝。这种偏色,就是我们要搞定的目标。

3.1 偏色现象:为什么相机看到的颜色和人眼不一样?

人眼有个很牛的能力叫「色彩恒常性」。不管光源怎么变,我们看一张白纸,它始终是白的。但相机传感器没这个本事,它老老实实地记录进入镜头的光线。光源变了,记录下来的 RGB 值就变了。

我举个例子。你在办公室的日光灯下拍一张白纸,RGB 值大概是 (240, 245, 250)。拿到家里暖色台灯下再拍,可能就变成了 (255, 220, 180)。你看,蓝色通道掉下去了,红色通道爆表了,整张图就偏黄。

偏色的本质: 光源的光谱能量分布不同,导致物体反射到传感器上的 RGB 比例发生偏移。这不是物体本身的颜色变了,是光源在「捣鬼」。

常见的偏色场景我整理了一下:

光源类型 色温范围 (K) 偏色倾向 典型场景
白炽灯 2500 - 3000 偏黄/橙 家庭卧室、餐厅
日光灯 4000 - 5000 偏绿/白 办公室、教室
正午日光 5000 - 6500 标准白光 户外晴天
阴天/阴影 7000 - 9000 偏蓝 阴天户外、树荫下

嗯,这里要注意。色温越高,颜色越偏蓝;色温越低,颜色越偏红。这个和直觉是反的,别搞混了。

3.2 白平衡校正目标:让白色回归白色

校正目标其实就一句话:让图像中原本是白色的物体,在任意光源下都呈现为白色。

一旦白色被校正了,其他所有颜色都会跟着被拉回正确的色域。这就是白平衡的底层逻辑——以白点为锚点,校正整个色偏。

数学上怎么描述?假设我们有一个偏色图像,它的 R、G、B 三个通道需要分别乘上一个增益系数,让白色区域的 R、G、B 值相等。

// 校正后的像素值 = 原始像素值 × 增益系数
R' = R × gain_R
G' = G × gain_G
B' = B × gain_B

// 校正目标:对于白色区域,R' = G' = B'

说白了,就是找到三个系数,把偏色的通道拉回来。那问题来了:增益系数怎么算?

这就引出了两个最经典的假设——灰度世界假设和完美反射假设。几乎所有自动白平衡算法,都是在这两个假设的基础上发展出来的。

3.3 灰度世界假设 (Gray World Assumption)

这个假设很直观:一张色彩丰富的自然图像,所有像素的 R、G、B 平均值应该趋于同一个灰度值。

为什么?因为自然界中各种颜色混合在一起,统计上会趋向于灰色。如果图像偏黄,说明 R 通道的平均值偏高,B 通道的平均值偏低。那我们就把 R 的增益调低,B 的增益调高,让三个通道的平均值相等。

我的经验: 灰度世界假设在色彩丰富的场景下效果非常好。但我曾经在一个全是蓝色天空的航拍图上栽过跟头——整张图只有蓝色,平均值严重偏蓝,算法硬是把蓝色拉成了灰色,结果天空变成了灰蒙蒙一片。所以,这个假设的前提是「色彩丰富」,场景单一的时候千万别用。

计算步骤很简单:

  1. 计算整张图像 R、G、B 三个通道的平均值:avg_R, avg_G, avg_B
  2. 计算三个平均值的总平均:gray = (avg_R + avg_G + avg_B) / 3
  3. 计算每个通道的增益:gain_R = gray / avg_R, gain_G = gray / avg_G, gain_B = gray / avg_B
  4. 用增益系数校正每个像素
// 灰度世界假设的 Python 实现
import numpy as np

def gray_world(img):
    # img: HxWx3, dtype=np.uint8
    avg = np.mean(img, axis=(0, 1))  # 每个通道的平均值
    gray = np.mean(avg)              # 总平均灰度
    gain = gray / avg                # 增益系数
    corrected = (img * gain).clip(0, 255).astype(np.uint8)
    return corrected

3.4 完美反射假设 (Perfect Reflector Assumption)

另一个经典假设:图像中最亮的像素点,应该是纯白色的。 也就是说,图像中一定存在某个区域,它对所有波长的光都完全反射,不吸收任何颜色。

这个假设的出发点是:场景中总会有高光、镜面反射或者白色物体。这些区域的 RGB 值应该相等。如果它们不相等,说明存在色偏,我们就用这些「最亮点」来估计光源颜色。

避坑指南: 我曾经在一个夜景灯光图像上用过完美反射假设,结果翻车了。因为图像中最亮的点是红色的霓虹灯,算法把它当成了「完美反射点」,结果整张图被拉成了青绿色。所以,使用前一定要确认最亮点确实是白色物体或高光,而不是彩色光源本身。

实现思路:

  1. 找到图像中最亮的像素点(通常是 R+G+B 最大的前 0.1% 像素)
  2. 计算这些像素点的 R、G、B 平均值
  3. 用这些平均值作为光源颜色的估计
  4. 计算增益系数,使这些最亮点变为纯白色
// 完美反射假设的 Python 实现
def perfect_reflector(img, percentile=99.9):
    # 计算每个像素的亮度
    brightness = np.sum(img, axis=2)
    # 找到亮度最高的阈值
    threshold = np.percentile(brightness, percentile)
    # 提取高亮像素的掩码
    mask = brightness >= threshold
    # 计算高亮像素的 RGB 平均值
    avg = np.mean(img[mask], axis=0)
    # 增益系数:让高亮像素变为纯白 (255, 255, 255)
    gain = 255.0 / avg
    corrected = (img * gain).clip(0, 255).astype(np.uint8)
    return corrected

3.5 两大假设的对比与选择

这两个假设各有各的脾气。我平时做项目,会根据场景来选:

对比维度 灰度世界假设 完美反射假设
适用场景 色彩丰富、纹理复杂的自然图像 存在高光、镜面反射或白色物体的场景
失效场景 大色块单一场景(如蓝天、绿草地) 最亮点是彩色光源(如霓虹灯、彩色灯光)
计算复杂度 低,只需一次全局统计 中等,需要排序或阈值查找
鲁棒性 对噪声敏感度低 对最亮点的选择敏感

你想想看,如果一张图既有丰富的色彩,又有明显的白色高光,那这两个假设可以结合起来用。我个人的习惯是:先用灰度世界假设做一次粗校正,再用完美反射假设做一次细调。效果往往比单独用任何一个都好。

3.6 本章知识体系

下面这张图把本章的核心逻辑串起来了,你可以对照着梳理一下思路:

白平衡问题定义与核心假设 偏色现象 校正目标 两大经典假设 光源光谱差异 传感器无恒常性 白色回归白色 颜色整体校正 灰度世界假设 完美反射假设 RGB 平均值趋于灰色 计算增益:gray / avg 最亮点应为纯白色 计算增益:255 / avg 核心思路 通过统计假设估计光源颜色 → 计算三通道增益 → 校正偏色

嗯,到这里,白平衡问题的定义和两个最基础的假设就讲完了。灰度世界假设和完美反射假设虽然简单,但它们是所有高级白平衡算法的基石。后面的章节里,我们会看到它们是如何被改进、被组合、被应用到实际产品中的。