色彩校正矩阵: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 个系数怎么来?靠猜?当然不是。我们用多项式回归,说白了就是“用数据说话”。

具体做法:

  1. 拍一张标准色卡(比如 X-Rite ColorChecker)
  2. 记录每个色块的 sensor 输出 RGB 值
  3. 查表得到每个色块的标准 sRGB 值
  4. 建立方程组,求解最优的 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。

CCM 求解与优化流程 输入:色卡 RAW 数据 白平衡校正 提取色块 RGB + 标准值 最小二乘法求解 CCM 评估:ΔE / 肤色偏差 不满足则调整权重 输出:最终 CCM 矩阵 关键参数 • 色块数:≥ 9(建议 18) • 权重:肤色区域权重 10x • 约束:输出 RGB ≥ 0 常见问题 • 肤色偏黄 → 增加肤色权重 • 高光偏色 → 分段 CCM