首页 / 文章 / RGB 归一化该除以 255 还是 256?
← 返回
AI技术

RGB 归一化该除以 255 还是 256?

✍️ zhirenhun 📅 2026/6/2 👁 94 阅读 ⏱ 16 分钟
RGB 归一化该除以 255 还是 256?



30fps.net — 计算机图形学与编程,作者 Pekka Väänänen

假设你正在编写一个图像处理程序。程序读入一张图像,将其转换为浮点数,进行一些处理,最后将修改后的像素以 8 位颜色保存到磁盘上。今天的问题在于整数到浮点数的转换到底应该怎么做。有两种方法,用 Python 和 NumPy 写出来是这样的:

标准做法:除以 255

pixels = img / 255.0
result = process(pixels)
output = np.trunc(result * 255 + 0.5)

替代做法:除以 256

pixels = (img + 0.5) / 256.0
result = process(pixels)
output = np.trunc(result * 256)

我假设两种情况下在最终类型转换之前都会对输出值进行钳位:

output_8bit = output.clip(0, 255).astype(np.uint8)

标准做法将整数 0 映射到 0.0,整数 255 映射到 1.0。它工作得非常好,并且是 GPU 的做法。替代做法则加上 0.5 的偏置,然后除以 256,这样整数 0 被映射到 0.5/256 = 0.001953125。这很不方便,因为你的图像处理代码如果不知道这个常数,就无法检测到黑色像素等。结果就是,即使你在浮点数中计算,你的逻辑仍然与 8 位输入绑定。而采用标准做法,你总可以假设黑色是 0.0。

但有些程序员仍然会被替代做法所吸引。这是怎么回事呢?他们看中了什么?

反对除以 255.0 的理由

如果将标准做法画在数轴上,看起来确实很奇怪。下面是一个夸张的版本,用 3 位整数(范围 [0..7])映射到 [0,1]:

*

在 X 轴上我们有一条数轴,棕色圆圈的位置代表解码后的浮点数值。圆圈内的数字是整数输入。每个整数都有箭头指向它;这些箭头表示会四舍五入到该整数的一系列浮点数值。在本文余下部分,我将这些范围称为“箱子”(bins)。

边界处的箱子更小

图中第一个非常明显的问题是,标准公式的边界箱子凸超出了 [0,1] 范围。也许这张图不公平——两种方法都会对输出进行钳位,所以边界箱子可以无限延伸——但它清楚地展示了标准范围被“拉伸”了。拉伸后的范围比图像处理中假定的工作范围 [0,1] 要宽。

这意味着当将 [0,1] 范围内的浮点数值转换回整数时,边界箱子的宽度实际上只有其他箱子的一半。结果是,你的算法输出极端值会“更难”。例如,如果你生成均匀的 [0,1] 噪声,并使用标准公式将其四舍五入,那么值 0 和 255 出现的频率只有其他整数的一半。

我们可以通过生成一百万个均匀随机数,绘制成直方图来实证验证这一点,并观察到 0 和 255 这两个箱子的高度确实只有其他箱子的一半:

*

突出显示的裁剪部分:

*

直方图代码:

import numpy as np
import matplotlib.pyplot as plt

result = np.random.uniform(0, 1, 1000000)
final_values = np.trunc(result * 255 + 0.5).clip(0, 255).astype(np.uint8)
plt.hist(final_values, bins=256, range=(0, 255))
plt.show()

不过,我很难举出一个例子,说明这种偏离极端值的偏置会带来问题。当然,标准做法的浮点数分布在更宽的范围内,但原始图像仍然可以无损地来回转换(uint8 → float → uint8)。

此外,任何刚好超出 0.0 或 1.0 的结果值仍然会四舍五入到正确的箱子,从而均衡输出分布。举个例子:假设你的处理从浮点颜色中减去 0.005。在标准做法中,这会使黑色变成负值——超出 [0,1] 范围——而在替代做法中,值仍然是正的。但最终两者都输出整数 0:

标准:
trunc(255 * (-0.005) + 0.5) = 0

替代:
trunc(256 * (0.5 / 256 - 0.005)) = 0

在标准做法中,0 号箱子只有“一半大小”这一点并不重要。

不精确性

第二个问题是标准做法得到的浮点数值并不精确。例如 128/255.0 ≈ 0.501961,而 128/256.0 = 0.5。由于这种舍入误差,浮点数值之间的距离有微小变化。但这并不是一个真正的麻烦,因为误差确实非常。一个 32 位浮点数有 23 位小数(“有效数字”)。我们讨论的是其最低有效位中的舍入误差;抖动幅度小于 2^{-23}。即使在最复杂的图像处理任务中,0.00001% 的相对误差也肯定无关紧要。在这种情况下,不精确性是一个美学问题,而不是技术问题。

值不在整数中间

替代做法总是将每个浮点数值精确地放置在两个整数的中间位置。看看上面数轴图中垂直条是如何对齐的。可以认为中间位置是一种折衷;我们不知道原始的量化值到底是多少,因此两个连续整数之间的平均点是一个很好的猜测。

我确信有些应用场景会用到这个性质,尽管我自己很难举出例子。至少,Andrew Kesler 在 2015 年的博客文章 “Converting Color Depth”(以其 名片光线追踪器 而闻名)中认为,抖动用起来更方便。理由是可以在不用担心边界情况的情况下添加噪声。相比之下,标准公式的尴尬边界需要仔细处理,以保持噪声分布一致。

两种量化器

到目前为止,标准的“除以 255”公式看起来仍然稳固,或者至少足够稳固,值得继续使用。另一种思考这个问题的方法是退一步,将两种方法视为两种不同的均匀标量量化器。如果我们查阅 维基百科上关于量化的页面,我们会很快了解到有两种主要类型的量化器:

大多数针对有符号输入数据的均匀量化器可以归类为两种类型之一:中升型(mid-riser)和中平型(mid-tread)。这种术语基于值 0 附近区域的情况,并将量化器的输入输出函数视为一个楼梯。中平型量化器具有值为零的重建电平(对应于楼梯的踏板),而中升型量化器具有值为零的分类阈值(对应于楼梯的竖板)。

维基百科作为来源引用了一篇 1977 年的论文,该论文的标题和摘要排版如此令人难以置信,以至于我必须在这里重现:

  • “Quantization” by Allen Gresho. IEEE Communications Society Magazine, September 1977.

无论如何。当画在图上时,中升型和中平型量化器在过零点处的表现不同:

*

确实,中平型将零映射到零,而中升型将零映射到两个整数的中间(听起来熟悉吗?)。维基百科选择的符号用 x 表示输入实数,k 表示其编码(“分类”)整数值,y_k 表示重建实数。对应的量化器公式如下:

类型 分类(编码) 重建(解码)
中平型楼梯量化器 k = ext{trunc}(x L + 0.5) y_k = k/L
中升型楼梯量化器 k = ext{trunc}(x L) y_k = (k+0.5)/L

L 代表不同输出电平的数量(例如 256)。

如果我们将这些定义应用到我们两种竞争的方法中,我们可以称标准公式为 L=255 的“中升型”,替代做法为 L=256 的“中平型”。实际上,我将再次展示它们的代码并附上新的标签,以便清楚地连接到上述公式。代码片段本身与开头相同。

中平型量化器 (L=255)

pixels = img / 255.0
result = process(pixels)
output = np.trunc(result * 255 + 0.5)

中升型量化器 (L=256)

pixels = (img + 0.5) / 256.0
result = process(pixels)
output = np.trunc(result * 256)

从这个角度来看,我们可以说标准做法是一种奇怪的组合:它是对无符号输入的中升型量化器(引用中说的是“对于有符号输入数据”),并且选择了 L=255 个整数码。显然这对于 8 位输入来说不是最优的。同样,这一切都是为了编程的便利,使边界映射到 0.0 和 1.0。这引出了对标准公式的最终批评。

更高的量化误差?其实不然

如果我们设计一个系统,接收均匀分布的实数 x ∈ [0,1],将其编码为 8 位整数 k,最后重建为另一个实数 y_k,那么标准公式会浪费带宽。还记得 0 和 255 号箱子如何稍微凸超出了 [0,1] 范围的边缘吗?在标准做法中,可表示值的范围实际上是 [-0.5/255, 255.5/255],这意味着箱子的间距比严格需要的 [0,1] 输入更大,从而导致更高的重建误差。然而,误差的增加很小。根据 StackOverflow 用户 Peter Mudrievskij 的计算,对于 255 和 256 除数,平均绝对误差分别为 1/1020 和 1/1024。因此,从理论上讲,除以 256 更精确。

微妙之处在于,这种重建并不是我们实际在做的。前提是我们加载 8 位 RGB 图像,对它们进行处理,然后再次保存。我们无法控制它们在保存时是如何量化的;所有丢失的信息都永远消失了。换句话说,如果一幅图像的颜色乘以 255 并四舍五入,那么在加载时除以 256 并不能恢复任何精度。只有当我们同时控制保存和加载时,追求更低的重建误差才有意义。

事实上,使用替代公式来加载别人的图像会引入更多误差。这些图像很可能是通过标准公式量化的,因此理论上用错误的缩放因子解码是不正确的。在实践中,颜色并不是绝对测量值(即使 sRGB 规范声称如此),所有发生的事情就是我们的处理将在稍微更小的范围内进行,并带有一个小的偏移。微妙部分到此结束。

最后,永远不要混淆两种量化器的编码和解码步骤。那只是有 bug 的代码。不过,这是一个很容易犯的错误。

结论

回答标题中提出的问题:如果你处理的是陌生人给你的图像,你应当将 RGB 值除以 255 进行归一化。无论是浮点数值的不精确性,还是某种对更高重建误差的抽象感受,都不是采用替代做法的好理由。但是,如果你同时控制图像的保存和加载,不需要零映射到零,并且愿意将你的处理代码与 8 位动态范围绑定,那么你可以考虑除以 256 来多挤出一点精度。只是当你的同事仍然用标准公式加载你的图像,毁掉你的宏伟计划时,别怪我就行。

其他观点

Jonathan Blow 在 2002 年的文章中讨论了中升型和中平型量化器,但没有提及它们的名称。我从那里得到了图表思路。

之前提到的 2015 年博客文章(Andrew Kesler 著)支持替代公式。不幸的是,它是对标准公式进行比较,但标准公式没有包含取整步骤,这使得大部分分析无效。

我正在写一本关于颜色缩减算法的书。如果你感兴趣,可以在此注册

Mastodon
Bluesky
YouTube
Feed

30fps.net

**

——

🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

← 上一篇
10 年老 Xeon 搞定 Gemma-4:CPU 推理优化实战
下一篇 →
dav2d:VideoLAN 社区的 AV2 开源解码器

📌 相关推荐

qMLX:通过极致优化Mac Studio最大化我的AI幻觉
2026/7/12
Mesh LLM:基于iroh的分布式AI计算
2026/7/12
结构化输出 vs 工具使用 vs 预填充:2026年从Claude获取JSON
2026/7/11
← 返回文章列表