サムネイル画像作成の高速化

JListを用いてサムネイル画像を表示する場合、サムネイル画像作成の高速化とサムネイル画像のキャッシュが必要になる。
JListは、ListCellRenderer#getListCellRendererComponent()を用いて表示するコンテンツのイメージを作成している。そして、次の条件に合致するときこのメソッドは実行される(若干の環境?依存はありそうだけど)。

  1. リスト内容新規表示時(全リスト項目対象)
  2. リスト表示内容更新時(更新対象の項目)
  3. リスト選択項目変更時(前に選択されていた項目と新規に選択された項目)
  4. マウスがJList内を移動したとき(マウスの下にある項目)

なので、100個の項目をJListにて表示する場合、同時に10個しか項目が表示されていなかったとしても、計110回、ListCellRenderer#getListCellRendererComponent()が呼ばれることになる。さらに、スクロールなどして表示内容を変化させた場合、変化させた分だけこのメソッドが呼ばれることになる。そのため、JListを用いてサムネイル画像を表示する場合、サムネイル画像取得処理をどれだけ高速化できるかが、重要となる。その高速化として、ここでは、サムネイル画像作成処理自体の高速化を検討してみる。

なお、サムネイル画像に画質は求めない。画質が悪くても可能な限り高速な方法を採用する。

サムネイル画像の作成方法

サムネイル画像を作成する方法はいくつかある。

  1. 一度画像を読み込み、縮小する。
  2. ImageIOにて画像読み込み時に直接サムネイル化する。
  3. 画像に登録されているサムネイル情報を利用してサムネイル画像を取得する。

ここでは、1と2について扱う。
なお、サムネイル化サイズはここでは便宜上64x64にしているが、ツール側ではちゃんと計算している。

方法1:Image#getScaledInstance(int,int,int)

BufferedImage img = ImageIO.read(file);
BufferedImage thumbnail = new BufferedImage(64, 64, img.getType());
Image tmp= img.getScaledInstance(64, 64, BufferedImage.SCALE_FAST);
thumbnail.getGraphics().drawImage(tmp, 0, 0, null);

この方法の利点は、コードが完結であること、性能/画質のトレードオフを簡単に指定できることだ。当然欠点もある。欠点は、根本的に性能が悪い。注意点として、getScaledInstanceは非同期ロードであるため、getScaledInstanceの戻り値をサムネイル画像としてキャッシュすると、リスト内の全項目について一度サムネイル画像の作成をしている段階では元画像はメモリ内に展開されたままとなり、OutOfMemoryErrorが発生する。そのため、サムネイル画像用のBufferedImageを用意しておく必要がある。

Graphics#drawImage(Image,int,int,int,int,ImageObserver)

BufferedImage img = ImageIO.read(file);
BufferedImage thumbnail = new BufferedImage(64, 64, img.getType());
thumbnail.getGraphics().drawImage(tmp, 0, 0, 64, 64, null);

この方法の利点も、先ほどと同じくコードが完結であることだ。画質に関しては、Graphicsオブジェクトを取得した際、Graphicsの描画品質オプションを設定することで、性能/画質のトレードオフを指定できる。

ImageReadParam

Iterator<ImageReader> ireaders = ImageIO.getImageReadersBySuffix(getSuffix(file));
while(ireaders.hasNext()){
    ImageReader ir = ireaders.next();
    ImageInputStream iis = null;
    try {
        iis = ImageIO.createImageInputStream(file);
        ir.setInput(iis, true, true);
        ImageReadParam p = ir.getDefaultReadParam();
        int w = ir.getWidth(0)/64;
        int h = ir.getHeight(0)/64;
        p.setSourceSubsampling(w, h, w/2, h/2);
        BufferedImage img = ir.read(0, p);
        return img;
    }catch(IOException e){
        e.printStackTrace();
    }finally{
        if( iis != null ){
            try{ iis.close(); }catch(IOException e){}
        }
        if( ir != null ){
            ir.dispose();
        }
    }
}
return null;

ちょっとコードが複雑になってしまった。
この方法は、ImageIOの読み込みパラメータを変更し、画像読み込み時に間引きしながら読み込む方法だ。
メリットとして、性能が良いこと。デメリットとして、画質のコントロールができないこと、またファイルにしか適用できないことが挙げられる。ほかの二つは、画像イメージがメモリに展開されていれば、縮小できるのに対し、この方法は、ファイルにしか適用できない(正確には性能の恩恵を受けるにはファイルしかないということ)。

性能の確認

284ファイル、総計37.3MBのネットからの拾い物エロ画像について、リスト表示の時間について、計測してみた。測定方法は、ストップウォッチwである。各サイズについて3回測定を行い、もっとも性能が良かった数字を記載している。

方法 32x32 64x64 128x128
getScaledInstance 17.17s 15.84s 15.31s
drawImage 8.15s 7.89s 7.55s
ImageReadParam 4.65s 4.84s 4.94s

この結果より、サンプリングしながら画像を読み込む方式が圧倒的に性能が良いことがわかる。

まとめ

これまでの結果より、サンプリング方式でのサムネイル作成を行う採用した。
なお、より良いサムネイル作成のために、次は別の観点で性能について考えていく予定。