Monday, June 14, 2010

Images: BMPs and Thumbnails

This article address the subject: how do I quickly create a BMP thumbnail?

In my last article I presented an architecture that included a memory-efficient model for scaling images. This article will use that architecture to kick some scaling butt.

Surely (?) developers have come before me that tackled this issue, right? This sounds like something ImageIO might have addressed already, but when I googled "ImageIO to create BMP thumbnail" I didn't find any promising leads. (I did find this page which mentions the BMP reader doesn't support thumbnails.)

Reading BMPs


The trickiest part of this subject is: we need to be able to read BMPs. Yes, there is existing code that can do this. Mostly I'm thinking of ImageIO classes, but I'm sure there are other options too. The problem is: these interface with BufferedImages, and my goal here is interface with my new PixelIterator class.

So I dusted off the file specifications and wrote my own BMP decoder.

As of this writing: this decoder is not fully featured. It supports 1, 4, 8, 24 and 32-bit uncompressed BMPs, but nothing else. It is my experience that the vast majority of BMPs in the world are uncompressed, though.

This functionality is nicely wrapped up in a few static classes in the BmpDecoder.

Creating Thumbnails


With a BytePixelIterator to work with, and the previously established ScalingIterator: it's incredibly easy to scale BMP data on-the-fly.
public static BufferedImage createThumbnail(InputStream bmp,
Dimension maxSize) throws IOException {
  PixelIterator i = BmpDecoderIterator.get(bmp);
  int srcW = i.getWidth();
  int srcH = i.getHeight();

  float widthRatio = ((float)maxSize.width)/((float)srcW);
  float heightRatio = ((float)maxSize.height)/((float)srcH);
  float ratio = Math.min(widthRatio, heightRatio);

  if(ratio<1) {
    i = ScalingIterator.get(i, ratio);
  }
  return BufferedImageIterator.create( i, null );
}

Writing BMPs

Also for good measure: I included a BmpEncoder. The motivation for this class was actually for a separate project I may discuss later, but since it's related I'll include it here.

Results

So far you might be underwhelmed by this article: there's nothing especially clever or innovative here.

But consider this example. I have a folder on my computer called "bigpix". (A QA guy at work put this folder together.) I don't know where these files came from -- so I'm probably not legally allowed to distribute any of them -- but there's a file here called "ElPerroDelMar-Portratt.bmp". It's 2,469 pixels by 3,234 pixels.

If I wanted to create a thumbnail of this file that fit inside a 128x128 image, my first reaction is simply to use ImageIO to read the file and then scale that image to the appropriate size. This requires creating an image that is 2469*3234*(3 colors) bytes (22 MB). I just allocated 22 MB to create an eventual 48 KB image.

This is ridiculous. And if the user is trying to navigate a folder of two dozen similar images (and you're creating thumbnails on-the-fly for all of them): you just used over 512 MB of RAM.

The Bmp.jar (source included) compares the cost of creating a thumbnail using the ImageIO approach and the BmpDecoder class. Also (without scaling) it pits the encoding ability of ImageIO against BmpDecoder. Here is the output when I ask the Bmp.jar to work with the ElPerroDelMar image:

OS = Mac OS X (10.5.8), i386
Java Version = 1.5.0_24
apple.awt.graphics.UseQuartz = false

Running Thumbnail Tests...
BmpThumbnail: 72 ms, 332944 bytes RAM
ImageIO: 1065 ms, 23317200 bytes RAM

Running Encode Tests...
BmpEncoder: 585 ms, 332952 bytes RAM, 22987298 bytes file
ImageIO: 649 ms, 14932160 bytes RAM, 22987326 bytes file

As expected: the thumbnail generation is a huge improvement. The BmpDecoder takes less than 1.5% of the memory ImageIO uses, and less than 7% of the time.

I was surprised (but pleased) to see such a substantial advantage in the encoding tests, too. I'm not entirely sure what causes this, but the huge memory allocation suggests the ImageIO classes are extracting all the image data before writing anything (instead of efficiently piping the image data).

No comments:

Post a Comment