我是不是需要对现代计算机/显示器上的最终颜色输出进行伽玛校正
Posted
技术标签:
【中文标题】我是不是需要对现代计算机/显示器上的最终颜色输出进行伽玛校正【英文标题】:Do I need to gamma correct the final color output on a modern computer/monitor我是否需要对现代计算机/显示器上的最终颜色输出进行伽玛校正 【发布时间】:2014-05-26 09:49:38 【问题描述】:我一直假设我的伽马校正管道应该如下:
对 (GL_SRGB8_ALPHA8
) 中加载的所有纹理使用 sRGB 格式,因为所有艺术程序都会预先伽玛校正其文件。在着色器中从GL_SRGB8_ALPHA8
纹理采样时,OpenGL 将自动转换为线性空间。
在线性空间中进行所有光照计算、后期处理等。
在写入将在屏幕上显示的最终颜色时转换回 sRGB 空间。
请注意,在我的情况下,最终颜色写入涉及我从 FBO(它是线性 RGB 纹理)写入后台缓冲区。
我的假设受到了挑战,好像我在最后阶段进行了伽玛校正,我的颜色比应有的更亮。我设置了由值 255, 106, 0 的灯光绘制的纯色,但是当我渲染时我得到 255, 171, 0 (由印刷筛选和颜色挑选确定)。而不是橙色,我得到黄色。如果我在最后一步 不 gamma 不正确,我会得到正确的值 255, 106, 0 。
根据some resources 的说法,现代 LCD 屏幕模仿 CRT 伽马。他们总是吗?如果不是,我怎么知道我是否应该伽玛正确?我在其他地方出错了吗?
编辑 1
我现在注意到,即使我用光写的颜色是正确的,但我使用纹理颜色的地方是不正确的(但在没有伽马校正的情况下,我期望的颜色要深得多)。我不知道这种差异来自哪里。
编辑 2
在为我的纹理尝试GL_RGBA8
而不是GL_SRGB8_ALPHA8
之后,一切看起来都很完美,即使在光照计算中使用纹理值时也是如此(如果我将光强度减半,则输出颜色值减半)。
我的代码在任何地方都不再考虑 gamma 校正,而且我的输出看起来正确。
这让我更加困惑,不再需要/使用伽玛校正了吗?
编辑 3 - 回复 datenwolf's answer
经过更多的试验后,我对这里的几点感到困惑。
1 - 大多数图像格式都是非线性存储的(在 sRGB 空间中)
我已经加载了一些图像(在我的例子中是 .png 和 .bmp 图像)并检查了原始二进制数据。在我看来,图像实际上是在 RGB 色彩空间中,就好像我将像素值与图像编辑程序与我在程序中得到的字节数组进行比较,它们完全匹配。由于我的图像编辑器给了我 RGB 值,这将表明图像存储在 RGB 中。
我正在使用 stb_image.h/.c 加载我的图像,并一直跟随它加载一个 .png,并且在加载时没有看到它对图像进行伽玛校正的任何地方。我还在十六进制编辑器中检查了 .bmp,磁盘上的值与它们匹配。
如果这些图像实际上存储在磁盘上的线性 RGB 空间中,我应该如何(以编程方式)知道何时指定图像位于 sRGB 空间中?是否有某种方法可以查询功能更强大的图像加载器可能会提供的信息?或者是否由图像创建者将他们的图像保存为伽马校正(或不校正) - 这意味着建立一个约定并为给定的项目遵循它。我问了几位艺术家,他们都不知道伽马校正是什么。
如果我指定我的图像是 sRGB,它们会太暗,除非我最终进行伽玛校正(如果显示器输出使用 sRGB,这将是可以理解的,但请参阅第 2 点)。
2 - “在大多数计算机上,有效的扫描 LUT 是线性的!但这意味着什么?”
我不确定我能在你的回复中找到这个想法在哪里结束。
据我所知,经过试验,我在输出线性值上测试过的所有显示器。如果我绘制了一个全屏四边形并在着色器中使用硬编码值对其进行着色,并且 no 伽马校正,则监视器将显示我指定的正确值。
我上面从你的回答和我的结果中引用的句子会让我相信现代显示器输出线性值(即不模拟 CRT 伽马)。
我们的应用程序的目标平台是 PC。对于这个平台(不包括使用 CRT 或非常旧的显示器的人),做任何你对 #1 的反应是否合理,然后对于 #2 到 not 伽玛正确(即不执行最终的 RGB ->sRGB 转换 - 手动或使用 GL_FRAMEBUFFER_SRGB)?
如果是这样,GL_FRAMEBUFFER_SRGB 适用于哪些平台(或者现在可以在哪些平台上使用它),或者使用线性 RGB 的显示器真的那么新(鉴于 GL_FRAMEBUFFER_SRGB 是 2008 年推出的)?
--
我与学校的其他一些图形开发人员交谈过,从他们的声音来看,他们都没有考虑到 gamma 校正,他们也没有发现任何不正确的地方(有些人甚至没有意识到这一点)。一位开发人员特别表示,在考虑 gamma 时他得到了错误的结果,因此他决定不担心 gamma。 鉴于我在网上/看到的项目信息相互矛盾,我不确定在我的项目中针对我的目标平台做什么。
编辑 4 - 回复 datenwolf's updated answer
是的,确实如此。如果在信号链的某个地方应用了非线性变换,但所有像素值从图像到显示器都没有被修改,那么这种非线性已经预先应用在图像的像素值上。这意味着,图像已经处于非线性色彩空间中。
如果我正在检查显示器上的图像,您的回答对我来说是有意义的。为了确保我清楚,当我说我正在检查图像的字节数组时,我的意思是我正在检查内存中纹理的数值,而不是屏幕上的图像输出(我做了 执行第 2 点)。对我来说,我能看到你所说的是真的唯一方法是图像编辑器是否在 sRGB 空间中给我值。
还请注意,我确实尝试检查监视器上的输出,以及修改纹理颜色(例如,除以一半或加倍)并且输出看起来正确(使用方法测量我在下面描述)。
您是如何测量信号响应的?
不幸的是,我的测量方法比你的粗鲁得多。当我说我在我的显示器上进行实验时,我的意思是我输出纯色全屏四边形,其颜色在着色器中硬编码到普通的 OpenGL 帧缓冲区(写入时不会进行任何颜色空间转换)。当我输出白色、75% 灰色、50% 灰色、25% 灰色和黑色时,会显示正确的颜色。现在在这里我对正确颜色的解释肯定是错误的。我截取屏幕截图,然后使用图像编辑程序查看像素值是多少(以及视觉评估以确保这些值有意义)。如果我理解正确,如果我的显示器是非线性的,我需要在将它们呈现给显示设备之前执行 RGB->sRGB 转换以确保它们正确。
我不会撒谎,我觉得我在这里有点超出我的深度。我在想我可能会为我的第二个困惑点(最终的 RGB->sRGB 转换)提出的解决方案将是一个可调整的亮度设置,并将其默认为在我的设备上看起来正确的值(无伽马校正)。
【问题讨论】:
@NicolBolas 写了一篇关于这个主题的优秀教程:arcsynthesis.org/gltut/Texturing/Tutorial%2016.html 这还不够详尽 @datenwolf 我将其链接为上面的“一些资源”。我已经阅读了它,但我得到的结果似乎与教程所指示的相反 - 除非我误解了它。 我借此机会进一步撰写了有关 OpenGL 和匹配色彩空间的文章。稍后我会发布一个摘录的答案。 好的,我的答案已经出来了。虽然有点长。 @PeterClark 亲爱的彼得,我想知道经过长时间的讨论,您是否终于找到了问题的答案。因为我面临着类似的问题:我拍摄了一张 bmp 图像,我检查它是否为 sRGB 格式,这意味着使用gamma = 1/2.2
进行伽玛校正,然后为了进行灰度我使用 gamma=2.2
对所有颜色通道应用逆校正,然后使用线性变换 'gray = 0.2126 * r + 0.7152 * g + 0.0722 * b' 然后用 gamma = 1/2.2
变换灰色,然后保存为 bmp。令人惊讶的是,没有伽马校正,我得到了更好的灰度图像......
【参考方案1】:
首先您必须了解,应用于颜色通道的非线性映射通常不仅仅是一个简单的幂函数。 sRGB 非线性可以近似为 x^2.4,但这并不是真正的交易。无论如何,您的主要假设或多或少是正确的。
如果您的纹理以更常见的图像文件格式存储,它们将包含呈现给图形扫描输出的值。现在有两种常见的硬件场景:
扫描输出接口输出线性信号,然后显示设备将在内部应用非线性映射。旧的 CRT 显示器由于其物理特性是非线性的:放大器只能将这么多的电流放入电子束中,荧光粉饱和等等——这就是为什么首先引入整个伽马的原因,以模拟 CRT 显示器的非线性.
现代 LCD 和 OLED 显示器要么在其驱动放大器中使用梯形电阻器,要么在其图像处理器中使用伽马斜坡查找表。
然而,有些设备是线性的,并要求图像生成设备为扫描输出所需的输出颜色配置文件提供适当的匹配 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;
图形输出是用 Modelline 配置的
"1440x900_60.00" 106.50 1440 1528 1672 1904 900 903 909 934 -HSync +VSync
(因为这是平板运行的相同模式,而我使用的是克隆模式)
绿色通道上的 gamma=2 LUT。 蓝色通道上的线性 (gamma=1) LUT这是单条扫描线的信号的样子(上曲线:Ch2 = 绿色,下曲线:Ch1 = 蓝色):
您可以清楚地看到 x⟼x² 和 x⟼x 映射(曲线的抛物线和线性形状)。
现在经过这个小弯路,我们知道,进入主帧缓冲区的像素值按原样进入:OpenGL 线性斜坡没有进一步变化,只有在应用非线性扫描输出 LUT 时,它才会改变发送的信号到显示器。
无论哪种方式,您呈现给扫描输出的值(即屏幕上的帧缓冲区)都会在信号链中的某个点进行非线性映射。对于所有标准消费设备,此映射将根据 sRGB 标准,因为它是最小的公因子(即,以 sRGB 色彩空间表示的图像可以在大多数输出设备上再现)。
由于大多数程序(如网络浏览器)假定输出经过 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 中使用glEnable(GL_FRAMEBUFFER_SRGB);
启用 sRGB 光栅化模式,则线性色彩空间渲染操作的结果将转换为 sRGB 色彩空间。
另一种方法是将所有内容渲染到屏幕外 FBO 并在后处理着色器中进行颜色转换。
但这只是渲染信号链的输出端。您还获得了纹理形式的输入信号。这些通常是图像,它们的像素值是非线性存储的。因此,在将这些图像用于线性图像操作之前,必须先将这些图像带入线性颜色空间。让我们暂时忽略,将非线性色彩空间映射到线性色彩空间会在其自身上打开几罐蠕虫——这就是为什么 sRGB 色彩空间如此小得离谱的原因,即避免这些问题。
因此,为了解决这个问题,引入了扩展 EXT_texture_sRGB
,结果证明它非常重要,以至于它从未成为 ARB,而是直接进入 OpenGL 规范本身:看看 GL_SRGB…
内部纹理格式。
以这种格式加载的纹理在用于源样本之前会经过 sRGB 到线性 RGB 颜色空间的转换。这给出了线性像素值,适用于线性渲染操作,然后在进入主屏幕帧缓冲区时可以将结果有效地转换为 sRGB。
关于整个问题的个人说明:在目标设备颜色空间中的屏幕帧缓冲区上呈现图像恕我直言,这是一个巨大的设计缺陷。在这样的设置中,没有办法在不发疯的情况下做所有事情。
真正想要的是在一个线性的、接触的颜色空间中拥有屏幕上的帧缓冲区;自然的选择是CIEXYZ。渲染操作自然会在相同的接触颜色空间中进行。在接触颜色空间中执行所有图形操作,避免了前面提到的试图将名为线性 RGB 的方形钉穿过名为 sRGB 的非线性圆孔时所涉及的蠕虫罐的打开。
虽然我不太喜欢 Weston/Wayland 的设计,但至少它提供了实际实现这样一个显示系统的机会,通过让客户端渲染和合成器在接触色彩空间中操作并应用输出最后一个后处理步骤中的设备颜色配置文件。
接触颜色空间的唯一缺点是,必须使用深色(即每个颜色通道 > 12 位)。事实上 8 位是完全不够的,即使是非线性 RGB(非线性有助于掩盖可感知分辨率的不足)。
更新
我已经加载了一些图像(在我的例子中是 .png 和 .bmp 图像)并检查了原始二进制数据。在我看来,图像实际上是在 RGB 颜色空间中,就好像我将像素值与图像编辑程序与我在程序中得到的字节数组进行比较,它们完全匹配。由于我的图像编辑器给了我 RGB 值,这将指示存储在 RGB 中的图像。
是的,确实如此。如果在信号链的某个地方应用了非线性变换,但所有像素值从图像到显示器都没有被修改,那么这种非线性已经预先应用在图像的像素值上。这意味着,图像已经处于非线性色彩空间中。
2 - “在大多数计算机上,有效的扫描 LUT 是线性的!但这意味着什么?
我不确定我能在你的回复中找到这个想法在哪里结束。
这个想法在接下来的部分中进行了详细说明,我在其中展示了您放入普通 (OpenGL) 帧缓冲区的值如何直接进入监视器,未经修改。 sRGB 的理念是“将值完全按照发送到显示器的方式放入图像中,并构建消费者显示器以遵循 sRGB 色彩空间”。
据我所知,经过试验,我在输出线性值上测试过的所有显示器。
您是如何测量信号响应的?您是否使用校准的功率计或类似设备来测量显示器响应信号发出的光强度?你不能相信你的眼睛,因为就像我们所有的感官一样,我们的眼睛有对数信号响应。
更新 2
对我来说,我能看到你所说的是真的唯一方法是图像编辑器是否在 sRGB 空间中给我值。
确实如此。因为颜色管理是事后才添加到所有广泛使用的图形系统中的,所以大多数图像编辑器在其目标颜色空间中编辑像素值。请注意,sRGB 的一个特定设计参数是,它应该仅追溯指定非托管、直接值传输颜色操作,因为它们在消费设备上完成(并且大部分仍然完成)。由于根本没有颜色管理,图像中包含的值和编辑器中操作的值必须已经在 sRGB 中。这可以持续很长时间,因为长图像不是在线性渲染过程中综合创建的;如果是后者,渲染系统必须考虑目标颜色空间。
我截取了一张截图,然后使用图像编辑程序查看像素的值是什么
这当然只为您提供扫描输出缓冲区中的原始值没有伽马 LUT 和应用的显示非线性。
【讨论】:
很棒的答案,它确实回答了我的主要问题,但也为我提出了更多问题。我现在会接受答案,但会在下周尝试制定(并希望回答)我的其他问题。最好的 SO 做法是在此处用我的发现编辑我的问题,还是提出(如果可以的话,回答)新问题? @PeterClark:如果新问题是对原始问题的改进,请将它们编辑到问题中。如果他们创建了一个新主题,最好提出不同的问题并交叉引用它们。 我已经用我的困惑点更新了我的问题。我很抱歉花了这么长时间。 我已经编辑了我的问题并回复了您的更新。再次感谢您抽出时间提供帮助。 我认为更新 2 足以让我使用和纠正我们的颜色管道,非常感谢您的澄清!以上是关于我是不是需要对现代计算机/显示器上的最终颜色输出进行伽玛校正的主要内容,如果未能解决你的问题,请参考以下文章