4、Gamma 曲线数学原理:幂律函数、Gamma 编码与解码、线性光与非线性光
Gamma 校正,说白了就是一条幂律曲线在管着你的画面。很多朋友调了几年色,问起 Gamma 到底是什么,还是支支吾吾。今天咱们就把这层窗户纸捅破。
4.1 幂律函数:Gamma 的数学骨架
Gamma 校正的数学基础,就是一个极其简单的幂律函数:
V_out = (V_in) ^ γ
其中 V_in 是输入值,V_out 是输出值,γ 就是 Gamma 值。就这么一个公式,撑起了整个显示系统的亮度映射体系。
我刚开始接触时也觉得太简单了,不就是个指数函数吗?后来才发现,正是这个「简单」让它在硬件上实现起来极其高效——早期 CRT 显示器用几个电阻电容就能天然实现这个曲线。
关键点:
- γ < 1:压缩暗部,提亮整体(解码曲线)
- γ = 1:线性映射,不做校正
- γ > 1:拉伸暗部,压暗整体(编码曲线)
举个例子。假设你有一个像素亮度是 0.5(归一化后),如果 γ=2.2,输出就是 0.5^2.2 ≈ 0.217。你看,原本中间灰被压到了接近暗部。这就是编码的过程。
4.2 Gamma 编码:为什么要「故意扭曲」亮度?
你想想看,人眼对暗部的变化极其敏感,对亮部的变化相对迟钝。但摄像头传感器是线性器件——它老老实实地记录光子数量。这就产生了矛盾。
Gamma 编码的目的,就是把线性传感器捕获的亮度值,按照人眼的视觉特性重新分配。说白了,把更多的数据位分配给暗部,亮部少占一点。
# Gamma 编码(模拟)
def gamma_encode(linear_value, gamma=1/2.2):
return linear_value ** gamma
# 示例:8位系统
linear = [0, 0.1, 0.2, 0.5, 0.8, 1.0]
encoded = [gamma_encode(v) for v in linear]
print(encoded)
# 输出近似:[0, 0.35, 0.48, 0.73, 0.91, 1.0]
看到没?0.1 的线性值被编码成了 0.35,暗部细节被「放大」了。这就是为什么 8 位图像能保留那么多暗部层次——Gamma 编码在背后做了手脚。
我的经验: 做 HDR 合成时,很多人直接把线性 HDR 数据丢进 8 位显示器,结果暗部一片死黑。正确的做法是先做 Gamma 编码再显示。我曾经在这个坑里摔过两次,后来养成了习惯:任何显示操作前先确认色彩空间。
4.3 Gamma 解码:显示器必须做的事
显示器收到编码后的信号,必须做反向操作才能还原正确的亮度。这就是 Gamma 解码:
V_display = (V_encoded) ^ γ_display
有意思的是,CRT 显示器的物理特性天然就是 γ≈2.5 的幂律曲线。所以早期系统设计时,发送端做 γ=1/2.2 的编码,接收端(显示器)自然解码,正好凑出接近线性的最终效果。
| 环节 | Gamma 值 | 作用 |
|---|---|---|
| 摄像头/软件编码 | 1/2.2 ≈ 0.4545 | 压缩亮部,扩展暗部 |
| 显示器物理特性 | ≈ 2.5(CRT) | 天然幂律响应 |
| 最终系统 Gamma | ≈ 1.0(理想) | 线性光输出 |
不过现在 LCD/OLED 显示器没有天然 Gamma 了,全靠内部 LUT 模拟。所以你会看到显示器菜单里有「Gamma 1.8、2.2、2.4」的选项——那就是在切换解码曲线。
注意: 编码 Gamma 和解码 Gamma 不是互为倒数的关系。实际系统中,编码端用 1/2.2,解码端用 2.2 或 2.4,最终系统 Gamma 大约在 1.0~1.1 之间。完全精确的倒数会导致画面发灰——我亲自验证过。
4.4 线性光 vs 非线性光:一个让无数人翻车的概念
线性光,就是物理世界真实的光子数量。你给传感器两倍的光子,它就输出两倍的电压。非线性光,就是经过 Gamma 编码后的信号——它和物理亮度不成正比。
为什么这个概念重要?因为几乎所有图像处理算法都假设你在线性空间工作。如果你在非线性空间做模糊、混合、缩放,结果全是错的。
# 错误示范:在非线性空间做混合
pixel1_nonlinear = 0.5 # 编码后的中间灰
pixel2_nonlinear = 0.5
blend_nonlinear = (pixel1_nonlinear + pixel2_nonlinear) / 2
# 结果:0.5,看起来没问题?
# 正确做法:先解码到线性空间
pixel1_linear = pixel1_nonlinear ** 2.2 # 解码
pixel2_linear = pixel2_nonlinear ** 2.2
blend_linear = (pixel1_linear + pixel2_linear) / 2
blend_nonlinear_correct = blend_linear ** (1/2.2) # 重新编码
# 结果:≈ 0.73,完全不一样!
你看,同样的操作,线性空间和非线性空间的结果差了这么多。这就是为什么你在 Photoshop 里用「柔光」图层混合模式,有时候效果怪怪的——很可能是因为没有在正确的色彩空间里操作。
黄金法则:
- 计算、混合、滤波 → 线性空间
- 存储、传输、显示 → 非线性空间(Gamma 编码)
- 永远不要在非线性空间做数学运算
4.5 一张图看懂 Gamma 全流程
下面这张 SVG 图把整个 Gamma 编码-解码链路画清楚了。我建议你仔细看看箭头上的 Gamma 值,理解每个环节的变换。
4.6 避坑指南:我踩过的 Gamma 坑
做色彩校正这些年,我在 Gamma 上翻车的次数最多。说几个典型的:
- sRGB 不等于 Gamma 2.2:sRGB 在暗部有一段线性区(斜率 12.92),不是纯幂律。直接用 pow(x, 1/2.2) 做 sRGB 解码,暗部会有偏差。我早期一个项目因此被客户打回三次。
- 浮点精度陷阱:Gamma 编码后的 0.0 和 1.0 是边界值,0^γ=0,1^γ=1。但中间值经过多次编解码会累积误差。我曾经在 16 位浮点流水线里反复编解码 5 次,结果中间灰漂了 3 个码值。
- 不同标准混用:Rec.709 用 Gamma 2.4,sRGB 用近似 2.2,DCI-P3 用 2.6。混用这些标准做色彩转换,画面不是发灰就是对比度过高。我建议项目一开始就定死色彩空间标准。
我的习惯: 写任何图像处理代码,第一行就做色彩空间声明。用注释写明「此函数输入为线性 sRGB」或「此函数输出为 Gamma 编码的 Rec.709」。这样三个月后回来看代码,自己也不会懵。
4.7 小结
Gamma 曲线不是什么玄学。它就是一条幂律曲线,编码端压暗部、解码端拉回来,最终让人眼看到接近线性的亮度。记住三个数字:编码用 1/2.2,解码用 2.2~2.4,系统总 Gamma 约 1.0。下次调色时,多想想你当前在哪个色彩空间里操作——这个习惯能帮你避开 80% 的坑。