1996年,HP和Microsoft联合发布了一个名为sRGB的色彩空间规范。这份文档的初衷很简单:让数字图像在不同设备上显示一致。将近三十年后,这个目标依然没有完全实现——你在电脑上精心调整的照片发到手机上看起来完全不同,设计师交付的作品在客户显示器上"面目全非"。

问题的根源不在于设备质量,而在于一个被大多数开发者忽视的概念:色彩空间与伽马校正。这不是设计师的玄学问题,而是一个有着明确数学定义和物理原理的工程问题。

一个直觉性的问题:RGB(128,128,128)真的是50%亮度吗

打开任意图像编辑软件,创建一个RGB值为(128, 128, 128)的灰色。直觉上,这应该是"中等灰色"——介于纯黑(0,0,0)和纯白(255,255,255)的正中间。但物理上,这个像素发出的光能量只有纯白色的约22%,而不是50%。

这不是显示器的缺陷,而是人眼感知的特性。人眼对亮度的感知是非线性的:要让一个光源看起来比另一个"亮一倍",实际光能量需要增加约4.5倍(更精确地说是2.2次方的变化)。换句话说:

  • 物理亮度翻倍 → 人眼感知只增加约40%
  • 人眼感知翻倍 → 物理亮度需要增加到约218%

这个非线性关系可以用幂函数描述:

$$L_{\text{perceived}} \approx L_{\text{physical}}^{0.45}$$

其中$L_{\text{physical}}$是物理光能量,$L_{\text{perceived}}$是人眼感知的亮度。这个指数约等于$1/2.2 \approx 0.45$,这正是伽马校正的核心数值。

CRT显示器的意外遗产

伽马值为2.2的历史来源是一个有趣的巧合。

早期的CRT(阴极射线管)显示器有一个物理特性:电子枪的输入电压与屏幕亮度之间存在非线性关系。具体来说,亮度的对数与电压的对数呈线性关系,斜率约为2.2到2.5。

$$I \propto V^{\gamma}$$

其中$I$是亮度,$V$是电压,$\gamma$(gamma)是幂指数,典型值约为2.2。

这个特性源于CRT的物理工作原理:电子从阴极发射后,需要被加速电压加速才能轰击荧光粉。电子束的电流与控制电压的关系服从Child-Langmuir定律,这是一个幂律关系。

在CRT时代,这个非线性被视为一个需要"补偿"的问题。电视广播系统在发送信号时预先对图像进行"伽马编码"(提升暗部),让CRT的非线性响应将其"解码"回来。结果就是:广播系统和CRT显示器形成了一个互相抵消的非线性变换。

然后人们发现了一个有趣的巧合:CRT的伽马响应(约2.2)正好与人眼感知亮度特性的逆函数(约1/2.2)非常接近。这意味着:

  • 如果图像数据是"伽马编码"的(已经过$1/2.2$变换)
  • CRT显示器的伽马响应会自动将其转换回线性亮度
  • 最终结果恰好匹配人眼的感知特性

这是一个"两个错误变成一个正确"的典型案例。CRT的非线性原本是个缺点,但恰好与人类视觉的非线性互补。

sRGB的诞生:妥协的标准化

1996年,HP和Microsoft面临一个实际问题:网络上的数字图像在不同设备上显示得乱七八糟。每个显示器有自己的伽马值,每个扫描仪有自己的色彩响应,没有任何统一标准。

他们的解决方案是sRGB(standard RGB)。这个色彩空间的设计目标很明确:最小化色彩管理的开销

sRGB规范包含三个核心定义:

1. 色域(Gamut)

定义了RGB三个原色的精确坐标,基于当时主流CRT显示器使用的荧光粉:

  • R: x=0.64, y=0.33
  • G: x=0.30, y=0.60
  • B: x=0.15, y=0.06

这些数值来自ITU-R BT.709标准,原本是为高清电视制定的。

2. 白点

D65标准光源,色温约6500K,坐标x=0.3127, y=0.3290。这代表了"平均日光"的颜色。

3. 传递函数(Transfer Function)

这是最关键的部分。sRGB的传递函数不是纯粹的幂函数,而是一个分段函数:

编码(线性→sRGB):

$$C_{\text{sRGB}} = \begin{cases} 12.92 C_{\text{linear}} & C_{\text{linear}} \leq 0.0031308 \\ 1.055 C_{\text{linear}}^{1/2.4} - 0.055 & C_{\text{linear}} > 0.0031308 \end{cases}$$

解码(sRGB→线性):

$$C_{\text{linear}} = \begin{cases} C_{\text{sRGB}} / 12.92 & C_{\text{sRGB}} \leq 0.04045 \\ ((C_{\text{sRGB}} + 0.055) / 1.055)^{2.4} & C_{\text{sRGB}} > 0.04045 \end{cases}$$

注意这个分段设计:在极暗区域($C \leq 0.0031308$),函数是线性的。这避免了幂函数在零点处的斜率无穷大问题,让数值计算更稳定。

sRGB的整体伽马值约等于2.2,但不是纯粹的$x^{2.2}$。这个细微差异在某些精确计算中会产生影响。

为什么sRGB能节省存储空间

理解伽马编码的另一个角度是位深的利用效率

假设用8位(256个等级)存储亮度信息。如果按物理亮度线性编码:

  • 等级128代表的物理亮度是255的50%
  • 人眼对这个亮度的感知约为73%(因为$0.5^{0.45} \approx 0.73$)
  • 这意味着暗部只用了128个等级,亮部却浪费了128个等级

人眼对暗部变化更敏感。在暗环境下,人眼可以分辨约1-2%的亮度差异;在亮环境下,需要5-10%的变化才能察觉。这意味着暗部需要更多的编码精度。

伽马编码恰好解决了这个问题:

  • 等级128代表约22%的物理亮度
  • 人眼感知到的亮度约为50%
  • 编码等级均匀分布在感知空间中

Gamma编码与线性编码对比

图片来源: cdn.cambridgeincolour.com

上图展示了线性编码(左)和伽马编码(右)在8位精度下的差异。线性编码在暗部出现明显的条纹,因为256个等级在暗部分布过于稀疏。伽马编码则能呈现平滑的渐变。

实际上,要用线性编码达到同样的暗部精度,大约需要11-12位的数据深度——这就是伽马编码带来的"免费"压缩效果。

线性工作流:为什么游戏引擎要"多此一举"

现代游戏引擎(Unity、Unreal)都强调"线性渲染"的重要性。这意味着所有光照计算都在线性色彩空间进行,而不是直接在sRGB空间计算。

光照计算的物理正确性

考虑一个简单的光照场景:一个红色光源(强度0.5)照射到一个白色表面(反射率0.8)。物理上,结果是两者的乘积:

$$L_{\text{out}} = L_{\text{in}} \times R = 0.5 \times 0.8 = 0.4$$

但如果数据是sRGB编码的,直接相乘会得到错误结果:

  • sRGB值0.5对应的线性值约为0.218
  • sRGB值0.8对应的线性值约为0.604
  • 正确结果是$0.218 \times 0.604 \approx 0.132$
  • 转回sRGB约为0.39

而直接在sRGB空间相乘得到0.4,差异显著。

渐变与混合的问题

更明显的问题出现在颜色混合上。假设要在一个线性渐变中混合纯红(1,0,0)和纯绿(0,1,0):

  • 线性混合(正确):中点是黄色(0.5, 0.5, 0),物理上确实是两种光的叠加
  • sRGB混合(错误):中点看起来偏暗,因为sRGB空间的插值没有考虑物理意义

颜色混合对比

图片来源: bottosson.github.io

上图展示了三种混合方式的差异:感知混合(上)、线性混合(中)、sRGB直接混合(下)。sRGB直接混合产生了不自然的暗色过渡。

抗锯齿的陷阱

抗锯齿通过对边缘像素进行颜色混合来平滑锯齿。如果这个混合在sRGB空间进行,边缘会出现异常的暗边:

  • 假设边缘两侧是纯黑(0,0,0)和纯白(1,1,1)
  • 理想的抗锯齿应该产生物理亮度50%的灰色
  • 在sRGB空间直接混合得到(0.5, 0.5, 0.5)
  • 显示时,这对应约22%的物理亮度,而不是50%
  • 结果:边缘看起来"脏"且过暗

抗锯齿问题

图片来源: blog.johnnovak.net

上图右侧展示了伽马错误导致的文字渲染过粗、边缘发暗的问题。

正确的线性工作流程

要在图形渲染中正确处理色彩空间,需要遵循以下流程:

1. 输入处理

纹理类型区分

  • 颜色纹理(diffuse、base color):通常是sRGB编码的,需要解码到线性空间
  • 数据纹理(normal、roughness、metallic):通常是线性编码的,不需要转换

现代图形API(OpenGL、DirectX、Vulkan)都支持sRGB纹理格式,硬件会自动进行转换:

// OpenGL中创建sRGB纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);

2. 中间计算

所有光照、混合、滤波操作都在线性空间进行。这是物理正确的计算方式。

3. 输出处理

最终输出到显示器前,将线性数据编码为sRGB。同样,现代API支持sRGB帧缓冲:

// 启用sRGB帧缓冲
glEnable(GL_FRAMEBUFFER_SRGB);

或手动转换:

vec3 gammaCorrect(vec3 linearColor) {
    return pow(linearColor, vec3(1.0/2.2));
}

为什么LCD和OLED显示器仍然使用伽马

CRT显示器已经消失,但伽马校正依然存在于所有现代显示器中。原因有两点:

1. 向后兼容

几十年来积累的图像、视频、网页都是sRGB编码的。如果显示器改为线性响应,这些内容都会显示错误。

2. 感知优化的编码效率

即使在CRT消失后,伽马编码的价值依然存在:它让有限的位深得到更合理的分配。8位的sRGB图像在视觉上可以达到接近10位线性图像的质量。

LCD和OLED显示器通过内部查找表(LUT)来模拟伽马响应。显卡驱动也会参与这个过程,确保输出符合sRGB标准。

广色域显示器带来的新问题

近年来,支持P3色域的显示器越来越普及。P3色域比sRGB大约25%,能够显示更饱和的红色和绿色。

这带来了新的问题:

  1. 内容不匹配:大多数网页和应用假定显示器是sRGB的。如果显示器配置为P3,sRGB内容会被"拉伸"到P3空间,导致颜色过饱和。

  2. 色彩管理缺失:很多软件(尤其是老软件)不考虑色彩管理,直接把RGB值送到显示器。在广色域显示器上,这些值会被错误解释。

  3. 工作流不一致:设计师的显示器可能设置为sRGB,客户的显示器可能是P3,同一张图片看起来截然不同。

解决方案是色彩管理:图像文件嵌入ICC配置文件,操作系统和应用程序正确解释这些配置,并在显示时进行色彩空间转换。

HDR时代的传递函数

sRGB是为SDR(标准动态范围)设计的,最大亮度约80-100 nits。HDR显示器可以达到1000甚至4000 nits峰值亮度,需要新的传递函数。

PQ(Perceptual Quantizer)

定义在SMPTE ST 2084中,设计目标是覆盖0.0001到10,000 nits的亮度范围。PQ函数比伽马函数更复杂,设计时考虑了人眼在不同亮度下的对比敏感度。

$$L = \left(\frac{\max(V^{1/m_2} - c_1, 0)}{c_2 - c_3 V^{1/m_2}}\right)^{1/m_1}$$

其中$m_1, m_2, c_1, c_2, c_3$是常数,$V$是信号值,$L$是亮度值。

HLG(Hybrid Log-Gamma)

由BBC和NHK联合开发,目标是兼容SDR显示。HLG在暗部使用传统的伽马曲线,在亮部使用对数曲线:

$$V = \begin{cases} a \ln(1 + bL) + c & L > 1 \\ rL & L \leq 1 \end{cases}$$

HLG的优势是:即使在不支持HDR的显示器上,也能显示合理的图像,只是高光被压缩。

实践中的色彩管理建议

对于开发者

  1. 始终启用线性渲染:在游戏引擎中开启线性色彩空间选项
  2. 正确标记纹理:颜色纹理使用sRGB格式,数据纹理使用普通格式
  3. 最后一步进行伽马校正:确保中间计算都在线性空间
  4. 测试不同显示器:在sRGB和P3显示器上都测试你的应用

对于设计师

  1. 使用色彩管理软件:Photoshop、Lightroom等都有完整的色彩管理
  2. 嵌入ICC配置文件:导出图像时选择嵌入配置文件
  3. 校准显示器:使用校色仪创建显示器ICC配置文件
  4. 选择合适的色彩空间:网页内容用sRGB,打印用Adobe RGB或P3

对于内容消费者

  1. 理解显示器预设:很多显示器有不同的色彩模式(sRGB、P3、电影等),根据用途选择
  2. 保持系统色彩管理开启:Windows和macOS都有色彩管理,不要关闭
  3. 避免随意调整:显示器的亮度、对比度调整会影响色彩准确性

色彩空间的数学本质

从数学角度,色彩空间是一个从三维实数空间到感知颜色的映射。sRGB的定义包括:

  1. 线性变换矩阵:从CIE XYZ空间转换到线性RGB空间
  2. 非线性传递函数:从线性RGB编码为非线性RGB
  3. 原色和白点定义:定义了矩阵的具体数值

完整的转换链:

$$\text{XYZ} \xrightarrow{\text{矩阵}} \text{Linear RGB} \xrightarrow{\text{传递函数}} \text{sRGB}$$

逆转换:

$$\text{sRGB} \xrightarrow{\text{逆传递函数}} \text{Linear RGB} \xrightarrow{\text{逆矩阵}} \text{XYZ}$$

理解这个链条,就能理解为什么色彩空间转换会产生误差,以及为什么"两个错误有时能变成一个正确"。


参考资料

  1. Stokes, M., Anderson, M., Chandrasekar, S., & Motta, R. (1996). A Standard Default Color Space for the Internet - sRGB. W3C.
  2. IEC 61966-2-1:1999. Multimedia systems and equipment - Colour measurement and management - Part 2-1: Colour management - Default RGB colour space - sRGB.
  3. Novak, J. (2016). What every coder should know about gamma. https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/
  4. Gritz, L., & d’Eon, E. (2007). The Importance of Being Linear. GPU Gems 3, Chapter 24. NVIDIA.
  5. Poynton, C. (1998). Rehabilitation of Gamma. SPIE Conference on Human Vision and Electronic Imaging III.
  6. Wikipedia. sRGB. https://en.wikipedia.org/wiki/SRGB
  7. Wikipedia. Gamma correction. https://en.wikipedia.org/wiki/Gamma_correction
  8. Cambridge in Colour. Understanding Gamma Correction. https://www.cambridgeincolour.com/tutorials/gamma-correction.htm
  9. Ottosson, B. (2020). How software gets color wrong. https://bottosson.github.io/posts/colorwrong/
  10. LearnOpenGL. Gamma Correction. https://learnopengl.com/Advanced-Lighting/Gamma-Correction
  11. Nine Degrees Below. History of the Very Odd sRGB Color Space. https://ninedegreesbelow.com/photography/srgb-history.html
  12. International Color Consortium. Display calibration. https://www.color.org/displaycalibration.xalter
  13. EIZO. Understanding Monitor Gamma & Gamma Correction. https://www.eizo.com/library/basics/lcd_display_gamma/
  14. ITU-R BT.2100. Image parameter values for high dynamic range television for use in production and international programme exchange.
  15. SMPTE ST 2084. High Dynamic Range Electro-Optical Transfer Function of Mastering Reference Displays.
  16. Wikipedia. Weber-Fechner law. https://en.wikipedia.org/wiki/Weber%E2%80%93Fechner_law
  17. Wikipedia. Stevens’s power law. https://en.wikipedia.org/wiki/Stevens%27s_power_law
  18. Wikipedia. CIE 1931 color space. https://en.wikipedia.org/wiki/CIE_1931_color_space
  19. Unity Documentation. Linear or gamma workflow. https://docs.unity3d.com/Manual/LinearRendering-LinearOrGammaWorkflow.html
  20. NVIDIA Developer. The Importance of Being Linear. https://developer.nvidia.com/gpugems/gpugems3/part-iv-image-effects/chapter-24-importance-being-linear
  21. Medium. Why DCI-P3 and not Adobe RGB? https://news.ycombinator.com/item?id=11546949
  22. W3C. PNG Specification: Gamma Tutorial. https://www.w3.org/TR/PNG-GammaAppendix.html
  23. Wikimedia Commons. CIE1931xy gamut comparison. https://commons.wikimedia.org/wiki/File:CIE1931xy_gamut_comparison_of_sRGB_P3_Rec2020.svg