2010/07/09

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

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

<その 1>

<その 2>

 

Java でサムネイルを作る方法についての最終回です。

前回、単純にバイキュービックなどの補間法でイメージを縮小しても、クオリティが低いということを書きました。

ところで、下のイメージを見比べてみてください。

イメージの比較

右のイメージに比べ、あきらかに左の方がジャギが少ないと思いませんか。

この 2 つのイメージは両方ともバイリニアで縮小しています。違いは縮小率。

左が 50%、右が 48% です。

50% の場合、縮小した時に対応するピクセルは単純に求めることができます。4 ピクセルを 1 ピクセルにすればいいだけですから。

このため、品質が高いまま縮小が可能になるのです。これは最近傍だとだめなのですが、バイキュービックでも大丈夫です。ただ、パフォーマンス的にはバイリニアの方が速いので。

さて、ここで、発想を変えてみます。

つまり、縮小を 1 回で済ませなくてもいいのではないかということです。

具体的には、最終的な縮小率に近づくまで、50% に縮小することを繰りかえすという手法。

もちろん、縮小を複数回行なうので、パフォーマンスは劣化します。ただ、前回の Image#getScaledImage メソッドよりはかなり速く縮小できるはずです。

たとえば、最終的な縮小率が 10% であれば、半分に縮小したイメージを、更に半分、もう一度半分にします。これで、縮小率は 0.125。後は 0.125 のイメージを 0.1 に縮小します。

これで縮小は 4 回行なうので、単純にバイリニアに比べると遅くなります。しかし、50 倍以上遅かった Image#getScaledImage メソッドに比べれば、雲泥の差のはずです。

では、実際に試してみましょう。

サンプルのソース ImageScale3.java

右下が今回の手法を使用した縮小イメージです。よく分るように、今回の手法で縮小したイメージの拡大イメージを示しておきます。

ImageScale1 の実行結果
最適化手法
最近傍

今までの手法の中でもっとも品質が高かった Image.SCALE_SMOOTH と比べても、まったく遜色がないことが分ります。

処理速度はどうでしょう。ある時点での結果は次のようになりました。

Nearest Neighbor: 0.210803ms
Bilinear: 0.97409ms
Bicubic: 2.872409ms
Default: 180.350298ms
Smooth: 240.238665ms
最適: 12.833274ms

今回の手法は約 13ms。確かにバイリニアやバイキュービックに比べるとパフォーマンスは落ちます。しかし、SCALE_SMOOTH と比べると、その違いは明らか。

実をいうと、この方法は櫻庭が考えたものではありません。イメージ処理の手法としては、よく知られたものなのです。

たとえば、この手法は Filthy Rich Clients にも紹介されています。今回のソースも Filthy Rich Client に掲載されていたものに手を加えたものです。

この縮小イメージを生成するメソッドを次に示します。

    private BufferedImage getOptimalScalingImage(BufferedImage inputImage,
                                                 double scaleFactor) {
        // 現在のイメージのサイズ
        int currentWidth = inputImage.getWidth();
        int currentHeight = inputImage.getHeight();
 
        // 最終的なイメージのサイズ
        int endWidth = (int)(currentWidth * scaleFactor);
        int endHeight = (int)(currentHeight * scaleFactor);
 
        // 現在のイメージ
        BufferedImage currentImage = inputImage;
 
        // 最終的なサイズと現在のイメージの差
        int delta = currentWidth - endWidth;
        // 次に縮小するサイズ
        int nextPow2 = currentWidth >> 1;
 
        while (currentWidth > 1) {
            // 最終的なイメージとサイズの差が、次に縮小するサイズよりも
            // 小さいかどうか調べる
            if (delta <= nextPow2) {
                // イメージのサイズの差が小さい場合
                if (currentWidth != endWidth) {
                    // 最終的な縮小率が 1/2n にならない場合
                    BufferedImage tmpImage
                        = new BufferedImage(endWidth,
                                            endHeight,
                                            BufferedImage.TYPE_INT_RGB);
                    Graphics2D g = (Graphics2D)tmpImage.getGraphics();
                    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 
                                       RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                    g.drawImage(currentImage, 
                                0, 0, 
                                tmpImage.getWidth(), 
                                tmpImage.getHeight(), null);
                    g.dispose();
 
                    currentImage = tmpImage;
                }
 
                return currentImage;
            } else {
                // イメージのサイズの差が大きい場合
                // 更に半分に縮小する
                BufferedImage tmpImage 
                    = new BufferedImage(currentWidth >> 1,
                                        currentHeight >> 1,
                                        BufferedImage.TYPE_INT_RGB);
                Graphics2D g = (Graphics2D)tmpImage.getGraphics();
                g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 
                                   RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                g.drawImage(currentImage,
                            0, 0,
                            tmpImage.getWidth(), 
                            tmpImage.getHeight(), null);
                g.dispose();
 
                // 変数の更新
                currentImage = tmpImage;
                currentWidth = currentImage.getWidth();
                currentHeight = currentImage.getHeight();
                delta = currentWidth - endWidth;
                nextPow2 = currentWidth >> 1;
            }
        }
 
        return currentImage;
    }

2 で割る代わりに、ここではシフトを使っています。

このメソッドはまだ最適化の余地を残しています。たとえば、縮小イメージを表す BufferedImage オブジェクトを毎回生成していますが、使い回すこともできるはずです。

また、サムネイルをクライアントで表示する場合、単なる BufferedImage オブジェクトを使うのではなく、コンパチブルイメージにすると更にパフォーマンスが向上します。

ただし、ヘッドレスのサーバでサムネイルを作成する場合には関係ないので、ここでは触れないことにします。興味がある方はぜひ Filthy Rich Client を読んでみてください。

ということで、今回のまとめ。

  • 縮小率 50 % でバイリニアを複数回行なう手法は、品質が高く、かつパフォーマンスもよい、バランスのいい手法である

 

参考文献

"Filty Rich Cliients" Chet Haase, Romain Guy, 訳 松田晃一、小沼千絵

0 件のコメント: