Saturday, June 12, 2010

Images: Scaling Down

It's easy to create thumbnails of BufferedImages:
BufferedImage thumb = new BufferedImage( (int)(source.getWidth()*scale), (int)(source.getHeight()*scale), BufferedImage.TYPE_INT_ARGB );
Graphics2D g = thumb.createGraphics();
g.scale(scaleFactor, scaleFactor);
g.drawImage(source, 0, 0, null);
g.dispose()
... but when scaleFactor is small enough, the image quality really suffers. Sure I tried adding the right RenderingHints -- interpolation, quality, etc -- but they only barely made an improvement.

Here is a screenshot of the example that brought this to my attention:


As a sidenote: I should mention that this is only a problem using Sun's rendering engine. If we use Quartz on Mac: the scaling is great. However I swore of Quartz a long time ago (for this and that, among other reasons).

So if I'm not using Quartz, and I don't like how Java2D handles scaling: I wanted to explore other approaches to get better results.

Filthy Rich Clients

I really appreciate Laszlo pointing this out to me in the comments: Romain Guy already addressed the subject of smooth scaling in Filthy Rich Clients.

He clearly identified the problem: bilinear interpolation starts to fall apart when the scaling factor is less than 50%. (Which is often the case when you're creating thumbnails.) But at 50% it still looks good.

His solution was brilliantly simple: just scale multiple times. So if you want to scale to 25%: scale your source image at 50% twice. The code he uses (without comments/javadocs) is less than 50 lines. I found a copy here under an LGPL or BSD license.

The downside to this approach, though, is it's very wasteful. Consider creating a thumbnail of an image that is 4,000 x 3,000 pixels. We'd first scale it to 2,000 x 1,500. Then 1,000 x 750. Then 500 x 375. At 4 bytes per pixel: the intermediate two images add up to 14.3 MB of memory. Although these would be quickly discarded for garbage collection: I'd rather not rely on a spare 14 MB of memory to get the job done.

A Different Solution

In this immediate context I'm only interested in scaling BufferedImages. However in the long-term I want a slightly more abstract model. The ImageProducer class comes to mind, but it's too abstract; I want something that iterates up or down an image in rows at a time. Nothing more, nothing less.

So I wrote the PixelIterator, and it's two specific subclasses: BytePixelIterator and IntPixelIterator.

These classes iterate over an image -- either top-to-bottom or bottom-to-top -- reading entire rows at a time. The BufferedImageIterator iterates over the pixels in a BufferedImage -- which is all we need for this example.

The ScalingIterator is what took the longest time to write in this project. This is a subclass of PixelIterator that acts as a filter for incoming data. It reads one row from the source filter at a time, and then it combines color channels for the destination pixels. Then optionally other rows are also read: eventually the sums are averaged into the filtered (scaled) row. So it may read several rows from the source image and collapse them into one row, and it collapses multiple x-values (columns) into a single x-value.

This doesn't strictly follow any particular scaling algorithm, but it gets the job done quickly. (And although the original implementation of this class only supported scaling down, I've since revised it to scale up, too.)

The Scaling class offers a few convenient static methods for scaling that use the ScalingIterator.

Before and After

Visually the results (when using the Scaling class) are great:


Here's a closer look (the Graphics2D-based version is on the left, the Scaling version is on the right):



The grass, the clouds, and the border are a lot less jagged when the Scaling class is used.

Performance Comparison

How does this class perform? I needed to compare my new Scaling methods against other antialiased approaches. Here are the three possible scaling models I came up with:

  • Scaling: using the static methods in the Scaling class.
  • GraphicsUtilities: using Romain Guy's GraphicsUtilities class.
  • Image.getScaledInstance: calling Image.getScaledInstance() on a BufferedImage.

  • The ScalingDemo app compares these three methods by scaling an image of increasing size to 25%. Here are the results on my Mac 10.5 machine running Java 1.6:

    The getScaledInstance() method is clearly the loser in this race. This is unfortunately because it's the only method that's built-in, but developers have been warning against using this method for years: it's practically deprecated.

    Of the other two methods: they're practically the same regarding speed. More importantly: I'm confident that the Scaling approach is more efficient when it comes to memory allocation than the GraphicsUtilities approach: so these facts combined validate this experiment for me.

    The results here may not dazzle you at first, but this undertaking is a stepping stone for a couple of other scaling projects: see this article about JPEGs and PNGs and this article about BMPs for details.

    All the classes mentioned here are available in this jar (source included).

    No comments:

    Post a Comment