关于浮点数计算时的精度问题

来源:互联网 发布:数控编程软件ug 编辑:程序博客网 时间:2024/06/05 18:19

那个有问题的缩略图生成的方法发布之后,短短半天就有很多朋友响应,其中指出了不少方法中的不少问题,有些也是我没有意识到的。果然集体的智慧是无穷的,一段代码在许多人的眼皮底下经过,想留有bug也不容易。不过,我在这里只能谈一下我写那篇文章的本意了,我认为那篇文章中最主要的问题是,在计算图片尺寸时没有处理好浮点数计算的精度问题。

为了凸现主要逻辑,我把之前那个方法中计算图片尺寸的代码单独抽取成一个方法:

public static void GetThumbnailSize(    int originalWidth, int originalHeight,    int desiredWidth, int desiredHeight,    out int newWidth, out int newHeight){    // If the image is smaller than a thumbnail just return it    if (originalWidth <= desiredWidth && originalHeight <= desiredHeight)    {        newWidth = originalWidth;        newHeight = originalHeight;        return;    }    // scale down the smaller dimension    if ((decimal)desiredWidth / originalWidth < (decimal)desiredHeight / originalHeight)    {        decimal desiredRatio = (decimal)desiredWidth / originalWidth;        newWidth = desiredWidth;        newHeight = (int)(originalHeight * desiredRatio);    }    else    {        decimal desiredRatio = (decimal)desiredHeight / originalHeight;        newHeight = desiredHeight;        newWidth = (int)(originalWidth * desiredRatio);    }}

我们通过简单的代码试验一下:

int newWidth, newHeight;GetThumbnailSize(200, 200, 100, 100, out newWidth, out newHeight);Console.WriteLine("{0}, {1}", newWidth, newHeight);GetThumbnailSize(300, 300, 100, 100, out newWidth, out newHeight);Console.WriteLine("{0}, {1}", newWidth, newHeight);

得到的结果是:

100, 10099, 100

第一个结果自然没有问题,但是在第二个结果中为什么是99而不是100?为此,我们再通过以下的代码来观察一番:

ratio: 0.3333333333333333333333333333new value: 99.99999999999999999999999999to int: 99

可见,虽然使用了decimal,精度已经非常高的,但是在经过了一除一乘,它还是没有恢复到最精确值。虽然一直说要注意浮点数计算时的精度问题,但是对于这个问题许多朋友往往只是理解到“不能直接两个浮点数相等”,包括我自己的第一印象。但事实上,从上面的结果也可以看出,把一个浮点数直接转换成整形,它便是使用了“去尾”而不是“四舍五入”的方法。因此,虽然newValue的值无比接近100,但是在强制去尾后它还是变成了99。

如果要在原来的方法中改变这个问题,最简单的方法可能是把最后的强制转型替换成Math.Round方法。Math.Round方法使用四舍五入,应该能够解决问题。不过如果只是这样的话收获不大,我们再仔细想想,应该如何做到尽可能的精确。

两个浮点数相除可能会丧失精度,但如果是乘法操作,在一般情况下精度是不会丢失的,除非发生了溢出的话,或者小数位数太多。因此在计算过程中为了保持精度,我们应该尽可能的做乘法,而不是作除法。例如以下的判断:

if ((decimal)desiredWidth / originalWidth < (decimal)desiredHeight / originalHeight)

其实最好改写成“等价”的乘法操作(假设没有溢出):

if (desiredWidth * originalHeight < desiredHeight * originalWidth)

同理,如果可以的话,在作计算的时候,也最好先乘再除:

if (desiredWidth * originalHeight < desiredHeight * originalWidth){    newWidth = desiredWidth;    newHeight = (int)Math.Round((decimal)originalHeight * desiredWidth / originalWidth);}else{    newHeight = desiredHeight;    newWidth = (int)Math.Round((decimal)originalWidth * desiredHeight / originalHeight);}

这么做,我们就避免了使用scaleRatio这个已经丧失部分精度的值来参与计算,这样1 * 3 / 3便可以等于1,而不像1 / 3 * 3等于0.99…。因此,最终我们CreateThumbnail的代码便修改为:

/// <summary>/// Creates a thumbnail from an existing image. Sets the biggest dimension of the/// thumbnail to either desiredWidth or Height and scales the other dimension down/// to preserve the aspect ratio/// </summary>/// <param name="imageStream">stream to create thumbnail for</param>/// <param name="desiredWidth">maximum desired width of thumbnail</param>/// <param name="desiredHeight">maximum desired height of thumbnail</param>/// <returns>Bitmap thumbnail</returns>public Bitmap CreateThumbnail(Bitmap originalBmp, int desiredWidth, int desiredHeight){    // If the image is smaller than a thumbnail just return it    if (originalBmp.Width <= desiredWidth && originalBmp.Height <= desiredHeight)    {        return originalBmp;    }    int newWidth, newHeight;    // scale down the smaller dimension    if (desiredWidth * originalBmp.Height < desiredHeight * originalBmp.Width)    {        newWidth = desiredWidth;        newHeight = (int)Math.Round((decimal)originalBmp.Height * desiredWidth / originalBmp.Width);    }    else    {        newHeight = desiredHeight;        newWidth = (int)Math.Round((decimal)originalBmp.Width * desiredHeight / originalBmp.Height);    }    // This code creates cleaner (though bigger) thumbnails and properly    // and handles GIF files better by generating a white background for    // transparent images (as opposed to black)    // This is preferred to calling Bitmap.GetThumbnailImage()    Bitmap bmpOut = new Bitmap(newWidth, newHeight);        using (Graphics graphics = Graphics.FromImage(bmpOut))    {        graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;        graphics.FillRectangle(Brushes.White, 0, 0, newWidth, newHeight);        graphics.DrawImage(originalBmp, 0, 0, newWidth, newHeight);    }    return bmpOut;}

当然,在前文中很多朋友指出的其他一些问题也很有道理,例如:

  • 没有做参数校验。
  • 直接返回源图片的做法让方法的含义不同。
  • 经过计算后newWidth和newHeight可能为0。

例如还有朋友提出对GIF的处理不很妥当等等——如果您有其他想法的话,也可以继续讨论。或者,你也来分享一下代码或工作中发现的问题?

from: http://blog.zhaojie.me/2009/11/precision-of-float-point-calculation.html

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 墙壁被小孩用彩色笔画花了怎么办 传图识字里有些字识别不了怎么办 一岁四个月宝宝智力发育迟缓怎么办 小打架受伤对方家长不配合怎么办 宝宝要上幼儿园了家长该怎么办 断奶涨奶怎么办又能防止乳房变形 孩子在幼儿园被小朋友打了怎么办 孩子在幼儿园被小朋友咬了怎么办 孩子被同学打了家长该怎么办? 孩子把同学打了打人家长该怎么办 孩子和同学发生矛盾家长该怎么办 孩子同学给孩子要东西家长该怎么办 如果遇到不讲理的孩子和家长怎么办 孩子调皮又被老师留校了怎么办 孩子拼音f和sh发音不清怎么办 自己在家生的孩子怎么办出生证明 在家念地藏经招来众生不走怎么办 家是济宁孩子上学想在济南上怎么办 高一孩子成绩严重下滑家长怎么办 商铺租客不交租金又不搬走怎么办 考试试卷找不到了明天要交怎么办 8个月宝宝不爱吃蔬菜泥怎么办 5个月的宝宝拉肚怎么办 一岁宝宝大便拉不出来怎么办 八个月宝宝便秘拉不出来怎么办 10个月宝宝大便拉水怎么办 4个月的孩子大便拉水怎么办 五个多月宝宝大便拉水怎么办 七个月宝宝大便还没成行怎么办 宝宝一岁了还没长牙怎么办 宝宝什么都会就差不会独占怎么办 两岁宝宝肚子不舒服还吐怎么办 两岁宝宝吃坏肚子吐怎么办 1岁半儿童牙烂了怎么办 一岁宝宝吃了就吐怎么办 两岁宝宝龋齿门牙掉了怎么办 一岁宝宝冻着了呕吐怎么办 两岁宝宝喉咙有痰呼呼响怎么办 1岁宝宝吐的水水怎么办 宝宝吐了5 6次了怎么办 7岁儿童吃了就吐怎么办