最近我在 webkit 源码中发现了这个有趣的东西,与颜色转换(hsl 到 rgb)相关:
http://osxr.org/android/source/external/webkit/Source/WebCore/platform/graphics/Color.cpp#0111
const double scaleFactor = nextafter(256.0, 0.0); // it's here something like 255.99999999999997
// .. some code skipped
return makeRGBA(static_cast<int>(calcSomethingFrom0To1(blablabla) * scaleFactor),
(int)(value * 255.999999)
使用这种技术到底正确吗?为什么不直接使用像 round(blabla * 255) 这样的东西? 是C/C++的特性吗?严格来说,在 100 种情况下,有 27 种情况会返回并不总是正确的结果。请参阅电子表格 https://docs.google.com/spreadsheets/d/1AbGnRgSp_5FCKAeNrELPJ5j9zON9HLiHoHC870PwdMc/edit?usp=sharing
请有人解释一下——我认为这应该是一些基本的东西。
简短回答:
(int)(blabla * 255.99999999999997)
应该是首选。
解释如下。
通常我们希望将(闭)区间
x
中的实值 [0,1]
映射到 j
范围内的整数值 [0 ...255]
。
我们希望以“公平”的方式做到这一点,这样,如果实数在范围内均匀分布,则离散值将近似等概率:256 个离散值中的每一个都应该获得“相同的份额”(1 /256) 来自
[0,1]
区间。也就是说,我们想要这样的映射:
[0 , 1/256) -> 0
[1/256, 2/256) -> 1
...
[254/256, 255/256) -> 254
[255/256, 1] -> 255
我们不太关心过渡点 [*],但我们确实希望覆盖整个范围 [0,1]。如何实现?
如果我们简单地做
j = (int)(x *255)
:值255几乎永远不会出现(仅当x=1
时);其余值 0...254
将分别获得间隔的 1/255。 无论在极限点的舍入行为如何,这都是不公平的。
如果我们这样做
j = (int)(x * 256)
:这个分区将是公平的,除了一个单一问题:当 x=1
[**] 时,我们会得到值 256(超出范围!)
这就是为什么
j = (int)(x * 255.9999...)
(其中 255.9999...
实际上是小于 256 的最大双精度)。
另一种实现(也是合理的,几乎等效)是
j = (int)(x * 256);
if(j == 256) j = 255;
// j = x == 1.0 ? 255 : (int)(x * 256); // alternative
但这会更笨拙并且可能效率更低。
round()
在这里没有帮助。例如,j = (int)round(x * 255)
会将 1/255 分配给整数 j=1...254
,并将该值的一半分配给极值点 j=0
、j=255
。
[*] 我的意思是:我们对“小”邻域(例如 3/256)中发生的情况并不是非常感兴趣:四舍五入可能会得到 2 或 3,这并不重要。但我们对极值感兴趣:我们想要分别为
x=0
和 x=1
获得 0 和 255。
[**] IEEE 浮点标准保证这里没有舍入歧义:整数承认精确的浮点表示,乘积将是精确的,并且转换将始终给出 256。此外,我们保证
1.0 * z = z
。
总的来说,我认为
(int)(blabla * 255.99999999999997)
比使用 round()
更正确。
为什么?
因为使用
round()
,0 和 255 的范围只有 1-254 的“一半”。如果您 round()
,则 0-0.00196078431 会映射到 0,而 0.00196078431-0.00588235293 会映射到 1。这意味着 1 出现的概率比 0 高 200%,严格来说,这是一种不公平的偏差。
相反,如果 1 乘以 255.99999999999997,然后再乘以底数(这就是转换为整数所做的事情,因为它会被截断),那么从 0 到 255 的每个整数都有相同的可能性。
如果您的电子表格以小数百分比计数(即每次以 0.01% 而不是 1% 计数),则可能会更好地显示这一点。 我制作了一个简单的电子表格来展示这一点。如果您查看该电子表格,您会发现 0 在
round()
时存在不公平的偏差,但使用其他方法时,情况是公平且平等的。
转换为 int 与向下取整函数具有相同的效果(即截断)。当你调用 round 时,会四舍五入到最接近的整数。
他们做不同的事情,所以选择你需要的。