为什么每个程序员都应该知道Gamma

作者: koo叔 分类: OpenGL与图形学 发布时间: 2018-01-23 17:08 编辑

首先来做一个小的测试

如果你曾经写过或正在计划写图像处理方面相关的代码,你应该试着完成一下这个测试。如果测试结果中有一个或多个的回答是“是”,那么你写的代码很可能有一些错误导致输出不正确的结果。但你可能不会立即觉察出来,因为这个问题并不那么明显,在有些情况下可能会比较容易发现。

测试如下:

  • 我不知道什么是伽马校正(吁!)
  • Gamma是CRT显示器时代的遗留问题,现在几乎都用LCDs了,可以安全的忽略它。
  • Gamma在要求颜色显示非常精确的打印产业比较重要,一般的图像处理可以忽略它。
  • 我是一个游戏程序员,我不需要知道gamma。
  • 操作系统的图形库已经正确的处理了gamma问题。
  • 我用的流行图形库如OpenGL,DirectX等,已经正确处理了gamma问题。
  • (128,128,128)RGB值的像素显示亮度大约是(255,255,255)RGB值的像素显示亮度的一半。
  • 只需直接使用一些随机库和图像处理算法,就可以将像素数据从流行的图像格式(JPEG,PNG,GIF等)加载到buffer中。

如果你的大多数答案都是“是”,不用沮丧。因为一周之前我大部分的回答也是“是”。在某种程度上,Gamma问题并没有引起用户的注意(包括编写商业图形软件的程序员),而且到目前为止,大多数图形库、图像查看器,图形编辑器和绘图软件仍然没有正确处理Gamma问题,并显示了不正确的结果。

接着往下看,到最后,你将比大多数程序员都更了解Gamma问题。

神秘的Gamma校正

视觉可以说是人机交互中最重要的感官输入通道。但另人惊讶的是Gamma校正是程序员间谈论最少的话题之一,而且在技术文献中也很少提到,包括计算机图形学相关的书籍。在大多数计算机图形学的教科书里都没有明确提到Gamma校正的重要性,有些书虽然提到了Gamma校正,但表达的比较模糊和抽象,既没有举例说明实际应该怎么做,也没有展示不正确处理Gamma校正而产生的错误图像的例子。

在我写光线跟踪器时发现需要正确的处理Gamma问题,然而我当时对这个问题的理解也非常浅显。所以花了几天时间在网上查找相关资料,结果发现大部分文章都比较抽象和模糊,有一些包含了许多有趣但无关的细节,另一些则缺少形象的例子。其实Gamma问题并不是一个很难理解的概念,但是找到一篇能正确,完整和清晰的解释这个问题的文章却没那么容易。

什么是Gamma问题,我们为什么需要它?

我尝试从零开始,全面的解释一下Gamma问题

为了确保文中图像示例显示的正确性,需要在显示器(CRT或LCD无所谓)上使用现代浏览器查看。与显示器相比,平板电脑和手机通常显示的不准确,尽量避免使用。

光发射 vs 感知亮度

不管你信不信,下面图像中任意两条相邻竖线之间的光能发射的差是一个常数。换句话说,屏幕发出的光能量从左到右,从一个竖条到另一个竖条是以一个常量增加的。

image<center>图像1--均匀分布的发射光强的灰度条</center>
现在考虑下面这幅图像:

image<center>图像2--均匀分布的感知光强的灰度条</center>

上面哪幅图像的渐变显的更均匀呢?明显是第二幅!但为什么呢?我们刚刚建的第一幅图可是从最暗的黑色到最亮白色均匀增加的。但是为什么我们看到的不是一个自然的渐变呢。为什么我们感觉第二幅图才是均匀的线性渐变呢?


答案就在于人眼对光强度的反应,是非线性的。在第一幅图中,两个相邻竖条之间的差值是常数的:

<center>Δlinear=In − In−1</center>

而在第二幅图像中,坚条与坚条间并不是常数,而是遵循幂律关系。所有人类的感觉,知觉在刺激的大小和感知强度方面也都遵循相似的幂律关系

因此,我们认为在实际的物理光强度和感知亮度之间存在幂律关系
## 物理vs性线感知
假设我们想在电脑上存储一幅反应现实世界的图像(让我们假设现实世界存在完美的灰度渐变),下面是“现实世界对象”看起来的样子:

image<center>图像3--理想的灰度渐变</center> 现在假设我们有一个特定的计算机系统,只能存储5-bit灰度图像,这可以表示32种从纯黑到纯白的不同灰度的色调。另外,在这台特定的电脑上,灰度值与相应的物理的光强度成正比,结果会显示如图1那样的有32个竖条的灰度图,这个灰度图表示光强度值之间是连续线性的。

如果我们只使用这32种灰度值编码表示平滑渐变,我们得到类似下图的显示:

image<center>图像4--理想的用32位物理线性灰度值表示的平滑灰度渐变</center> 然而,这个过渡看起来相当不自然,尤其是左边,因为我们只有32种灰度值。如果眯一眯眼睛,就很容易让自己相信这是大概“精确”的平滑渐变。但注意这个之所以看起来比较平滑是因为左边比右边的跨度大,这样做是因为光发射是线性的,而我们的眼睛的感知是非线性的。 上面这种显示方式,虽然人眼看起来是比较平滑的渐变了,但明显失去了许多黑色的精度值,多了更多的白色的精度值,我们最好选择另一套不同的32种灰度值,让灰度值与感知上的灰度一致,而不是线性的光强度值。这样我们得到与图4差不多的图像: image<center>图5--理想的用32位物理线性灰度值表示的平滑灰度渐变</center>
我们在这里讨论的非线性是我们之前提到的幂律关系,将物理线性灰度值变换成感知线性灰度值的过程叫做Gamma校正
## 高效的图像编码
为什么上述的对应关系比较重要呢?所谓的“真彩色”或“24位”位图图像中的颜色数据被存储为三组8bit数据。每组能表示256种不同的颜色强度,如果这些颜色信息是物理线性的,那么我们将失去大量的暗色调颜色,而多了许多不必要的亮色调颜色,就像上面展示的那样。

显然,这并不是理想的结果。一种解决方案是将每个8位表示增加到16位(或更多).这将增加两倍或更多的存储需求,而这显然不是一个好的方案。另一种方案就是将256种表示成感知线性尺度上的强度值,使用这种编码,绝大多数图像都能充分的表示出来。

通过算法或线性捕捉设备(如数码相机或扫描仪的CMOS)生成的物理线性强度转换成离散的感知线性强度的过程叫Gamma编码
现在几乎所有的消费级电子设备都采用每通道8位的Gamma编码值表示光强度。如果你还记得前面讨论过的RGB(128,128,128)像素的问题,这个值不是RGB(255,255,255)像素的50%光强度,而只有22%左右。还是因为人类视觉感知是非线性的,在人类视觉看来,光源实际衰减到原来光强的22%,恰好是视觉感知的亮度的50%。RGB(128,128,128)像素亮度看起来是RGB(255,255,255)的一半。如果你发现这里比较困惑,那就稍微思考一下,对目前所讨论的内容有一个坚实的理解是非常重要的。

当然,Gamma编码总是假设图像最终是由人在计算机屏幕上看。可以将其看作是一个图像的有损MP3压缩。如果是其它目的(如用于科学分析或后处理图像),则通常使用线性比例是更好的选择。这一点稍后会说到
## Gamma转换函数
将线性空间转换为Gamma空间的过程称为Gamma编码(或Gamma压缩),反之则称为Gamma解码(或Gamma解压)
这两个转换公式非常简单,只需要使用上述的幂律函数:
<center>Vencoded=Vlinear1/γ</center> <center>Vlinear=Vencodedγ</center> 计算机显示中使用的标准γ值是2.2.主要是因为Gamma的2.2近似于人眼知觉的灵敏度。虽然受照明或其它条件的影响,每个人的确切的值不一定相同,但必须选一个标准值的话,2.2这个值已经证明足够好。
还有一个问题是,许多文章并没有提到一个非常重要的问题,那就是输入值必须在0-1的范围内,输出也将映射到相同的范围。可以看出,0-1之间的Gamma值用于编码(压缩),大于1的用于解码(解压)。下图演示了编码和解码的Gamma转换函数,加上Gamma值为1的情况。
image<center>图6--Gamma转换函数
a)Gamma编码或Gamma压缩(γ=1/2.2=0.4545)(输入是物理亮度,输出是感知亮度)
b)线性Gamma(γ=1.0)
c)Gamma解码或Gamma解压(γ=2.2)(输入是感知亮度,输出是物理亮度) </center> 到目前为止,我们只看了灰度值的例子,但对于RGB图像也没什么特别的--我们也仅仅是单独对每一个颜色通道使用同样的方法编码和解码。 ## Gamma vs sRGB sRGB是一种颜色空间,是目前消费级电子设备(包括显示器,数码相机,扫描仪,打印机和手持设备)的事实标准。也是互联网上图像的标准颜色空间。
sRGB规范定义了Gamma,用于编码和解码sRGB图像。sRGB的Gamma值非常接近标准的2.2Gamma值。但它有一个短的直线在非常暗的范围,以避免出现斜率为0导致的无穷大的情况。
其实不需要了解这些细节。最重要的是要知道,在99%的情况下,你可以使用sRGB代替普通gamma。这是因为自2005年左右,所有的显卡都在硬件上支持了sRGB颜色空间,大大节省了编码和解码时间。你的显示器的原生颜色空间几乎都是sRGB(除非是于于照片或视频作品的专业图像显示器),所以把一个sRGB编码的像素数据直接输出到帧缓冲,最终在屏幕上生成的图像看起来是正确的。流行的图像格式,如JPEG和PNG可以存储颜色空间的信息,但通常的图像不包含这些数据,在这种情况下,几乎所有的图片查看器和浏览器都按约定将颜色解释到sRGB空间。
## Gamma校正
上面已经说完了Gamma的编码和解码,但什么是Gamma校正呢?
虽然现在99%的显示器都使用sRGB色彩空间,但由于制造误差,绝大多数显示器会额外的使用Gamma校正达到最佳的显示效果。你可能从没校准过你的显示器,但并不意味着,它不会使用Gamma。
通过Gamma校正的微调,显示器可以使终在sRGB空间工作。通过校正显示器的Gamma曲线(在显卡或操作系统级别),可以得到更接近理想的Gamma函数。 ## 处理Gamma编码图像
如果整个世界默认为sRGB,那么通过相机生成的sRGB JPEG文件,就可以直接解码JPEG图像数据,复制到显卡的帧缓冲区,图像会正确的在sRGB LCD显示器中显示(这里的“正确”,意味着能更准确的反映拍摄的真实场景)
这问题就发生在用任意图像处理算法直接处理sRGB像素缓冲区的时候。由于Gamma编码是一个非线性变换,sRGB编码是 γ=1/2.2的非线性变换。但几乎在所有计算机图形学的书上涉及的图像处理算法,都假设是按实际光强线性编码的。这意味着直接将sRGB编码数据做为这些算法的输入时,渲染结果可能发生微妙或明显的错误。包括,缩放,模糊,组合,插值,抗锯齿等常见操作。 # 错误处理Gamma的效果 说了这么多的理论,来看看这些错误实际显示是什么样子的!我们将探讨最常见的当直接处理sRGB编码数据时的不正确结果。除了说明性的目的,这些示例,也有助于在绘图程序和图像处理库中发现Gamma处理不正确的的行为或错误。
这些例子能清楚的表明,gamma不正确的问题。多数情况下,生动,饱和的颜色最明显。柔和的颜色可能不那么明显,甚至可以忽略不计。但误差总是存在的。 ## 渐变 下面的图像显示了渐变在线性空间(上面的部分)和sRGB空间(下面的部分)显示的不同之处。直接在sRGB空间插值会产生更暗或更饱和的图像。
仅看这些,可能更喜欢sRGB空间的图像,特别是最后两个。但这并不是光在真实世界中的表现方式(想像两种颜色的光源照亮白色的墙。结果更像线性空间显示的情况)
image<center>图7--在图中的每一对渐变中,上半部分是两种颜色在线性空间插值后转换成sRGB(Gamma正确)的结果,下半部分是同样的两种颜色直接在sRGB空间(Gamma不正确)的插值结果</center>
几乎每个人都错了:CSS渐变和过渡是错误的(看这里了解细节),Photoshop是错误的(如CS6),甚至没有一个选项来修复它。
两个绘图程序得到的是正确结果,KritaPixelmator。SVG也可以让用户指定是否使用线性空间或sRGB空间进行插值,组合和动画.
## 颜色混合
使用没正确处理Gamma的绘图程序中的笔刷画图时,在某些生动的颜色组合的情况下,会产生奇怪的黑乎乎的过渡带。(笔刷就是一个小的渐变)
一些人在Adobe论坛中宣称如何在Photoshop中混合颜色来模仿现实生活中的画面。这其实和那无关,这只是由于直接到sRGB像素数据编程的结果,导致的遗留问题。
image<center>图8--Gamma不正确的颜色混合,左边是Gamma校正的图像(通过在Photoshop CS6启用“使用Gamma1.0混合颜色”选项),右边是不正确的Gamma(关闭Gamma1.0混合选项,使用默认的过期模式)</center>
## Alpha 混合或合成
作为颜色混合的另一种变体,来看看Alpha混合是如何显示的。看下面的彩色矩阵图像。左边Gamma正确的图像模仿了光在现实生活中的行为,而右边的sRGB空间混合显示了一些怪异的色调和亮度变化。
image<center>图9--Gamma不正确对Alpha混合的影响,上面的条是100%不透明,下面是50%透明.左图是Gamma校正图像(在Photoshop CS6处理)右图是不正确的结果,类似图8</center> 当将两张照片混合是,颜色错误的问题也很明显。在下图中左侧的Gamma正确的图像上,皮肤上的红色和黄色被保留并以自然的方式过渡到蓝色,而在右图则有一个明显的绿色投射。同样,这可能是你喜欢的你一个效果,但并不是正确的Alpha合成结果。
image<center>图10--合成照片时,Gamma不正确的显示结果,上面两个原始图像分别放在彼此的底部,在蓝色图像上有60%的不透明度。左图是Gamma校正的图像,右图是不正确的(由Photoshop cs6测试)</center> ## 图像缩放 下面的图像包含一个简单的黑白棋盘像素图案(左边是100%缩放,右边是400%的缩放)。眼睛眯一些来看,你会看到一个介于绝对黑色和白色的灰色强度(即50%的灰度)。
image<center>图像11--黑白格子图案常用于简单的Gamma校正程序,图像平均光强,等于50灰色正方形的平均光像,右图是放大400%后的实际图案。</center>
从上面来看,如果将图像缩到50%,结果应该是一个相似的处理过程,得到一个50%灰度的矩形填充。
来试一试。在下图A是一个棋盘模式,B是直接在sRGB空间缩放到50%(使用双立方插值),C是线性空间插值,然后转换到sRGB空间。
image<center>图12--Gamma不正确的情况下图像调整的效果。A是一个棋盘模式,B是在sRGB空间调整的结果(Photoshop CS6 8位RGB模式),C是缩放之前先转换成线性空间再缩放,然后再转回sRGB的结果(Photoshop CS6 32 RGB模式)</center>
毫不奇怪,C给出了正确的结果。但是没有正确的Gamma校正,灰色阴影可能不是模糊棋盘图案的精确匹配。甚至数学上也清楚的表明:一个50%像素的灰色像素,其亮度比白色像素高出一半,其RGB值大约为(186,186,186),看以下Gamma编码: <center>0.51/2.2≈0.72974
0.72974*255 = 186 </center>
(不用担心图像的50%灰度是RGB(187,187,187),这小差异是因为图像是sRGB编码,这里只是使用了更简化的Gamma公式)
Gamma不正确的大小调整也会导致一些图像上奇怪的色调偏移。
## 抗锯齿
当遇到Gamma校正时,反走样也不例外。在Gamma=2.2的空间里,文字有些厚,像粗体(如下面的右边图像),在线性空间运行看起来会好些(左图),虽然这个看起来又有点瘦。Photoshop的反走样字体默认使用gamma=1.42,确实产生了漂亮的结果(如中间图像),这是因为大部分字体设计的是Gamma不正确的,如果使用线性空间,字体看起来会更瘦一些。
image<center>图13--文字反走样在Gamma不正确时的效果。左图使用"Blend Text Colors Using Gamma"设置成1.0,中间设置成1.45,右边设置成2.2</center>
## 基于物理的渲染(PBR)
如果Gamma校正在一种问题上必须需要的话,那就是PBR。为了使结果更有真实感,Gamma在整个渲染管线都应该正确的处理。但通常会在这个过程中出现问题,以下是两种常见的问题:
- 在线性空间计算,但转换最终图像到sRGB空间时失败了,则通过对材质和灯光做微小的调整来补偿。 - 从sRGB空间转到线性空间失败(或使用硬件加速设置sRGB标志)
这两种基本错误以各种有趣的方式出现。但最终结果总是不能得到正确的实际场景。(如,二次光不出现二次了,亮度夸大了,奇怪的色相和饱和度变化等)。
用我的ray tracer演示第一个错误,下面的左边图像是一个非常简单但看起来非常自然的物理光照。这个渲染在线性空间中进行,然后在写入磁盘之前,将帧缓冲区数据转换到sRGB空间。
然后,看右图所示,展示的是,省略了最后一步的转换导致的结果,我试图调整光线亮度以匹配Gamma正确的亮度。很明显这不是正确的做法。所有物体看起来明暗差别非常强烈,所以我减淡材料的颜色,在靠近左边的物体附近补灯。但注定是一场失败的战争,再多的调整都不能使图像看起来在物理意义上是正确的。即使使用照明设置一个看起来可接受的特定场景。任何场景的变化都可能需要一轮新的调整使结果看起来正确。更重要的是选择的材质和照明参数完全没有任何物理意义。只是随机去适应特定的场景,而且不能适合其它场景。
image<center>图14--在漫反射球上的Gamma不正确的结果。右图是不正确的Gamma,通过调整参数,以适应左边正确的Gamma图像</center>
在3D渲染中找出不正确的Gamma也很重要,在一些老游戏中"fake plasticky CGI look"问题(人物看起来像塑料)就是其中之一。见下图,在gamma不正确的方式上渲染真实感的人物皮肤几乎是不可能的。高光看起来从来都不正确。修复这些问题应该从源头上解决问题而不是用各种微调来拟补错误。
image<center>图15--在人头像上Gamma不正确时的显示结果。上面部分看起来像真实的人脸,下面看起来像蜡像(图像来源由Unity3D manual)</center>
# 结论
这就是关于Gamma编码和解码的问题,恭喜你,现在是一个官方认证的gamma-compliant程序员!:)
回顾一下,使用Gamma编码的唯一原因,就是它可以让我们在有限的位编码长度上更有效的存储图像。它利用了人类视觉以幂律函数感知亮度的特点。大多数图像处理算法都是接收的线性编码,因此在运行这些算法之前,需要将gamma编码的图像先解码到线性空间。通常计算结果需要再转回到Gamma空间以便存储到硬盘或在支持gamma编码的显卡中显示(大部分显卡都支持)。标准的sRGB空间使用的Gamma值大约是2.2。互联网上的图像和大多数显示器,扫描仪和打印机默认的都是sRGB空间。当有疑问时,使用sRGB空间。
从终端用户的角度来看,大多数应用程序和软件库,都没有正确的处理Gamma,因此在工作中使用它们时一定用要充分的了解和大量的测试。为了线性的工作流,所有工作流的中间链上都要保证Gamma100%正确性。
如果你是一个图形程序员,请确保做正确的事情。在文档中显式的声明输入和输出的色彩空间,以保证Gamma的正确性。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表评论

你的email不会被公开。必填项已用*标注

更多阅读
标签云