Selasa, 14 April 2009

Image Processing in Android

While working on WallSwitch, I ran into a number of hairy problems with image processing. The worst, by far, was this:


04-15 02:20:34.693: ERROR/dalvikvm-heap(165): 50331648-byte external allocation too large for this process.
04-15 02:20:34.693: ERROR/(165): VM won't let us allocate 50331648 bytes
04-15 02:20:38.233: ERROR/AndroidRuntime(165): java.lang.OutOfMemoryError: bitmap size exceeds VM budget


I got it when trying to do a simple BitmapFactory.decodeFile() on a jpeg I had on the sd card. The jpeg was huge (~4MB) but that should exceed the 16MB heap I get, right? I found some decent help here and here, but nothing that really explained what was going on. I knew I was hitting the limit, but had no idea why.





Then, I remembered something a friend had told me a few years ago about how jpegs are compressed (duh) but that it had to first decompress to a bitmap before painting to your screen. It makes sense, after all. If I've got 1024x768 pixels on my monitor and I want to paint a picture over all of it, I'm going to need 768,432 bytes (depending on bit-depth).




I whipped out my calculator and had a facepalm moment. Needless to say 50,331,648 is 4096x6144 * 2, which corresponds to the size of my jpeg, when fully decompressed.




Fixing this was, happily, very easy. You just need to set inSampleSize on your BitmapFactory.Options to something useful. It'll sample the image you get back, returning something 1/2, 1/4, ... the size. The key here is to try to keep the sample size a power of two. It isn't necessary, but it makes the processing faster and it'll make sure the image you get back keeps the same proportions.





// First, get the dimensions of the image
Options options = new Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath, options);

// Only scale if we need to
// (16384 buffer for img processing)
Boolean scaleByHeight = Math.abs(options.outHeight - targetHeight) >= Math.abs(options.outWidth - targetWidth);
if(options.outHeight * options.outWidth * 2 >= 16384){
// Load, scaling to smallest power of 2 that'll get it <= desired dimensions
double sampleSize = scaleByHeight
? options.outHeight / targetHeight
: options.outWidth / targetWidth;
options.inSampleSize =
(int)Math.pow(2d, Math.floor(
Math.log(sampleSize)/Math.log(2d)));
}

// Do the actual decoding
options.inJustDecodeBounds = false;
options.inTempStorage = new byte[IMG_BUFFER_LEN];
Bitmap output = BitmapFactory.decodeFile(filePath, options);


It's still a pretty slow operation to perform, but this at least makes it possible!

Tidak ada komentar:

Posting Komentar