Get the percentage usage of every colour in an image

10,747

Solution 1

GetPixel is not really a fast way to access image data. Use the LockBits method.

EDIT:

Well you're doing a lot of things with strings. Building the pixelData Dictionary that way is pretty useless, why don't you process the distinct colors right away? Color is an immutable struct, so that's a good key for our dictionary already.

Dictionary<Color, int> frequency = new Dictionary<Color, int>();
for (int i = 0; i < image.Height; i++) {
  for (int j = 0; j < image.Width; j++) {
    pixel = image.GetPixel(i, j);
    if (frequency.ContainsKey(pixel)) frequency[pixel]++;
    else frequency.Add(pixel, 1);
  }
}

// and finally
int totalPixels = image.Width * image.Height;
foreach (var kvp in frequency) {
  Console.WriteLine("Color (R={0},G={1},B={2}): {3}", kvp.Key.R, kvp.Key.G, kvp.Key.B, kvp.Value / (double)totalPixels);
}

And that should do it, except when you want to make it even faster and use LockBits instead of GetPixel.

Some other observations:

int hmm = (image.Height * image.Width);
double offset = 100 / double.Parse(hmm.ToString());

You're using a very strange and slow way of casting from int to double. You can just write double offset = 100 / (double)hmm; and it's the same (you could also write 100.0 and not 100 and the compiler will create a double for you so you don't need to cast hmm).

This made me laugh:

IEnumerable<string> query = pixelData.Values.Where(fruit => fruit == value);

Why fruit!? Seems like you copied this from somewhere.

Solution 2

It seems like this is part of a larger image processing goal. The Aforge framework is the go-to choice for image analysis and processing in .NET, and it is extremely fast. It's likely that it already has the code you need.

You mentioned a tolerance system - to me, this sounds like you need quantization - color rounding.

Once you have a quantized bitmap, you can make and array with a length that matches the palette size, LockBits the bitmap, and use the color index for each pixel as the array index for each pixel to accumulate usage statistics.

Could you share more information about your goals for this code? What it is supposed to do exactly?

Share:
10,747
Neo
Author by

Neo

Updated on June 07, 2022

Comments

  • Neo
    Neo almost 2 years

    I have this one working but it is so damn slow on jpeg images and also needs some changing.

    I need to know the individual colours in an image (with a tolerance of +/- 1 for RGB) and the % of the image that is that colour.

    so if an image was black and white it would say something like White : 74% Black : 26%

    The code below works like I said but I need to add a tolerance system as well and I have no idea on how I would do that.

    private Dictionary<string, string> getPixelData(Bitmap image)
    {
        Dictionary<string, string> pixelData = new Dictionary<string, string>();
        //int col, row;
        //int r, g, b;
        Color pixel;
    
        double offset = 0.000001;
        int hmm = (image.Height * image.Width);
        double current = 0;
        offset = 100 / double.Parse(hmm.ToString());// 0.01;// 100 / (image.Height * image.Width) * 10000;
    
        try
        {
            for (int i = 0; i < image.Height; i++)
            {
                for (int j = 0; j < image.Width; j++)
                {
                    current = current + offset;
                    pixel = image.GetPixel(i, j);                        
                    pixelData.Add(i + "," + j, (pixel.R.ToString() + " " + pixel.G.ToString() + " " + pixel.B.ToString()));
                    pBarprocess.Value = int.Parse(Math.Floor(current).ToString());
                    pBarprocess.Update();
                    Application.DoEvents();
                }
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show("Unable to parse image " + ex);
        }
    
        return pixelData;
    }
    

    And the other function

    private void btnProcess_Click(object sender, EventArgs e)
    {
        pBarprocess.Value = 0;
        pBarprocess.Enabled = false;
        Bitmap foo = Bitmap.FromFile(@txtFileName.Text) as Bitmap;
        Dictionary<string, string> pixelData = new Dictionary<string, string>();
    
        lblProcess.Text = "Processing pixel map";
        pixelData = getPixelData(foo);
    
        lblProcess.Text = "Calculating Density";
        lblProcess.Update();
    
        var distinctList = pixelData.Values.Distinct().ToList();
    
        Console.WriteLine("DL = " + distinctList.Count);
        double offset = 100 / double.Parse(distinctList.Count.ToString());
        double current = 0;
    
        foreach (var value in distinctList)
        {
            IEnumerable<string> query = pixelData.Values.Where(fruit => fruit == value);
            double perc = (double.Parse(query.Count().ToString()) / double.Parse(pixelData.Count.ToString())) * 100;
            Console.WriteLine(value + " = " + query.Count() + "(" + perc + "%)");
            txtAnalysis.Text = "Colour " + value + " : " + query.Count() + " (" + perc.ToString() + "%)\r\n" + txtAnalysis.Text;
            txtAnalysis.Update();
            pBarprocess.Value = int.Parse(Math.Floor(current).ToString());
            pBarprocess.Update();
            Application.DoEvents();
        }
    
        lblProcess.Text = "Finished.";
        pBarprocess.Value = 0;
        pBarprocess.Enabled = false;
    }
    
  • Neo
    Neo over 12 years
    Getting the pixels isn't the issue that is done in a fraction of a second its the data processing that is taking the time
  • Neo
    Neo over 12 years
    lol yep that I did never used LINQ before hand so just messed with what I had to get it working :) will try your implementation now and will mark if it works :)
  • Neo
    Neo over 12 years
    Yeah basically its required to check for colour density within an image, but instead of coming back with 12 shades of black it should come back with one, so for instance RGB 0, 0, 0 to 5, 5, 5 would come back as a single colour and 200, 200, 200 to 255, 255, 255 would come back as white
  • Lilith River
    Lilith River over 12 years
    Luminosity is different per color - for example, a better range for black would be 0,0,0 to 13,5,50, if you're looking for perceived luminosity ranges. Color density is usually represented by a histogram - perhaps you could provide more info on what users will be using this for?
  • Andreas
    Andreas over 12 years
    Basically you have to decide for each color with which other color you'd like to merge it or if you want to merge both with a new intermediate color... there's no straight-forward solution. The easiest way would be to follow ComputerLinguist's suggestion to apply dithering/quantization to the images. You can use .NET or an image processing library to reduce the image to e.g. 256 colors or if it's a grayscale to 16 shades of gray. This is an article from already mentioned AForge.NET.
  • Neo
    Neo over 12 years
    to detect chip density in a material, the chips can be any colour but need to knoe the overall % coverage of the chips compared to the material.
  • Lilith River
    Lilith River over 12 years
    Definitely quantize/posterize, then. You can choose how many colors you want to round to, that way. And you should be able to get very good performance out of the algorithm mentioned in my answer.
  • Clément
    Clément over 11 years
    +1, like your "some other observations" section. Answers going further than the original questions are the best to learn from.
  • alex
    alex almost 10 years
    please, put a screenshot here. linking to some unknown .rar files is not the best way of answering...
  • Neo
    Neo almost 10 years
    Nice post but you failed to read the original question, this relies on 3rd party software and the procedure was required for C#