常识上哪个是正确的:(int) blabla * 255.99999999999997 还是 round(blabla*255)?

7

最近我在 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), 

我在这里找到了同样的内容:http://www.filewatcher.com/p/kdegraphics-4.6.0.tar.bz2.5101406/kdegraphics-4.6.0/kolourpaint/imagelib/effects/kpEffectHSV.cpp.html

该链接指向一个名为kpEffectHSV.cpp的文件,属于kolourpaint图像编辑器中的效果库。
(int)(value * 255.999999)

这种技术的使用是否正确?为什么不直接使用round(blabla * 255)之类的方法呢?这是C/C++的特性吗?严格来说,它并不能总是返回正确的结果,在100个案例中只有27个正确。请参见https://docs.google.com/spreadsheets/d/1AbGnRgSp_5FCKAeNrELPJ5j9zON9HLiHoHC870PwdMc/edit?usp=sharing电子表格。请有人解释一下——我认为这应该是一些基础知识。

1
你可能忘记了舍入误差,有些情况下,在浮点数领域进行乘法运算可能会返回一个值,如果在舍入之后,可能会比预先舍入的情况高或低。 - trumpetlicks
1
一个截断,一个四舍五入 - 哪个是正确的取决于意图,而这并不清楚。 - Alan Stokes
《计算机科学家应该了解的浮点运算知识》(What Every Computer Scientist Should Know About Floating-Point Arithmetic) - πάντα ῥεῖ
3
那是一篇很棒的文章,但实际上没人会真正阅读它,而且在这里也不是特别相关。 - Cornstalks
1
@Cornstalks 我同意这与所讨论的特定行为无关。 - πάντα ῥεῖ
@Cornstalks:嗯,这与“这是否正确?”有些相关,因为它不是一般情况。顺便说一下,由于颜色值的范围从0.0到1.0,因此在颜色转换代码中没有区别。但是,如果可以假设原则上输入可以是任何值,则乘法技巧是错误的,因为它会意外地起作用或不起作用,具体取决于您使用它的值。 - Damon
3个回答

26

通常我们希望将闭区间[0,1]中的实数值x映射到范围为[0 ... 255]的整数值j

我们希望以“公平”的方式进行,因此,如果实数在范围内均匀分布,则离散值将近似等概率:每个256个离散值应该从[0,1]间隔中获得“相同的份额”(1/256)。也就是说,我们需要像这样映射:

[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=0j=255

enter image description here

[*] 我的意思是:我们对于在3/256的“小”邻域内发生的事情不是非常感兴趣:四舍五入可能会得到2或3,这并不重要。但我们对于极值很感兴趣:我们想要在x=0x=1时分别获得0和255。

[**] IEEE浮点标准保证这里没有舍入歧义:整数具有精确的浮点表示,乘积将是精确的,并且转换将始终给出256。此外,我们保证 1.0 * z = z


优秀。好答案。 - Mooing Duck
1
+1 针对那个漂亮的图形。 (顺便问一下,您用什么制作它?) - Cornstalks
@Cornstalks:没有什么花哨的,只是 Paint.NET :-) - leonbloy
啊,好老的“手工制作”策略,是吧?我在想你可能做了类似于xkcd/974xkcd/1319的事情。 - Cornstalks
谢谢!我们相信正义。 - gaRex

7
一般来说,(int)(blabla * 255.99999999999997)比使用round()更正确。为什么?因为通过round(),0和255只有1-254的一半范围。如果你用round(),那么0-0.00196078431会被映射到0,而0.00196078431-0.00588235293则会被映射到1。这意味着1的出现概率比0高200%,严格来说,这是一种不公平的偏见。相反,如果乘以255.99999999999997然后向下取整(这是转换为整数所做的,因为它截断),则0到255的每个整数的可能性都是相等的。如果电子表格按小数百分比计数(即每次增加0.01%而不是1%),那么您的电子表格可能会更好地显示此内容。在这里我建立了一个简单的电子表格。如果您查看该电子表格,您会发现当使用round()时,0会受到不公平的偏待,但使用另一种方法,结果就会变得公平且相等。

5
将值转换为整数与 floor 函数具有相同的效果(即截断)。 当您调用 round 函数时,它会向最接近的整数四舍五入。
它们执行不同的操作,请选择您需要的操作。

那么为什么Webkit会选择不太直观的(int)(value * 255.999999)而不是更直观的round(value * 255)呢? - Mooing Duck
@MooingDuck,我的猜测和你一样糟糕。你得问谁写了这个程序。 - vonbrand

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接