程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> .NET實例教程 >> 關於浮點數計算時的精度問題

關於浮點數計算時的精度問題

編輯:.NET實例教程

那個有問題的縮略圖生成的方法發布之後,短短半天就有很多朋友響應,其中指出了不少方法中的不少問題,有些也是我沒有意識到的。果然集體的智慧是無窮的,一段代碼在許多人的眼皮底下經過,想留有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, 100 
99, 100

  第一個結果自然沒有問題,但是在第二個結果中為什麼是99而不是100?為此,我們再通過以下的代碼來觀察一番:

ratio: 0.3333333333333333333333333333 
new value: 99.99999999999999999999999999 
to 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的處理不很妥當等等——如果您有其他想法的話,也可以繼續討論。或者,你也來分享一下代碼或工作中發現的問題?

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved