归一化困境:你应该将 RGB 除以 255 还是 256?

在编写图像处理软件时,第一个障碍之一就是将 8 位整数颜色值(0–255)转换为用于计算的浮点数。虽然这看起来微不足道,但在开发者之间存在着一场微妙的争论:你应该通过除以 255.0 进行归一化,还是应该使用带有偏差的除以 256.0?

这个选择会影响你的算法如何感知“黑色”和“白色”,如何处理舍入,以及如何与其它软件生成的图像进行交互。理解这两种方法的数学含义对于保持颜色精度和确保兼容性至关重要。

两种方法

为了说明这个问题,请考虑将 8 位整数转换为浮点数及其逆过程的两种常见方法:

标准方法(除以 255)

  • 转换为浮点数: pixels = img / 255.0
  • 转换为整数: output = np.trunc(result * 255 + 0.5)

替代方法(除以 256)

  • 转换为浮点数: pixels = (img + 0.5) / 256.0
  • 转换为整数: output = np.trunc(result * 256)

在这两种情况下,最终输出通常会被限制在 [0, 255] 范围内,并转换回无符号 8 位整数 (uint8)。

标准方法的理由

标准方法是行业默认值,被 GPU 和大多数图像处理库所采用。其主要优势在于语义清晰度:整数值 0 准确地映射到 0.0(绝对黑),而 255 准确地映射到 1.0(绝对白)。

对于大多数开发者来说,这是唯一逻辑的选择。如果你的处理逻辑需要检测黑色像素或执行其中 0.0 代表空信号的算术运算,标准方法允许你这样做,而无需知道输入图像的具体位深。

“半箱体”问题

标准方法的批评者指出一个可视化问题:当将浮点数转换回整数时,极端的箱体(0 和 255)实际上只有内部箱体宽度的一半。如果你在 0.0 到 1.0 之间生成均匀随机噪声并使用标准公式进行舍入,那么 0 和 255 这两个值出现的频率将是任何其他整数的一二分之一。

然而,在实际的图像处理中,这很少成为问题。因为原始图像已经过量化,往返转换(uint8floatuint8)仍然是无损的。任何在处理过程中略微超出 [0, 1] 范围的值仍会被限制在正确的极端箱体中,从而使分布均匀化。

替代方法的理由

替代方法将转换视为一种均匀标量量化器。通过添加 0.5 的偏差并除以 256,每个整数都被映射到其相应浮点数范围的正中心。

理论精度

从信号处理的角度来看,替代方法是一种“中点踏步”量化器。从理论上讲,这可以减少平均绝对重建误差。如果你控制着图像的保存和加载,你可以榨取出一丁点额外的精度。

抖动与噪声

有人认为替代方法简化了抖动。因为每个值都处于中心位置,向信号中添加噪声在整个范围内都更加一致,避免了标准公式中那些需要通过仔细限制来保持噪声分布均匀的“尴尬的极端情况”。

技术反驳点与现实世界的约束

虽然数学争论很有趣,但社区强调了几个经常会覆盖这些理论关注点的实际现实:

1. 兼容性与“陌生人”问题

如果你正在处理由用户或其它软件提供的图像,你必须使用 255 除数。大多数在野外的图像都是使用标准公式量化的。使用 256 比例因子进行解码会引入系统性偏移和缩放误差,实际上是在比预期稍小的范围内处理图像。

2. 硬件层面的现实

正如一些开发者所指出的,当处理硬件层面的信号时,这些差异变得至关重要。例如,当通过微控制器生成 VGA 信号时,整数到电压水平的映射是绝对的。如果不同的颜色通道使用不同的位深(例如,蓝色通道 3 位,红色通道 8 位),归一化中的错位可能会导致无法实现纯灰色,从而导致可见的蓝色或黄色色调。

3. 颜色空间的角色

有人认为,整个争论的次重要性在于颜色空间的选取。无论你使用 255 还是 256,其重要性都不如你是否在 linear RGB 或 gamma 修正后的空间(如 sRGB)中工作。特定格式的 OETF(光电转换函数)和 EOTF(电光转换函数)对整数值的定义远比除数更具决定性。

结论:你应该使用哪一个?

对于绝大多数应用,请使用 255 进行归一化。它保留了 0.0 和 1.0 的语义意义,确保了与整个图像生态系统的兼容性,并也是 GPU 硬件的标准。

仅在以下情况考虑替代方法(除以 256):

  1. 你控制着整个流水线(包括保存和加载)。
  2. 你不需要 0.0 代表绝对黑。
  3. 你正在进行高精度信号处理,其中中点踏步量化误差是一个可衡量的关注点。

否则,引入其它人生成的图像中的偏移风险——以及它给其它开发者带来的困惑——远大于理论精度带来的边际收益。

Sources