4. AWB增益计算

好,咱们进入AWB最核心的计算环节。前面几章讲了色温估计、光源判断,那些都是「侦察」工作。这一章要讲的是「执行」——知道了场景偏色,怎么算出增益把它拉回来?

说白了,AWB增益计算就是三个问题:算多少、别算过头、分区域算。我一个个拆开讲。

4.1 R/G和B/G增益计算:从偏差到补偿

先明确一个基本逻辑。AWB的目标是让图像中原本是白色的物体,在R、G、B三个通道上响应一致。如果某个通道偏弱,我们就给它补增益;偏强,就压它。

标准做法是这样的:

  1. 统计全图或感兴趣区域的R、G、B均值,记为R_avg、G_avg、B_avg。
  2. 以G通道为参考(因为人眼对绿色最敏感,且Bayer阵列中G像素占一半),计算R/G和B/G的比值。
  3. 增益就是这些比值的倒数。

公式很简单:

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增益计算全景

下面这张图把整个增益计算的流程串起来了。从统计到计算,从钳位到多区域融合,每一步都有对应的工程考量。

AWB增益计算流程 AWB增益计算全景流程 输入:R/G/B通道统计均值 增益计算 R_gain = G_avg / R_avg | B_gain = G_avg / B_avg 增益钳位与限制(三层防线) 钳位 [0.25, 4.0] → 比率限制 [0.5, 2.0] → 帧间平滑 ≤20% 全局统一增益 多区域增益(M×N网格) 输出:全局增益(简单快速) 输出:插值增益(空间平滑) 工程要点:多区域AWB需要平衡精度与计算量,建议从3×3网格起步,配合白点置信度做自适应融合

4.5 实战建议:从理论到落地的几个要点

最后,我总结几条实战中容易忽略的点:

  • 增益精度:不要用float直接算完就丢给硬件。很多ISP的增益寄存器是定点数,比如Q8.8格式。要提前做好定点化,否则精度损失会导致色偏。
  • 增益更新时机:我建议在每一帧的VBLANK期间更新增益,不要在帧中间切换。否则同一帧内上半部分和下半部分色温不同,会出现「阴阳脸」。
  • 调试手段:把每个块的增益值叠加到图像上显示出来,调试时一目了然。我习惯用伪彩色图表示增益大小——蓝色表示增益小,红色表示增益大。
  • 回退机制:当场景中找不到任何白点时(比如纯色场景),多区域AWB会失效。这时候要回退到全局AWB,甚至用上一次有效的增益值。别让算法在无白点场景下「瞎猜」。

一句话总结: AWB增益计算不是简单的除法,而是一套包含统计、钳位、平滑、分区的系统工程。每个环节的细节都决定了最终图像的颜色还原质量。