应该用255还是256来归一化RGB值?

Lobsters Hottest 新闻

摘要

文章比较了归一化RGB值的两种方法(除以255 vs 除以256),并解释了浮点数转换和舍入的后果,包括在极端值处不均匀的区间宽度。

<p><a href="https://lobste.rs/s/uuvuhv/should_you_normalize_rgb_values_by_255_256">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/01 14:32

# 应该将 RGB 值除以 255 还是 256? 来源:https://30fps.net/pages/255-vs-256-division/ 假设你在编写一个图像处理程序。该程序读入一张图像,将其转换为浮点数,进行一些处理,最后将修改后的像素以 8 位色彩保存到磁盘。今天的问题涉及整数到浮点数的转换应该如何具体执行。有两种方法,用 Python 和 NumPy 写出来如下所示: 标准方法(除以 255)替代方法(除以 256) `` pixels = img / 255.0 result = process(pixels) output = np.trunc(result * 255 + 0.5) `` `` pixels = (img + 0.5) / 256.0 result = process(pixels) output = np.trunc(result * 256) `` 我假设两种情况下输出值在最终类型转换之前都会被截断: `` # 截断并转换为 8 位 output_8bit = output.clip(0, 255).astype(np.uint8) `` 标准方法将整数 0 映射为 0.0,255 映射为 1.0。它工作得非常好,并且是 GPU 的做法(https://microsoft.github.io/DirectX-Specs/d3d/archive/D3D11_3_FunctionalSpec.htm#UNormtoFLOAT)。替代方法则添加了 0.5 的偏置,并除以 256,因此整数 0 被映射为 0.5/256 = 0.001953125。这很不方便,因为你的图像处理代码无法在不了解上述常数的情况下检测黑色像素等。其结果是,即使你以浮点数计算,你的逻辑也会与 8 位输入绑定。而使用标准方法,你始终可以认为黑色就是 0.0。 但有些程序员仍然倾向于替代方法。这是怎么回事?他们看中了它什么? ## 反对 255.0 的理由 当在数轴上绘制时,标准方法看起来确实相当奇怪。下面是一个夸张的版本,用 3 位整数 [0..7] 映射到 [0,1]: X 轴上有一条数轴,棕色圆圈的位置代表解码后的浮点数值。里面的数字是整数输入。每个整数都有箭头指向它;这些箭头表示会四舍五入到该整数的浮点数值范围。在本文的剩余部分,我将这些范围称为“桶”。 ### 极值处的桶更小 图中首先明显的问题是标准公式的极端桶超出了 [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 `` 在标准方法中,零桶只有“一半大小”并不重要。 ### 不精确性 第二个问题是标准方法的浮点数值并不精确。例如,128/255.0 ≈ 0.501961,但 128/256.0 = 0.5。由于这种舍入误差,浮点数值之间的距离会有微小变化。但这并不是真正的问题,因为误差确实很小。32 位浮点数具有 23 位小数(“尾数”)。我们讨论的是其最低有效位的舍入误差;抖动幅度小于 2^(-23)。当然,即使是最复杂的图像处理任务,0.00001% 的相对误差也是无关紧要的。在这种情况下,不精确性是一个美学问题,而不是技术问题。 ### 数值不正好位于整数之间 替代方法总是将每个浮点数值恰好放置在两个整数的中间。请观察上面数轴图中垂直条的对齐方式。半程位置可以被视为一种折衷;我们不知道原始量化值究竟是什么,因此两个连续整数之间的平均点是一个很好的猜测。 我确信在某些应用中这个特性是有用的,尽管我自己很难想出例子。不过,至少抖动(dithering)会更方便,Andrew Kesler(以他的名片光线追踪器(http://eastfarthing.com/blog/2016-01-12-card/)闻名)在 2015 年的博客文章“颜色深度转换”(http://eastfarthing.com/blog/2015-12-19-color/)中这样认为。理由是可以添加噪声而无需担心边界情况。相比之下,标准公式中尴尬的极端情况需要小心处理(https://computergraphics.stackexchange.com/questions/5904/whats-a-proper-way-to-clamp-dither-noise/8777#8777)以保持噪声分布一致。 ## 两种类型的量化器 到目前为止,标准的“除以 255”公式看起来仍然很稳妥,至少足够坚固,仍然值得使用。另一种思考这个问题的方式是退一步,将这两种方法视为两种不同的*均匀标量量化器*。如果我们查看维基百科上关于量化(https://en.wikipedia.org/wiki/Quantization_(signal_processing))的页面,我们会很快了解到量化器主要有两种类型: > 大多数用于有符号输入数据的均匀量化器可以分为两种类型之一:**中升型(mid-riser)** 和 **中平型(mid-tread)**。该术语基于零值附近区域发生的情况,并将量化器的输入-输出函数类比为楼梯。中平型量化器有一个零值重建电平(对应楼梯的踏板),而中升型量化器有一个零值分类阈值(对应楼梯的踢面)。 维基百科引用了一篇 1977 年的论文(https://ieeexplore.ieee.org/document/1089500),其标题和摘要排版如此惊人,以至于我必须在此重现: “量化” 作者:Allen Gresho。IEEE 通信学会杂志,1977 年 9 月。 无论如何,当绘制在图表上时,中升型和中平型量化器在零点交叉处有所不同: 中平型确实将零映射到零,而中升型则将零映射到两个整数的中间(听起来熟悉吗?)。维基百科使用的符号表示输入实数为 x,其编码(“分类”)后的整数值为 k,重建的实数为 y_k。对应的量化器公式如下: 类型 分类(编码) 重建(解码) 中升型阶梯量化器 k = trunc(x L) y_k = (k+0.5)/L 中平型阶梯量化器 k = trunc(x L + 0.5) y_k = k/L L 代表不同的输出电平数量(例如 256)。 如果我们将这些定义应用于我们竞争中的两种方法,我们可以将标准公式称为“中升型”且 L=255,将替代方法称为“中平型”且 L=256。实际上,我将再次显示它们的代码,并加上新标签,以便与上述新公式联系起来。代码片段本身与开头相同。 中升型量化器(L=255)中平型量化器(L=256) `` pixels = img / 255.0 result = process(pixels) output = np.trunc(result * 255 + 0.5) `` `` 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 的计算(https://stackoverflow.com/a/79805625),使用 255 和 256 除数时的平均绝对误差分别为 1/1020 和 1/1024。因此,理论上除以 256 更精确。 微妙之处在于,这种重建并不是我们正在做的事情。前提是我们加载 8 位 RGB 图像,对其进行处理,然后再次保存。我们无法控制它们在保存时是如何量化的;所有丢失的信息都已永远消失。换句话说,如果图像的色彩乘以 255 并舍入,那么在加载时除以 256 并不会恢复任何精度。只有当我们同时控制保存和加载时,追求更低的重建误差才有意义。 事实上,使用替代公式加载他人的图像会引入*更多误差*。这些图像很可能是通过标准公式量化的,因此使用错误的缩放因子解码在理论上是不正确的。在实践中,颜色并不是绝对测量值(即使 sRGB 规范声称如此),而且所有发生的事情就是我们会在一个稍小范围且带有小偏移的情况下进行处理。微妙的部分到此结束。 最后,永远不要混淆两种量化器的编码和解码步骤。那只是错误的代码。不过,这是一个容易犯的错误。 ## 结论 回答标题中提出的问题:如果你正在处理由陌生人提供的图像,你应该将 RGB 值除以 255。无论是浮点数值不精确,还是某种抽象的重建误差更大的感觉,都不是选择替代方法的充分理由。但如果你同时控制图像的保存和加载,不需要零映射到零,并且觉得将处理代码与 8 位动态范围绑定没问题,那么你可以考虑除以 256 以榨取更多的精度。只是当你的同事仍然使用标准公式加载你的图像,破坏你的完美计划时,不要怪我。 ## 其他观点 Jonathan Blow 2002 年的文章(https://web.archive.org/web/20240706043551/https://number-none.com/product/Scalar%20Quantization/index.html)讨论了中升型和中平型量化器,但没有提及它们的名称。我从那里得到了图表灵感。 之前提到的 Andrew Kesler 2015 年的博客文章(http://eastfarthing.com/blog/2015-12-19-color/)主张使用替代公式。不幸的是,与标准公式的比较没有包括四舍五入,这使大部分分析失效。 *我正在写一本关于颜色缩减算法的书。如果你感兴趣,请在此注册(https://paletteprogramming.com/)。*

相似文章

偏差累积,方差抵消

Hacker News Top

本文证明,对BF16优化器状态使用随机舍入可以匹配FP32性能,因为无偏误差随时间抵消,而四舍五入则因累积偏差而停滞。一项使用MLP的实验表明,BF16+SR在减少内存使用的同时达到了与FP32相似的损失。