4、色彩校正基础:CCM推导、最小二乘法与肤色保护
色彩校正矩阵(CCM)这东西,说白了就是把传感器拍到的原始RGB值,映射到人眼真正看到的颜色空间。我刚开始做ISP调试那会儿,总觉得这步很简单——不就是个3x3矩阵嘛。后来被一个偏色严重的案子狠狠教育了一顿,才发现里面门道不少。
今天咱们就把CCM的推导、求解和肤色保护策略,一次性讲透。
4.1 为什么需要色彩校正矩阵?
先看一个事实:CMOS传感器的光谱响应曲线,和人眼视锥细胞的响应曲线,长得完全不一样。不信你拿相机拍一张标准色卡,直接输出的RAW图,颜色一定是偏的——红色不够正,蓝色可能发紫。
CCM的作用就是做这个线性变换:
[R'] [a11 a12 a13] [R]
[G'] = [a21 a22 a23] [G]
[B'] [a31 a32 a33] [B]
其中R、G、B是传感器原始值,R'、G'、B'是校正后的目标值。矩阵里的9个系数,就是我们要解的。
4.2 CCM推导:从色卡到矩阵
推导过程其实不复杂。我们拿一张标准24色卡(比如X-Rite ColorChecker),在受控光源下拍摄。然后测量两个东西:
- 传感器输出值:每个色块在RAW图上的RGB均值
- 目标参考值:同一色块在标准光源下的Lab或sRGB值
有了这24组对应关系,问题就变成了:找一个3x3矩阵,让传感器值经过变换后,尽可能接近目标值。
写成数学形式:
对于第i个色块:
R'_i = a11*R_i + a12*G_i + a13*B_i
G'_i = a21*R_i + a22*G_i + a23*B_i
B'_i = a31*R_i + a32*G_i + a33*B_i
我们希望 R'_i ≈ R_ref_i, G'_i ≈ G_ref_i, B'_i ≈ B_ref_i
嗯,这里要注意:我们通常把RGB值归一化到[0,1]区间再算,不然数值量级差异会影响求解精度。
4.3 最小二乘法求解:让误差最小
9个未知数,24组方程,这是个超定系统。最常用的解法就是最小二乘法。
我习惯把问题拆成三个独立的子问题——分别对R、G、B通道求解。以R通道为例:
对于24个色块,构建矩阵方程:
[R_ref_1] [R_1 G_1 B_1] [a11]
[R_ref_2] = [R_2 G_2 B_2] [a12]
[ ... ] [ ... ... ] [a13]
[R_ref_24] [R_24 G_24 B_24]
简写为:Y = X * A
最小二乘解就是:
A = (X^T * X)^{-1} * X^T * Y
代码实现其实很简洁:
import numpy as np
def solve_ccm(sensor_rgb, ref_rgb):
"""
sensor_rgb: (N, 3) 传感器原始值
ref_rgb: (N, 3) 目标参考值
返回: (3, 3) CCM矩阵
"""
ccm = np.zeros((3, 3))
for ch in range(3):
Y = ref_rgb[:, ch] # 目标通道值
X = sensor_rgb # 传感器RGB
A, _, _, _ = np.linalg.lstsq(X, Y, rcond=None)
ccm[ch, :] = A
return ccm
4.4 肤色保护策略:为什么需要?怎么做?
肤色保护,说白了就是:在整体色彩校正的同时,保证人脸肤色不偏绿、不偏黄、不偏紫。
我在一个手机项目上吃过亏。当时用标准色卡调出来的CCM,拍风景和静物都很好,但一拍人像,脸色发青。客户直接打回重调。后来我加了肤色保护,才过。
常用的肤色保护策略有三种:
| 策略 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 加权最小二乘 | 给肤色色块更高的权重 | 实现简单,效果可控 | 需要手动标定肤色区域 |
| 色域约束 | 限制CCM输出在肤色色域内 | 不会出现离谱偏色 | 可能牺牲整体精度 |
| 后处理补偿 | 先做全局CCM,再对肤色区域做二次校正 | 灵活性高 | 计算量大,需要肤色检测 |
我个人最常用的是加权最小二乘。做法很简单:
- 在24色卡中,选出肤色相关的色块(比如第1、2、3、4块)
- 给这些色块分配更高的权重,比如w=10,其他色块w=1
- 求解加权最小二乘:A = (X^T * W * X)^{-1} * X^T * W * Y
代码实现:
def solve_ccm_weighted(sensor_rgb, ref_rgb, weights):
"""
weights: (N,) 每个色块的权重
"""
ccm = np.zeros((3, 3))
W = np.diag(weights)
for ch in range(3):
Y = ref_rgb[:, ch]
X = sensor_rgb
XtWX = X.T @ W @ X
XtWY = X.T @ W @ Y
A = np.linalg.solve(XtWX, XtWY)
ccm[ch, :] = A
return ccm
4.5 知识结构总览
下面这张图,把CCM的完整流程串起来了:
4.6 验证与调优
CCM算出来之后,别急着用。先做两件事:
- 客观指标:计算24色卡的平均ΔE和最大ΔE。一般要求平均ΔE < 3,最大ΔE < 6
- 主观评价:拍几张真实场景——人像、风景、食物。重点看肤色、草地、天空
如果肤色偏了,回到加权最小二乘那步,调整权重再算。我一般迭代3-5轮就能收敛。