2010/07/02

Java でサムネイル作成 その 2

このエントリーをはてなブックマークに追加

<その 1>

 

前回はイメージを扱うクラスと、イメージのロード方法について書きました。

今回は BufferedImage クラスのデータ形式について書こうと思ったのですが...

検証用のサンプルで確かめていたら、J2SE 1.5 と Java SE 6 では実行結果が違うのです。グラフィックアクセラレータの使い方がかなり変わってきているようです。J2SE 1.5 まではアクセラレートされなかったものも、Java SE 6 ではアクセラレートされるようになっています。

このように、Java のバージョンによって処理アルゴリズムが更新され、過去のイディオムが使えないということもあります。古いバージョンで言われてきたことが、最新のバージョンでは違うということが多々あるので、そこらへんはご注意ください。

 

で、今回はイメージの縮小について。

イメージを縮小するには、複数の手法があります。g を Graphics2D オブジェクト、image を Image オブジェクトだとします。

  1. g.drawImage(image, x, y, width, height, null);
  2. g.drawImage(image, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null);
  3. g.translate(x, y);
    g.scale(sx, sy);
    g.drawImage(image, 0, 0, null);
  4. AffineTransform at = new AffineTransform();
    at.translate(x, y);
    at.scale(sx, sy);
    g.drawImage(image, at, null);
  5. Image scaledImage = image.getScaledInstance(w, h, hints);
    g.drawImage(scaledImage, x, y, null);

1 と 2 は Graphics#drawImage メソッドで直接拡大/縮小を指定する方法。3 と 4 は AffineTransfform クラスを使って移動と拡大/縮小する方法。3 では表面的には AffineTransform オブジェクトは出てきませんが、内部的に使ってます。

5がImage#getScaledInstance メソッドで拡大/縮小したイメージを作成する方法です。

ぶちゃっけていうと、1 から 4 までの方法はほとんんど差がありません。どれを使っても一緒です。

それよりも拡大/縮小の品質に関わるのは、拡大/縮小に使用するアルゴリズムです。Java 2D で使用できるアルゴリズムは次の 3 つです。

  • 最近傍補間 (ニアレストネイバー)
  • バイリニア補間
  • バイキュービック補間

一般的には下に行くほど補間の品質が高いことになっています。

これらのアルゴリズムの指定には RenderingHints クラスを使用します。

たとえば、バイキュービックを指定するのであれば次のようにします。

    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 
                       RenderingHints.VALUE_INTERPOLATION_BICUBIC);

さて、Image#getScaledInstance メソッドにも hints という引数があります。ここにもレンダリングヒントを指定できるのですが、RenderingHints クラスではなく、Image クラスに定義されている定数を使用します。

  • SCALE_DEFAULT
  • SCALE_REPLICATE
  • SCALE_FAST
  • SCALE_SMOOTH
  • SCALE_AREA_ABERAGING

でも、getScaledInstance メソッドのソースを見てみると実際には 2 種類の方法を使っているようです。

    public Image getScaledInstance(int width, int height, int hints) {
        ImageFilter filter;
        if ((hints & (SCALE_SMOOTH | SCALE_AREA_AVERAGING)) != 0) {
            filter = new AreaAveragingScaleFilter(width, height);
        } else {
            filter = new ReplicateScaleFilter(width, height);
        }
        ImageProducer prod;
        prod = new FilteredImageSource(getSource(), filter);
        return Toolkit.getDefaultToolkit().createImage(prod);
    }

ではこれらの 5 種類のうちどれを使えばいいのでしょう? さっそくやってみましょう。

サンプルのソース ImageScale1.java

このサンプルを実行すると、上段に Graphics#drawImage メソッドで縮小した 3 つのイメージ、下段に Image#getScaledInstance メソッドで縮小したイメージが表示されます。

ImageScale1 の実行結果

まぁ、これでもいいんですけど、それぞれのイメージを 4 倍に拡大したものを以下に示しておきます。

最近傍
最近傍
バイリニア
バイリニア
バイキュービック
バイキュービック
SCALE_DEFAULT
SCALE_DEFAULT
SCALE_SMOOTH
SCALE_SMOOTH

これを見ると、SCALE_SMOOTH 以外の手法は斜めの線がギザギザになってしまっています。こういうのをギャザといいます。

確かによく見ると、再近傍 < バイリニア < バイキュービック の順で品質が上がっています。でも、バイキュービックであっても品質的にはちょっとですよね。

とすると、SCALE_SMOOTH を使えばいいのかというと、これがまたダメなんです。

何がダメかというと、パフォーマンス。

このサンプルプログラムは描画にどれくらい時間がかかっているかを表示しています。

注意しなくてはいけないのが、getScaledInstance メソッドは非同期でイメージを生成するので、getScaledInstance メソッドだけで時間を計ってしまうと、とても短い時間になってしまいます。

なので、最後の g.drawImage が終了するまでかかる時間を比較します。

ある時点での結果は次のようになりました。

Nearest Neighbor: 1.987399ms
Bilinear: 2.083085ms
Bicubic: 8.362403ms
Default: 50.887474ms
Smooth: 89.932671ms

ここでは、1024×680 のイメージを 1/10 に縮小しています。

この結果からは、最近傍が最も速く、下に行くほど処理時間がかかっていることが分ります。それにしても、最近傍と SCALE_SMOOTH は約 50 倍もの処理時間の違いがあります。

このサンプルはイメージが 1 枚ですし、イメージもそれほど大きくないので、この程度になっています。しかし、1000万画素のイメージを 20 枚だとすると約 450 分もかかってしまうことになります。

これではとてもじゃないけど使えないですね。

というわけで今日のまとめ。

  • クオリティが低いアルゴリズムは、パフォーマンスが高い
  • クオリティが高いアルゴリズムは、パフォーマンス低い
  • でも、バイキュービックでもクオリティは低すぎ

クオリティ的に受けいれられるのは、SCALE_SMOOTH だけです。しかし、処理速度が...

ということで、次回はお勧めの方法を紹介します。

 

7/5 <<追記>>

バイキュービックのクオリティが低いと書きましたが、それはあくまでもイメージを縮小するときの場合です。

もともと、最近傍、バイリニア、バイキュービックなどはイメージを拡大する時に使われる補間アルゴリズムです。したがって、その本領が発揮されるのはイメージを拡大する時。

たとえば、上記の拡大図は、エッジを維持したいため、最近傍補間を使用しています。

常にバイキュービックの品質が悪いわけではないのではないので、ご注意ください。

0 件のコメント: