写在前面
我相信几乎所有做图像处理方面的人都听过伽马校正(Gamma Correction)这一个名词,但真正明白它是什么、为什么要有它、以及怎么用它的人其实不多。我也不例外。
最初我查过一些资料,但很多文章的说法都不一样,有些很晦涩难懂。直到我最近在看《Real Time Rendering,3rd Edition》这本书的时候,才开始慢慢对它有所理解。
本人才疏学浅,写的这篇文章很可能成为网上另一篇误导你的“伽马传说”,但我尽可能把目前了解的资料和可能存在的疏漏写在这里。如有错误,还望指出。
伽马的传说
关于这个方面,龚大写过一篇文章,但我认为其中的说法有不准确的地方。
从我找到的资料来看,人们使用伽马曲线来进行显示最开始是源于一个巧合:在早期,CRT几乎是唯一的显示设备。但CRR有个特性,它的输入电压和显示出来的亮度关系不是线性的,而是一个类似幂律(pow-law)曲线的关系,而这个关系又恰好跟人眼对光的敏感度是相反的。这个巧合意味着,虽然CRT显示关系是非线性的,但对人类来说感知上很可能是一致的。
我来详细地解释一下这个事件:在很久很久以前(其实没多久),全世界都在使用一种叫CRT的显示设备。这类设备的显示机制是,使用一个电压轰击它屏幕上的一种图层,这个图层就可以发亮,我们就可以看到图像了。但是,人们发现,咦,如果把电压调高两倍,屏幕亮度并没有提高两倍啊!典型的CRT显示器的伽马曲线大致是一个伽马值为2.5的幂律曲线。显示器的这类伽马也称为display gamma。由于这个问题的存在,那么图像捕捉设备就需要进行一个伽马校正,它们使用的伽马叫做encoding gamma。所以,一个完整的图像系统需要2个伽马值:
- encoding gamma:它描述了encoding transfer function,即图像设备捕捉到的场景亮度值(scene radiance values)和编码的像素值(encoded pixel values)之间的关系。
- display gamma:它描述了display transfer function,即编码的像素值和显示的亮度(displayed radiance)之间的关系。
如下图所示:
而encoding gamma和display gamma的乘积就是真个图像系统的end-to-end gamma。如果这个乘积是1,那么显示出来的亮度就是和捕捉到的真实场景的亮度是成比例的。
上面的情景是对于捕捉的相片。那么对于我们渲染的图像来说,我们需要的是一个encoding gamma。如果我们没有用一个encoding gamma对shader的输出进行校正,而是直接显示在屏幕上,那么由于display gamma的存在就会使画面失真。
至此为止,就是龚大 所说的伽马传说。由此,龚大认为全部的问题都出在CRT问题上,跟人眼没有任何关系。
但是,在《Real-time Rendering》一书中,指出了这种乘积为1的end-to-end gamma的问题。看起来,乘积为1的话,可以让显示器精确重现原始场景的视觉条件。但是,由于原始场景的观察条件和显示的版本之间存在两个差异:1)首先是,我们能够显示的亮度值其实和真实场景的亮度值差了好几个数量级,说通俗点,就是显示器的精度根本达不到真实场景的颜色精度(大自然的颜色种类几乎是无穷多的,而如果使用8-bit的编码,我们只能显示256^3种颜色);2)这是一种称为surround effect的现象。在真实的场景中,原始的场景填充了填充了观察者的所有视野,而显示的亮度往往只局限在一个被周围环境包围的屏幕上。这两个差别使得感知对比度相较于原始场景明显下降了。也就是我们一开始说的,对光的灵敏度对不同亮度是不一样的。如下图所示(来源: Youtube: Color is Broken):
为了中和这种现象,所以我们需要乘积不是1的end-to-end gamma,来保证显示的亮度结果在感知上和原始场景是一致的。根据《Real-time Rendering》一书中,推荐的值在电影院这种漆黑的环境中为1.5,在明亮的室内这个值为1.125。
个人电脑使用的一个标准叫sRGB,它使用的encoding gamma大约是0.45(也就是1/2.2)。这个值就是为了配合display gamma为2.5的设备工作的。这样,end-to-end gamma就是0.45 * 2.5 = 1.125了。
这意味着,虽然CRT的display gamma是2.5,但我们使用的encoding gamma应该是1.125/2.5 = 1/2.2,而不是1/2.5。这样才能保证end-to-end gamma为1.125,从而在视觉上进行了补偿。
虽然现在CRT设备很少见了,但为了保证这种感知一致性(这是它一直沿用至今的很重要的一点),同时也为了对已有图像的兼容性(之前很多图像使用了encoding gamma对图像进行了编码),所以仍在使用这种伽马编码。而且,现在的LCD虽然有不同的响应曲线(即display gamma不是2.5),但是在硬件上做了调整来提供兼容性。
重要:上面的说法主要来源于Real-time Rendering》一书。
来自其他领域的伽马传说
今天很幸运听了知乎上韩世麟童鞋的讲解。在听了他的讲座后,我听到了另一个版本的伽马传说。和上面的讨论不同,他认为伽马的来源完全是由于人眼的特性造成的。对伽马的理解和职业很有关系,长期从事摄影、视觉领域相关的工作的人可能更有发言权。我觉得这个版本更加可信。感兴趣的同学可以直接去知乎上领略一下。
我在这里来大致讲一下他的理解。
事情的起因可以从在真实环境中拍摄一张图片说起。摄像机的原理可以简化为,把进入到镜头内的光线亮度编码成图像(例如一张JEPG)中的像素。这样很简单啦,如果采集到的亮度是0,像素就是0,亮度是1,像素就是1,亮度是0.5,像素就是0.5。这里,就是这里,出现了一点问题!如果我们假设只用8位空间来存储像素的话,以为着0-1可以表示256种颜色,没错吧?但是,人眼有的特性,就是对光的灵敏度在不同亮度是不一样的。还是这张图Youtube: Color is Broken:
这张图说明一件事情,即亮度上的线性变化在人眼看来是非均匀的,再通俗点,从0亮度变到0.01亮度,人眼是可以察觉到的,但从0.99变到1.0,人眼可能就根本差别不出来,觉得它们是一个颜色。也就是说,人眼对暗部的变化更加敏感,而对亮部变化其实不是很敏感。也就是说,人眼认为的中灰其实不在亮度为0.5的地方,而是在大约亮度为0.18的地方(18度灰)。强烈建议去看一下Youtube上的视频, Color is Broken。
那么,这和拍照有什么关系呢?如果在8位图中,我们仍然用0.5亮度编码成0.5的像素,那么暗部和亮部区域我们都使用了128种颜色来表示,但实际上,亮部区域使用这么多种其实相对于暗部来说是种存储浪费。不浪费的做法是,我们应该把人眼认为的中灰亮度放在像素值为0.5的地方,也就是说,0.18亮度应该编码成0.5像素值。这样存储空间就可以充分利用起来了。所以,摄影设备如果使用了8位空间存储照片的话,会用大约为0.45的encoding gamma来对输入的亮度编码,得到一张图像。0.45这个值完全是由于人眼的特性测量得到的。
那么显示的时候到了。有了一张图片,显示的时候我们还是要把它还原成原来的亮度值进行显示。毕竟,0.454只是为了充分利用存储空间而已。我们假设一下,当年CRT设备的输入电压和产生亮度之间完全是线性关系,我们还是要进行伽马校正的。这是为了把用0.45伽马编码后的图像正确重现在屏幕上。巧合的是,当年人们发现CRT显示器竟然符合幂律曲线!人们想,“天哪,太棒了,我们不需要做任何调整就可以让拍摄的图像在电脑上看起来和原来的一样了”。这就是我们一直说的“那个巧合”。当年,CRT的display gamma是2.5,这样导致最后的end-to-end gamma大约是0.45 * 2.5 = 1.125,其实是非1的。
直到后来,微软联合爱普生、惠普提供了sRGB标准,推荐显示器中display gamma值为2.2。这样,配合0.45的encoding gamma就可以保证end-to-end gamma为1了。当然,上一节提到的两个观察差异,有些时候我们其实更希望end-to-end gamma非1的结果,例如,在电影院这种暗沉沉的环境中,end-to-end gamma为1.5我们人看起来更爽、更舒服,而在明亮的办公室这种环境中1.125的end-to-end gamma值更舒服、更漂亮。所以,我们可以根据环境的不同,去选择使用什么样的display gamma。
总之, 伽马校正一直沿用至今说到底是人眼特性决定的。你会说,伽马这么麻烦,什么时候可以舍弃它呢?按 韩世麟童鞋的说法,如果有一天我们对图像的存储空间能够大大提升,通用的格式不再是8位的时候,例如是32位的时候,伽马就没有用了。因为,我们不需要为了提高精度而把18度灰编码成0.5像素,因为我们有足够多的颜色空间可以利用,不需要考虑人眼的特性。
好啦,上面就是来自摄影、建筑领域的看法和理解。希望这两种看法可以让大家更深地理解伽马校正的存在意义。
这和渲染有什么关系
其实,对伽马传说的理解就算有偏差,也不会影响我们对伽马校正的使用。我们只要知道,根据sRGB标准,大部分显示器使用了2.2的display gamma来显示图像。
前面提到了,和渲染相关的是encoding gamma。我们知道了,显示器在显示的时候,会用display gamma把显示的像素进行display transfer之后再转换成显示的亮度值。所以,我们要在这之前,像图像捕捉设备那样,对图像先进行一个encoding transfer,与此相关的就是encoding gamma了。