4. 传统 ISP Pipeline 进阶:白平衡、色彩校正矩阵、Gamma 校正、色调映射

好,咱们继续往下走。上一章我们把 ISP 的基础流程捋了一遍,从 RAW 域到 RGB 域,算是打了个底。这一章,咱们要深入进去,聊聊几个真正决定「画质」的关键模块——白平衡、色彩校正矩阵、Gamma 校正,还有色调映射。

这几个模块,说白了,就是让相机「看懂」颜色,并且把颜色「漂亮地」呈现出来。我当年刚接触 ISP 时,觉得白平衡不就是调个色温嘛,有什么难的?后来在项目里被各种偏色问题折磨得够呛,才明白这里面的水有多深。

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

白平衡,全称是自动白平衡(Auto White Balance, AWB)。它的目标很简单:不管光源是日光灯、白炽灯还是阴天,都要让画面中的白色物体看起来是白的。

为什么需要这个?因为不同光源的色温不同。色温低的光(比如蜡烛),偏红;色温高的光(比如阴天),偏蓝。人眼会自动适应,但 CMOS 传感器不会。它老老实实地记录下真实的光谱,结果就是——在暖色灯下拍的照片,整个画面都黄黄的。

白平衡的核心,就是估计当前光源的色温,然后对 R、G、B 三个通道分别施加增益,把「偏色」拉回来。

核心公式(简化版):

R_out = R_in * gain_R
G_out = G_in * gain_G
B_out = B_in * gain_B

其中 gain_G 通常固定为 1.0,gain_R 和 gain_B 根据色温查表或计算得到。

我个人的习惯是,在项目中优先使用「灰度世界」算法做粗调,再用「色温曲线」做精调。灰度世界假设场景中所有颜色的平均值是灰色,这个假设在很多场景下都成立,但遇到大面积单色物体(比如一片蓝天、一面红墙)时,就会翻车。

避坑指南:我曾经在一个无人机航拍项目里,用了纯灰度世界算法。结果飞机飞到一片绿色麦田上空,整个画面被算法强行拉成了紫色。后来我加了一个「置信度检测」——如果场景中某个颜色占比过高,就降低灰度世界算法的权重,改用色温估计。问题才解决。

常见的白平衡算法对比:

算法 原理 优点 缺点
灰度世界 假设场景平均色为灰色 简单、计算快 大面积单色场景失效
完美反射 假设场景中最亮点为白色 对高光场景效果好 无高光时失效
色温估计 基于色温曲线查表 稳定、物理意义明确 需要标定、泛化性一般
基于学习 用 CNN 直接回归增益 鲁棒性强、适应复杂场景 计算量大、需要数据集

4.2 色彩校正矩阵(CCM)—— 把颜色「掰正」

白平衡做完之后,颜色大致对了,但还不够准。为什么?因为 CMOS 传感器的色彩滤镜(CFA)的响应曲线,和人眼视锥细胞的响应曲线不一样。说白了,传感器看到的「红色」,和人眼看到的「红色」,不是同一个红色。

色彩校正矩阵(Color Correction Matrix, CCM)就是干这个的。它是一个 3x3 的矩阵,作用是把传感器 RGB 空间转换到标准 RGB 空间(比如 sRGB 或 BT.709)。

CCM 公式:

[R_out]   [a11 a12 a13]   [R_in]
[G_out] = [a21 a22 a23] * [G_in]
[B_out]   [a31 a32 a33]   [B_in]

这个矩阵怎么来的?靠标定。用相机拍一张标准色卡(比如 X-Rite ColorChecker),然后通过最小二乘法,让拍出来的颜色和标准颜色之间的误差最小。

嗯,这里要注意一点:CCM 的系数可以是负数。你想想看,为了把某个偏红的颜色拉回标准红色,可能需要从红色通道里减去一点绿色或蓝色。负系数意味着可能会产生「溢出」或「截断」的问题,这是实际工程中需要处理的细节。

我的经验:在调试 CCM 时,不要只看色卡的 ΔE 值(色差)。ΔE 很小不代表肤色好看。我一般会在标定完 CCM 后,专门拍几张不同人种的肤色照片,用肉眼确认一下。毕竟,用户最敏感的是人脸颜色,而不是色卡上的第 18 号色块。

4.3 Gamma 校正 —— 人眼不是线性器件

Gamma 校正,这个名字你可能听过很多次了。它的本质,是对图像进行非线性映射。

为什么需要非线性?两个原因:

  1. 人眼的亮度感知是非线性的。人眼对暗部细节更敏感,对亮部细节相对迟钝。如果直接线性存储亮度,暗部会浪费大量码字,亮部又不够用。
  2. 显示设备的响应也是非线性的。老式 CRT 显示器,输入电压和输出亮度之间是幂律关系,Gamma 值大约 2.2。现在的 LCD/LED 显示器,虽然物理原理不同,但为了兼容,依然沿用了 Gamma 2.2 的标准。

标准的 Gamma 校正曲线:

V_out = V_in ^ (1 / gamma)

其中 gamma 通常取 2.2。也就是说,在编码端(相机),我们对图像做 1/2.2 次幂的映射;在解码端(显示器),再做 2.2 次幂的映射。两者相乘,正好是线性。

注意:Gamma 校正是在线性 RGB 空间做的。也就是说,它必须在白平衡和 CCM 之后执行。如果顺序搞反了,颜色会完全乱掉。我见过有新手把 Gamma 放在 CCM 前面,结果出来的颜色怎么调都不对——因为非线性空间下,矩阵乘法不再成立。

Gamma 校正的另一个作用,是视觉上的「对比度增强」。它把暗部拉开,把亮部压缩,让画面看起来更通透。但这也意味着,亮部的细节可能会丢失。所以,高动态范围(HDR)场景下,单纯的 Gamma 校正是不够的,需要引入色调映射。

4.4 色调映射(Tone Mapping)—— 把大动态塞进小容器

现实世界的动态范围,可以轻松超过 100dB(比如从阴影到太阳直射)。而普通的 8-bit 显示器,只有大约 48dB 的动态范围。色调映射(Tone Mapping)要解决的问题就是:如何把宽动态范围的场景,压缩到显示设备能呈现的范围内,同时保留尽可能多的细节。

色调映射算法可以分为两大类:

  • 全局映射:对整幅图像使用同一个映射曲线。优点是简单、高效;缺点是局部对比度可能不足。
  • 局部映射:根据像素周围的局部亮度,自适应调整映射曲线。优点是细节保留好;缺点是容易出现光晕(Halo)伪影,计算量大。

我最早接触色调映射,是在做手机 HDR 模式的时候。那时候手机 CMOS 的动态范围也就 60dB 左右,但拍逆光场景,天空一片死白,人脸一片死黑。后来用了多帧融合 + 色调映射,效果才勉强能看。

一个经典的全局色调映射函数(Reinhard 算子):

L_out = L_in / (1 + L_in)

其中 L_in 是归一化后的亮度值(0~∞),L_out 是映射后的亮度值(0~1)。这个函数能把任意大的输入值压缩到 [0, 1) 区间内。

但 Reinhard 算子有个问题:它会把整体亮度压暗。所以实际工程中,通常会先做一个「自动曝光」的预处理,把场景的平均亮度映射到中间灰,然后再做色调映射。

我个人比较喜欢用「分段映射」的思路:暗部用 Gamma 曲线提亮,亮部用对数曲线压缩,中间区域平滑过渡。这样既能保证暗部细节,又能防止高光过曝。

4.5 本章知识体系

下面这张图,把本章四个核心模块的流程和关系画出来了。你可以看到,白平衡和 CCM 负责「颜色准确性」,Gamma 和色调映射负责「亮度感知优化」。它们之间是串联关系,顺序不能乱。

ISP 进阶模块流程 ISP 进阶模块流程 RAW 域数据 白平衡 (AWB) 色彩校正 (CCM) Gamma 校正 色调映射 关键顺序 1. 先做颜色校正(AWB+CCM) 2. 再做亮度映射(Gamma+Tone) 3. 顺序颠倒会导致颜色错误 输出 RGB 图像

这四个模块,每一个单独拿出来都可以讲一整天。但在实际项目中,它们往往是作为一个整体来调优的。你调整了 CCM,可能会影响白平衡的增益;你改了色调映射的曲线,可能会让 Gamma 校正的效果变得不明显。所以,我的建议是:先理解每个模块的物理意义,再通过实验找到它们之间的平衡点。

调试小技巧:在调试 ISP Pipeline 时,我习惯在每个模块后面都加一个「旁路开关」。这样我可以随时 bypass 某个模块,快速定位问题出在哪一步。比如,如果画面偏色,我可以先 bypass 掉 CCM,看看是不是白平衡的问题;如果 bypass 后颜色正常了,那问题就在 CCM 上。