2012年10月26日 星期五

用 Java 產生高品質縮圖的方法

使用原生的 Graphic2D 的函式庫要改變圖片大小不難,不過要產生高品質的縮圖就稍微複雜一點,原因在 [2] 中有描述。
[2] 提到在做圖片的放大縮小時,選擇 RenderingHints 是一件很重要的事情(這是必然的 XD)
當目的是要放大圖時,選項比較簡單:
  • 求快:RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR
  • 求高品質:RenderingHints.VALUE_INTERPOLATION_BILINEAR
  • 求更高品質:RenderingHints.VALUE_INTERPOLATION_BICUBIC
但如果是縮小圖片時,狀況就稍微複雜一點~以下節錄 [2] 對於縮圖的說明:
When downscaling an image, the choice is slightly more complex. The same advice regarding RenderingHints given for upscaling is generally applicable to downscaling as well. However, be aware that if you try to downscale an image by a factor of more than two (i.e., the scaled instance is less than half the size of the original), and you are using the BILINEAR or BICUBIC hint, the quality of the scaled instance may not be as smooth as you might like. If you are familiar with the quality of the old Image.SCALE_AREA_AVERAGING (or Image.SCALE_SMOOTH) hint, then you may be especially dismayed. The reason for this disparity in quality is due to the different filtering algorithms in use. If downscaling by more than two times, the BILINEAR and BICUBIC algorithms tend to lose information due to the way pixels are sampled from the source image; the older AreaAveragingFilter algorithm used by Image.getScaledInstance() is quite different and does not suffer from this problem as much, but it requires much more processing time in general.
大意大概是說如果縮圖的目標尺寸低於原始尺寸的一半以下時,會因為演算法的取樣方式的關係,導致產生的縮圖沒有想像中的平滑。
而 [2] 使用的解法是重複多次的縮圖,每次都把圖片縮小一半,以保持最多的圖片原始資訊
直到尺寸接近目標尺寸時(即縮小一半的話會小於目標尺寸時),才做最後一次變更,將圖片尺寸改變為目標尺寸。
不過 [2] 也有提到這個作法會重複多次的縮圖,效率上會有影響,並且縮圖過程中會產生很多個暫存的圖片資源,會消耗比較多記憶體。

更詳細的原理說明請直接參考 [2] 的內容吧!

以下是轉貼自 [2] 的程式碼~
/**
 * Convenience method that returns a scaled instance of the provided
 * {@code BufferedImage}.
 * 
 * @param img
 *            the original image to be scaled
 * @param targetWidth
 *            the desired width of the scaled instance, in pixels
 * @param targetHeight
 *            the desired height of the scaled instance, in pixels
 * @param hint
 *            one of the rendering hints that corresponds to
 *            {@code RenderingHints.KEY_INTERPOLATION} (e.g.
 *            {@code RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR},
 *            {@code RenderingHints.VALUE_INTERPOLATION_BILINEAR},
 *            {@code RenderingHints.VALUE_INTERPOLATION_BICUBIC})
 * @param higherQuality
 *            if true, this method will use a multi-step scaling technique
 *            that provides higher quality than the usual one-step technique
 *            (only useful in downscaling cases, where {@code targetWidth}
 *            or {@code targetHeight} is smaller than the original
 *            dimensions, and generally only when the {@code BILINEAR} hint
 *            is specified)
 * @return a scaled version of the original {@code BufferedImage}
 */
static public BufferedImage getScaledInstance(BufferedImage img,
    int targetWidth, int targetHeight, Object hint,
    boolean higherQuality) {
  int type = (img.getTransparency() == Transparency.OPAQUE) ? BufferedImage.TYPE_INT_RGB
      : BufferedImage.TYPE_INT_ARGB;
  BufferedImage ret = (BufferedImage) img;
  int w, h;
  if (higherQuality) {
    // Use multi-step technique: start with original size, then
    // scale down in multiple passes with drawImage()
    // until the target size is reached
    w = img.getWidth();
    h = img.getHeight();
  } else {
    // Use one-step technique: scale directly from original
    // size to target size with a single drawImage() call
    w = targetWidth;
    h = targetHeight;
  }
  do {
    if (higherQuality && w > targetWidth) {
      w /= 2;
      if (w < targetWidth) {
        w = targetWidth;
      }
    }
      if (higherQuality && h > targetHeight) {
      h /= 2;
      if (h < targetHeight) {
        h = targetHeight;
      }
    }
      BufferedImage tmp = new BufferedImage(w, h, type);
    Graphics2D g2 = tmp.createGraphics();
    g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
    g2.drawImage(ret, 0, 0, w, h, null);
    g2.dispose();
      ret = tmp;
  } while (w != targetWidth || h != targetHeight);
    return ret;
}

呼叫上述程式碼去做縮圖的範例如下:
File originalImgFile = new File("0.jpg");
int width = 100;   // Resized width
int height = 121;  // Resized height
BufferedImage bufferedResizedImage = getScaledInstance(
    ImageIO.read(originalImgFile), width, height,
    RenderingHints.VALUE_INTERPOLATION_BICUBIC, true);

測試時,我是用潔西卡艾芭(jessica-alba)這篇部落格文上的照片來測試
縮圖前的尺寸為 500x607,縮圖後目標尺寸是 100x121。
同樣使用 RenderingHints.VALUE_INTERPOLATION_BICUBIC~
用原始的方法直接縮圖,跟使用上述的 getScaledInstance 作多次的重複縮圖的結果差異如下圖。


原始圖是比較大張的(上圖左),應該可以看出單次縮圖(上圖中)跟重複縮圖(上圖右)的效果差異。

至於效率的差異~單次縮圖與重複縮圖的時間差如下:
One-step: 121ms
Multi-step: 230ms
測試過幾次,時間大約都是需要 1.5 倍到 2 倍左右的縮圖時間,能不能接受就要看使用情境了。

其實上述的這個範例,最大的問題還是存在於他是使用 Graphics2D API 來實作調整大小的功能
因此每次調整時都會不可避免地產生中繼的圖片資源在記憶體上,就比較沒有效率了
最理想的方法當然還是直接用 byte 的方式在記憶體裡一次把東西處理完,而不是每處理一次都把 byte 輸出成圖片
然後下次又把圖片再讀回 byte 的形式回到記憶體....吧 XD。

其他種方法可以參考 [3] 的討論,網友 stivlo 有分享了他的經驗:使用 java-image-scaling
不過那是個 GPL License......有興趣的話就請自行參考吧。

參考資料:
1、High Quality thumbnail in java
2、The Perils of Image.getScaledInstance()
3、Resize image in java lose quality

沒有留言: