[HOW TO] Get Average Color of a Drawable or Bitmap

Hi all,

I’ve recently been working on a pretty major update to my Fake iPhone app. In the process, I came across the need to calculate a suitable background color for icons on the home screen.

It had to be a color that would blend well with the icon, while still being clearly differentiated from the icon border itself.

Apparently Google Chrome already does this, by using a complex algorithm to calculate the most dominant color in an image. And I’ve noticed Windows 7 and Ubuntu’s Unity interface do a similar thing.

From my research, this process is generally known as image quantization. There are several free libraries available to automate the process for entire images, but I couldn’t find anything ready-made for Android. So I wrote my own function :slight_smile:

For purposes of my app I decided these algorithms would be too computationally intensive (and too difficult for me to recreate in my spare time!). So instead I chose to use the average color of the icon, based on a sample of a limited set of pixels. This way I could avoid iterating through every pixel in the image, but still come up with a reasonable result.

The source code for this function getAverageColor() is listed below. It takes a BitmapDrawable as input, loops through a linear sampling of pixels, and comes up with an average value by finding the individual average of each HSV component. These are then combined together to return a single Color, encoded as an integer.

[java]
/**

  • Gets the average color in a drawable

  • This is calculated by sampling a subset of pixels

  • All calculations are done in HSV color space

  • Written by David Webb (http://makingmoneywithandroid.com/)

  • @param image The drawable to sample

  • @return An integer representing the average Color of the sampled pixels
    */
    private int getAverageColor(Drawable image) {
    //Setup initial variables
    int hSamples = 20; //Number of pixels to sample on horizontal axis
    int vSamples = 20; //Number of pixels to sample on vertical axis
    int sampleSize = hSamples * vSamples; //Total number of pixels to sample
    float[] sampleTotals = {0, 0, 0}; //Holds temporary sum of HSV values

    //If white pixels are included in sample, the average color will
    // often have an unexpected shade. For this reason, we set a
    // minimum saturation for pixels to be included in the sample set.
    // Saturation < 0.1 is very close to white (see http://mkweb.bcgsc.ca/color_summarizer/?faq)
    float minimumSaturation = 0.1f; //Saturation range is 0…1

    //By the same token, we should ignore transparent pixels
    // (pixels with low alpha value)
    int minimumAlpha = 200; //Alpha range is 0…255

    //Get bitmap
    Bitmap b = ((BitmapDrawable)image).getBitmap();
    int width = b.getWidth();
    int height = b.getHeight();

    //Loop through pixels horizontally
    float[] hsv = new float[3];
    int sample;
    for(int i=0; i<width; i+=(width/hSamples)) {
    //Loop through pixels vertically
    for(int j=0; j<height; j+=(height/vSamples)) {
    //Get pixel & convert to HSV format
    sample = b.getPixel(i, j);
    Color.colorToHSV(sample, hsv);

     	//Check pixel matches criteria to be included in sample
     	if((Color.alpha(sample)&gt;minimumAlpha) && (hsv[1] &gt;= minimumSaturation)) {
     		//Add sample values to total
     		sampleTotals[0] += hsv[0];	//H
     		sampleTotals[1] += hsv[1];	//S
     		sampleTotals[2] += hsv[2];	//V
     	}
    

// else {
// Log.d(TAG, “Pixel rejected: Alpha “+Color.alpha(sample)+”, H: “+hsv[0]+”, S:”+hsv[1]+", V:"+hsv[1]);
// }
}
}

//Divide total by number of samples to get average HSV values
float[] average = new float[3];
average[0] = sampleTotals[0] / sampleSize;
average[1] = sampleTotals[1] / sampleSize;
average[2] = sampleTotals[2] / sampleSize;

//Return average tuplet as RGB color
return Color.HSVToColor(average);

}
[/java]

The color that comes out of this function will probably look pretty ugly for most images. If you want to make it a bit prettier, just put this line immediately before the final return statement:

[java]
//Pretty some things up :slight_smile:
average[2] = 0.8f;
[/java]

This manually sets the “Value” component of the HSV tuplet to the value you specify (between 0.0 and 1.0). I’ve found 0.8 works quite well as a background color for icons.

I hope this code can be helpful for anyone else who’s trying to accomplish a similar thing. I don’t claim to be an expert on the topic - I basically know the difference between RGB and HSV, but that’s about it! :wink: Coming up with this algorithm and then implementing it was an interesting learning experience for me.

This function sacrifices a lot of accuracy, for the sake of being simple and efficient. I’m definitely open to ideas for improvement, if any gurus out there have suggestions feel free to post them!

And if you do use this algorithm or code in your app, please let me know. It would be great to know where it’s been implemented! :slight_smile:

Further reading:
http://mkweb.bcgsc.ca/color_summarizer/?faq#descriptions
http://stackoverflow.com/q/8471236/663469
http://stackoverflow.com/q/622573/663469
http://forums.codeguru.com/archive/index.php/t-306787.html

You can improve your algo by adding more priority to the outside pixels. An easy way to do would be simply to spare the inside pixels from the average calculation.

That would speed up calculation time too and with most of the icons you can get their “natural color” by using their background which is nearly always visible in the outside areas of the icon.

But you need to try that how it looks :slight_smile:

Im on my phone at the moment and cannot read the code section.
But basicly if you are using an averange result it can give strange results.

Let me illustrate with an example.
Lets say we have 2 blue pixels and 1 red pixel. The correct result should be blue, but an averange would come up with a purple color. This is why you should use a mean to calculate the end result. I cannot read the source code in the article you linked at the moment, but the guy in the comment had a good explanation. It should not be to hard to code.