4. 白平衡算法:灰度世界、完美反射、色温估计方法,以及移动端实时白平衡的工程落地

白平衡这东西,说白了就是让白色在照片里看起来是白的。你想想看,同一张白纸,在日光灯下发青,在白炽灯下发黄,在人眼看来它还是白的——这叫色彩恒常性。但CMOS传感器没这个本事,它老老实实把光谱记录下来,所以我们必须用算法把色偏拉回来。

我在项目中遇到过不少白平衡翻车的案例。最典型的就是拍夕阳,算法硬把暖色调掰成冷白,用户直接骂街。所以这一章,咱们把三种经典方法讲透,再聊聊移动端怎么落地。

4.1 灰度世界法(Gray World)

这个假设很简单:一张色彩丰富的自然图像,所有像素的R、G、B均值应该趋于相等。如果某个通道均值偏高,说明存在色偏,我们就把它压下来。

公式长这样:

// 计算各通道均值
float avgR = mean(R_channel);
float avgG = mean(G_channel);
float avgB = mean(B_channel);

// 计算全局均值
float avgGray = (avgR + avgG + avgB) / 3.0f;

// 计算增益并应用
float gainR = avgGray / avgR;
float gainG = avgGray / avgG;
float gainB = avgGray / avgB;

R_out = R_in * gainR;
G_out = G_in * gainG;
B_out = B_in * gainB;
我的经验:灰度世界法对场景要求很高。如果画面里大面积是蓝色天空或绿色草地,均值会严重偏离灰色,结果就是色偏矫正过头。我一般只在预览阶段用它做快速粗调。

4.2 完美反射法(Perfect Reflector)

这个方法的思路是:图像中最亮的像素点,理论上应该是纯白色。找到这些“完美反射点”,用它们的RGB值来推算光源色温。

具体步骤:

  1. 把图像转成YUV或计算亮度,找到亮度最高的前0.1%~1%像素
  2. 统计这些像素的R、G、B均值
  3. 用这些均值计算增益,矫正整图
// 伪代码:完美反射法
int numPixels = width * height;
int topPercent = 1;  // 取前1%最亮像素
int sampleCount = numPixels * topPercent / 100;

// 按亮度排序,取前N个
sort(pixels, byLuminance);
float sumR = 0, sumG = 0, sumB = 0;
for (int i = 0; i < sampleCount; i++) {
    sumR += pixels[i].r;
    sumG += pixels[i].g;
    sumB += pixels[i].b;
}
float avgR = sumR / sampleCount;
float avgG = sumG / sampleCount;
float avgB = sumB / sampleCount;

// 假设完美反射点R=G=B=255
float gainR = 255.0f / avgR;
float gainG = 255.0f / avgG;
float gainB = 255.0f / avgB;
注意:如果场景里根本没有白色物体(比如拍夜景),完美反射法会失效。我曾经在暗光场景下用过,结果把噪点当成了白色参考点,输出一片惨白。所以这个方法必须配合场景检测来用。

4.3 色温估计法

这个方法更接近硬件ISP的做法。它基于一个事实:不同色温的光源,在R/G和B/G比值平面上有固定的轨迹。我们通过统计图像中灰点的R/G和B/G比值,反推出当前光源色温。

核心流程:

  • 灰点检测:找到图像中接近中性色(R≈G≈B)的像素
  • 比值统计:计算这些灰点的R/G和B/G均值
  • 色温查表:用比值查预标定的色温曲线,得到估计色温
  • 增益计算:根据色温查增益表,应用矫正
色温(K) R/G比值 B/G比值 典型场景
2500 2.12 0.65 白炽灯
4000 1.50 0.85 荧光灯
5500 1.00 1.00 日光
6500 0.85 1.20 阴天
8000 0.72 1.45 阴影
核心观点:色温估计法的精度取决于灰点检测的准确性和标定数据的质量。我个人习惯在实验室用DNP色卡做全色温段标定,这样查表出来的结果最稳。

4.4 移动端实时白平衡的工程落地

到了移动端,事情就复杂了。你不能把整张图都算一遍,那样太慢。我总结了一套工程落地的套路:

4.4.1 降采样预处理

先把图像缩放到320x240或640x480,在这个小图上做统计。移动端的CPU/GPU都扛不住全分辨率直算,降采样后精度损失很小,速度能快10倍以上。

4.4.2 多算法融合策略

单一算法都有短板。我常用的组合是:

  • 预览阶段:灰度世界法做快速粗调,保证画面不偏色太离谱
  • 拍照阶段:色温估计法+完美反射法做加权融合,精度优先
  • 视频模式:加入帧间平滑,防止白平衡闪烁

4.4.3 帧间平滑与防抖

白平衡参数不能跳变,否则视频会一闪一闪的。我一般用一阶低通滤波:

// 帧间平滑
float alpha = 0.3f;  // 平滑系数,越小越平滑
current_gainR = alpha * new_gainR + (1 - alpha) * prev_gainR;
current_gainG = alpha * new_gainG + (1 - alpha) * prev_gainG;
current_gainB = alpha * new_gainB + (1 - alpha) * prev_gainB;

4.4.4 场景自适应切换

不同场景用不同策略。我做过一个场景分类器,根据统计特征自动切换:

场景类型 检测特征 推荐算法
户外日光 亮度高,饱和度适中 色温估计法
室内灯光 亮度低,色偏明显 完美反射法+色温估计
夜景 极低亮度,噪点多 灰度世界法(保守增益)
人像 肤色占比高 肤色保护+色温估计
夕阳/日出 暖色调,高动态 色温估计法(保留暖色)
避坑指南:我曾经在某个项目里,把白平衡的增益上限设得太高,结果在低色温场景下蓝色通道增益飙到2.5,蓝色噪点被放大到无法直视。后来我加了增益限幅,R/G/B各通道增益不超过1.8,效果好了很多。

4.5 知识体系总览

下面这张图把本章的核心逻辑串起来了。你可以看到,从输入图像到最终矫正,中间经历了降采样、灰点检测、多算法融合、帧间平滑和场景自适应这几个关键环节。

移动端实时白平衡算法流程 输入RAW/Bayer图像 降采样至640x480 灰点检测与统计 灰度世界法 完美反射法 色温估计法 场景自适应融合 输出矫正后图像 帧间平滑 一阶低通滤波,防止闪烁 增益限幅 各通道增益 ≤ 1.8