假设您正在使用 RGB 颜色:每种颜色用三个 强度 或亮度表示。您必须在“线性 RGB”和“sRGB”之间进行选择。现在,我们将通过忽略三种不同的强度来简化事情,并假设您只有一种强度:也就是说,您只处理灰色阴影。
在线性色彩空间中,您存储的数字与它们所代表的强度之间的关系是线性的。实际上,这意味着如果将数字加倍,强度就会加倍(灰色的亮度)。如果您想将两个强度加在一起(因为您要根据两个光源的贡献来计算强度,或者因为您要在不透明对象的顶部添加一个透明对象),您可以通过添加两个数字在一起。 如果您要进行任何类型的 2D 混合或 3D 着色,或几乎任何图像处理,那么您希望您的强度在线性色彩空间中,这样您就可以进行加、减、乘和将数字相除以对强度产生相同的影响。大多数颜色处理和渲染算法只能在线性 RGB 上给出正确的结果,除非你给所有东西添加额外的权重。
这听起来很简单,但有一个问题。人眼对光的敏感度在低强度下比在高强度下要好。也就是说,如果你列出所有你能分辨的强度,那么暗的比亮的多。换句话说,你可以比浅灰色更好地区分深灰色。特别是,如果您使用 8 位来表示您的强度,并且您在线性颜色空间中执行此操作,您最终会得到太多的浅色调,而没有足够的深色调。您在黑暗区域会出现条带,而在明亮区域,您会在用户无法区分的不同色调的近白色上浪费位。
为了避免这个问题,并充分利用这 8 位,我们倾向于使用 sRGB。 sRGB 标准告诉您要使用的曲线,以使您的颜色非线性。曲线在底部较浅,因此您可以使用更多深灰色,而在顶部更陡峭,因此您可以使用更少的浅灰色。如果你把数字加倍,你的强度就会增加一倍以上。这意味着如果将 sRGB 颜色加在一起,最终会得到比应有的颜色更亮的结果。如今,大多数显示器将其输入颜色解释为 sRGB。因此,当您在屏幕上添加颜色或将其存储在每通道 8 位纹理中时,请将其存储为 sRGB,这样您就可以充分利用这 8 位。
您会注意到我们现在遇到了一个问题:我们希望我们的颜色在线性空间中处理,但存储在 sRGB 中。这意味着您最终会在读取时进行 sRGB 到线性的转换,并在写入时进行线性到 sRGB 的转换。正如我们已经说过的,线性 8 位强度没有足够的暗度,这会导致问题,所以还有一条更实用的规则:如果可以,不要使用 8 位线性颜色躲开它。遵循 8 位颜色始终为 sRGB 的规则已成为惯例,因此您在将强度从 8 位扩大到 16 位或从整数到浮点的同时进行 sRGB 到线性的转换;同样,当您完成浮点处理后,在转换为 sRGB 的同时缩小到 8 位。如果您遵循这些规则,您就不必担心伽马校正。
当您读取 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 通道是颜色的整体亮度,并且比其他两个通道使用更多的位(或具有更高的空间分辨率)。这样,您就不需要(总是)像使用 RGB 强度那样应用曲线。 YUV 是一个线性颜色空间,因此如果将 Y 通道中的数字加倍,颜色的亮度就会加倍,但不能像 RGB 颜色那样将 YUV 颜色相加或相乘,所以它不用于图像处理,仅用于存储和传输。
我认为这回答了您的问题,所以我将以简短的历史记录结束。在 sRGB 之前,旧的 CRT 曾经内置了非线性。如果将像素的电压加倍,则强度将增加一倍以上。每个监视器还有多少不同,这个参数被称为 gamma。这种行为很有用,因为它意味着你可以获得比亮更多的暗,但这也意味着你无法判断你的颜色在用户的 CRT 上有多亮,除非你先校准它。 Gamma 校正 意味着转换您开始使用的颜色(可能是线性的)并将它们转换为用户 CRT 的 Gamma。 OpenGL 来自这个时代,这就是为什么它的 sRGB 行为有时会有点令人困惑。但是 GPU 供应商现在倾向于使用我上面描述的约定:当您在纹理或帧缓冲区中存储 8 位强度时,它是 sRGB,而当您处理颜色时,它是线性的。例如,一个 OpenGL ES 3.0,每个帧缓冲区和纹理都有一个“sRGB 标志”,您可以打开以在读取和写入时启用自动转换。您根本不需要明确地进行 sRGB 转换或伽马校正。