
面向人类和设备的颜色模型
图像在网络上无处不在。我每天都会查看、创建、发送和编辑多张图像,并以多种不同方式将图像添加到我的应用程序中。但是,是什么让图像看起来相同或不同,在哪些情况下?我甚至写过关于图像的博客文章,我学到的越多,就越发现幕后发生了多少事情才能让图像看起来正确。在这个系列博客文章中,我将解释大多数图像格式使用的核心原理,从我们的眼睛如何看到光线,到设备如何解释和再现颜色。
如果你是一名开发人员、设计师,或者只是好奇为什么你的图像在不同设备上看起来不总是相同,那么这篇博文将深入探讨色彩视觉理论和颜色表示背后的数学原理,以解释人类和机器如何表示、转换和感知颜色和图像。
屏幕上的图像
如果我们从屏幕开始,图像是由屏幕像素表示的一组发光区域。每个屏幕像素都以不同的强度和波长发光。这种波长的差异被人类(以及猫,但那是另一回事)感知为颜色的差异。
注意:光线可以被认为是微小的粒子集合(光子)或不可见的电磁波。你应该考虑到只有少数设备可以发出“纯粹”(单色)的颜色——只包含一个波长的颜色。大多数情况下,我们将不同波长的混合物感知为颜色,而不是单个波长。我们显示器中的二极管不会发出“纯粹”的颜色。
人眼和大脑中的图像
屏幕上图像发出的光线穿过瞳孔,通过晶状体折射,并聚焦在视网膜上。视网膜包含称为视杆细胞和视锥细胞的特殊细胞。视杆细胞帮助我们在弱光下看东西,但它们不检测不同的波长——这就是为什么在黑暗中所有东西都看起来是灰度深浅的原因。然而,在白天,视杆细胞不活跃。我们感兴趣的大多数模型都是为白天条件而构建的。
视锥细胞负责颜色视觉。有三种类型,每种类型对略微不同的波长光谱敏感。S 视锥细胞对蓝色最敏感,M 视锥细胞对绿色最敏感,L 视锥细胞对红色最敏感。这些细胞发出的信号考虑了光敏感度。视网膜处理所有这些细胞的信号,然后信号被发送到大脑。
本质上,光信号可以分解为来自不同细胞类型的三种响应,每种响应覆盖一个广泛的光谱。我们的大脑和神经系统无法直接处理这个光谱,因此它们使用机制来解释它。本质上,红色、绿色和蓝色信息被映射到其他三个参数:亮度、红-绿和蓝-黄。这种映射使我们能够区分一种颜色与另一种颜色。实际上,颜色并不存在——它们只是我们人类感知电磁波的方式。
注意:亮度和亮度是不同的概念。亮度是测量单位为坎德拉每平方米的物理量,而亮度是基于人类感知并与光照条件相关(例如,灯是开着还是关着)。
大脑在检测模式和边缘方面比缓慢变化的梯度要好得多。还有专门负责边缘检测的机制。然而,人类对高频空间成分不那么敏感。
左侧是原始图像,右侧是边缘检测算法的输出图像。尽管原始图像中的大部分信息已被移除,但你仍然可以在长凳上感知到一只乌鸦。
人们如何感知图像极大地影响了图像数据的存储和处理方式。图像格式并不是为了以尽可能高效的方式保存像素——它们需要压缩信息,同时考虑人类感知和显示设备的特性。
研究和纸张上的颜色
CIE(国际照明委员会)进行了大量实验,创建了代表普通观察者如何感知光线的颜色模型。第一个示例展示了普通观察者如何感知不同波长(即不同颜色)。
从图表中可以明显看出,我们对绿光更敏感。眼睛中视锥细胞的机制导致了你可能熟悉的第一个颜色表示:每种视锥细胞类型的RGB(红、绿、蓝)。我们有针对此的 CIE 图表——颜色匹配函数。这些函数表示不同类型视锥细胞的不同响应。
我们可以使用此图表获取颜色的 RGB 表示。要获得三个数字,我们需要获取光线中包含的每个波长的响应总和。这些数字将代表我们的信号。是的,不同的光信号可以具有相同的 RGB 分量。
您可能还会注意到上一个图表存在一个问题:红色敏感度变为负值,当然,没有负色。为了解决这个问题,CIE 提出了另一种称为 XYZ 的颜色表示,它是 RGB 的一个简单转换,没有负值。结果,我们得到了一个这样的图表
让我们看看它是如何从 RGB 导出的
请注意绿色颜色的一个大系数。人眼对绿光最敏感,因此绿色的系数应该相当高。Y 代表感知亮度,这至关重要,因为人类可以快速注意到亮度的变化。这就是为什么这些信息需要尽可能准确地存储和表示。X 和 Z 负责颜色。这使我们能够将颜色信息与亮度分离,这与大脑处理信号的方式有关。
颜色空间
现在我们有三个数字来表示一个光信号。所有这些数字的可能三元组构成了一个颜色空间。我们也可以将其表示为 3D 形状。如果你有 Mac,可以使用ColorSync Utility应用程序来查看它的外观。
然而,3D 并不总是方便的。让我们通过丢弃亮度 (Y) 来展平图片。我们将得到一个著名的色度图。
注意:在绘制色度图之前,我们需要将 X 和 Z 值除以 X + Y + Z 来进行归一化。
马鞍形图形表示标准 CIE 观察者可以感知的所有颜色。这个图表和 XYZ 颜色空间的问题在于,它不能像观察者感知那样表示颜色之间的差异。对于编码像素值来说,它也效率低下,因为许多 XYZ 值无法映射到可感知的颜色,这导致了比特的浪费。
然而,XYZ 是构建设备特定颜色空间的非常有用的工具。每个设备都有其独特的颜色渲染特性。例如,两个红色物体在两个设备上可能看起来略有不同。为了解决这个问题,引入了设备特定颜色空间。
这里是它如何工作的大致思路:我们获取对应于(1, 0, 0)
的红色颜色信号,测量设备发出的光线,并计算该点的 XYZ。然后我们对绿色、蓝色和白色(当 RGB 的所有值都达到最大时)执行相同的操作。这些 XYZ 点称为RGB 色原。
瞧,我们得到了一组系数,用于将设备特定的 RGB 基色转换为设备独立的 XYZ 值,反之亦然。我们通常在网络上使用的 RGB 称为 sRGB 或标准 RGB。它是一组针对“标准”设备的特定系数。它们在这里
sRGB 无法表示人眼能感知的所有可能颜色。让我们回到我们的图表。这里是所有可能的颜色
这是用于获取 sRGB 的 RGB 系数的点。我们被 sRGB 三角形所限制,因此无法表示更多的颜色。
L*a*b* 中的颜色
为了解决颜色距离问题,CIE 引入了另一个颜色空间——CIE L*a*b*,它可以从 XYZ 导出。为了使其能够表示与人类感知相似的颜色接近度,XYZ 中的所有坐标都以特定的方式进行转换。这些转换不是线性的(仅通过线性转换不可能表示颜色接近度)。L* 代表亮度,并从 Y 导出,a* 和 b* 代表颜色维度,并从 X 和 Z 导出。
Lab 颜色空间使用两个轴来表示颜色:a*
和 b*
。a*
编码红绿色分量,而 b*
编码蓝黄色分量。在 L*a*b* 颜色空间中,无法同时表示具有强绿色和红色分量的光信号。然而,a*
和 b*
可以是负值。对于 RGB,这也不可能:如果我们进行 (1, 1, 0)
这样的操作,我们将得到黄色。
让我们以 RGB 转换为 Lab 的例子来看看它是如何工作的。
颜色 | sRGB | Lab (L*, a*, b*) | 解释 |
---|---|---|---|
红色 | 1, 0, 0 | 53.23, 80.11, 67.22 | +a*(强红),+b*(偏黄) |
绿色 | 0, 1, 0 | 87.74, -86.18, 83.19 | -a*(强绿),+b*(偏黄) |
蓝色 | 0, 0, 1 | 32.30, 79.20, -107.85 | +a*(偏红),-b*(强蓝) |
黄色 | 1, 1, 0 | 97.14, -21.55, 94.49 | -a*(略带绿),+b*(强黄) |
这些类型的颜色空间称为基于对立的。大脑处理信号的方式被编码到颜色空间中。Lab 空间中两种颜色之间的距离旨在映射到感知距离。请看示例:第一行中两种颜色之间的距离为 7.5,第二行中为 148.09。
如果两种颜色在人们看来相似,那么它们在 Lab 空间中的距离就会更小。
还有更多!CIE L*a*b* 不依赖于任何设备,因为它源自 XYZ。然而,这种推导考虑了光照条件,以用于另一个人类感知特征:色适应。为了计算 Lab 值,我们需要提供“正常日光条件”的 XYZ 坐标,这通常被称为白点(或“D65”)。最终公式可以在维基百科上找到。
更直观的 L*a*b* 变换
如果你现在感到不知所措或困惑,让我们尝试用其他例子来理解。看看下面两种 Lab 颜色,试着猜测它们有什么关系。
63.76, 29.08, 66.2 63.76, -66.2, 29.08
如果你不确定,请查看 L*a*b* 空间的一种变换,称为 LCH(亮度、色度、色相)。亮度保持不变,C 是 a*2 + b*2 的平方根,H 是以度为单位的角度。这种表示对大多数人来说更直观。让我们将之前的颜色转换为 LCH,看看我们会得到什么
63.76, 72.31, 66.29 63.76, 72.31, 156.29
所以现在我们有了相同的亮度,相同的强度,但色相不同了!
.first {
background-color: lch(63.76 72.31 66.29);
}
.second {
background-color: lch(63.76 72.31 calc(66.29 + 90));
}
现代 L*a*b*
CIE L*a*b* 空间早在 1976 年就开发出来了,它不是现今唯一存在的 Lab 空间。CSS Color Module Level 4 指定了两种 Lab 空间。第一种是 CIE L*a*b*,另一种是 Oklab。
注意:CSS Color Module Level 4 还定义了额外的 CSS 映射,用于前面讨论的颜色空间。
Oklab 是在 2020 年更晚开发的,旨在解决 CIE L*a*b* 的几个问题。
- Oklab 甚至更均匀,尤其是对于蓝色调。
- Oklab 更适合机器,因为数值计算更稳定。
- Oklab 源自 sRGB,因此对开发人员来说更熟悉和直观。
Oklab 有自己的 LCH 表示,称为 Oklch。它的计算方式与 LCH 分量类似,旨在实现更均匀和直观的效果。
最常见的 sRGB 空间及其朋友 YUV
L*a*b* 和 XYZ 是丰富的颜色空间,可以表示人类可以感知的每一种颜色。然而,由于它们的值范围广,它们在存储像素值方面效率不高。此外,它们是现代的,旨在与能够承担浪费一些比特来表示颜色和亮度微小细节的设备一起工作。
图像和视频格式通常使用 sRGB 颜色空间,GPU 硬件也针对 sRGB 进行了优化(我们稍后会讨论)。RGB 的灵感来自于人眼的工作方式(红色、绿色和蓝色);然而,它对于图像处理来说不是很方便。这就是为什么使用另一个颜色空间——YUV。YUV 是一组三个值:Y(亮度)、U(蓝差)和 V(红差)。这个空间的主要优点是它将颜色(U 和 V)与亮度(Y)分离。
注意:关于亮度、红绿和蓝黄系统的命名存在一些混淆。您可能会找到 YUV、YCbCr 和 Y'CbCr 的引用。YUV 和 YCbCr 似乎可以互换使用。然而,Y 和 Y' 是不同的东西。请注意,与 L*a*b* 相比,YUV 在感知上不均匀。
如果你查看图像编解码器的源代码,很可能会发现大量与 YUV 转换相关的代码。不要忘记 sRGB 是与设备相关的。如果我们取一个填充了某种颜色(例如 (0, 255, 0)
(绿色))的矩形,它在不同的设备上会看起来不同。让我们看看 YUV 坐标是如何从 RGB 导出的。我们之前见过这个原理,首先我们分离亮度
再一次,我们看到绿光贡献最大。U 和 V 是蓝光和红光与 Y 的差异。请注意,分母中的系数是为了将 U 和 V 从 -0.5 缩放到 0.5。
让我们在松鼠上试试,并比较原始图像与分离的 Y、U 和 V 通道。
这种分离之所以如此重要,是因为人们对亮度的敏感度高于对颜色的敏感度。为了正确处理图像,通常只对 Y 通道进行一些转换,或者对 Y 和 UV 通道使用不同的转换。这些通道通常以不同的方式进行压缩。
人类和设备的感知方式不同
让我们讨论感知的另一个特征——我们如何感知亮度,这与设备如何再现亮度非常不同。我们以对数方式感知亮度,而设备以线性方式再现亮度。对数感知意味着我们可以区分各种亮度范围。例如,5 级对数刻度(1:100000)很好;如果我们的眼睛适应了环境条件,它甚至可以更大。如果你好奇深入挖掘,这种效应可以用韦伯-费希纳定律来描述。
这是一个问题,因为普通设备可以捕获和显示的亮度范围比我们能感知的范围要小得多。许多专业相机和显示器支持更广的亮度范围,因此它差异很大。让我们考虑一个例子:哪个渐变看起来更均匀?
.first {
background-image: linear-gradient(
to right,
rgb(0 0 0),
rgb(128 128 128),
rgb(255 255 255)
);
}
.second {
background-image: linear-gradient(
to right,
rgb(0 0 0),
rgb(180 180 180),
rgb(255 255 255)
);
}
对于大多数人来说,第一个渐变看起来更平滑,而真正的“线性”渐变是第二个。如果你仔细观察这个例子,你可能会注意到一些小小的谎言。第一个渐变中间有 128(255 / 2)。这正是线性的,对吗?实际上不是,因为我们在 CSS 中使用的 RGB 值是伽马校正过的。伽马校正用于提供更好的感知图像并更有效地存储位。
为了从线性值获取伽马校正值,我们将该值缩放至 [0, 1],然后使用幂函数(如韦伯-费希纳定律所示)。我们使用 2.2 作为幂,因为它接近人类感知的运作方式。
sRGB 中使用的真实公式稍微复杂一些,但与上述公式非常接近。这是一个实际的转换:每个通道 R、G、B 都以相同的方式进行转换。
这也给暗影“更多比特”。“真实”的平均值大约在 180 左右,而不是 128。这与人类感知的运作方式一致——我们对暗影更敏感。
Y' 是 Y 的伽马校正版本。它的计算方式与 Y 相同,但使用伽马校正的 R、G、B。用于伽马校正的特定公式取决于颜色空间。这称为传输函数。
从相机到屏幕
我们经常想用手机拍照,然后把照片分享到我们最喜欢的社交网络。当你的朋友在她的笔记本电脑上看照片时,如果颜色看起来一样,那该多好,对吧?
要实现这一点,手机摄像头捕获的颜色应正确映射到屏幕上的颜色。请记住,高效的颜色空间是设备相关的。这意味着,例如,(127, 0, 0)
的 RGB 值在不同设备上会呈现不同的颜色。为了使颜色保持一致,图像中会编码一个附加的信息通道。设备颜色空间有三个特定之处
- 三种基色
- 白点
- 传输函数
让我们看看它如何适用于AVIF 图像。像许多图像格式一样,AVIF 尽力准确再现颜色。它使用两件事来实现这一点:颜色配置文件和 CICP(独立于编码的代码点)。颜色配置文件用于在创建图像的设备上生成的图像格式中,并允许正确重建颜色。它提供精确的颜色控制,并包含颜色基色、传输函数和白点的系数。它还可以存储带有预计算转换的查找表。CICP 通常用于视频,它比 ICC 配置文件简单得多。
注意:您可以在GitHub 上的 libavif wiki上找到一个很棒的解释器。
它由三个值组成。每个值都引用公开 H.273 标准中表格的行。
- MC
-
一组系数,用于将 RGB 转换为您想要的任何颜色空间。通常用于将 RGB 转换为 YUV。
- TC
-
一种非线性传输函数,用于考虑设备特定特性并执行伽马校正。
- CP
-
将 RGB 转换为 XYZ 的基色。
例如,它可以是 1/13/6,这意味着从标准中的特定表格中取出第 1、13 和 6 行。如果图像包含颜色配置文件,则 CP 和 TC 值取自配置文件,但 MC 仍然取自 CICP。
让我们看看 AVIF 图像的处理过程。我们正在读取 CICP 并得到 2/2/6,这意味着
- CP - 未指定
- TC - 未指定
- MC #6 定义了标准的 RGB 到 YUV 转换,与 JPEG 使用的相同。
要获取更多信息,我们需要查看 ICC 配置文件。在这里我们找到颜色基色、传输函数和白点。此信息用于将解码的字节流转换为设备颜色空间。在此过程中,图像可能会转换为 XYZ 和中间颜色空间,然后再转换为设备颜色空间。
解码过程超出了本文的范围,但有一点需要提及的是使用了 MC 系数。结果被发送到 GPU 并在屏幕上显示。
如果我想要令人惊叹的照片怎么办?
当我看到日落时,我可以看到天空和建筑物的细节。然而,如果我用 JPEG 格式拍摄一张照片,我必须选择捕捉天空还是建筑物。在这种情况下,明亮的天空曝光良好,但建筑物则融合成一个单一的黑暗模糊。
我们怎样才能做得更好?我们能够感知到的颜色和亮度范围比低端设备能够捕捉和再现的范围要广得多。这个广阔的范围通常被称为高动态范围(HDR)。
要获得图像的最佳效果,我们需要四件事
- 能够捕捉宽广颜色和亮度范围的设备(例如专业相机或某些智能手机相机)
- 可以高效存储此范围的文件格式
- 能够正确解码图像的软件
- 可以准确再现该范围的显示器
如果所有这些条件都满足,我们就能得到细节更丰富的阴影和高光,以及鲜艳的色彩的图像。然而,如果这些条件中的任何一个不满足,我们最终得到的将是 SDR(标准动态范围)图像。
专业相机擅长捕捉 HDR 照片。它们使用无损 RAW 格式,其文件大小比标准图像文件大得多。通过适当的后期处理,可以通过对图像不同部分应用不同的转换来揭示细节。
至于软件——现代浏览器在某些操作系统上支持 HDR。
许多屏幕可以再现 HDR 图像,但如果设备无法显示 HDR,则图像必须转换为 SDR。为了保留细节,可以使用不同的色调映射技术。这些技术可以由浏览器自动应用,也可以由用户手动应用。如果没有色调映射,图像可能会显得太暗、太亮或暗淡。
让我们检查一下它在你的情况中是如何工作的。比较两张图片
如果第二张图片看起来更鲜艳,则您的设备和浏览器可以渲染 HDR 图像。
色调映射的问题在于它均匀地应用于整个图像。一个更好的方法是使用增益图来调整特定区域——甚至每个像素——的亮度。您可以将增益图视为图像的灰度版本,其中每个像素的实际亮度乘以增益图中相应的值。此增益图嵌入在图像文件中。虽然 JPEG 和 AVIF 支持增益图,但该功能仍未标准化且处于实验阶段。
即使显示器能够再现 HDR,存储和传输 RAW 文件也是昂贵且低效的。最直接的解决方案是使用具有更高位深度的现代图像格式——例如 AVIF。AVIF 可以每通道存储 10 或 12 位,从而允许更宽的颜色和亮度范围。
总结
在这篇文章中,我们学习了人类感知如何影响光信号在设备中的表示方式。让我们总结一下我们涵盖的内容。
颜色空间用于表示光信号。有些颜色空间是设备特有的,而另一些则是通用的且与设备无关。某些颜色空间,如 sRGB,经过优化以表示图像和视频中的光信号。另一些,如 LCH(亮度、色度、色相),旨在以符合人类感知的方式表示颜色关系。
为了在不同设备上保持一致的颜色,我们必须在设备特定颜色空间和通用颜色空间(例如 sRGB 或 XYZ)之间进行转换。此外,还会应用伽马校正等特殊转换,以考虑人类感知光和颜色的非线性方式。某些颜色空间可以将颜色与亮度分离,这对于图像压缩非常有益,我们将在下一篇文章中看到这一点。