3. ISP核心模块解析(中):RGB域处理
上一节我们把RAW域的几个关键模块聊完了。今天咱们进入RGB域。说白了,就是从传感器拿到的原始数据,经过解马赛克变成RGB三通道之后,怎么让颜色看起来「对」。
RGB域处理,我个人认为是ISP里最「玄学」的部分。为什么?因为RAW域处理大多有物理依据——坏点就是坏点,黑电平就是黑电平。但RGB域处理,尤其是白平衡和颜色校正,很大程度上取决于「你觉得什么颜色好看」。我在项目里见过不少团队,算法跑分很高,但拍出来的照片就是「不对味」。嗯,这里面的门道,咱们一个一个说。
3.1 白平衡(AWB)——让白色回归白色
白平衡的目标很简单:不管在什么光源下,白色物体拍出来都应该是白的。但实现起来,远没那么简单。
你想想看,人眼有色彩恒常性——日光灯下的一张白纸,到了白炽灯下,我们看着还是白的。但传感器没这个本事。它老老实实地记录光谱,结果就是:日光灯下偏蓝,白炽灯下偏黄。
白平衡要做的,就是估算当前光源的色温,然后对RGB三个通道分别乘一个增益,把「偏色」拉回来。
3.1.1 经典算法:灰度世界与白点检测
业界最常用的两种思路:
- 灰度世界法:假设场景中所有颜色的平均值是灰色。那么R、G、B三个通道的平均值应该相等。如果不相等,就调整增益让它们相等。这个方法简单,但遇到大面积单色场景(比如一片蓝天)就崩了。
- 白点检测法:在画面中找出「应该是白色」的区域,然后用这些区域的RGB值来估算色温。这个方法更准,但难点在于——你怎么知道哪些区域是白色的?
我在项目中遇到过一个问题:用白点检测法,在室内暖光灯下拍白墙,效果很好。但同一个算法拿到户外拍雪景,白点检测器把雪地误判成「偏蓝」,结果算法拼命加红色,雪变成粉色的了。后来怎么解决的?加了一个场景分类器,先判断是室内还是室外,再切换不同的白点检测策略。
3.1.2 色温估计与增益计算
估算出色温之后,怎么算增益?这里有个标准流程:
- 查表得到当前色温下的标准R/G、B/G比值。
- 用实际R/G、B/G与标准值做除法,得到增益。
- 限制增益范围(通常0.5~2.0),防止噪声放大。
举个例子:
// 假设标准D65光源下 R/G = 0.45, B/G = 0.65
// 实际测得 R/G = 0.38, B/G = 0.72
R_gain = 0.45 / 0.38 = 1.184
B_gain = 0.65 / 0.72 = 0.903
G_gain = 1.0 // 绿色通道通常作为参考
你看,实际R/G偏小,说明红色不够,所以R_gain > 1。实际B/G偏大,说明蓝色太多,所以B_gain < 1。这就是白平衡的本质——补不足,损有余。
3.2 颜色校正矩阵(CCM)——把「偏色」掰回来
白平衡做完,颜色看起来「差不多」了。但还不够。为什么?因为传感器的光谱响应和人眼不一样。
人眼有三种锥细胞,每种对红绿蓝的敏感曲线是固定的。但CMOS传感器的滤色片(CFA)做不到和人眼完全一致。结果就是:同一个物体,传感器看到的颜色和人眼看到的颜色有偏差。CCM就是用来修正这个偏差的。
3.2.1 CCM的数学本质
说白了,CCM就是一个3x3的矩阵:
[ R_out ] [ a11 a12 a13 ] [ R_in ]
[ G_out ] = [ a21 a22 a23 ] * [ G_in ]
[ B_out ] [ a31 a32 a33 ] [ B_in ]
每个输出通道,都包含了输入通道的线性组合。比如R_out = a11*R_in + a12*G_in + a13*B_in。这意味着什么?意味着红色通道里可以混入一点绿色和蓝色,用来修正传感器的光谱偏差。
我在项目中见过一个典型问题:某款传感器对红色敏感度偏高,拍出来的红色总是「过艳」。用CCM把a11调低到0.85,同时把a12(绿色混入红色)设为0.15,效果立竿见影。但要注意,矩阵系数不能太大,否则会引入颜色噪声。
3.2.2 矩阵标定与调优
CCM的系数怎么来?不是拍脑袋定的。标准做法是用24色卡:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 拍摄色卡 | 在D65光源下拍摄标准24色卡 |
| 2 | 提取色块值 | 取每个色块中心区域的RGB平均值 |
| 3 | 对比标准值 | 与色卡的标准Lab值做对比 |
| 4 | 最小二乘法拟合 | 求解3x3矩阵,使色差最小 |
| 5 | 主观调优 | 根据肤色、蓝天等典型场景微调 |
这里我要强调一点:客观指标(比如ΔE色差)和主观感受经常打架。我记得有一次,客观指标优化到了ΔE < 2,但测试人员反馈肤色「发青」。后来发现是矩阵对肤色区域的拟合过于追求整体精度,牺牲了局部色调。最后我们给肤色区域加了权重,才平衡过来。
3.3 Gamma校正——让亮度「听话」
Gamma校正,很多人觉得就是套一个幂函数。没错,数学上确实简单。但为什么需要它?这里面有两个原因:
- 人眼感知特性:人眼对暗部变化更敏感,对亮部变化不敏感。线性编码会浪费大量码字在亮部,暗部却不够用。
- 显示设备特性:CRT显示器、LCD屏幕,都有非线性的亮度响应。如果不做Gamma校正,显示出来的图像会偏暗或偏亮。
3.3.1 标准Gamma曲线
最常用的Gamma曲线是:
V_out = V_in ^ (1/γ)
其中 γ 通常取 2.2
输入是线性光强,输出是编码后的值。γ=2.2时,暗部被拉伸,亮部被压缩。这样,8bit的码字(0~255)就能更好地匹配人眼的感知。
但实际工程中,很少直接用纯幂函数。为什么?因为纯幂函数在暗部斜率太大,会放大暗部噪声。我见过一个方案,暗部直接用线性段替代,到一定阈值再切换为幂函数。这样既保证了暗部信噪比,又保留了Gamma的压缩特性。
3.3.2 分段Gamma与自适应Gamma
现在手机ISP里,更常用的是分段Gamma:
| 区间 | 输入范围 | 曲线类型 | 作用 |
|---|---|---|---|
| 暗部 | 0 ~ 0.1 | 线性(斜率2.0~3.0) | 抑制噪声放大 |
| 中间调 | 0.1 ~ 0.5 | 幂函数(γ=2.2) | 保持对比度 |
| 亮部 | 0.5 ~ 1.0 | 幂函数(γ=1.8~2.0) | 防止高光过曝 |
自适应Gamma就更高级了。它会根据场景的直方图分布,动态调整曲线形状。比如夜景模式,暗部区域大,就把暗部斜率调高,提亮阴影。逆光场景,亮部区域大,就把亮部压缩得更狠一些。
3.4 三个模块的协同关系
白平衡、CCM、Gamma校正,不是各自为战的。它们之间有严格的顺序依赖:
- 先做白平衡:消除光源色温的影响,让颜色回到「中性」。
- 再做CCM:修正传感器的光谱偏差,让颜色更准确。
- 最后做Gamma:适配显示设备,同时做动态范围压缩。
为什么顺序不能乱?因为CCM假设输入已经是白平衡后的数据。如果先做Gamma再做CCM,非线性变换会破坏颜色的线性关系,CCM就失效了。同样,如果先做CCM再做白平衡,CCM会把色温偏差也「校正」进去,导致颜色混乱。
下面这张图,是我在项目中常用的RGB域处理流程:
你看,这个流程是单向的、线性的。每个模块的输出质量,直接影响下一个模块的效果。白平衡偏了,CCM再怎么调也救不回来。Gamma曲线设错了,前面再准的颜色也显示不对。
所以,做ISP设计,不能只盯着单个模块的指标。要站在整个链路上看问题。我个人的习惯是:先保证白平衡的稳定性,再优化CCM的准确性,最后用Gamma做微调。每一步都留有余地,给后面的模块留出调整空间。