Tuesday, April 14, 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!

9 comments:

  1. Do you think this is better than using Bitmap.createScaledBitmap() ?

    ReplyDelete
  2. Yes- here's why. Bitmap.createScaledBitmap() requires that you have a source bitmap loaded already. On my droid, a full image takes nearly all the 16M I have available. The technique shown here does the sampling while the image is being loaded, so you don't have the massive memory allocation.

    ReplyDelete
  3. Hi!
    I've been trying do use your implementation but it doesn't seem to work with:
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    InputStream is = connection.getInputStream();
    BitmapFactory.decodeStream(is, null, options);

    Why do you define options.inTempStorage?
    What is a reasonable IMG_BUFFER_LEN size?

    ReplyDelete
  4. Hi guys,

    I have a similar problem to this with which I am struggling right now.

    In my app, I am creating a bitmap from its colors code like this :

    int width=getImageWidth();
    int height=getImageHeight();
    int[] array=getbitmap();
    int[] colorsAsIntegers = new int[width*height];
    int i = 0;
    int j = 0;

    while (i<width*height*4) {
    colorsAsIntegers[j] = Color.argb(array[i], array[i+1], array[i+2], array[i+3]);
    i += 4;
    j++;
    }

    myBitmap=Bitmap.createBitmap(colorsAsIntegers,
    width,
    height,
    Bitmap.Config.ARGB_8888);

    And often I get the outofmemoryerror :(
    So how can I use the BitmapFactory optimisation to avoid this problem? because I don't have an input stream or a file, I only have an array containing my pixels

    Thank you

    ReplyDelete
  5. Good solution!
    I was using createScaledBitmap, but that was taking considerable time and memory too.
    instead your solution works best, the output bitmap is subsampled and consumes less memory.
    Thanks.
    My only concern is, although this solution is fine for low resolution devices where high level details of bitmaps are not required as such, how would the subsampled bitmaps look on high resolution devices like 10.1" tablets?

    ReplyDelete
  6. Why the factor in ons.outHeight * options.outWidth * 2 expression is 2 but not 3?

    ReplyDelete
  7. Hi, could you tell me what is "targetHeight" and "targetWidth"??

    Thanks in advance

    ReplyDelete