在线性和非线性RGB空间中使用颜色时的实际区别是什么?

130

线性RGB空间的基本特征是什么,非线性RGB空间的基本特性又是什么?当谈论每个通道中8(或更多)位的值时,会发生什么变化?

在OpenGL中,颜色是3+1个值,也就是指RGB+alpha,每个通道保留8位,这是我理解清楚的部分。

但是当涉及到伽马校正时,我不明白在非线性RGB空间中工作的影响是什么。

由于我知道如何在图像处理软件中使用曲线,我的解释是在线性RGB空间中,您采用现有的值,并且没有进行任何操作和附加数学函数,而在非线性空间中,每个通道通常按照经典的幂函数行为进行演变。

即使我将此解释视为真实解释,我仍然不清楚真正的线性空间是什么,因为在计算后,所有非线性RGB空间都变成了线性空间,最重要的是我不明白非线性颜色空间更适合人眼的部分,因为据我所知,最终所有RGB空间都是线性的。


实际上,在SVG中,您可以指定线性或标准RGB作为您的颜色空间,但我不清楚其效果是什么,除了这似乎很重要 :-) - Michael Mullany
@MichaelMullany 这个线性的东西看起来更像是给用户的提示,而不是真正的独特品质。 - Ken
有件事帮助了我:我想是一位数学老师告诉我“听到 linear 就想成直线”。不确定这是否会对你有所帮助,但对我来说,那真是个恍然大悟的时刻。 - Joe Plante
2个回答

321

假设你正在使用RGB颜色:每个颜色由三个强度或亮度表示。你需要选择“线性RGB”和“sRGB”之间的一个。现在,我们将简化问题,忽略三种不同的强度,只假设你只有一种强度:也就是说,只处理灰度。

在线性颜色空间中,你存储的数字与它们所表示的强度之间的关系是线性的。实际上,这意味着如果你把数字加倍,那么强度(灰度的亮度)就会加倍。如果你想要将两个强度加在一起(因为你正在计算两个光源的强度贡献,或者因为你正在在不透明物体上添加透明物体),你可以简单地将这两个数字相加。如果你进行任何2D混合、3D阴影或几乎所有的图像处理,那么你需要的是线性颜色空间中的强度,这样你可以通过加、减、乘、除数字来对强度产生相同的影响。大多数颜色处理和渲染算法只能使用线性RGB才能得到正确的结果,除非你为所有内容添加额外的权重。

听起来非常简单,但是存在一个问题。人类眼睛对光的敏感度在低强度下更加细微,而在高强度下则更粗糙。也就是说,如果你列出所有你可以区分的强度,那么较暗的强度比较亮的强度更多。换句话说,你可以更好地区分灰色的深浅。特别地,如果你使用8位来表示你的强度,并且在线性颜色空间中进行表示,你会得到过多的亮色阴影,而较暗的区域则出现带状图案,同时在亮区域中,你浪费了许多位数来区分用户看不出差异的近白色阴影。

为了避免这个问题,并最大程度利用这8位,我们倾向于使用sRGB。sRGB标准告诉你使用哪种曲线来将颜色变成非线性的。曲线在底部越浅,就可以拥有更多的深灰色,而在顶部则越陡峭,使你拥有更少的浅灰色。如果你把数字加倍,则强度增加的不止一倍。这意味着如果你将sRGB颜色相加,最终结果会比预期的轻。如今,大多数显示器解释它们的输入颜色为sRGB。因此,当你要在屏幕上显示一种颜色或将其存储在每通道8位的纹理中时,请将它存储为sRGB,以便最大限度地利用这8位。

现在我们遇到了一个问题:我们想要在线性空间中处理颜色,但是要将其存储在sRGB中。这意味着你需要在读取时进行sRGB到线性转换,并在写入时进行线性到sRGB转换。由于我们已经说过,线性8位强度没有足够的深灰色,因此这会引起问题,因此还有一条实用规则:如果可以避免使用8位线性颜色,则不要使用8位线性颜色。现在约定俗成的做法是,8

float s = read_channel();
float linear;
if (s <= 0.04045) linear = s / 12.92;
else linear = pow((s + 0.055) / 1.055, 2.4);

反过来,如果你想将一张图片写成sRGB格式,对于每个线性强度值应使用下面的公式:

float linear = do_processing();
float s;
if (linear <= 0.0031308) s = linear * 12.92;
else s = 1.055 * pow(linear, 1.0/2.4) - 0.055; ( Edited: The previous version is -0.55 )
在这两种情况下,浮点值s的范围都从0到1,因此如果您正在读取8位整数,则首先要除以255,如果您正在写入8位整数,则最后要乘以255,与通常的操作方式相同。这就是您需要了解的所有关于使用sRGB的内容。
到目前为止,我只处理了一种强度,但是有更聪明的方法来处理颜色。人眼可以更好地分辨不同的亮度而不是不同的色调(更确切地说,它具有比色度更好的亮度分辨率),因此您可以通过将亮度与色调分开存储来更好地利用您的24位。这就是YUV、YCrCb等表示方法尝试做的事情。Y通道是颜色的总体亮度,并且使用的比其他两个通道更多的位数(或具有更高的空间分辨率)。这样,您就不需要像处理RGB强度那样总是应用曲线。YUV是一个线性颜色空间,因此如果您在Y通道中的数字翻倍,您将使颜色的亮度翻倍,但是您无法像使用RGB颜色那样将YUV颜色相加或相乘,因此它不用于图像处理,只用于存储和传输。
我认为这回答了您的问题,因此我会以一条快速的历史注释结束。在sRGB之前,旧的CRT显示器内建有非线性性。如果您为像素加倍电压,则亮度不仅会加倍。对于每个监视器,这个变量都不同,称为伽马。这种行为很有用,因为它意味着您可以获得比光更暗的颜色,但是这也意味着除非您事先进行校准,否则您无法确定用户的CRT上颜色的亮度。 伽马校正表示转换您开始使用的颜色(可能是线性),并将其转换为用户CRT的伽马。OpenGL来自这个时代,这就是为什么它的sRGB行为有时会让人感到困惑的原因。但是现在GPU供应商往往遵循我上面描述的约定:当您在纹理或帧缓冲区中存储8位强度时,它是sRGB的;当您处理颜色时,它是线性的。例如,在OpenGL ES 3.0中,每个帧缓冲区和纹理都有一个“sRGB标志”,您可以打开它以在读取和写入时启用自动转换。您根本不需要显式执行sRGB转换或伽马校正。

12
非常感谢您的惊人回答,看到事情被解释得如此清楚总是很惊人。我只想问一下,在您看来,哪本书或资源对于了解颜色空间和用于进行转换 sRGB <-> 线性的公式,或者可以近似这种行为的功能会足够好呢? - Ken
很抱歉,我不知道有哪些好的书籍或资源。维基百科页面非常全面,包括关于白点和色域范围有多小的所有内容(我没有提到,因为大多数人不需要知道),但这使得它有点难以理解。 - Dan Hulme
请查看http://en.wikipedia.org/wiki/RGB_color_space,http://en.wikipedia.org/wiki/Gamma_correction和http://www.libpng.org/pub/png/spec/1.2/PNG-GammaAppendix.html。 - Olaf Dietsche
http://www.poynton.com/notes/colour_and_gamma/GammaFAQ.htmlhttp://www.poynton.com/notes/colour_and_gamma/ColorFAQ.html - bcorso

1

我不是“人类颜色检测专家”,但我在YUV->RGB转换中遇到了类似的问题。对于R/G/B通道,有不同的权重,因此如果您将源颜色更改x,则RGB值会以不同的数量更改。

正如所说,我不是专家,但是我认为,如果您想进行一些颜色校正转换,应该在YUV空间中进行,然后将其转换为RGB(或在RGB上执行数学等效操作,注意数据丢失)。此外,我不确定YUV是否是颜色的最佳本地表示,但视频摄像机提供了该格式,这就是我遇到问题的地方。

这里是神奇的YUV->RGB公式,包括秘密数字:http://www.fourcc.org/fccyvrgb.php


1
注意RGB<->YUV转换及其反向转换。我不确定是否在每种情况下都是如此,但是YUV颜色空间有时会转换为16-235的范围,而不是24位RGB中的0-255。因此,每次进行颜色空间转换时,您可能会丢失数据。大多数人倾向于说尽可能保持在同一颜色空间内。 - Joe Plante

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接