4. 色彩还原与白平衡:色彩校正矩阵(CCM)、自动白平衡(AWB)算法、色彩饱和度与自然度的平衡

色彩还原这件事,说简单也简单,说复杂也复杂。简单到你可以把它理解成「让红色看起来是红色」,复杂到它背后牵扯着传感器光谱响应、光源色温估计、人眼视觉偏好等一系列问题。

我个人习惯把色彩还原拆成两个阶段来看:先解决「对不对」的问题,再解决「好不好看」的问题。白平衡解决的是前者,色彩校正矩阵(CCM)和饱和度调整解决的是后者。

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

你想想看,一张白纸在日光灯下偏蓝,在白炽灯下偏黄。人眼会自动适应这种变化,但传感器不会。所以AWB要做的,就是估计当前光源的色温,然后把图像拉回到「标准光源」下的样子。

4.1.1 灰度世界假设

这是最经典的AWB方法。它的核心假设是:一张色彩丰富的自然图像,所有像素的R、G、B均值应该趋于相等。如果R均值明显偏高,说明光源偏红,需要降低R增益。

// 灰度世界算法伪代码
float avgR = mean(image[:,:,0]);
float avgG = mean(image[:,:,1]);
float avgB = mean(image[:,:,2]);

float gainR = avgG / avgR;
float gainB = avgG / avgB;

// 应用增益
image[:,:,0] = clamp(image[:,:,0] * gainR, 0, 255);
image[:,:,2] = clamp(image[:,:,2] * gainB, 0, 255);
注意:灰度世界假设在单色场景下会完全失效。比如拍一片蓝天,R均值很低,B均值很高,算法会误判为光源偏蓝,结果把蓝天拉成灰色。我在项目中遇到过这种情况,后来加了场景检测才解决。

4.1.2 完美反射假设

另一种思路是:图像中最亮的像素应该接近白色。找到这些「参考白点」,用它们的RGB比值来估计光源。

实际工程中,我建议把两种方法结合起来:

  • 灰度世界:适合色彩丰富的场景
  • 完美反射:适合有高光区域的场景
  • 加权融合:根据场景统计信息动态调整权重

4.1.3 色温曲线与增益映射

更工程化的做法是预先标定一组色温曲线。在不同色温下拍摄灰卡,记录R/G和B/G的比值,形成一条色温曲线。运行时,根据统计的R/G和B/G值,查表得到估计色温,再查表得到对应的R、B增益。

色温(K) R/G 比值 B/G 比值 R 增益 B 增益
2800(白炽灯) 1.85 0.52 0.54 1.92
4000(荧光灯) 1.35 0.78 0.74 1.28
5500(日光) 1.00 1.00 1.00 1.00
6500(阴天) 0.82 1.18 1.22 0.85
经验之谈:我曾经调试过一款手机摄像头,在混合光源场景下(比如室内既有日光灯又有窗外的自然光),单一色温估计完全不够用。后来我们引入了分区AWB,把画面分成多个区域分别估计色温,再根据区域权重做融合。效果好了不少。

4.2 色彩校正矩阵(CCM)——从传感器RGB到标准RGB

白平衡做完之后,图像的颜色大致对了,但还不够准。为什么?因为传感器的RGB响应曲线和人眼并不一样。传感器对某些波长的光敏感度偏高或偏低,导致颜色偏移。

CCM的作用就是用一个3x3的矩阵,把传感器RGB空间转换到标准RGB空间(比如sRGB)。

// CCM 矩阵乘法
R_out = CCM[0][0]*R_in + CCM[0][1]*G_in + CCM[0][2]*B_in
G_out = CCM[1][0]*R_in + CCM[1][1]*G_in + CCM[1][2]*B_in
B_out = CCM[2][0]*R_in + CCM[2][1]*G_in + CCM[2][2]*B_in

4.2.1 CCM的标定

标定CCM需要用到标准色卡,比如24色卡。流程是这样的:

  1. 在标准光源下拍摄色卡
  2. 提取每个色块的传感器RGB值
  3. 与标准色卡的标准RGB值做对比
  4. 用最小二乘法求解最优的3x3矩阵

嗯,这里要注意:CCM是跟光源相关的。不同色温下,传感器的响应曲线会有变化,所以通常需要标定多组CCM,比如低色温、中色温、高色温各一组,运行时根据AWB估计的色温做插值。

4.2.2 CCM的副作用

CCM不是万能的。它有一个明显的副作用:放大噪声。矩阵的非对角元素越大,噪声放大的程度就越严重。特别是B通道,在低照度下噪声本来就大,经过CCM之后可能更明显。

核心权衡:CCM的校正强度与噪声放大是一对矛盾。校正越强,颜色越准,但噪声越明显。我一般会在实验室里拍一组色卡,然后人眼判断「颜色可接受」和「噪声可接受」的平衡点。

4.3 色彩饱和度与自然度的平衡

颜色准了,但用户不一定喜欢。你想想看,真实世界的颜色有时候看起来「不够鲜艳」。用户喜欢更饱和的蓝天、更翠绿的草地、更红润的肤色。

所以ISP里通常会在CCM之后再加一个饱和度调整模块。这个模块的核心问题就是:饱和度拉到多高才算「好看」?

4.3.1 饱和度控制策略

我见过几种常见的做法:

  • 全局饱和度:对所有像素统一乘一个饱和度系数。简单粗暴,但容易让肤色失真。
  • 分区饱和度:对不同颜色区域施加不同的饱和度增益。比如对蓝色和绿色区域加得多一些,对肤色区域加得少一些。
  • 自适应饱和度:根据亮度、饱和度、色相做非线性映射。低饱和度区域保持自然,高饱和度区域适当增强。

我个人比较推荐第三种。它能在保持肤色自然的同时,让风景照看起来更讨喜。

4.3.2 肤色保护

肤色保护是饱和度调整中最关键的一环。我曾经调试过一款产品,饱和度拉上去之后,人脸看起来像「蜡像」——颜色太浓,失去了皮肤的质感。

解决方案是在YCbCr空间里,对Cb和Cr分量做非线性映射。在肤色区域(Cb和Cr落在特定范围内),降低饱和度增益;在其他区域,正常增强。

// 肤色保护饱和度调整伪代码
for each pixel:
    if (Cb in skinCbRange && Cr in skinCrRange):
        satGain = 1.0 + (globalSatGain - 1.0) * skinProtectFactor
    else:
        satGain = globalSatGain
    
    Cb_out = Cb * satGain
    Cr_out = Cr * satGain
小技巧:肤色区域的范围不是固定的。不同人种、不同光照下,肤色在CbCr空间的位置会漂移。我习惯在标定时采集多组肤色样本,建立一个动态的肤色模型,运行时根据AWB估计的色温做调整。

4.4 整体流程与知识结构

把上面这些串起来,色彩还原的完整流程是这样的:

色彩还原与白平衡整体流程 RAW 输入 自动白平衡 (AWB) 色彩校正矩阵 (CCM) 饱和度与肤色保护 色彩校正输出 色温曲线查表 R/G、B/G → 色温 → 增益 多组CCM插值 低/中/高色温各一组 肤色区域检测 CbCr范围 + 动态模型

从RAW输入到最终输出,每一步都有它的作用,每一步也都有它的代价。AWB解决了色偏问题,但可能在单色场景下翻车;CCM提高了色彩准确度,但放大了噪声;饱和度调整让画面更讨喜,但过度增强会破坏自然感。

总结一下我的经验:色彩还原没有「最优解」,只有「最适合当前场景的平衡点」。调试时,我通常会准备一组覆盖各种场景的测试图,包括室内、室外、人像、风景、低照度等。然后反复调整AWB、CCM和饱和度的参数,直到所有场景都能达到「颜色准确、噪声可控、观感自然」的状态。