1. 色彩科学基础:人眼视觉特性、颜色三要素、RGB与XYZ色彩空间转换
各位同学好,欢迎来到《自动白平衡与色彩校正矩阵调优实战》的第一章。
做图像处理这么多年,我越来越觉得,色彩科学不是「背公式」,而是理解「人眼怎么骗」和「相机怎么还原」。说白了,你连人眼怎么看颜色的都不清楚,调出来的白平衡参数大概率是瞎蒙。
这一章,我们就从最底层开始,把色彩科学的三个核心问题讲透:人眼怎么感知颜色?颜色由哪三个要素决定?RGB 和 XYZ 之间到底怎么转?
1.1 人眼视觉特性:我们是怎么「看见」颜色的?
先问一个问题:为什么同一张照片,在手机上看偏蓝,在显示器上看偏黄?
答案其实不在设备,而在人眼。人眼不是一台精密的色度计,而是一个会自适应、会疲劳、会「脑补」的器官。
核心知识点:人眼视网膜上有两类感光细胞——视杆细胞(负责明暗,约1.2亿个)和视锥细胞(负责颜色,约600万个)。视锥细胞又分三种,分别对长波(L,红)、中波(M,绿)、短波(S,蓝)敏感。
我在项目中遇到过一件事:一个客户说我们 ISP 的白平衡算法「偏绿」,结果我拿色度计一测,色温完全准确。后来发现是客户办公室的日光灯是绿色的,他眼睛已经适应了那个环境。你看,人眼的色适应能力反而成了「干扰项」。
这里有几个关键特性,做白平衡调优时必须记住:
- 色适应:人眼会自动调整对不同光源的感知。你走进一个暖色灯光的房间,过几分钟就不觉得「黄」了。这就是为什么自动白平衡需要模拟人眼的色适应机制。
- 同时对比:一个灰色方块放在红色背景上,你会觉得它偏青。这是 ISP 色彩校正里最头疼的「色貌」问题。
- 韦伯-费希纳定律:人眼对亮度的感知不是线性的。暗部差 1 个灰度级你都能看出来,亮部差 10 个灰度级你可能都没感觉。这就是为什么 gamma 校正那么重要。
我的经验:做白平衡调优时,不要只看色温表。一定要在多种光照环境下用人眼做主观评价。我习惯在 D65、D50、A 光源、TL84 四种光源下各拍一张灰卡,然后对比人眼感受和仪器读数。两者差距超过 200K 时,说明算法有问题。
1.2 颜色三要素:色相、饱和度、明度
你想想看,我们描述一个颜色,通常会说「这个红色很正」、「那个蓝色有点灰」。这些描述背后,其实对应着三个物理量:
| 要素 | 英文 | 通俗理解 | 在图像处理中的意义 |
|---|---|---|---|
| 色相 | Hue | 是什么颜色(红、绿、蓝) | 白平衡不准时,色相会整体偏移 |
| 饱和度 | Saturation | 颜色浓不浓(鲜艳 vs 灰暗) | 色彩校正矩阵主要调整的就是饱和度 |
| 明度 | Lightness | 颜色亮不亮(亮白 vs 暗黑) | gamma 校正和色调映射影响明度 |
我记得刚入行时,有个老工程师跟我说:「调颜色就是调这三个东西,其他都是花架子。」当时觉得他太,后来做了十年 ISP,发现还真是这么回事。
举个例子:自动白平衡做错了,色相会偏。比如一张本该是白色的墙,拍出来偏蓝,那就是色相整体向蓝色方向偏移了。这时候你调色彩校正矩阵,本质上是把每个像素的 RGB 值重新映射,让色相回到正确的位置。
注意:饱和度不是越高越好。很多手机厂商为了「讨喜」,把饱和度拉得很高,结果肤色发红、草地发假。我在做某款旗舰机 ISP 调优时,发现饱和度超过 110% 后,人脸肤色就开始出现「塑料感」。最终我们定在 105% 作为默认值。
1.3 RGB 与 XYZ 色彩空间转换
好了,前面讲的是「人怎么感知颜色」。现在要讲「机器怎么表示颜色」。
相机传感器输出的是 RGB 值,但 RGB 有一个致命问题:它依赖于设备。你的屏幕 RGB 和我的屏幕 RGB,显示同一个 (128, 128, 128) 可能完全不一样。这就需要一个「设备无关」的色彩空间——XYZ。
XYZ 色彩空间是 CIE 在 1931 年定义的。它基于人眼视觉实验,用三个假想的原色 X、Y、Z 来表示所有可见光。其中 Y 代表亮度,X 和 Z 代表色度。
转换公式其实不复杂,但要注意矩阵的精度。我直接给代码:
import numpy as np
# sRGB 到 XYZ 的转换矩阵(D65 白点)
rgb_to_xyz = np.array([
[0.4124564, 0.3575761, 0.1804375],
[0.2126729, 0.7151522, 0.0721750],
[0.0193339, 0.1191920, 0.9503041]
])
def srgb_to_xyz(rgb):
"""
rgb: [R, G, B] 归一化到 [0, 1] 的线性值
返回: [X, Y, Z]
"""
# 先做 gamma 解码(sRGB 是 gamma 编码的)
rgb_linear = np.where(rgb <= 0.04045,
rgb / 12.92,
((rgb + 0.055) / 1.055) ** 2.4)
# 矩阵乘法
xyz = np.dot(rgb_to_xyz, rgb_linear)
return xyz
# 反过来:XYZ 到 sRGB
xyz_to_srgb = np.linalg.inv(rgb_to_xyz)
def xyz_to_srgb(xyz):
"""
xyz: [X, Y, Z]
返回: [R, G, B] 归一化到 [0, 1] 的线性值
"""
rgb_linear = np.dot(xyz_to_srgb, xyz)
# gamma 编码
rgb = np.where(rgb_linear <= 0.0031308,
12.92 * rgb_linear,
1.055 * (rgb_linear ** (1/2.4)) - 0.055)
return np.clip(rgb, 0, 1)
避坑指南:我曾经在项目中犯过一个低级错误——直接用 sRGB 的转换矩阵去处理 Adobe RGB 的图片。结果颜色全偏了。不同色彩空间的转换矩阵是不一样的,一定要先确认输入图像的色彩空间。常用的有 sRGB、Adobe RGB、DCI-P3、Rec.709、Rec.2020,每个都有自己的转换矩阵。
为什么会这样?因为不同色彩空间的色域范围不同。sRGB 覆盖了大约 35% 的 CIE 色域,而 Adobe RGB 覆盖了约 50%。你用 sRGB 的矩阵去解 Adobe RGB 的数据,相当于用一个小尺子去量一个大东西,肯定不准。
下面这张图展示了从人眼感知到色彩空间转换的完整流程:
1.4 本章小结
这一章我们讲了三个核心概念:
- 人眼视觉特性:色适应、同时对比、韦伯-费希纳定律。这些是白平衡算法设计的「生理学基础」。
- 颜色三要素:色相、饱和度、明度。调颜色就是调这三个东西,没有第四样。
- RGB 与 XYZ 转换:从设备相关的 RGB 到设备无关的 XYZ,矩阵乘法 + gamma 校正,代码就那几行,但矩阵选错全盘皆输。
一句话总结:不懂人眼,调不好白平衡;不懂 XYZ,做不好色彩校正。这一章是后面所有实战内容的「地基」,地基不稳,楼盖得再高也得塌。