graphics - 在线性与非线性 RGB 空间中处理颜色时有哪些实际区别?

标签 graphics colors rgb color-space color-coding

线性RGB空间的基本属性是什么,非线性空间的基本属性是什么?当谈论这 8 个(或更多)位中每个 channel 内的值时,有什么变化?

在 OpenGL 中,颜色是 3+1 值,我的意思是 RGB+alpha,每个 channel 保留 8 位,这是我清楚的部分。

但是当谈到 Gamma 校正时,我不明白在非线性 RGB 空间中工作的效果是什么。

由于我知道如何在图形软件中使用曲线进行照片编辑,我的解释是,在线性 RGB 空间中,您按原样取值,不进行任何操作,也不附加任何数学函数,而是在每个曲线都非线性时 channel 通常遵循经典的幂函数行为演变。

即使我把这个解释当作真实的解释,我仍然不明白什么是真正的线性空间,因为在计算之后所有非线性 RGB 空间都变成了线性,最重要的是我没有得到非线性空间的部分-linear 颜色空间更适合人眼,因为最终所有 RGB 空间对于我的理解都是线性的。

最佳答案

假设您正在使用 RGB 颜色:每种颜色都用三种强度或亮度表示。您必须在“线性 RGB”和“sRGB”之间进行选择。现在,我们将通过忽略三种不同的强度来简化事情,并假设您只有一种强度:也就是说,您只处理灰色阴影。

在线性色彩空间中,您存储的数字与其代表的强度之间的关系是线性的。实际上,这意味着如果将数字加倍,强度(灰色的亮度)也会加倍。如果您想将两个强度加在一起(因为您是根据两个光源的贡献计算强度,或者因为您要在不透明对象的顶部添加一个透明对象),您可以通过添加两个数字在一起。 如果您正在进行任何类型的 2D 混合或 3D 着色,或几乎任何图像处理,那么您希望您的强度处于线性色彩空间 ,因此您可以对数字进行加、减、乘和除,以对强度产生相同的影响。大多数颜色处理和渲染算法只能使用线性 RGB 给出正确的结果,除非您为所有内容添加额外的权重。

这听起来很容易,但有一个问题。人眼对光的敏感度在低强度下比在高强度下要好。也就是说,如果你把你能分辨的所有强度列一个 list ,暗的比亮的多。换句话说,与浅灰色相比,您可以更好地区分深灰色。特别是,如果您使用 8 位来表示您的强度,并且您在线性色彩空间中执行此操作,那么您最终会得到太多的浅色调,而没有足够的深色调。您会在黑暗区域出现 strip ,而在明亮区域,您会在用户无法分辨的不同深浅白色上浪费一些东西。

为了避免这个问题,并充分利用这 8 位,我们倾向于使用 sRGB。 sRGB 标准告诉您要使用的曲线,以使您的颜色非线性。曲线底部较浅,因此可以有更多的深灰色,而顶部的曲线较陡,因此浅灰色较少。如果你把数字加倍,你的强度就会增加一倍以上。这意味着,如果您将 sRGB 颜色添加在一起,您最终会得到比应有的颜色更亮的结果。如今,大多数显示器将其输入颜色解释为 sRGB。所以,当您在屏幕上放置颜色,或将其存储在每 channel 8 位的纹理中时,请将其存储为 sRGB ,因此您可以充分利用这 8 位。

你会注意到我们现在有一个问题:我们希望我们的颜色在线性空间中处理,但存储在 sRGB 中。这意味着您最终会在读取时进行 sRGB 到线性转换,并在写入时进行线性到 sRGB 转换。正如我们已经说过的,线性 8 位强度没有足够的暗度,这会导致问题,所以还有一个更实用的规则:不要使用 8 位线性颜色 如果你能避免它。遵循 8 位颜色始终为 sRGB 的规则已成为惯例,因此您在将强度从 8 位扩展到 16 位或从整数到浮点数的同时进行 sRGB 到线性转换;同样,当您完成浮点处理后,您在转换为 sRGB 的同时缩小到 8 位。如果您遵循这些规则,您就不必担心 Gamma 校正。

当您读取 sRGB 图像,并且需要线性强度时,请将此公式应用于每个强度:

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 channel 是颜色的整体亮度,比其他两个 channel 使用更多位(或具有更高的空间分辨率)。这样,您(总是)不需要像处理 RGB 强度那样应用曲线。 YUV 是一个线性颜色空间,因此如果将 Y channel 中的数字加倍,则颜色的亮度也会加倍,但是您不能像使用 RGB 颜色那样将 YUV 颜色相加或相乘,因此它不用于图像处理,仅用于存储和传输。

我认为这回答了您的问题,所以我将以简短的历史记录结束。在 sRGB 之前,旧的 CRT 曾经内置了非线性。如果将像素的电压加倍,强度就会增加一倍以上。每台显示器有多少不同,这个参数被称为 Gamma 。这种行为很有用,因为它意味着您可以获得比亮更多的暗,但这也意味着您无法判断用户 CRT 上的颜色有多亮,除非您先对其进行校准。 Gamma 校正意味着转换您开始使用的颜色(可能是线性的)并将它们转换为用户 CRT 的 Gamma。 OpenGL 来自这个时代,这就是为什么它的 sRGB 行为有时会让人有点困惑。但是 GPU 供应商现在倾向于使用我上面描述的约定:当您在纹理或帧缓冲区中存储 8 位强度时,它是 sRGB,而当您处理颜色时,它是线性的。例如,一个 OpenGL ES 3.0,每个帧缓冲区和纹理都有一个“sRGB 标志”,您可以打开它以在读写时启用自动转换。您根本不需要明确地进行 sRGB 转换或 Gamma 校正。

关于graphics - 在线性与非线性 RGB 空间中处理颜色时有哪些实际区别?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12524623/

相关文章:

python - 替换所有高于阈值的 RGB 值

iphone - iOS RGB 颜色不正确?

c# - 我想强制渲染,但尽可能快地绘制 (InvalidateVisual/CompositionTarget.Rendering)

css - 为什么文本颜色属性称为 'color' ?

c# - 合并两个图像以进行 "fade"过渡并改变不透明度? (C#/PNG)

使用 cout<<term_cc<color, default, attrib> 输出到 Windows 终端的 C++ 在 Windows 上正确输出颜色和属性,但在 Linux 上不正确

javascript - 色方算法

python - numpy,其中 RGB channel 大于 [0,0,0]

java - 在 Swing Java 中绘图

c++ - 没有图形 API 的可移植实时渲染器窗口