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值,就代表了当前光源的颜色。
算法流程是这样的:
- 找到图像中最亮的若干个像素(通常是前0.1%~1%)
- 计算这些像素的R、G、B平均值
- 用这个平均值估算光源颜色
- 计算增益,把最亮区域拉回白色
代码实现思路:
// 伪代码:完美反射假设
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分别是三种锥状细胞的适应增益。
完整的流程是:
- 把RGB图像转换到LMS锥状细胞空间
- 在LMS空间里应用对角增益矩阵
- 再转换回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流水线里,它们通常是组合使用的。
我画了一张图,帮你理清它们的关系:
在实际的高通Spectra平台上,AWB模块通常是这样工作的:
- 先同时运行灰度世界和完美反射两个假设,各自算出一组增益
- 再用色温估计理论验证这两组增益是否合理
- 根据场景置信度,把两组增益做加权融合
- 最后通过Von Kries变换(或简化版)应用到图像上
核心总结: 灰度世界假设适合色彩丰富的场景,完美反射假设适合有明确白色参考的场景,色温估计提供物理约束,Von Kries变换保证色彩适应的人眼一致性。四者缺一不可。