JRubyでFilthyRichClients その5
第4章はスケーリングに関する品質とパフォーマンスの話題です。
Java2Dでは画像のスケーリングに使えるオプションがたくさんあり、オプションによって品質とパフォーマンスが異なっている。
Filthy Rich Clientsでは以下のスケーリングの品質を比較している。
(この順番は品質の低い順番から並んでいるとのこと。)
- NEAREST_NEIGHBOR
- BILINEAR
- BICUBIC
- AREA_AVERAGING
本によると、AREA_AVERAGINGは、品質は良いけどパフォーマンスがその他のアルゴリズムより
かなり悪く、あまり推奨されていないようだ。(どうか使わないでと書いてあった)
代わりに、AREA_AVERAGINGのように品質が良くて、かつパフォーマンスも良いProgressive Bilinearという方法が紹介されていた。
Progressive Bilinearというのは以下のようなもの。
- BILINEAR方式は、50%より小さく縮小処理を行った場合に品質が悪くなることが分かっている。
- 50%より小さい縮小処理を行うときは、50%の縮小を繰り返して目的のサイズに縮小する。
これらのスケーリングの品質とパフォーマンスを、比較するためのコードがJavaで書かれていたので、JRubyで書き直して検証してみた。
NEAREST_NEIGHBOR、BILINEAR、BICUBIC、AREA_AVERAGINGを使うには、Graphicsインスタンスのメソッドにオプションを渡せばよい。
Progressive Bilinearは、BILINEARを反復する処理を記述する必要があるので、moduleにまとめた。(ImageUtil#get_scaled_instance)
require 'java' include_class %w( JFrame JComponent SwingUtilities ).map{|s| 'javax.swing.' + s} include_class %w( Color RenderingHints BorderLayout Image Transparency ).map{|s| 'java.awt.' + s} include_class 'javax.imageio.ImageIO' include_class 'java.awt.image.BufferedImage' class ScaleTest < JComponent FULL_SIZE = 190 PADDING = 5 QUAD_SIZE = FULL_SIZE / 2 SCALE_FACTOR = 0.17 def initialize super() @original_image = BufferedImage.new(FULL_SIZE, FULL_SIZE, BufferedImage::TYPE_INT_RGB); @original_image_painted = false end def paintComponent(g) paint_original_image() unless @original_image_painted x = 5 y = 20 # nearest neighbor benchmark(g, x, y, "NEAREST") do draw_image(g, y) end # bilinear y += FULL_SIZE + PADDING g.setRenderingHint(RenderingHints::KEY_INTERPOLATION, RenderingHints::VALUE_INTERPOLATION_BILINEAR) benchmark(g, x, y, "BILINEAR") do draw_image(g, y) end # bicubic y += FULL_SIZE + PADDING g.setRenderingHint(RenderingHints::KEY_INTERPOLATION, RenderingHints::VALUE_INTERPOLATION_BICUBIC) benchmark(g, x, y, "BICUBIC") do draw_image(g, y) end # getScaledInstance y += FULL_SIZE + PADDING g.setRenderingHint(RenderingHints::KEY_INTERPOLATION, RenderingHints::VALUE_INTERPOLATION_NEAREST_NEIGHBOR) benchmark(g, x, y, "getScaled") do draw_image_getScaledInstance(g, y) end # Progressive Bilinear y += FULL_SIZE + PADDING benchmark(g, x, y, "Progressive") do draw_image_progressive_bilinear(g, y) end # Draw image sizes yield_draw_parametor do |x, scaled_size| g.drawString("#{scaled_size} x #{scaled_size}", x + [0, scaled_size/2 - 20].max, 15) end end private def paint_original_image g = @original_image.getGraphics() # clear background g.setColor(Color::BLACK) g.fillRect(0, 0, FULL_SIZE, FULL_SIZE) draw_rgb_rect(g, 0, 0, QUAD_SIZE) draw_picture(g, QUAD_SIZE, 0, QUAD_SIZE) draw_smile(g, 0, QUAD_SIZE, QUAD_SIZE) draw_grid(g, QUAD_SIZE, QUAD_SIZE, QUAD_SIZE) g.dispose() @original_image_painted = true end def draw_rgb_rect(g, x, y, width) colors = [Color::RED, Color::GREEN, Color::BLUE] 0.step(width, colors.size) do |i| colors.each_with_index do |color, j| g.setColor(color) g.drawLine(x+i+j, y, x+i+j, y+width) end end end def draw_picture(g, x, y, width) picture = ImageIO.read(java.io.File.new("lib/images/BBGrayscale.png")) # Center picture in quadrant area x_diff = width - picture.getWidth() y_diff = width - picture.getHeight() g.drawImage(picture, x + x_diff/2, y + y_diff/2, nil); end def draw_smile(g, x, y, width) g.setColor(Color::WHITE) g.fillRect(x, y, width, width) g.setColor(Color::BLACK) g.drawOval(x + 2, y + 2, width-4, width-4) g.drawArc(x + 20, y + 20, width - 40, width -40, 190, 160) eye_size = 7 eye_pos = 30 - (eye_size / 2) g.fillOval(x + eye_pos, y + eye_pos, eye_size, eye_size) g.fillOval(x + width - eye_pos - eye_size, y + eye_pos, eye_size, eye_size) end def draw_grid(g, x, y, width) g.setColor(Color::WHITE) g.fillRect(x, y, width, width) g.setColor(Color::BLACK) 1.step(width, 4) do |i| g.drawLine(x, y+i, x+width, y+i) g.drawLine(x+i, y, x+i, y+width) end end def yield_draw_parametor x = 100 delta = SCALE_FACTOR * FULL_SIZE FULL_SIZE.step(1, -delta) do |scaled_size| yield(x, scaled_size) x += scaled_size + 20 end end def draw_image(g, y) yield_draw_parametor do |x, scaled_size| g.drawImage(@original_image, x, y + (FULL_SIZE-scaled_size)/2, scaled_size, scaled_size, nil) end end def draw_image_getScaledInstance(g, y) yield_draw_parametor do |x, scaled_size| scaled_image = @original_image.getScaledInstance(scaled_size, scaled_size, Image::SCALE_AREA_AVERAGING) g.drawImage(scaled_image, x, y + (FULL_SIZE - scaled_size)/2, nil) end end def draw_image_progressive_bilinear(g, y) yield_draw_parametor do |x, scaled_size| scaled_image = ImageUtil.get_scaled_instance(@original_image, scaled_size, scaled_size) g.drawImage(scaled_image, x, y + (FULL_SIZE - scaled_size)/2, nil) end end def benchmark(g, x, y, type) start_time = java.lang.System.nanoTime() yield draw_result_time(g, x, y + FULL_SIZE/2, type, java.lang.System.nanoTime(), start_time) end def draw_result_time(g, x, y, type, end_time, start_time) g.drawString(type + " ", x, y) g.drawString(((end_time - start_time) / 1000000).to_s + " ms", x, y + 15); end end module ImageUtil module_function # progressive bilinear def get_scaled_instance(img, target_width, target_height, hint=RenderingHints::VALUE_INTERPOLATION_BILINEAR) type = (img.getTransparency() == Transparency::OPAQUE) ? BufferedImage::TYPE_INT_RGB : BufferedImage::TYPE_INT_ARGB ret = img w, h = img.getWidth(), img.getHeight() scratch_image = BufferedImage.new(w, h, type) g = scratch_image.createGraphics() prev_w, prev_h = ret.getWidth(), ret.getHeight() while(w != target_width || h != target_height) if w > target_width w /= 2 w = target_width if w < target_width end if h > target_height h /= 2 h = target_height if h < target_height end g.setRenderingHint(RenderingHints::KEY_INTERPOLATION, hint) g.drawImage(ret, 0, 0, w, h, 0, 0, prev_w, prev_h, nil) prev_w, prev_h = w, h ret = scratch_image end g.dispose() if (target_width != ret.getWidth() || target_height != ret.getHeight()) scratch_image = BufferedImage.new(target_width, target_height, type) g = scratch_image.createGraphics() g.drawImage(ret, 0, 0, nil) g.dispose() ret = scratch_image end ret end end SwingUtilities.invokeLater do f = JFrame.new f.add(ScaleTest.new) f.setSize(900, 50 + (5 * ScaleTest::FULL_SIZE) + (6 * ScaleTest::PADDING)) f.setDefaultCloseOperation(JFrame::EXIT_ON_CLOSE) f.setVisible(true) end
品質
縮小率が小さくなると、品質は、AREA_AVERAGINGとProgressive Bilinearが
他のアルゴリズムより良いと思われる。
パフォーマンス
アルゴリズム | 時間(ms) |
---|---|
NEAREST NEIGHBOR | 2 |
BILINEAR | 1 |
BICUBIC | 97 |
AREA_AVERAGING | 55 |
Progressive Bilinear | 10 |
AREA_AVERAGINGが一番遅いと思っていたが、BICUBICの方が遅い。
品質の良かったProgressive Bilinearがパフォーマンスもなかなか。
この結果を踏まえると、普段はNEAREST NEIGHBORを使って、品質を高めたいときはProgressive Bilinearを考えるのが良いのかも。