4. RGB域图像增强
各位同学,欢迎来到第四章。前面我们聊了Bayer域的去噪和去马赛克,图像终于从“黑白马赛克”变成了“彩色点阵”。但这时候的颜色,说实话,还不太靠谱。
你想想看,同样一张白纸,在日光灯下拍出来偏蓝,在白炽灯下拍出来偏黄。人眼会自动修正这种色偏,但CMOS传感器不会。所以,我们需要一套机制来告诉相机:“嘿,现在的光源是啥颜色?你得把白色给我还原回来。”
这一章,我们就来拆解RGB域的三个核心模块:白平衡(AWB)、颜色校正矩阵(CCM)、Gamma校正与色调映射。这三个模块串联起来,决定了你最终看到的颜色准不准、好不好看。
核心逻辑链:
AWB负责“色温适应” → CCM负责“传感器到标准色域的转换” → 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调试,本质上就是在做“全局的权衡”——你要在色温准确性、颜色饱和度、噪声水平、动态范围之间找到一个平衡点。