现代计算机/显示器的最终颜色输出是否需要进行伽马校正?

27

我一直以为我的伽马校正管道应该如下:

  • 对于所有加载的纹理都使用sRGB格式(GL_SRGB8_ALPHA8),因为所有绘画程序在文件预伽马校正之前。当在着色器中从GL_SRGB8_ALPHA8纹理中采样时,OpenGL会自动转换为线性空间。
  • 所有光照计算、后期处理等都在线性空间中进行。
  • 将最终颜色转换回sRGB空间,以便在屏幕上显示。

请注意,在我的情况下,最终颜色写入涉及从FBO(线性RGB纹理)写入到后备缓冲区。

我的假设受到了挑战,因为如果我在最后一步进行伽马校正,则我的颜色比它们应该亮。我设置一个固定的颜色{ 255, 106, 0 },但是当我渲染时,我得到的是{ 255, 171, 0 }(通过打印屏幕并选取颜色确定)。我得到了黄色而不是橙色。如果我在最后一步进行伽马校正,我得到的值恰好是{ 255, 106, 0 }。

根据某些资源,现代液晶屏幕模拟CRT伽马。它们总是这样吗?如果不是,我如何确定是否应该进行伽马校正?我在其他地方做错了吗?


编辑1

我现在注意到,即使使用颜色正确的灯光写入的颜色,使用来自纹理的颜色的地方也不正确(比我预期的要暗得多,没有伽马校正)。我不知道这种差异来自哪里。


编辑2

在将我的纹理从GL_SRGB8_ALPHA8更改为GL_RGBA8之后,一切看起来都很完美,即使在使用纹理值进行光照计算时也是如此(如果我减半光线的强度,则输出的颜色值也会减半)。

我的代码不再考虑伽马校正,我的输出看起来正确。

这让我更加困惑,伽马校正是否不再需要/使用?


编辑3-回答datenwolf的答案

经过更多的实验,我对以下几点感到困惑。

1-大多数图像格式都以非线性方式(在sRGB空间中)存储

我加载了一些图像(在我的情况下是.png和.bmp图像),并检查了原始二进制数据。在我的程序中,如果我使用图像编辑程序的值与字节数组进行比较,则似乎这些图像实际上位于RGB颜色空间中,并且它们完全匹配。由于我的图像编辑器正在提供RGB值,这表明存储在RGB颜色空间中。
我使用stb_image.h/.c加载我的图像,并一直跟踪加载.png文件的过程,但没有看到任何地方在加载时进行伽马校正。我还在十六进制编辑器中检查了.bmp文件,发现磁盘上的值也完全匹配。
如果这些图像实际上是在线性RGB空间中存储在磁盘上,那么我应该如何在编程时知道何时指定图像为sRGB空间?是否有某种方式可以查询此信息,更高级的图像加载器可能会提供?还是由图像创建者保存其图像为伽马校正(或非伽马校正) - 这意味着建立一个约定并遵循特定项目。我问了几位艺术家,他们都不知道什么是伽马校正。
如果我指定我的图像是sRGB,则它们太暗,除非我最后进行伽马校正(如果监视器使用sRGB,则这是可以理解的,但请参见第2点)。
“在大多数计算机上,有效的扫描LUT是线性的!这是什么意思?” 我不确定能否找到您的回答中该想法的结束位置。
从我做实验得出的结果来看,我测试过的所有显示器输出线性值。如果我用着色器的硬编码值绘制全屏四边形并给其着色,而且没有伽马校正,监视器将显示我指定的正确值。
我引用了您在回答中提到的那句话和我的结果,这让我相信现代显示器输出线性值(即不模拟CRT伽马)。
我们应用程序的目标平台是PC。对于此平台(不包括使用CRT或非常老的显示器的人),按照1的响应方式进行操作,然后对于2,不要进行伽马校正(即不进行最终的RGB->sRGB转换 - 手动或使用GL_FRAMEBUFFER_SRGB)是否是合理的?
如果是这样,那么GL_FRAMEBUFFER_SRGB适用于哪些平台(或在今天使用它将是有效的),还是说采用线性RGB的显示器真的很新(考虑到GL_FRAMEBUFFER_SRGB是2008年推出的)?
我在学校与其他几个图形开发人员交谈后,听起来他们都没有考虑到伽马校正,并且他们没有注意到任何不正确的地方(有些人甚至没有意识到它)。特别是一个开发人员说,当考虑伽马时,他得到了错误的结果,因此他决定不担心伽马。给出了矛盾的网络信息/我的项目看到的情况,我不确定在我的目标平台上该做什么。

编辑4 - 回应 datenwolf的更新答案

没错,如果信号链的某个位置应用了非线性转换,但从图像到显示器的所有像素值都未被修改,那么该非线性已经在图像像素值上预先应用。这意味着图像已经处于非线性颜色空间中。

如果我正在我的显示器上查看图像,则您的回复对我有意义。为了确保我清楚,当我说我正在查看图像的字节数组时,我是指查看纹理在内存中的数值,而不是屏幕上的图像输出(对于第二点,我确实这样做了)。在我看来,唯一的可能是如果图像编辑器使用sRGB空间为我提供值。

还要注意,我确实尝试在监视器上检查输出,以及修改纹理颜色(例如,将其除以一半或加倍),并且输出似乎是正确的(使用我下面描述的方法进行测量)。

你如何测量信号响应?

不幸的是,我的测量方法远不如你的精密。当我说我在我的监视器上进行了实验时,我意味着输出一个固定颜色的全屏四边形,其颜色在着色器中硬编码为纯OpenGL帧缓冲区(写入时不执行任何颜色空间转换)。当我输出白色、75%灰色、50%灰色、25%灰色和黑色时,正确的颜色被显示。现在我的正确颜色的解释可能是错误的。我截屏,然后使用图像编辑程序查看像素值(以及视觉评估以确保值合理)。如果我正确理解,如果我的监视器是非线性的,我需要在呈现给显示设备之前执行RGB->sRGB转换才能使它们正确。

我不会撒谎,我感觉我有点深入了。我正在考虑为我的第二个困惑点(最终的RGB->sRGB转换)寻找的解决方案将是一个可调节的亮度设置,并将其默认设置为在我的设备上看起来正确的值(没有伽马校正)。


1
@NicolBolas在这个主题上写了一篇很棒的教程:http://arcsynthesis.org/gltut/Texturing/Tutorial%2016.html,而且这还不是详尽的。 - datenwolf
@datenwolf 我在上面链接了一些资源。我已经阅读过它,但是我得到的结果似乎与教程所示相反 - 除非我误解了它。 - Peter Clark
1
我抓住这个机会进一步撰写关于OpenGL和匹配色彩空间的文章。稍后我会发布一个摘录式的答案。 - datenwolf
1
好的,我的答案已经发布了。虽然有点长。 - datenwolf
@PeterClark 亲爱的 Peter,我想知道在长时间的讨论之后,你是否终于找到了解决问题的答案。因为我也面临着类似的问题:我拿到一个 bmp 图像,检查它是否是 sRGB 格式,这意味着使用 gamma = 1/2.2 进行 gamma 矫正,然后为了制作灰度图像,我对所有颜色通道应用反向矫正,使用 gamma=2.2,然后使用线性变换 'gray = 0.2126 * r + 0.7152 * g + 0.0722 * b',最后将灰度值使用 gamma = 1/2.2 进行转换,再保存为 bmp。令人惊讶的是,没有进行 gamma 矫正时,我得到了更好的灰度图像... - John Smith
显示剩余4条评论
2个回答

31

首先,您必须理解应用于颜色通道的非线性映射往往不仅仅是简单的幂函数。 sRGB非线性可以近似为x ^ 2.4,但这并不是真正的问题。无论如何,您的初步假设或多或少是正确的。

如果您的纹理存储在更常见的图像文件格式中,则它们将包含按照图形扫描输出呈现的值。现在有两种常见的硬件方案:

  • 扫描输出接口输出线性信号,显示设备将内部应用非线性映射。旧式CRT监视器由于其物理属性而是非线性的:放大器只能将有限的电流输入电子束,荧光体饱和等等,这就是为什么首先引入了整个Gamma事物,以模拟CRT显示器的非线性。

  • 现代LCD和OLED显示器要么在其驱动放大器中使用电阻器阶梯,要么在其图像处理器中具有Gamma斜坡查找表。

  • 然而,一些设备是线性的,并要求图像产生设备为扫描输出提供适当的匹配LUT,以获得所需的输出颜色配置文件。

在大多数计算机上,有效的扫描输出LUT是线性的!但这意味着什么?稍微来一点曲折:


为了说明问题,我快速连接了我的笔记本电脑模拟显示输出(VGA连接器)到模拟示波器:将蓝色通道连接到示波器通道1,将绿色通道连接到示波器通道2,外部触发线同步信号(HSync)。使用一个快速而简单的OpenGL程序,故意编写即时模式来生成线性颜色坡度:

#include <GL/glut.h>

void display()
{
    GLuint win_width = glutGet(GLUT_WINDOW_WIDTH);
    GLuint win_height = glutGet(GLUT_WINDOW_HEIGHT);

    glViewport(0,0, win_width, win_height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, 1, 0, 1, -1, 1);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    glBegin(GL_QUAD_STRIP);
        glColor3f(0., 0., 0.);
        glVertex2f(0., 0.);
        glVertex2f(0., 1.);
        glColor3f(1., 1., 1.);
        glVertex2f(1., 0.);
        glVertex2f(1., 1.);
    glEnd();

    glutSwapBuffers();
}

int main(int argc, char *argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
    
    glutCreateWindow("linear");
    glutFullScreen();
    glutDisplayFunc(display);

    glutMainLoop();

    return 0;
}

图形输出已经配置了模型线

"1440x900_60.00"  106.50  1440 1528 1672 1904  900 903 909 934 -HSync +VSync

(因为这是平板电视运行的相同模式,而我正在使用克隆模式)

  • 绿色通道上使用gamma=2 LUT。
  • 蓝色通道上使用线性(gamma=1) LUT。

以下是单个扫描行的信号样貌(上方曲线: Ch2 = 绿色,下方曲线: Ch1 = 蓝色):

Analogue Video Signals Gamma=1 and Gamma=2

您可以清楚地看到x⟼x²和x⟼x映射 (曲线的抛物线和线条形状)。


现在在这个小的迂回之后,我们知道,进入主帧缓冲区的像素值就按原样进入:OpenGL的线性RAM没有经历进一步的变化,只有当应用非线性扫描LUT时,它才会改变发送到显示器的信号。

无论如何,您呈现给扫描的值(这意味着屏幕上的帧缓冲区)将在信号链中的某个点上经历非线性映射。对于所有标准的消费者设备,此映射将遵循sRGB标准,因为它是最小公因数(即在sRGB颜色空间中表示的图像可以在大多数输出设备上再现)。

由于大多数程序(如Web浏览器)假设输出将经过sRGB到显示颜色空间的映射,因此它们仅将标准图像文件格式的像素值复制到屏幕帧缓冲区中,而不执行颜色空间转换,从而暗示这些图像中的颜色值处于sRGB颜色空间(或者如果图像颜色配置文件不是sRGB,则通常只会转换为sRGB);(如果,并且仅当写入帧缓冲区的颜色值未经扫描即可传送到显示器;假设扫描LUT是显示器的一部分),正确的做法是将其转换为显示器所需的指定颜色配置文件。

但这意味着,屏幕帧缓冲区本身就处于sRGB颜色空间中(我不想纠缠细节,关于这是多么愚蠢的事情,让我们接受这个事实)。

如何将此与OpenGL结合起来?首先,OpenGL执行所有的颜色操作都是线性的。然而,由于扫描输出期望在某种非线性颜色空间中,这意味着OpenGL的渲染操作的最终结果必须以某种方式带入屏幕帧缓冲区颜色空间。

这就是ARB_framebuffer_sRGB扩展(已经成为OpenGL-3的核心)发挥作用的地方,它引入了用于窗口像素格式配置的新标志:

New Tokens

    Accepted by the <attribList> parameter of glXChooseVisual, and by
    the <attrib> parameter of glXGetConfig:

        GLX_FRAMEBUFFER_SRGB_CAPABLE_ARB             0x20B2

    Accepted by the <piAttributes> parameter of
    wglGetPixelFormatAttribivEXT, wglGetPixelFormatAttribfvEXT, and
    the <piAttribIList> and <pfAttribIList> of wglChoosePixelFormatEXT:

        WGL_FRAMEBUFFER_SRGB_CAPABLE_ARB             0x20A9

    Accepted by the <cap> parameter of Enable, Disable, and IsEnabled,
    and by the <pname> parameter of GetBooleanv, GetIntegerv, GetFloatv,
    and GetDoublev:

        FRAMEBUFFER_SRGB                             0x8DB9

如果您的窗口配置了这样的sRGB像素格式,并在OpenGL中启用了sRGB光栅化模式,如glEnable(GL_FRAMEBUFFER_SRGB);,则线性色彩空间渲染操作的结果将转换为sRGB颜色空间。

另一种方法是将所有内容渲染到离屏FBO中,并在后处理着色器中进行颜色转换。

但这只是渲染信号链的输出端。您还有输入信号,以纹理形式存在。而那些通常是带有非线性存储像素值的图像。因此,在这些图像可以用于线性图像操作之前,必须首先将这些图像带入线性颜色空间。我们暂时忽略映射非线性颜色空间到线性颜色空间本身会引起一系列问题 - 这就是为什么sRGB颜色空间如此之小的原因,即为了避免这些问题。

因此,引入了扩展EXT_texture_sRGB,该扩展非常重要,以至于它从未经历过ARB,而是直接进入OpenGL规范本身:欣赏GL_SRGB…内部纹理格式。

使用此格式加载的纹理会在用于源采样之前经过sRGB到线性RGB颜色空间转换。这会产生适合于线性渲染操作的线性像素值,结果可以在转到主屏幕帧缓冲区时有效地转换为sRGB。

对于整个问题的个人看法:在目标设备颜色空间中呈现图像是一个巨大的设计缺陷。在这种设置下,没有办法做到一切正确,而不会变得疯狂。

人们真正想要的是将屏幕帧缓冲区放在一个线性、接触式的颜色空间中;自然的选择应该是CIEXYZ。渲染操作自然会在同一接触式颜色空间中进行。在接触式颜色空间中执行所有图形操作,可以避免尝试将名为线性RGB的正方形钉子推入名为sRGB的非线性、圆形孔时所涉及的问题。

尽管我不太喜欢Weston / Wayland的设计,但至少它提供了实际实现这样的显示系统的机会,方法是让客户端在接触式颜色空间中进行渲染并使合成器在最后一个后处理步骤中应用输出设备的颜色配置文件。

接触式颜色空间唯一的缺点是必须使用深色(即每个颜色通道> 12位)。实际上,即使是非线性RGB(非线性度可以帮助一些掩盖感知分辨率的不足),8位也完全不足够。

更新

我加载了几个图像(在我的例子中是 .png 和 .bmp 图像),并检查了原始的二进制数据。对我来说,这些图像似乎实际上是在 RGB 彩色空间中,因为如果我将像素值与图像处理程序进行比较,并将其与程序中获得的字节数组进行比较,则它们完全匹配。由于我的图像编辑器提供的是 RGB 值,这表明存储在 RGB 中。

确实如此。如果信号链的某个地方应用了非线性变换,但从图像到显示屏的所有像素值均未修改,则该非线性已经预先应用于图像的像素值。这意味着,图像已经处于非线性颜色空间中。

这个想法在接下来紧随其后的部分中阐述,我展示了如何将纯正(OpenGL)帧缓冲区中的值直接传递给监视器,而不会改变。sRGB 的思路是“将值放入与发送到监视器完全相同的图像中,并构建消费者显示器以遵循 sRGB 颜色空间”。

您是如何测量信号响应的?您是否使用校准功率计或类似设备测量显示器对信号的光强度响应?这不能信任您的眼睛,因为像我们所有的感官一样,我们的眼睛具有对数信号响应。


更新 2

对我来说,您所说的唯一可能性是,图像编辑器向我提供了 sRGB 空间中的值。

确实如此。由于色彩管理被添加到所有广泛使用的图形系统中作为事后添麻烦的操作,大多数图像编辑器会在其目标色彩空间中编辑像素值。请注意,sRGB 的一个特定设计参数是,它应该仅以回溯方式指定未经管理的直接值传输颜色操作,就像消费者设备上通常(并仍然)执行的操作一样。由于根本没有进行颜色管理,因此包含在图像中并在编辑器中操作的值必须已经处于 sRGB 中。只要图像不是通过线性渲染过程合成的,这种方法就可以使用;否则,在后一种情况下,渲染系统必须考虑目标色彩空间。

您截取屏幕截图,然后使用图像处理程序查看像素的值

这当然只给出了扫描缓冲区中的原始值,没有应用伽马 LUT 和显示器的非线性。


1
@PeterClark:如果新问题是您原始问题的细化,请将其编辑到问题中。如果它们创建了一个新主题,最好提出一个不同的问题并交叉引用它们。 - datenwolf
我已经更新了我的问题并列出了我困惑的点。很抱歉让您等这么久。 - Peter Clark
我认为更新2给了我足够的工作和纠正我们的颜色流程,非常感谢您的澄清! - Peter Clark
疯狂的事情确实存在。但它是由于8位量化的原因而存在的。由于我们的眼睛是对数的,因此在屏幕上使用power(2.2)会使量化中的线性坡道在我们的眼中呈现为线性。这对于位熵均匀性非常有利。如果我们到处使用线性存储,每个通道都需要10位。所以要么接受这种疯狂,要么让每个人扔掉一些位。这将是一个严重的行业转变。所有图像格式都存储每个通道的8位,除了HDR格式(exr、rgbe等)。 - v.oddou
@v.oddou:大多数OpenGL实现都可以很好地支持位深度格式。这就是这个问题和讨论的全部内容。顺便说一句:如果应用了非线性操作,您将无法正确执行图像操作(有趣的是,包括非常昂贵的许多图像处理程序在内,都会犯这个错误,请参见http://www.4p8.com/eric.brasseur/gamma.html了解详情)。当涉及到屏幕格式时,R10_G10_B10_A2也得到了广泛支持。几个月前,我在Wayland-Devel邮件列表上就此进行了交流。 - datenwolf
显示剩余10条评论

0

我想简单解释一下最初尝试出了什么问题,因为尽管被接受的答案深入探讨了色彩空间理论,但它并没有真正回答这个问题。

管道的设置完全正确:使用GL_SRGB8_ALPHA8用于纹理,使用GL_FRAMEBUFFER_SRGB(或自定义着色器代码)在最后转换回sRGB,并且所有中间计算都将使用线性光。

最后一个部分是你遇到麻烦的地方。你想要一种颜色为(255、106、0)的光- 但那是一种sRGB颜色,而你正在使用线性光工作。为了获得你想要的颜色,你需要将该颜色转换为线性空间,就像GL_SRGB8_ALPHA8对于你的纹理所做的那样。对于你的情况,这将是一个强度为(1、.1441、0)的vec3光-这是应用伽马压缩后的值。


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