2、色彩还原核心算法:色彩校正矩阵(CCM)、Gamma校正、色调映射(Tone Mapping)
好,咱们直接进入正题。色彩还原这件事,说白了就是让摄像头看到的颜色,跟你人眼看到的颜色尽可能一致。但这里头坑不少——传感器有它的“色盲区”,显示器有它的“非线性脾气”,高动态场景还有亮度溢出问题。所以这一章,我带你拆解三个最核心的算法模块:CCM、Gamma、Tone Mapping。
一句话总结本章脉络:CCM 负责把传感器颜色“掰正”,Gamma 负责匹配显示设备的非线性响应,Tone Mapping 负责把高动态范围“压”进低动态范围的同时保留细节。
2.1 色彩校正矩阵(CCM)——把偏色“算”回来
先问个问题:为什么 raw 图直出总是偏色?因为 CMOS 上的 Bayer 滤色片(R/G/B)的光谱响应跟人眼锥细胞的光谱响应不一样。说白了,传感器是个“色盲”,它看到的红绿蓝跟我们看到的不是一回事。
CCM 就是干这个的——用一个 3×3 的矩阵,把传感器 RGB 空间映射到标准色彩空间(比如 sRGB 或 BT.709)。
数学形式
| R_out | | a11 a12 a13 | | R_in |
| G_out | = | a21 a22 a23 | * | G_in |
| B_out | | a31 a32 a33 | | B_in |
每个输出通道都是三个输入通道的线性组合。举个例子,R_out 可能包含一部分 G_in 和 B_in,因为传感器的红色通道可能“漏”进了绿色光。
矩阵怎么来的?
我个人习惯用 24 色卡(X-Rite ColorChecker)做标定。拍一张色卡图,提取每个色块的传感器 RGB 值,再跟标准值做最小二乘拟合。嗯,这里有个坑——
我曾经踩过的坑:CCM 矩阵对光源极其敏感。你在 D65 光源下标定出来的矩阵,拿到 A 光源(白炽灯)下用,颜色直接翻车。所以实际产品里,我一般会准备 3~5 组 CCM,根据 AWB 估算的色温做插值切换。
CCM 的典型参数对比
| 光源类型 | 色温范围 | CCM 组数 | 典型应用场景 |
|---|---|---|---|
| D65(日光) | 6500K | 1 组主矩阵 | 户外、标准测试 |
| D50(印刷标准) | 5000K | 1 组 | 专业摄影、扫描 |
| A 光源(白炽灯) | 2856K | 1 组 | 室内暖光 |
| TL84(荧光灯) | 4000K | 1 组 | 办公室、商场 |
小技巧:CCM 的系数总和最好接近 1.0,否则会改变亮度。我一般会在拟合后加一个归一化约束,保证亮度不漂移。
2.2 Gamma 校正——给显示器“顺毛”
你想想看,为什么同一张图在电脑上看和在手机上亮度感觉不一样?因为显示器的亮度响应是非线性的——输入电压翻倍,亮度并不会翻倍。CRT 时代留下的 Gamma 2.2 标准,到现在还在用。
Gamma 校正的本质就是:在编码端做一次反向非线性,抵消显示端的非线性。
公式
V_out = V_in ^ (1 / γ)
γ 通常取 2.2。如果输入是 0~1 的线性值,输出就是经过 Gamma 编码后的值。
我遇到的实际问题
有一次做手机摄像头调试,发现拍出来的照片在电脑上看偏暗。查了半天,原来是 ISP 里做了两次 Gamma——一次在 raw 域,一次在 RGB 域。两次叠加等于 Gamma 4.4,不暗才怪。所以记住:Gamma 只能做一次,位置通常在 CCM 之后、Tone Mapping 之前。
关键点:Gamma 校正不是“美化”,而是“匹配”。它让图像在目标显示设备上看起来跟人眼感知一致。
2.3 色调映射(Tone Mapping)——把“过曝”和“死黑”救回来
HDR 场景下,最亮处可能 10000 nits,最暗处只有 0.1 nits。但你的屏幕只能显示 0~300 nits。怎么办?硬压肯定不行——亮部变灰,暗部一片黑。
Tone Mapping 就是干这个的:把高动态范围的亮度,压缩到低动态范围的同时,保留对比度和细节。
主流方法
- 全局映射(Global TM):整张图用一个映射曲线。简单,但容易丢失局部对比度。
- 局部映射(Local TM):根据每个像素周围的亮度做自适应映射。效果好,但计算量大,容易产生光晕(halo)。
- 基于 Retinex 的方法:把图像分解成光照层和反射层,只压缩光照层。我比较喜欢这种方法,物理意义清晰。
一个简单的全局映射示例(伪代码)
// 输入:线性 HDR 亮度 L (0~10000)
// 输出:显示亮度 L_disp (0~1)
// Reinhard 全局映射
L_disp = L / (1 + L)
// 或者带白点保护的改进版
L_white = 1000 // 白点阈值
L_disp = (L * (1 + L / (L_white * L_white))) / (1 + L)
我曾经踩过的坑:局部 Tone Mapping 的光晕问题。有一次在强逆光人像上做局部映射,人物边缘出现了一圈亮边,像“天使光环”。后来改用引导滤波(Guided Filter)做边缘保持的权重计算,才把这个问题压下去。
2.4 三者的串联流程
这三个模块不是各自为战的。在实际 ISP Pipeline 里,它们的顺序是固定的:
- Raw 域预处理(黑电平校正、去噪、去马赛克)
- CCM:把传感器 RGB 映射到标准色彩空间
- Gamma 校正:做非线性编码
- Tone Mapping:压缩动态范围
- 后续处理(锐化、饱和度调整、压缩输出)
顺序不能乱。如果先做 Tone Mapping 再做 Gamma,你会发现颜色饱和度会漂移——因为 Tone Mapping 改变了亮度,而 Gamma 对亮度敏感。
2.5 避坑指南与经验总结
- CCM 的精度:不要用 8bit 数据算 CCM,至少用 12bit raw 数据。我见过有人用 8bit JPEG 算矩阵,结果误差大得离谱。
- Gamma 的位数:Gamma 校正后至少保留 10bit 精度,否则暗部会出现条带(banding)。
- Tone Mapping 的亮度映射:如果场景里有大面积高亮(比如天空),全局映射会把整个画面压暗。我建议加一个亮度直方图分析,动态调整映射曲线的 knee 点。
- 调试顺序:先调 CCM 让颜色准,再调 Gamma 让亮度感知对,最后调 Tone Mapping 处理高动态场景。顺序反了,你会陷入“调了颜色又变、调了亮度又偏”的死循环。
我的个人习惯:在调试阶段,我会把 CCM、Gamma、Tone Mapping 的中间结果都 dump 出来,用肉眼对比。很多时候,问题出在“某个模块的边界条件没处理好”,而不是算法本身不对。
好了,这一章的内容就到这儿。三个算法——CCM、Gamma、Tone Mapping——是色彩还原的三大支柱。理解它们的原理、顺序和坑,你就能在 ISP 调试里少走很多弯路。