Bitmap quality, banding and dithering

Android 2.3 is finally out! I recently mentioned changes to default bitmaps and windows formats we made in Gingerbread and I would like to explain why these changes were made. I will also show you the difference between Android’s various bitmap formats and why you must use them carefully.

Early Android devices had limited memory and computing power and to optimize the system we chose to draw opaque applications, by default, on 16 bits surfaces. The encoding format we use for 16 bits opaque pixels is called RGB_565: red and blue components have 5 bits of precision, and green has 6 bits of precision. This work relatively well but can lead to severe visual artifacts when drawing with bitmaps encoded in other formats. The two other formats supported by Android are ARGB_8888 and ARGB_4444. Both formats allow the encoding of an alpha channel for translucency and only differ in the precision of the components. ARGB_8888 requires twice the amount of memory, as each pixel is encoded with 32 bits, but provides the best quality. It is also the format we use automatically when loading bitmaps with an alpha channel (PNG resources for instance.) You should ignore ARGB_4444, unless you really know what you are doing, because it looks awful.

What does it all mean for your application? To make it easier to understand, I wrote a simple application you can run on your device, or on the emulator, to compare the quality and performance of various source and destination formats. The application contains two activities, one running in 16 bits, and one running in 32 bits. When launched, each activity lets you switch between bitmaps loaded in the 3 formats I described earlier. You can also enable dithering and profiling.

Let’s start by comparing the three bitmap formats as they appear when drawn on a 16 bits window (click on the image for a full-size version that will make the differences more obvious):

Comparison for Android's three bitmap formats on a 16 bits window

To better compare the three formats, take a close look at the gray gradient on the left side of each screenshot:

Comparison of Android's three bitmap formats when drawn on a 16 bits window

You can see three types of artifacts in this image: banding, low precision and dithering. The three bitmaps are loaded from the same resource and converted at load time using BitmapFactory.Options.inPreferredConfig. The first bitmap, ARGB_8888, shows weird bands of colors, particularly visible in the gray gradient. You can also see that the bands have a slight green shift. This artifact appears when the 8 bits of precision are clamped to 5 or 6 bits of precision. ARGB_4444 exhibits a similar behavior because of the upscale from 4 bits of precision to 5 or 6 bits. You can also notice a dithering pattern. Finally, the RGB_565 shows a clear dithering pattern.

These atrocities are unfortunately the default behavior on Android before 2.3. Fortunately, In most situations the user will not notice these artifacts. These artifact show mostly on subtle gradients or very specific types of images. There are also ways to hide these artifacts. For instance, opaque images that will be loaded in RGB_565 can be pre-dithered in a tool like Adobe Photoshop, with more powerful and higher quality filters than the one used by Android. I would however not recommend this approach because it forces you to bake noise in your image, making it harder to reuse in the future. Another solution is apply dithering on 32 bits images. The following screenshot shows an ARGB_8888 bitmap drawn on a 16 bits window with dithering enabled:

Dithering enabled to draw a 32 bits bitmap on a 16 bits window

The terrible banding artifact we saw earlier is now gone! The only difference is enabling the dither flag on the paint (it also works on drawables.) Of course, because of its very nature, dithering is not perfect. A much better solution is simply to render 32 bits bitmaps on 32 bits surfaces, which Android 2.3 now does by default. The following screenshot shows the result of this new behavior:

Full color precision is achieved with 32 bits source and target

The following image shows the difference between the previous approach, a dithered 32 bits bitmap drawn onto a 16 bits surface, and the new approach, a 32 bits bitmap drawn onto a 32 bits surface:

A comparison between dithered 32 bits bitmap on 16 bits surface and 32 bits bitmap on 32 bits surface

This image clearly shows that dithering introduces a very special type of noise used to trick your eye into seeing a smooth gradient. However, the 32 bits surface shows no artifact whatsoever and clearly provides the best quality. This is why we changed the behavior of Android 2.3 to loads bitmaps and windows in 32 bits by default.

You should also remember that choosing the appropriate bitmap format can lead to better runtime performance. For instance, drawing a 32 bits bitmap onto a 16 bits surface requires a specific conversion that costs CPU time. The following table compares the time taken to draw bitmaps in different formats, with or without dithering, on both 16 and 32 bits windows:

Performance comparison when drawing various bitmap formats onto 16 and 32 bits surfaces

This simple performance test shows very clearly that using a compatible format (32 bits bitmap on a 32 bits window or 16 bits/565 bitmap on a 16 bits window) is the most efficient way to draw bitmaps. For this reason, you should always check the format of your bitmaps and windows and try to make them compatible with each other. You can choose a bitmap’s format when creating an empty bitmap or when decoding a resource or stream. You can of course check the format of an existing bitmap using Bitmap.getConfig(). You can choose and query the format of the window by using getWindow().getAttributes().format from your onCreate() method. You can also refer to this article’s example source code to see how to choose a particular format.

13 Responses to “Bitmap quality, banding and dithering”

  1. vovkab says:

    Thank you, very useful.

  2. superdry says:

    Thank you! I understood very easily.

  3. Thanks, very useful information!

  4. dakin80 says:

    Running the tests on my Nexus One running Android 2.2.1 gives:

    Bits Dithered Type Nexus One
    16 No ARGB_8888 15.8
    16 Yes ARGB_8888 16.7
    16 No ARGB_4444 16.5
    16 Yes ARGB_4444 17.4
    16 No RGB_565 11.2
    16 Yes RGB_565 11.2
    32 No ARGB_8888 20.7
    32 Yes ARGB_8888 20.9
    32 No ARGB_4444 20.7
    32 Yes ARGB_4444 20.7
    32 No RGB_565 19.7
    32 Yes RGB_565 19.7

    I’m assuming the figures in the article relate to tests done on a Google Nexus S as this is the only hardware running Android 2.3 that I’m aware of. I would hope that this would give performance improvements over my Nexus One running Android 2.2.1.

    Are the impressive speeds of 32 bit windows only realised in Android 2.3? For Android 2.2, using 32 bit windows as opposed to 16 bits seems to incur significant performance penalties.

    Is the article’s 16 bit ARGS_8888 times correct? I would expect dithering to increase times as my figures largely show. It’s very surprising that the non dithered version takes almost twice as long in the article’s times and the non dithered time is worse than my equivalent Android 2.2.1 figure.

    Something seems fishy here.

  5. David says:

    I set up a custom view to try out the speed of the different bitmaps and I am getting results that are completely off. I’m using a droid and since I’m on froyo (default 16bit window), I used rgb_565 and drawing the gradient bitmap only took 2-3ms. Upon inspecting your code, I see your custom view is based on LinearLayout and as far as I can tell, code looked similar. Is there a difference between just extending View vs LinearLayout? Thanks.

  6. Kleptine says:

    I’m not sure if you’ll read this, but since you’re the main rendering expert that I know of, I’ll try it anyway.

    I’m having some difficulties rendering bitmaps with any alpha level other than 255. When drawn (even on a 32bit surface, with a 32 bit image), the color is not perfectly recreated when drawing with transparency. While it’s likely impossible to notice, my application draws hundreds of small translucent bitmaps over each other (it’s a brush tool), which really makes the discoloration obvious. When the alpha is extremely low (11 or so) the color is completely lost altogether creating simply grey bitmaps.

    Here’s a comparison of bitmaps drawn at 255 vs 5:
    http://i54.tinypic.com/67jp8o.png

    I can only assume this is due to some optimization when rendering? It’s problematic that I can’t draw bitmaps at low alpha levels without losing color.

    Is there any way to fix this? I’m fine with any workarounds if need be. If there’s no way, I’d be curious if my reasoning was correct or if there’s some other problem.

    Thanks.

  7. Kleptine says:

    Hmmm… actually it seems to be a problem with rendering to a ARGB_8888 bitmap, which is then rendered itself to the surface.

    The problem still remains, though I don’t see why rendering to a ARGB_8888 bitmap would lose color any more than a 32bit surface.

  8. jnz says:

    @Kleptine: perhaps a loss of precision due to the use of premultiplied alpha?

  9. What type of dithering does Android implement?

  10. Kleptine says:

    @jnz That seems like a good answer. Can you think of any ways to deal with the premultiplied alpha? Setting pixels individually would be far too slow, but I can’t seem to figure a way to combine filters to get the job done.

  11. Mark Gjøl says:

    After upping the default bitmap quality, does the memory allocated for applications follow? I am having some problems staying within the allotted limits, and if my bitmaps suddenly get a twice as big (or more) memory footprint I will be in serious trouble!

  12. TechyInfo says:

    When drawing a bitmap on the canvas I use a paint object defined as follows:
    paint.setAntiAlias(true);
    paint.setFilterBitmap(true);
    paint.setDither(true);

    Yet I still have very pixelated images in Android 2.2. Is there some other parameter that I should add? Thanks.