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值来推算光源色温。
具体步骤:
- 把图像转成YUV或计算亮度,找到亮度最高的前0.1%~1%像素
- 统计这些像素的R、G、B均值
- 用这些均值计算增益,矫正整图
// 伪代码:完美反射法
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 知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以看到,从输入图像到最终矫正,中间经历了降采样、灰点检测、多算法融合、帧间平滑和场景自适应这几个关键环节。