4. RGB域图像增强:白平衡(AWB)原理与色温估计,颜色校正矩阵(CCM),Gamma校正与色调映射

各位同学,欢迎来到第四章。前面我们聊了Bayer域的去噪和去马赛克,图像终于从“黑白马赛克”变成了“彩色点阵”。但这时候的颜色,说实话,还不太靠谱。

你想想看,同样一张白纸,在日光灯下拍出来偏蓝,在白炽灯下拍出来偏黄。人眼会自动修正这种色偏,但CMOS传感器不会。所以,我们需要一套机制来告诉相机:“嘿,现在的光源是啥颜色?你得把白色给我还原回来。”

这一章,我们就来拆解RGB域的三个核心模块:白平衡(AWB)、颜色校正矩阵(CCM)、Gamma校正与色调映射。这三个模块串联起来,决定了你最终看到的颜色准不准、好不好看。

核心逻辑链:

AWB负责“色温适应” → CCM负责“传感器到标准色域的转换” → Gamma负责“亮度感知编码”。一步错,步步错。

RGB域图像增强核心流程 Bayer域输出 AWB 白平衡 色温估计 + 增益调整 CCM 颜色校正 3x3矩阵线性变换 Gamma 色调映射 各模块作用 • AWB:消除光源色温影响,让白色物体在任何光下都呈现白色 • CCM:将传感器RGB转换到标准色域(如sRGB/BT.709) • Gamma:对亮度进行非线性编码,适配显示设备特性

4.1 白平衡(AWB)—— 让白色回归白色

AWB的核心任务,说白了就是估计当前光源的色温,然后对R、G、B三个通道施加不同的增益,把“偏色”拉回来。

我个人习惯把AWB算法分成两大流派:基于统计的方法和基于学习的方法。在Qualcomm平台上,我们大量使用统计方法,因为它计算量小、稳定性高,适合实时预览。

4.1.1 色温估计的经典方法:灰度世界与完美反射

先看两个最基础的假设:

  • 灰度世界假设(Gray World):认为一幅彩色图像中,R、G、B三个通道的平均值应该相等。如果不等,说明有色偏。
  • 完美反射假设(White Patch):认为图像中最亮的像素点应该是白色,用这些点的RGB值来估计光源。

我在项目中遇到过一个问题:用灰度世界法处理大面积单色物体(比如一片蓝天)时,算法会把蓝色误判为色偏,强行往灰色拉,结果天空变成了灰蓝色。嗯,这就是灰度世界假设的局限性——它要求场景颜色足够丰富。

实战技巧:Qualcomm平台上的AWB通常不会只用单一假设,而是把图像分成多个区域,分别计算统计量,再通过加权融合得到最终的色温估计。这样能有效避免大面积单色场景的误判。

4.1.2 色温曲线与增益计算

得到色温估计值后,怎么算增益?

我们通常会在标定阶段,在多个标准色温光源下(如D65、D50、A光源)拍摄灰卡,记录下R/G和B/G的比值。把这些比值画在二维平面上,就得到了一条色温曲线。

光源类型 色温 (K) R/G 比值 B/G 比值
D65 (日光) 6500 1.02 0.98
D50 (标准) 5000 1.00 1.00
A光源 (白炽灯) 2856 1.25 0.72
TL84 (荧光灯) 4000 1.10 0.88

实际运行时,算法根据估计的色温值,在曲线上插值得到R/G和B/G的目标比值,然后反推出R和B通道的增益。G通道通常作为参考,增益固定为1.0。

注意:增益不能无限大。我曾经调试过一个低光场景,色温估计值偏到离谱,R通道增益被拉到2.5倍,结果暗部噪声被同步放大,画面全是红噪点。所以一定要给增益设置上限,比如最大1.8倍。

4.2 颜色校正矩阵(CCM)—— 从“传感器色”到“标准色”

AWB做完之后,颜色在“白平衡”意义上是对的,但还不够“准”。为什么?因为CMOS传感器的滤色片光谱响应和标准人眼视觉函数(CIE 1931)不一样。

说白了,传感器看到的红色,和标准定义的红色,不是同一个红色。所以我们需要一个3x3的线性变换矩阵,把传感器RGB空间映射到标准色域空间(比如sRGB或BT.709)。

4.2.1 CCM的标定与优化

CCM的标定通常用24色卡(ColorChecker)来做。拍摄色卡,得到每个色块的传感器RGB值,再和标准Lab值反算出来的目标RGB做线性回归,求解出3x3矩阵。

// CCM 计算示例(伪代码)
// sensor_rgb: 传感器读到的RGB值
// target_rgb: 标准色域下的目标RGB值
// 求解 M,使得 target_rgb = M * sensor_rgb

// 一个典型的CCM矩阵(D65光源下)
float ccm[3][3] = {
    { 1.85, -0.65, -0.20 },
    { -0.35,  1.60, -0.25 },
    { 0.05, -0.40,  1.35 }
};

// 应用CCM
for each pixel (r, g, b) {
    r_out = ccm[0][0]*r + ccm[0][1]*g + ccm[0][2]*b;
    g_out = ccm[1][0]*r + ccm[1][1]*g + ccm[1][2]*b;
    b_out = ccm[2][0]*r + ccm[2][1]*g + ccm[2][2]*b;
}

这里有个坑:CCM的系数有正有负,负系数意味着要去掉传感器通道之间的串扰。如果负系数值太大,会导致某些颜色区域出现色阶断层或负值截断。

我的经验:在Qualcomm平台上,我们通常会在标定CCM时加入一个“饱和度-噪声”的权衡参数。饱和度高意味着矩阵系数更“锐”,但噪声也会被放大。对于低光场景,我会适当降低CCM的饱和度,换取更干净的暗部。

4.2.2 多色温CCM切换

一个CCM矩阵不能覆盖所有色温。因为传感器的光谱响应会随色温变化。所以实际产品中,我们会标定多个色温下的CCM(比如D65、D50、A光源各一组),然后根据AWB输出的色温值做线性插值。

色温区间 使用CCM 适用场景
> 5500K D65 CCM 晴天户外、阴天
4000K - 5500K D50 CCM 室内混合光、荧光灯
< 4000K A光源 CCM 白炽灯、烛光

4.3 Gamma校正与色调映射—— 让亮度“看起来”对

Gamma校正的起源很有意思。早期的CRT显示器,输入电压和输出亮度之间不是线性关系,而是近似2.2次幂的关系。为了补偿这个特性,相机端需要预先做1/2.2次幂的编码,这就是Gamma校正。

但今天,Gamma校正的意义已经超越了“补偿显示器”。它本质上是一种感知量化——人眼对暗部细节更敏感,对亮部不敏感。Gamma曲线把更多的码字分配给暗部,让暗部量化更精细,减少banding。

4.3.1 标准Gamma曲线

标准的sRGB Gamma曲线是一个分段函数:

// sRGB Gamma 编码
float srgb_gamma(float linear) {
    if (linear <= 0.0031308) {
        return 12.92 * linear;
    } else {
        return 1.055 * pow(linear, 1.0/2.4) - 0.055;
    }
}

// 解码(反向Gamma)
float srgb_inv_gamma(float encoded) {
    if (encoded <= 0.04045) {
        return encoded / 12.92;
    } else {
        return pow((encoded + 0.055) / 1.055, 2.4);
    }
}

注意看,sRGB Gamma用的是2.4次幂,而不是2.2。这是因为在暗部加了一段线性区(斜率12.92),用来抑制噪声放大。

4.3.2 色调映射:从HDR到SDR

现代手机传感器支持HDR(高动态范围),可以捕捉超过12档的动态范围。但显示设备(屏幕)通常只能显示6-8档。所以我们需要色调映射(Tone Mapping),把宽动态范围压缩到窄动态范围,同时保留视觉细节。

在Qualcomm平台上,色调映射通常和Gamma校正合并处理,统称为LUT(Look-Up Table)。我们会在标定阶段生成一条256点的映射曲线,把12bit的线性数据映射到8bit的非线性输出。

关键点:色调映射不是简单的线性压缩。好的色调映射应该做到:

  • 暗部提亮,保留细节
  • 亮部滚降,防止过曝
  • 中间调保持对比度,不显得“灰”

我记得有一次调试夜景模式,默认的色调映射曲线把暗部提得太亮,结果天空变成了灰蒙蒙一片,失去了夜晚的氛围感。后来我手动调整了曲线的暗部斜率,让暗部稍微压暗一点,反而更接近人眼看到的真实夜景。

避坑指南:千万不要在Gamma校正之后再做大幅度的亮度调整。Gamma曲线已经对亮度做了非线性编码,后续的线性调整会破坏感知均匀性,导致颜色偏移。我曾经见过一个团队在Gamma之后又加了一个亮度滑块,结果红色饱和度和亮度一起变化,怎么调都不对。

4.4 本章小结

这一章我们走完了RGB域增强的三个关键步骤:

  • AWB:解决“光源是什么颜色”的问题,让白色回归白色
  • CCM:解决“传感器颜色不准”的问题,把颜色映射到标准色域
  • Gamma + 色调映射:解决“亮度感知和显示适配”的问题,让图像看起来自然

这三个模块在ISP Pipeline中是串联执行的,顺序不能乱。AWB做错了,CCM跟着错;CCM没调好,Gamma再漂亮也没用。做ISP调试,本质上就是在做“全局的权衡”——你要在色温准确性、颜色饱和度、噪声水平、动态范围之间找到一个平衡点。