色彩校正矩阵:CCM 原理与实战
各位同学,欢迎来到第三章。上一章我们聊了白平衡,把色温拉正了。但你会发现,白平衡后的图像颜色还是“不对”。比如红色偏品、绿色偏黄。为什么?因为 sensor 的响应曲线和人眼不一样。这时候,就需要色彩校正矩阵(CCM)登场了。
说白了,CCM 就是一组 3x3 的系数,把相机拍到的 RGB 值,映射到人眼感知的“真实”RGB 值。我刚开始做 ISP 时,总觉得这步是玄学,直到亲手调过几轮,才发现它其实是数学。
核心公式:
[R'] [a11 a12 a13] [R]
[G'] = [a21 a22 a23] * [G]
[B'] [a31 a32 a33] [B]
其中 R'G'B' 是校正后的颜色,RGB 是白平衡后的输入,a11~a33 就是我们要找的 9 个系数。
CCM 原理:为什么是 3x3?
你想想看,每个像素的 R、G、B 三个通道,理论上会互相串扰。比如 sensor 的红色通道里,可能混入了少量绿色和蓝色信息。CCM 就是通过线性组合,把这些串扰“解耦”掉。
我在项目中遇到过一种情况:用某款 sensor 拍蓝天,白平衡后天空发紫。排查了半天,发现是 sensor 的蓝色通道对近红外太敏感。最后靠 CCM 里的负系数,硬是把多余的红色分量减掉了。嗯,这里要注意:CCM 系数可以是负数,但最终输出不能为负,否则会截断。
多项式回归:从数据中学习
那这 9 个系数怎么来?靠猜?当然不是。我们用多项式回归,说白了就是“用数据说话”。
具体做法:
- 拍一张标准色卡(比如 X-Rite ColorChecker)
- 记录每个色块的 sensor 输出 RGB 值
- 查表得到每个色块的标准 sRGB 值
- 建立方程组,求解最优的 9 个系数
我习惯用 24 色卡,但实际只取其中 18 个非灰阶色块。为什么?因为灰阶主要用来调白平衡,对 CCM 贡献不大,反而会拉偏彩色区域的精度。
最小二乘法求解:数学之美
方程组通常是超定的(色块数 > 9),所以没有精确解。我们用最小二乘法,让校正后的颜色与标准颜色的误差平方和最小。
数学上,就是解这个正规方程:
X = (A^T * A)^(-1) * A^T * B
其中 A 是 sensor 输出矩阵(Nx3),B 是标准值矩阵(Nx3),X 就是我们要的 3x3 CCM。
代码实现其实很简单,我用 Python 写过:
import numpy as np
def solve_ccm(sensor_rgb, target_rgb):
# sensor_rgb: (N, 3) 矩阵
# target_rgb: (N, 3) 矩阵
A = np.hstack([sensor_rgb, np.ones((sensor_rgb.shape[0], 1))])
# 这里我加了一列偏置项,处理 sensor 的黑电平偏移
X, _, _, _ = np.linalg.lstsq(A, target_rgb, rcond=None)
return X[:3, :] # 返回 3x3 矩阵
我的小技巧: 求解前一定要做数据归一化。把 sensor 输出和标准值都归一化到 [0,1] 区间,否则大数值通道会主导误差,导致小数值通道(比如蓝色)精度很差。
肤色保护策略:别把脸调成“鬼”
这是 CCM 里最头疼的部分。你按色卡调出来的 CCM,在大部分场景下表现不错。但一到人像,肤色可能偏黄、偏绿,甚至偏紫。为什么?因为色卡的颜色分布太均匀了,而真实世界的肤色,恰好落在 CCM 误差较大的区域。
我曾经吃过这个亏。第一次调 CCM,对着色卡调了三天,误差降到 1% 以内。结果拿去拍人,人脸像得了黄疸。后来才明白:最小二乘法追求的是“全局误差最小”,但人眼对肤色极其敏感,哪怕 2% 的色差,看起来都像生病了。
解决方案有两种:
- 加权最小二乘法: 给肤色区域更高的权重。比如把肤色色块的权重设为 10,其他色块设为 1。这样求解时,肤色区域的误差会被优先降低。
- 后处理约束: 在 CCM 之后,单独对肤色区域做一次微调。比如检测到肤色像素后,往红色方向拉一点,降低黄色分量。
我个人的习惯是两者结合。先用加权法得到一个基础 CCM,再在肤色区域做 5% 以内的微调。注意,微调幅度不能太大,否则会破坏其他颜色的准确性。
避坑指南: 我曾经试过用神经网络直接预测 CCM,效果反而不好。因为神经网络会学到训练集里的噪声,导致泛化能力差。最小二乘法虽然“笨”,但稳定可靠,适合量产。
CCM 的评估指标
调完 CCM 怎么判断好坏?我一般看三个指标:
| 指标 | 含义 | 我的经验值 |
|---|---|---|
| ΔE (CIE Lab) | 色差,越小越好 | 平均 ΔE < 3,最大 ΔE < 8 |
| 肤色 ΔE | 单独看肤色区域 | 平均 ΔE < 2 |
| 饱和度偏差 | 颜色是否过艳或过淡 | 偏差 < 10% |
你想想看,如果平均 ΔE 只有 2,但肤色 ΔE 到了 5,那这组 CCM 还是不能用。所以评估时一定要分区域看。
CCM 的局限性
最后说一句大实话:CCM 是线性模型,它假设 sensor 的响应是线性的。但现实世界是非线性的。比如高光区域容易过曝,暗部区域噪声大,CCM 在这些区域的表现会变差。
我遇到过最极端的情况:用同一组 CCM,室内灯光下肤色正常,一到户外阳光下,人脸直接变成“关公”。后来发现是 sensor 在高光区的响应曲线发生了弯曲,线性 CCM 无法补偿。解决方案是分段 CCM:根据亮度不同,使用不同的系数矩阵。
嗯,这个我们后面章节会详细讲。今天先到这里,大家回去可以试试用 Python 跑一下最小二乘法,看看你手上的色卡数据能算出什么样的 CCM。