4. AWB增益计算
好,咱们进入AWB最核心的计算环节。前面几章讲了色温估计、光源判断,那些都是「侦察」工作。这一章要讲的是「执行」——知道了场景偏色,怎么算出增益把它拉回来?
说白了,AWB增益计算就是三个问题:算多少、别算过头、分区域算。我一个个拆开讲。
4.1 R/G和B/G增益计算:从偏差到补偿
先明确一个基本逻辑。AWB的目标是让图像中原本是白色的物体,在R、G、B三个通道上响应一致。如果某个通道偏弱,我们就给它补增益;偏强,就压它。
标准做法是这样的:
- 统计全图或感兴趣区域的R、G、B均值,记为R_avg、G_avg、B_avg。
- 以G通道为参考(因为人眼对绿色最敏感,且Bayer阵列中G像素占一半),计算R/G和B/G的比值。
- 增益就是这些比值的倒数。
公式很简单:
R_gain = G_avg / R_avg
B_gain = G_avg / B_avg
G_gain = 1.0 (或根据亮度调整)
嗯,这里要注意:G_gain不一定永远是1.0。如果你希望保持整体亮度不变,通常会把G_gain设为1.0,只调R和B。但有些ISP为了平衡信噪比,会适当调整G_gain,让三个通道的增益分布更均匀。
关键理解: 增益的本质是「补偿系数」。R_gain > 1.0 说明R通道偏弱,需要拉高;R_gain < 1.0 说明R通道偏强,需要压低。但实际工程中,我们很少用小于1.0的增益——因为压低通道会损失动态范围。所以大多数ISP的做法是:找出三个通道中的最大值,把所有增益归一化到1.0以上。
举个例子:假设统计得到R_avg=100, G_avg=200, B_avg=150。原始增益是R_gain=2.0, G_gain=1.0, B_gain=1.33。但这样G通道没变,R和B都拉高了,整体亮度会上升。更常见的做法是:
max_gain = max(R_gain, G_gain, B_gain) = 2.0
R_gain_final = 2.0 / 2.0 = 1.0
G_gain_final = 1.0 / 2.0 = 0.5
B_gain_final = 1.33 / 2.0 = 0.665
这样所有增益都≤1.0,整体亮度不会爆。但代价是G和B通道被压低了,信噪比会下降。所以实际工程中,归一化策略要根据场景和传感器特性来选,没有银弹。
4.2 增益钳位与限制:别让算法「发疯」
算出来的增益能直接用吗?不能。我在项目中遇到过好几次,算法在极端场景下算出了离谱的增益值——比如对着纯红色物体,R_gain算出来只有0.1,B_gain飙到8.0。如果直接应用,图像会变成一片惨绿。
所以必须加钳位和限制。我一般设三层防线:
| 层级 | 范围 | 作用 |
|---|---|---|
| 钳位 | [0.25, 4.0] | 防止单通道增益溢出或归零 |
| 比率限制 | R_gain / B_gain ∈ [0.5, 2.0] | 防止R/B通道失衡过大 |
| 帧间平滑 | 变化率 ≤ 20% / 帧 | 防止增益突变导致闪烁 |
钳位是最基础的。我习惯把下限设在0.25,上限设在4.0。为什么是4.0?因为超过4倍的增益,噪声会被放大到不可接受的程度。你想想看,增益4倍意味着噪声也放大4倍,暗部全是彩色噪点。
比率限制是我后来加的。有一次在黄昏场景下,R_gain和B_gain的比值达到了3:1,结果图像出现了严重的品红偏色。从那以后,我强制R_gain/B_gain的比值在0.5到2.0之间。
曾经踩过的坑: 帧间平滑的系数不能设成固定值。低光照下,统计噪声大,平滑系数要加大;高光照下,场景变化快,平滑系数要减小。我后来改用自适应平滑——根据场景亮度动态调整平滑强度。效果好了很多。
代码实现大概是这样的:
float clamp_gain(float gain, float min_g, float max_g) {
return fminf(fmaxf(gain, min_g), max_g);
}
float smooth_gain(float new_gain, float old_gain, float alpha) {
// alpha: 0.0~1.0, 越大响应越快
return old_gain * (1.0f - alpha) + new_gain * alpha;
}
// 实际调用
float r_gain_clamped = clamp_gain(r_gain_raw, 0.25f, 4.0f);
float b_gain_clamped = clamp_gain(b_gain_raw, 0.25f, 4.0f);
float r_gain_smoothed = smooth_gain(r_gain_clamped, r_gain_prev, 0.3f);
4.3 多区域AWB策略:全局统一还是分区自治?
全局统一增益的问题很明显——场景中不同区域可能有不同色温。比如室内窗边,窗外是日光(5500K),窗内是白炽灯(2800K)。你用全局增益,总有一边是偏色的。
多区域AWB就是解决这个问题的。基本思路是把图像分成M×N个块,每个块独立计算增益,然后做空间平滑过渡。
我常用的分块策略:
- 3×3网格:最简单,适合大部分场景
- 5×5网格:精度更高,但计算量大,且容易在小块上产生噪声
- 自适应分块:根据场景复杂度动态调整块大小——平坦区域用大块,纹理丰富区域用小块
分块之后,每个块都有自己的R_gain和B_gain。但你不能直接在每个像素上用对应块的增益——块边界会出现明显的色差。所以需要空间插值。
我一般用双线性插值:
// 假设图像分成M×N块,每个块中心有一个增益值
// 对于像素(x, y),找到其所在的4个相邻块中心
// 根据距离权重做双线性插值
float interpolate_gain(float x, float y, float gains[M][N]) {
int i0 = floor(x / block_w);
int j0 = floor(y / block_h);
int i1 = min(i0 + 1, M-1);
int j1 = min(j0 + 1, N-1);
float dx = (x - i0 * block_w) / block_w;
float dy = (y - j0 * block_h) / block_h;
float g00 = gains[i0][j0];
float g10 = gains[i1][j0];
float g01 = gains[i0][j1];
float g11 = gains[i1][j1];
return (1-dy)*((1-dx)*g00 + dx*g10) + dy*((1-dx)*g01 + dx*g11);
}
个人经验: 多区域AWB在手机摄像头上特别有用。因为手机镜头视场角大,边缘和中心的色温差异很明显。我做过一个项目,用5×5网格加双线性插值,边缘偏色从原来的Δuv=0.03降到了0.008。但代价是计算量增加了约3倍。所以嵌入式平台上要权衡。
还有一个细节:块内统计的置信度。如果一个块内全是纯色物体(比如一面红墙),它的统计结果不能代表色温。我通常会给每个块算一个「白点置信度」——块内有多少像素落在白点检测区域内。置信度低的块,它的增益会被相邻块「拉」回来。
4.4 知识结构图:AWB增益计算全景
下面这张图把整个增益计算的流程串起来了。从统计到计算,从钳位到多区域融合,每一步都有对应的工程考量。
4.5 实战建议:从理论到落地的几个要点
最后,我总结几条实战中容易忽略的点:
- 增益精度:不要用float直接算完就丢给硬件。很多ISP的增益寄存器是定点数,比如Q8.8格式。要提前做好定点化,否则精度损失会导致色偏。
- 增益更新时机:我建议在每一帧的VBLANK期间更新增益,不要在帧中间切换。否则同一帧内上半部分和下半部分色温不同,会出现「阴阳脸」。
- 调试手段:把每个块的增益值叠加到图像上显示出来,调试时一目了然。我习惯用伪彩色图表示增益大小——蓝色表示增益小,红色表示增益大。
- 回退机制:当场景中找不到任何白点时(比如纯色场景),多区域AWB会失效。这时候要回退到全局AWB,甚至用上一次有效的增益值。别让算法在无白点场景下「瞎猜」。
一句话总结: AWB增益计算不是简单的除法,而是一套包含统计、钳位、平滑、分区的系统工程。每个环节的细节都决定了最终图像的颜色还原质量。