<その 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
右下が今回の手法を使用した縮小イメージです。よく分るように、今回の手法で縮小したイメージの拡大イメージを示しておきます。
最近傍
今までの手法の中でもっとも品質が高かった 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 件のコメント:
コメントを投稿