每个程序员都应了解的伽马校正知识
摘要
这是一份深入指南,解释伽马校正、它对图像处理和渲染的重要性,以及程序员常遇到的陷阱。
暂无内容
查看缓存全文
缓存时间: 2026/06/15 21:00
# 每个程序员都应该知道的 Gamma 知识
来源: https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/
2016年9月21日 - 图形 (https://blog.johnnovak.net/tags/graphics/) - gamma (https://blog.johnnovak.net/tags/gamma/) - 线性工作流 (https://blog.johnnovak.net/tags/linear-workflow/)
## 目录
- 一个小测验 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#a-short-quiz)
- Gamma 校正的奥秘 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#the-arcane-art-of-gamma-correctness)
- 什么是 Gamma,为什么需要它? (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#what-is-gamma-and-why-do-we-need-it)
- 光发射 vs 感知亮度 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#light-emission-vs-perceptual-brightness)
- 物理线性 vs 感知线性 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#physical-vs-perceptual-linearity)
- 高效的图像编码 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#efficient-image-encoding)
- Gamma 传输函数 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#the-gamma-transfer-function)
- Gamma vs sRGB (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#gamma-vs-srgb)
- Gamma 校准 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#gamma-calibration)
- 处理 Gamma 编码的图像 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#processing-gamma-encoded-images)
- Gamma 不正确的后果 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#effects-of-gamma-incorrectness)
- 渐变 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#gradients)
- 颜色混合 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#colour-blending)
- Alpha 混合/合成 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#alpha-blending--compositing)
- 图像缩放 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#image-resizing)
- 抗锯齿 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#antialiasing)
- 基于物理的渲染 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#physically-based-rendering)
- 结论 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#conclusion)
- 参考文献与进一步阅读 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#references--further-reading)
- 通用 Gamma/sRGB 信息 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#general-gammasrgb-info)
- 线性光照与工作流 (LWF) (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#linear-lighting--workflow-lwf)
- 额外内容 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#bonus-stuff)
## 一个小测验
如果你曾经编写过,或计划编写*任何*涉及图像处理的代码,请先完成下面的测验。如果你对其中一个或多个问题回答了“是”,那么你的代码很可能在做了错误的事情,并会产生不正确的结果。你可能不会立即意识到这一点,因为这些问题可能很微妙,并且在不同问题领域中的可见程度也不一样。所以,测验如下:
- 我不知道什么是伽马校正(废话!)
- 伽马是 CRT 显示器时代的遗物;既然现在几乎所有人都在用 LCD,忽略它也没问题。
- 伽马只与印刷行业的图形专业人士有关,因为他们需要精确的色彩再现——对于一般的图像处理,忽略它也没问题。
- 我是游戏开发者,不需要了解伽马。
- 我操作系统的图形库已经正确处理了伽马。1 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/#fn:1)
- 我使用的流行图形库**已经正确处理了伽马。
- RGB 值为 (128, 128, 128) 的像素发出的光大约是 RGB 值为 (255, 255, 255) 像素的一半。
- 可以直接用某个随便找的库将 JPEG、PNG、GIF 等流行图像格式的像素数据加载到缓冲区中,然后直接对原始数据运行图像处理算法。
如果你对大多数问题回答了“是”,别难过!一周前我自己也会对大部分问题回答“是”。不知为何,伽马这个主题在大多数电脑用户(包括编写商业图形软件的程序员!)的雷达之下,以至于如今大多数图形库、图像查看器、照片编辑器和绘图软件仍然没有正确处理伽马,并产生错误结果。所以继续往下读吧,读完这篇文章后,你会比绝大多数程序员更了解伽马!
## Gamma 校正的奥秘
鉴于视觉可以说是人机交互中最重要的感觉输入通道,但令人惊讶的是,伽马校正是程序员之间极少谈论的话题之一,在技术文献中也相当少见,*包括*计算机图形学教材。大多数计算机图形学教材没有明确提及正确处理伽马的重要性,也没有以实际方式进行讨论,这完全无济于事(我大学时的 CG 教材 (https://sirkan.iit.bme.hu/~szirmay/szamgraf.html) 就属于这一类,我刚确认过)。一些书以含糊抽象的方式顺便提一下伽马校正,但既没有提供具体的实际示例说明如何正确操作,也没有解释不这样做会有什么后果,更没有展示错误处理伽马的图像示例。
我在编写我的光线追踪器 (https://blog.johnnovak.net/tags/ray-tracing/) 时遇到了正确处理伽马的需求,不得不承认自己对这一主题的理解相当肤浅和不完整。于是我花几天时间在网上阅读相关资料,但结果发现许多关于伽马的文章也没什么帮助,因为很多都太抽象、令人困惑,有些包含太多有趣但无关的细节,还有一些缺乏图像示例,或者干脆就是错误的或难以理解。
伽马本身并不是一个极其困难的概念,但出于某种神秘的原因,要找到正确、完整且以清晰语言解释这一主题的文章并不容易。
## 什么是 Gamma,为什么需要它?
好了,这是我尝试对伽马进行全面解释,只关注最重要的方面,并假设读者没有预先知识。本文中的图像示例假设你在电脑显示器(CRT 或 LCD,无所谓)上的现代浏览器中查看此网页。平板和手机通常比显示器准确度差得多,所以尽量别用。你应该在昏暗的房间里观看图像,确保屏幕上没有直射光或反光。
### 光发射 vs 感知亮度
信不信由你,下面图像中任何两个相邻竖条之间的**光能量发射**差异是一个*常数*。换句话说,你的屏幕从左到右每根相邻竖条发射的光能量增加了一个*恒定值*。
图1 — 按发射光强度均匀间隔的灰度条
图1 — 按发射光强度均匀间隔的灰度条 (Nim 源代码 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/src/gammaramp.nim))
现在考虑下面这张图像:
图2 — 按感知光强度均匀间隔的灰度条
图2 — 按感知光强度均匀间隔的灰度条 (Nim 源代码 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/src/gammaramp.nim))
哪张图像上的渐变看起来更均匀?是第二张!为什么会这样?我们刚刚确定,第一张图像中的条形在显示器能再现的最暗黑色和最亮白色之间,按发射光强度是均匀(*线性*)间隔的。但我们为什么不把它看作从黑到白平滑的渐变呢?而我们*感知*为线性渐变的第二张图像显示的是什么?
答案在于人眼对光强度的响应,这是*非线性*的。在第一张图像中,任意两个相邻条之间的名义光强度**差异**是常数:
$$\\Δ\_\{\\linear\} = I\_n\-I\_\{n-1\}$$
然而,在第二张图像中,这个差异不是常数,而是从条到条变化;确切地说,它遵循幂律关系。所有人类感官知觉在刺激大小和感知强度之间都遵循类似的幂律关系 (https://en.wikipedia.org/wiki/Stevens'_power_law)。因此,我们说**名义物理光强度**与**感知亮度**之间存在**幂律关系**。
### 物理线性 vs 感知线性
假设我们想将以下真实世界物体的表示存储为计算机图像文件(我们暂时假装完美的灰度渐变存在于真实世界中,好吧?)。下面是这个“真实世界物体”的样子:
图3 — 理想平滑灰度渐变
图3 — 理想平滑灰度渐变 (Nim 源代码 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/src/gammaramp.nim))
现在,我们假装在这个特定计算机系统上只能存储 5 位灰度图像,这给了我们从纯黑到纯白的 32 种不同的灰色阴影。此外,在这台计算机上,灰度值与对应的物理光强度成*比例*,这将产生如图1所示的 32 元素灰度渐变。我们可以说这个灰度渐变的连续值之间在*光发射*上是*线性*的。如果我们只用这 32 个灰度值来编码我们的平滑渐变,我们会得到类似这样的结果(为了简单起见,暂时忽略抖动):
图4 — 用 32 个物理线性灰度值表示的理想平滑灰度渐变
图4 — 用 32 个物理线性灰度值表示的理想平滑灰度渐变 (Nim 源代码 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/src/gammaramp.nim))
嗯,过渡相当粗糙,尤其是左侧,因为我们只有 32 个灰度值可用。如果我们眯起眼睛,很容易说服自己,就我们有限的位深度而言,这是对平滑渐变“准确”的表现。但请注意左侧的阶梯比右侧大得多——这是因为我们使用的灰度值在*发射光强度*上是*线性*的,但正如我们之前提到的,我们的眼睛并不是以线性方式感知光强度的!
这个观察结果有一些有趣的启示。原始图像和 5 位编码版本之间的误差在图像中是不均匀的;暗值的误差比亮值大得多。换句话说,我们丢失了暗值的表示精度,而对较浅的色调使用了相对过多的精度。显然,更好的做法是为我们有限的色调调色板选择另一组 32 种灰度,使得误差在整个范围内均匀分布,这样暗色调和亮色调都能以相同的精度表示。
如果我们用这样一种灰度值来编码原始图像,这种灰度值是*感知线性*的,因此相应地*非线性*于发射光强度,并且这种非线性能匹配人眼的非线性,我们就会得到我们已经在图2中看到过的完全相同灰度图像:
图5 — 用 32 个感知线性灰度值表示的理想平滑灰度渐变
图5 — 用 32 个感知线性灰度值表示的理想平滑灰度渐变 (Nim 源代码 (https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/src/gammaramp.nim))
我们在这里讨论的非线性就是之前提到的**幂律**关系,而我们需要应用到*物理线性*灰度值上、将其转换为*感知线性*值的非线性变换称为**伽马校正**。
### 高效的图像编码
为什么上述所有内容都很重要?在所谓的“真彩色”或“24 位”位图图像中,颜色数据以每个像素三个 8 位整数存储。用 8 位可以表示 256 种不同的强度级别,如果这些级别的间隔是物理线性的,我们就会像上面展示的那样,在暗色调上丢失大量精度,而在亮色调上却过于精确(相对而言)。显然,这并不理想。
一个解决方案是继续使用物理线性尺度,并将每个通道的位深度增加到 16(或更多)。这会使存储需求翻倍(甚至更糟),而在大多数常见图像格式被发明的时候,这是不可行的。因此,采用了另一种方法。
这个想法是用 256 个不同的级别在感知线性尺度上表示强度值,这样绝大多数图像可以用每颜色通道仅 8 位来充分表示。这种用于表示*物理线性*强度数据(无论是通过算法合成生成,还是由线性设备如数码相机的 CMOS 或扫描仪捕获)并映射到*感知线性*尺度的离散值上的变换,称为**伽马编码**。
几乎所有消费级电子设备上使用的 24 位 RGB 颜色模型 (https://en.wikipedia.org/wiki/RGB_color_model#Video_framebuffer) (RGB24) 每个通道使用 8 位伽马编码值 (https://en.wikipedia.org/wiki/RGB_color_model#Nonlinearity) 来表示光强度。如果你记得我们之前讨论过的,这意味着 RGB(128, 128, 128) 的像素*不会*发出大约 RGB(255, 255, 255) 像素 50% 的光能量,而只有大约 22%!这完全合理!由于人类视觉的非线性特性,一个光源需要衰减到其原始光强度的约 22%,才能在人眼中表现为一半亮。RGB(128, 128, 128) 在我们看来*看起来*比 RGB(255, 255, 255) 暗一半!如果你觉得这令人困惑,请稍微反思一下,因为扎实理解到目前为止讨论的内容至关重要(相信我,后面只会更让人困惑)。
当然,伽马编码总是假设图像最终将由人类在计算机屏幕上观看。在某种程度上,你可以把它看作是图像的一种有损 MP3 式压缩。对于其他用途(例如科学分析或需要进一步后处理的图像),使用浮点数并坚持线性尺度通常是更好的选择,我们稍后会看到。
### Gamma 传输函数
将值从线性空间转换到伽马空间的过程称为**伽马编码**(或*伽马压缩*),反向过程称为**伽马解码**(或*伽马扩展*)。这两个操作的公式非常简单,我们只需要使用前面提到的幂律函数:
$$\\V\_\{\\encoded\}$$
相似文章
我今天学到了关于GPU的知识
一位游戏开发者讲述了他在游戏《Blackshift》中修复GPU渲染bug的经历。问题是将8位邻接整数转换为浮点数时出现的浮点数精度问题,导致在部分NVIDIA GPU上出现视觉瑕疵,且该bug只在主渲染模式中出现,预览模式中并未出现。
应该用255还是256来归一化RGB值?
文章比较了归一化RGB值的两种方法(除以255 vs 除以256),并解释了浮点数转换和舍入的后果,包括在极端值处不均匀的区间宽度。
@pauliusztin_: 我刚找到了理解 GPU 最实用的资源之一。再也不用在不同文档、PDF 和论坛帖子之间跳来跳去了…
Modal Labs 发布了一个开源的 GPU 术语词典,将零散的 NVIDIA 文档、CUDA 细节及编译器参数整合为单一的可导航资源,旨在帮助工程师优化 LLM 的训练与推理。
@vivekgalatage:我发现的最好的GPU优化结构化参考资料——450篇论文,14年研究。一些技术已经进化……
一条推文分享了一个涵盖14年、450篇论文的GPU优化结构化参考资料,指出虽然一些技术已经发展,但心智模型仍然有用。还提到了Onur Mutlu关于GPU架构的讲座。
没人注意到 GPT Images 2.0 的“编辑”功能其实是全图重新生成吗?
这篇文章分析了 ChatGPT 的图片编辑功能,认为其基于网络流量和元数据证据,实际上是通过 DALL-E 执行全图重新生成,而非进行局部编辑。