<その 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 件のコメント:
コメントを投稿