处理图像的时候,你有没有纠结过这个问题:把 RGB 值从 0-255 转成浮点数,到底是除以 255 还是 256?
说白了,就是下面这两行代码的区别:
# 方案A:除以 255
pixels = img / 255.0
# 方案B:除以 256
pixels = (img + 0.5) / 256.0
看着挺像的,对吧?但就这一个小数点的差距,在程序员圈子里吵了快二十年。
除以 255 的逻辑很直白——把 0 映射到 0.0,255 映射到 1.0,中间的值均匀分布。这也是 GPU 在硬件层面采用的方案,微软 DirectX 规范里写得很清楚。
好处也很明显:你能直接判断黑色像素,因为黑色 = 0.0,不需要记什么偏置常数。你的图像处理逻辑跟输入位深解耦,换了 10-bit 图像,逻辑基本不用改。
但如果你把 255 方案画在数轴上,会发现一个奇怪的现象——
两头的"箱子"比中间小了一半。
啥意思呢?我来解释一下。当你把浮点数值转回整数时,0 和 255 这两个极值对应的浮点区间,宽度只有其他值的一半。这意味着,如果你的算法生成均匀分布的随机噪声,0 和 255 出现的概率会比 127 少一半。
我在意的不是这个偏差本身,而是——它暗示着 255 方案的浮点值范围其实是 [-0.5/255, 255.5/255],超出了 [0, 1] 的区间。
256 方案的支持者在想什么?
除以 256 的人,通常会提三个理由。
第一,精度更高。StackOverflow 上有人算过,255 方案的平均绝对误差是 1/1020,256 方案是 1/1024。虽然差了不到 0.5%,但理论上确实更准。
第二,浮点值更"干净"。128/255 ≈ 0.501961,但 128/256 = 0.5 整。128/256 是个精确的二进制小数,255 方案做不到这一点。不过讲道理,这个误差在 32 位浮点数里只有 2^{-23} 级别,0.00001% 的相对误差,绝大多数图像处理任务根本感觉不到。
第三,每个整数刚好落在两个整数的正中间。这个"折中"位置被认为是对量化误差更合理的估计——既然我们不知道原始值到底是多少,那就取两个整数之间的平均点。
拆开来看,其实是两类量化器
我把这个问题想明白,是在看了信号处理里"量化器"的分类之后。
拿 Wikipedia 的标准说法:
-
• Mid-tread(中梯型):零值对应一个梯面(tread),0 → 0
-
• Mid-riser(中升型):零值对应一个梯级(riser),0 → 中间值
对照一下:除以 255 是 mid-riser,L=255;除以 256 是 mid-tread,L=256。
从量化理论的角度看,255 方案确实不是最优的——它是把 mid-riser 量化器套在了无符号输入上,还选了 L=255 的码数,怎么看怎么别扭。
那到底该用哪个?
我的结论简单粗暴:如果你在加载别人的图片,乖乖除以 255。
理由有三:
但有一种情况可以考虑 256:如果你控制图片的保存和加载两端,不需要黑色映射到 0.0,也不介意处理代码跟 8-bit 位深绑定,那用 256 确实能挤出那么一丢丢精度。
我个人觉得吧,这个问题真正教会我的是——写代码的时候,知其然还要知其所以然。 很多我们习以为常的写法,背后都有一整套数学和工程的历史包袱。你多了解一层,遇到 bug 的时候就能多一分从容。而不仅仅是"别人都这么写,我也这么写"。
顺带一提,这篇文章的灵感来自 30fps.net 上的一篇深度分析,作者对各种量化场景做了详细对比,推荐一读。
你觉得呢?你在项目里用哪个?是凭着感觉来,还是真的算过?评论区聊聊 👇