20

In my C# (3.5) application I need to get the average color values for the red, green and blue channels of a bitmap. Preferably without using an external library. Can this be done? If so, how? Thanks in advance.

Trying to make things a little more precise: Each pixel in the bitmap has a certain RGB color value. I'd like to get the average RGB values for all pixels in the image.

Mats
  • 14,902
  • 33
  • 78
  • 110
  • 2
    Well, the naive method would be to go pixel by pixel and get the RGB values, which is I'm sure not what you're asking for. Can you elaborate what kind of average you're looking for? – lc. Jul 01 '09 at 10:30
  • You're right. Hope it's better now. – Mats Jul 01 '09 at 10:41
  • Going pixel by pixel can be done differently — see answers. I wonder whether GPU could help. – bohdan_trotsenko Jul 01 '09 at 11:49

3 Answers3

27

The fastest way is by using unsafe code:

BitmapData srcData = bm.LockBits(
            new Rectangle(0, 0, bm.Width, bm.Height), 
            ImageLockMode.ReadOnly, 
            PixelFormat.Format32bppArgb);

int stride = srcData.Stride;

IntPtr Scan0 = srcData.Scan0;

long[] totals = new long[] {0,0,0};

int width = bm.Width;
int height = bm.Height;

unsafe
{
  byte* p = (byte*) (void*) Scan0;

  for (int y = 0; y < height; y++)
  {
    for (int x = 0; x < width; x++)
    {
      for (int color = 0; color < 3; color++)
      {
        int idx = (y*stride) + x*4 + color;

        totals[color] += p[idx];
      }
    }
  }
}

int avgB = totals[0] / (width*height);
int avgG = totals[1] / (width*height);
int avgR = totals[2] / (width*height);

Beware: I didn't test this code... (I may have cut some corners)

This code also asssumes a 32 bit image. For 24-bit images. Change the x*4 to x*3

Philippe Leybaert
  • 168,566
  • 31
  • 210
  • 223
  • I *stole* the code above, fixed it wrapped it in a function to answer another SO question at http://stackoverflow.com/questions/6177499/how-to-determine-the-background-color-of-document-when-there-are-3-options-using/6185448#6185448 – Till May 31 '11 at 11:37
  • 3
    Can you explain why it's so much faster? – C. Ross Aug 03 '11 at 23:36
  • @C.Ross I believe it's because we convert all the image data into bytes and access the byte array instead of invoking and function to determine the rbg at at given point. Cost of this height*width + imagedata->bytes. Cost of other height*width*imagedata->rbg. – Patrick Lorio Nov 10 '11 at 00:42
  • The last three lines of the above code are incorrect. The color bytes in bitmap data area ordered BGR, so the red channel is really totals[2] and blue channel is totals[0]. – David Hay Apr 30 '13 at 06:46
  • Wow. The difference of doing it this way vs using `GetPixel` is astounding. Looping over a 500x600 image twice using `GetPixel` took ~1000 ms, using this method took ~13ms. – mpen May 21 '13 at 04:20
  • Question: why do we need to cast it to a void pointer before casting it to a byte pointer? Can't we cast directly to `(byte*)`? – mpen May 21 '13 at 05:10
  • there is a subtle bug in this code. Red and Blue are switched. the order of bytes in Scan0 is BGRA not RGBA. – Remco Ros Jan 23 '14 at 21:15
  • @RemcoRos yes, that's exactly what David Hay said in one of the earlier comments. – Philippe Leybaert Jan 23 '14 at 21:17
  • Me and my csc do not seem to understand where `dstData` is coming from. Is it a misspelling of `srcData` or some magic happening? – prokilomer May 27 '14 at 17:56
  • @prokilomer It's a typo. It should've been srcData. I will correct it. – Philippe Leybaert May 27 '14 at 22:48
  • How would I do this in UWP? – Arlo Mar 09 '19 at 20:35
14

This kind of thing will work but it may not be fast enough to be that useful.

public static Color GetDominantColor(Bitmap bmp)
{

       //Used for tally
       int r = 0;
       int g = 0;
       int b = 0;

     int total = 0;

     for (int x = 0; x < bmp.Width; x++)
     {
          for (int y = 0; y < bmp.Height; y++)
          {
               Color clr = bmp.GetPixel(x, y);

               r += clr.R;
               g += clr.G;
               b += clr.B;

               total++;
          }
     }

     //Calculate average
     r /= total;
     g /= total;
     b /= total;

     return Color.FromArgb(r, g, b);
}
live2
  • 3,771
  • 2
  • 37
  • 46
Loofer
  • 6,841
  • 9
  • 61
  • 102
14

Here's a much simpler way:

Bitmap bmp = new Bitmap(1, 1);
Bitmap orig = (Bitmap)Bitmap.FromFile("path");
using (Graphics g = Graphics.FromImage(bmp))
{
    // updated: the Interpolation mode needs to be set to 
    // HighQualityBilinear or HighQualityBicubic or this method
    // doesn't work at all.  With either setting, the results are
    // slightly different from the averaging method.
    g.InterpolationMode = InterpolationMode.HighQualityBicubic;
    g.DrawImage(orig, new Rectangle(0, 0, 1, 1));
}
Color pixel = bmp.GetPixel(0, 0);
// pixel will contain average values for entire orig Bitmap
byte avgR = pixel.R; // etc.

Basically, you use DrawImage to copy the original Bitmap into a 1-pixel Bitmap. The RGB values of that 1 pixel will then represent the averages for the entire original. GetPixel is relatively slow, but only when you use it on a large Bitmap, pixel-by-pixel. Calling it once here is no biggie.

Using LockBits is indeed fast, but some Windows users have security policies that prevent the execution of "unsafe" code. I mention this because this fact just bit me on the behind recently.

Update: with InterpolationMode set to HighQualityBicubic, this method takes about twice as long as averaging with LockBits; with HighQualityBilinear, it takes only slightly longer than LockBits. So unless your users have a security policy that prohibits unsafe code, definitely don't use my method.

Update 2: with the passage of time, I now realize why this approach doesn't work at all. Even the highest-quality interpolation algorithms incorporate only a few neighboring pixels, so there's a limit to how much an image can be squashed down without losing information. And squashing a image down to one pixel is well beyond this limit, no matter what algorithm you use.

The only way to do this would be to shrink the image in steps (maybe shrinking it by half each time) until you get it down to the size of one pixel. I can't express in mere words what an utter waste of time writing something like this would be, so I'm glad I stopped myself when I thought of it. :)

Please, nobody vote for this answer any more - it might be my stupidest idea ever.

MusiGenesis
  • 74,184
  • 40
  • 190
  • 334