3. AWB算法核心原理:灰度世界、完美反射、色温估计与Von Kries变换

各位好,欢迎来到第三章。这一章我们聊点实在的——AWB算法到底是怎么算的?

说实话,我刚入行那会儿,觉得白平衡不就是找个白色物体吗?后来被项目狠狠教育了一顿。嗯,这里面的门道,比你想象的多得多。

今天我把四个最核心的理论掰开揉碎讲给你听。它们分别是:灰度世界假设、完美反射假设、色温估计理论,以及Von Kries色适应变换。这四个东西,是你在高通Spectra平台上调AWB绕不开的基石。

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

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

说白了,就是“世界平均是灰色的”。

数学表达长这样:

R_avg = G_avg = B_avg = Gray

如果实际图像不满足这个条件,那一定是光源颜色在捣鬼。我们只需要算出R、G、B各自的增益,把平均值拉回同一个水平线就行。

增益计算公式:

R_gain = G_avg / R_avg
G_gain = 1.0  (通常以G通道为参考)
B_gain = G_avg / B_avg

我在项目中遇到过一个问题:当画面里大面积都是同一颜色时——比如一片蓝天,或者一片绿草地——灰度世界假设就崩了。整张图偏色偏得离谱。

避坑指南: 我曾经在一个户外监控项目里,直接用灰度世界假设做全局AWB。结果画面里全是树叶,算法把绿色强行拉成灰色,整个画面变成了紫红色。后来我加了场景分类器,只在色彩丰富的场景才用这个假设。

所以,灰度世界假设的适用场景是:色彩丰富、分布均匀的自然场景。单一主色调的场景,慎用。

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

这个假设和灰度世界正好相反。它认为:图像中最亮的那个像素点,应该是一个完美的白色反射体。

你想想看,如果有一个纯白物体在场景里,它反射了所有波长的光。那么它身上的R、G、B值,就代表了当前光源的颜色。

算法流程是这样的:

  1. 找到图像中最亮的若干个像素(通常是前0.1%~1%)
  2. 计算这些像素的R、G、B平均值
  3. 用这个平均值估算光源颜色
  4. 计算增益,把最亮区域拉回白色

代码实现思路:

// 伪代码:完美反射假设
float findMaxLuminance(Image img) {
    float maxY = 0;
    for each pixel in img {
        float Y = 0.299*R + 0.587*G + 0.114*B;
        if (Y > maxY) maxY = Y;
    }
    return maxY;
}

// 取亮度前0.5%的像素
vector<Pixel> getWhitePixels(Image img, float threshold) {
    vector<Pixel> whitePixels;
    for each pixel in img {
        if (pixel.Y >= threshold * 0.995) {
            whitePixels.push_back(pixel);
        }
    }
    return whitePixels;
}

个人经验: 我建议在实际工程中,不要把阈值设得太死。高通Spectra平台上有专门的统计模块,可以输出亮度直方图。我一般取亮度最高的0.3%~0.5%区域,同时加一个饱和度判断——太饱和的亮像素可能是高光,不是白色。

3.3 色温估计理论(Color Temperature Estimation)

色温这个概念,说白了就是黑体辐射体在不同温度下发出的光的颜色。单位是开尔文(K)。

常见的色温范围:

光源类型 色温范围(K) 视觉感受
烛光/日出日落 1800 - 3000 暖黄/橙红
白炽灯 2700 - 3300 暖白
荧光灯(暖白) 3000 - 4500 中性偏暖
日光(正午) 5000 - 6500 标准白
阴天/阴影 6500 - 8000 冷白/偏蓝
蓝天(无云) 8000 - 12000 深蓝

色温估计的核心,就是根据图像中的R/G和B/G比值,反推出当前光源的色温。

在高通Spectra平台上,有一个经典的色温查找表(LUT)方法:

// 色温查找表示例
struct ColorTempEntry {
    float colorTemp;   // 色温值 (K)
    float rgRatio;     // R/G 比值
    float bgRatio;     // B/G 比值
};

ColorTempEntry colorTempLUT[] = {
    { 2700, 1.85, 1.20 },   // 白炽灯
    { 4000, 1.45, 1.35 },   // 荧光灯
    { 5500, 1.20, 1.50 },   // 日光
    { 6500, 1.05, 1.70 },   // 阴天
    { 8000, 0.95, 1.90 },   // 阴影
};

// 根据R/G和B/G插值得到色温
float estimateColorTemp(float rg, float bg) {
    // 在LUT中查找最近的条目,做线性插值
    // ...
}

为什么会这样?因为不同色温的光源,在R和B通道上的能量分布是完全不同的。低色温光源R强B弱,高色温光源R弱B强。这个规律非常稳定。

关键点: 色温估计不是万能的。混合光源场景(比如室内既有日光又有白炽灯)会让算法很头疼。高通Spectra的AWB模块里有一个“多光源检测”机制,专门处理这种情况。

3.4 Von Kries色适应变换

好了,前面我们估算了光源颜色,也计算了增益。但怎么把这些增益正确地应用到图像上?

直接乘?太粗暴了。人眼的视觉系统不是这么工作的。

Von Kries变换的核心思想是:人眼的三种锥状细胞(L、M、S,分别对应长波、中波、短波)各自独立地适应光源的变化。

数学上,它是在锥状细胞响应空间里做对角变换:

| L' |   | k_L  0   0  | | L |
| M' | = | 0   k_M  0  | | M |
| S' |   | 0   0   k_S | | S |

其中k_L、k_M、k_S分别是三种锥状细胞的适应增益。

完整的流程是:

  1. 把RGB图像转换到LMS锥状细胞空间
  2. 在LMS空间里应用对角增益矩阵
  3. 再转换回RGB空间

转换矩阵长这样(标准CIE 1931 XYZ到LMS的转换):

| L |   |  0.4002   0.7076  -0.0808 | | X |
| M | = | -0.2263   1.1653   0.0457 | | Y |
| S |   |  0.0000   0.0000   0.9182 | | Z |

我的建议: 在实际的高通Spectra平台上,Von Kries变换通常被简化了。因为LMS转换计算量不小。很多场景下,直接在RGB空间做对角增益也能得到不错的效果。但如果你追求极致的色彩还原精度——比如医疗影像或专业摄影——那还是老老实实走Von Kries流程。

3.5 四种理论的关系与选择

这四种理论不是互斥的。在实际的高通Spectra AWB流水线里,它们通常是组合使用的。

我画了一张图,帮你理清它们的关系:

AWB核心理论关系图 AWB核心理论关系图 原始RAW图像 灰度世界假设 统计全局RGB均值 完美反射假设 提取最亮像素统计 色温估计理论 R/G与B/G查表 多假设融合与置信度加权 根据场景动态调整权重 增益计算(R_gain, G_gain, B_gain) 基于估计的光源颜色 Von Kries色适应变换 RGB → LMS → 对角增益 → RGB 校正后的图像 实际工程建议 低端平台:RGB对角增益 高端平台:完整Von Kries 混合光源:多区域AWB

在实际的高通Spectra平台上,AWB模块通常是这样工作的:

  1. 先同时运行灰度世界和完美反射两个假设,各自算出一组增益
  2. 再用色温估计理论验证这两组增益是否合理
  3. 根据场景置信度,把两组增益做加权融合
  4. 最后通过Von Kries变换(或简化版)应用到图像上

核心总结: 灰度世界假设适合色彩丰富的场景,完美反射假设适合有明确白色参考的场景,色温估计提供物理约束,Von Kries变换保证色彩适应的人眼一致性。四者缺一不可。