8

I would like to be able to detect the 3-4 main colors of a jpg image file.

Example images and sample code below:

image example 1 - red, black, white

image example 2 - white, green, pink

image example 3 - blue, yellow, black

I have modified some code to get the below, but still has trouble grouping colors.

    public static int RoundColorToGroup(int i)
    {
        int r = ((int)Math.Round(i / 10.0)) * 10;
        if (r > 255)
            r = 255;
        return r;
    }

    [TestMethod]
    public void AverageColorTest_WebExample()
    {          
        Bitmap bm = new Bitmap("C:\\Users\\XXXX\\Desktop\\example1.jpg");

        int width           = bm.Width;
        int height          = bm.Height;
        int red             = 0;
        int green           = 0;
        int blue            = 0;
        int minDiversion    = 15; // drop pixels that do not differ by at least minDiversion between color values (white, gray or black)
        int dropped         = 0; // keep track of dropped pixels                

        int bppModifier     = bm.PixelFormat == System.Drawing.Imaging.PixelFormat.Format24bppRgb ? 3 : 4; // cutting corners, will fail on anything else but 32 and 24 bit images
        BitmapData srcData  = bm.LockBits(new System.Drawing.Rectangle(0, 0, bm.Width, bm.Height), ImageLockMode.ReadOnly, bm.PixelFormat);
        int stride          = srcData.Stride;
        IntPtr Scan0        = srcData.Scan0;

        Dictionary<string, Int64> dicColors = new Dictionary<string, long>(); // color, pixelcount i.e ('#FFFFFF',100);

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

            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    int idx = (y * stride) + x * bppModifier;
                    red     = p[idx + 2];
                    green   = p[idx + 1];
                    blue    = p[idx];

                    red     = RoundColorToGroup(red);
                    green   = RoundColorToGroup(green);
                    blue    = RoundColorToGroup(blue);

                    if (Math.Abs(red - green) > minDiversion || Math.Abs(red - blue) > minDiversion || Math.Abs(green - blue) > minDiversion)
                    {
                        string htmlColorGroup = ColorTranslator.ToHtml(Color.FromArgb(red, green, blue));

                        if (dicColors.ContainsKey(htmlColorGroup))
                        {
                            dicColors[htmlColorGroup]++;
                        }
                        else
                        {
                            dicColors.Add(htmlColorGroup, 1);
                        }
                    }
                    else
                    {
                        dropped++;
                    }
                }
            }
        }

        dicColors = dicColors.OrderByDescending(x => x.Value).ToDictionary(pair => pair.Key, pair => pair.Value);

        Console.WriteLine(dicColors.ElementAt(0).Key); // should ouput main color 1
        Console.WriteLine(dicColors.ElementAt(1).Key); // should ouput main color 2
        Console.WriteLine(dicColors.ElementAt(2).Key); // should ouput main color 3

    }
  • output for example1.jpg is (#FF6E8C, #FF6482, #FA6E8C) - 3 shades of red/pink - should be red, black and white
  • output for example2.jpg is (#F0C8C8,#C8DC6E,#E6C8C8) - 2 shades of pinkand green - should be light pink, green, white
  • output for example3.jpg is (#FFDC50,#640A28,#8C1E3C) - 3 shades of blue - should be blue, yellow, black

Ideally needs to ignore background color (#FFFFFF) and black outlining/shading.

Can copy paste html colors online here

Community
  • 1
  • 1
Dere_2929
  • 365
  • 3
  • 9
  • You are not maximising the .NET capabilities by relying on unsafe code (there are quite a few properties allowing you to have full control on each pixel). In any case, what you are after is a validation-/tuning-based development: you should play around with the parameters until reaching the performance you want (e.g., minDiversion variable). You shouldn't expect someone to deliver a perfectly-working solution for you (at least, not here); but do some testing on an acceptably-working code until reaching the performance you want. – varocarbas Nov 06 '13 at 09:58
  • looking at the images its easy to see what the main colors are, but the code outputs just the main colors in 3 shades, where I want the 3 main colors – Dere_2929 Nov 06 '13 at 10:00
  • have added in RoundColorToGroup which rounds r,g,b values to nearest 10 and ammended the minDiversion but it still struggles with red colors and shadings – Dere_2929 Nov 06 '13 at 10:08
  • As said, this is a tuning based development. I personally wouldn't start as you did it: I would start comparing the given colour versus the target (e.g., red or yellow or...), both defined by their RGB values, by bearing certain error into account. That is: Math.Abs(red_pixel - red_targetColor) <= acceptableVariation (same for green and blue); and I would test quite a few cases to find out the ideal value for acceptableVariation (for each target color and RGB value). – varocarbas Nov 06 '13 at 10:24
  • so define a list of colors such as red, light red, dark red, and give them target values is that what you are suggesting? – Dere_2929 Nov 06 '13 at 10:35
  • No. You have a list of target colors which you are checking to see which three ones are the most common ones in the given picture. I don't know how many colors are you considering; I would assume that you consider only: red, black, white, green, pink, blue, yellow. You have to define each of these colors by certain set of RGB and a certain variation from this RGB. For example: assume that red is defined by RGB 20, 20, 20; and, after doing some tests, you see that pixels with RGB from 0, 0, 0 to 40, 40, 40 can still be considered red... – varocarbas Nov 06 '13 at 10:39
  • ... in your loop, you would check whether the RGB of the given pixel fits within this range (0,0,0 to 40,40,40) and, if this is the case, it would be considered to be red. Same analysis for all the other target colours. Even by using your approach (or a different one), this is the basic idea behind this development: you are arbitrarily defining a certain target ("red" as such does not exist, but a range of colours which, more or less, fit within what you recognise as red); if your algorithm does not deliver what you want, you should extend/reduce/change the definition of the range... – varocarbas Nov 06 '13 at 10:42
  • 1
    ... and you can do that only by testing (or by taking the work from a person who has performed this testing work before you; but who might not consider red the same than you do and whose algorithm might not deliver what you want). As said from the start: this is a validation-based development; you shouldn't expect anyone else doing this work for you (unless hiring him) or, at least, not in SO. – varocarbas Nov 06 '13 at 10:44
  • Wouldn't it be easier to use HSL instead of RGB? (http://en.wikipedia.org/wiki/HSL_and_HSV) – C.Evenhuis Nov 06 '13 at 10:51
  • @C.Evenhuis I am not sure about the exact advantage of HSL over RGB in this situation. But I do think that, for what the OP wants, RGB values + the suggested level of error should deliver a pretty good performance. – varocarbas Nov 06 '13 at 10:58
  • there are 16,777,216 web colors, most color pickers have about 48 different colors, thought this would be a common problem with a solution that is all, but thanks for the hints I will take this onboard – Dere_2929 Nov 06 '13 at 10:59
  • I am afraid that you haven't got the idea: you are not referring to name colors but to a combinations of RGB: 255*255*255. There are not all these names. If you want to account for all the potential combinations of RGB you can output the average value; for example: 156,20,100 (name? something half redish, half blueish with no name). The named colours are much less. For example, you can account for the .NET Color structure which have quite a few ones (not sure, but don't think that more than 500). When I say that you have to define your target colors, I didn't mean manually... – varocarbas Nov 06 '13 at 11:17
  • ... you can automate the process. For example, by getting the corresponding RGB values of the aforementioned named Color class and adding/substracting certain value. All this requires testing in any case. Asking here requires a minimal knowledge (which you don't seem to have; other than the one required to copy/paste code written by others) and a certain effort from your side (which, again, you don't seem to have done). Good luck with your search of help. PS: if you don't write @ before the nick of the person you talk to and are not in his Q/A, he might not get your messages :) – varocarbas Nov 06 '13 at 11:20
  • 1
    What's exactly the main goal of this ? (it can help us to gives you a closer solution for what you want, since it's a tuning problem) – Nicolas Voron Nov 07 '13 at 16:04

4 Answers4

1

I've done such any Color Analyzer App where you can see how it works. You are fully on the right track. Scale down the image if needed to small size like 128x128 to keep performance.

Color Analyzer

Then, you need to convert the RGB value into Color Angle using C#.

Now you need to create any sorting of Color Count. I fully recommend to scale down the color angles from 360° = 360 items into about 36 color items only for example. Now you can just pickup the first color in sorted list with most color counts.

Community
  • 1
  • 1
Nasenbaer
  • 4,810
  • 11
  • 53
  • 86
1

Many thanks to your hints, and this article.

Here is solution:

  • GetWebColors() - Populates list of all 'Named' Web Colors (http://www.w3schools.com/html/html_colornames.asp)

  • GetNamedWebColor_NearestMatch(Color) - For any given color (there are 16,777,216!) returns nearest 'Named' web color (1 of 140)

  • GetMainXColors() - Returns first X many colors that differ by min HSL properties passed in (this will stop 3 shades of same color being passed back)

Tuning: ammend below code values for this (i.e for hue, we want 10 degree min change between colors.)

    float minHueDiff        = (float)10;  // 0 to 360
    float minBrightDiff     = (float)0.1; // 0 to 1
    float minSatDiff        = (float)0.1; // 0 to 1

Now Outputs

  • output for example1.jpg is: Gainsboro, Crimson, DarkSlateGray : now close to white, red, black

  • output for example2.jpg is: Gainsboro, DarkKhaki, Pink : now close to white, green, pink

  • output for example3.jpg is: Black, DarkSlateBlue, Gold : correct

Code:

public void AverageColorTest_WebExample_FineTuned()
{                
    Bitmap bm = new Bitmap("C:\\Users\\XXX\\Desktop\\example1.jpg");

    int width = bm.Width;
    int height = bm.Height;
    int red = 0;
    int green = 0;
    int blue = 0;
    float minDiversion = 30 / 100; // drop pixels that do not differ by at least minDiversion between color values (white, gray or black)
    int dropped = 0; // keep track of dropped pixels                

    int bppModifier = bm.PixelFormat == System.Drawing.Imaging.PixelFormat.Format24bppRgb ? 3 : 4; // cutting corners, will fail on anything else but 32 and 24 bit images
    BitmapData srcData = bm.LockBits(new System.Drawing.Rectangle(0, 0, bm.Width, bm.Height), ImageLockMode.ReadOnly, bm.PixelFormat);
    int stride = srcData.Stride;
    IntPtr Scan0 = srcData.Scan0;

    Dictionary<Color, Int64> dicColors = new Dictionary<Color, long>(); // color, pixelcount i.e ('#FFFFFF',100);

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

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                int idx = (y * stride) + x * bppModifier;
                red = p[idx + 2];
                green = p[idx + 1];
                blue = p[idx];

                if (red == 255 && green == 255 && blue == 255)
                continue;

                Color GroupedColor = GetNamedWebColor_NearestMatch(red, green, blue);

                if (dicColors.ContainsKey(GroupedColor))
                {
                    dicColors[GroupedColor]++;
                }
                else
                {
                    dicColors.Add(GroupedColor, 1);
                }
            }
        }
    }

    // sort dictionary of colors so that most used is at top
    dicColors = dicColors.OrderByDescending(x => x.Value).ToDictionary(pair => pair.Key, pair => pair.Value);

    List<Color> MainColors  = null;
    Int16 numberOf          = 3;
    float minHueDiff        = (float)10;
    float minBrightDiff     = (float)0.1;
    float minSatDiff        = (float)0.1;

    MainColors = GetMainXColors(dicColors.Keys.ToList(), numberOf, minHueDiff, minBrightDiff, minSatDiff);

    foreach (Color MainColor in MainColors)
    {
        Console.WriteLine(ColorTranslator.ToHtml(MainColor)); // should ouput main colors
    }

}   

/// <summary>
/// returns first x many colors that differ by min HSL properties passed in
/// </summary>
/// <param name="listIn"></param>
/// <param name="ReturnMaxNumberOfColors"></param>
/// <param name="minHueDiff"></param>
/// <param name="minBrightDiff"></param>
/// <param name="minSatDiff"></param>
/// <returns></returns>
private static List<Color> GetMainXColors(List<Color> listIn, Int32 ReturnMaxNumberOfColors, float minHueDiff, float minBrightDiff, float minSatDiff)
{
    List<Color> response = new List<Color>();

    Int32 i = 0;
    while (response.Count < ReturnMaxNumberOfColors && i < listIn.Count)
    {
        bool  blnUniqueMainColor = true; // want main colors ie dark brown, gold, silver, not 3 shades of brown
        Color nextColor          = listIn[i];

        float brightness    = nextColor.GetBrightness();
        float sat           = nextColor.GetSaturation();
        float hue           = nextColor.GetHue();

        for (Int32 j = 0; j < response.Count; j++)
        {

            float brightnessOther   = response[j].GetBrightness();
            float satOther          = response[j].GetSaturation();
            float hueOther          = response[j].GetHue();

            // hue is 360 degrees of color, to calculate hue difference                        
            // need to subtract 360 when either are out by 180 (i.e red is at 0 and 359, diff should be 1 etc)
            if (hue - hueOther > 180) hue -= 360;
            if (hueOther - hue > 180) hueOther -= 360;

            float brightdiff        = Math.Abs(brightness - brightnessOther);
            float satdiff           = Math.Abs(sat - satOther);
            float huediff           = Math.Abs(hue - hueOther);
            int matchHSL            = 0;

            if (brightdiff <= minBrightDiff)
                matchHSL++;

            if (satdiff <= minSatDiff)
                matchHSL++;

            if (huediff <= minHueDiff) 
                matchHSL++;

            if (matchHSL != 0  & satdiff != 1))
            {
                blnUniqueMainColor = false;
                break;
            }
        }
        if (blnUniqueMainColor)
        {       // color differs by min ammount of HSL so add to response
            response.Add(nextColor);
        }
        i++;
    }
    return response;
}   

private static List<Color> WebColors;
/// <summary>
/// Returns the "nearest" color from a given "color space"
/// </summary>
/// <param name="input_color">The color to be approximated</param>
/// <returns>The nearest color</returns>        
public static Color GetNamedWebColor_NearestMatch(double dbl_input_red, double dbl_input_green, double dbl_input_blue)
{
    // get the colorspace as an ArrayList
    if (WebColors == null) 
        WebColors = GetWebColors();
    // the Euclidean distance to be computed
    // set this to an arbitrary number
    // must be greater than the largest possible distance (appr. 441.7)
    double distance = 500.0;
    // store the interim result
    double temp;
    // RGB-Values of test colors
    double dbl_test_red;
    double dbl_test_green;
    double dbl_test_blue;
    // initialize the result
    Color nearest_color = Color.Empty;
    foreach (Color o in WebColors)
    {
        // compute the Euclidean distance between the two colors
        // note, that the alpha-component is not used in this example                
        dbl_test_red    = Math.Pow(Convert.ToDouble(((Color)o).R) - dbl_input_red, 2.0);
        dbl_test_green  = Math.Pow(Convert.ToDouble(((Color)o).G) - dbl_input_green, 2.0);
        dbl_test_blue   = Math.Pow(Convert.ToDouble(((Color)o).B) - dbl_input_blue, 2.0);
        temp            = Math.Sqrt(dbl_test_blue + dbl_test_green + dbl_test_red);
        // explore the result and store the nearest color
        if (temp < distance)
        {
            distance = temp;
            nearest_color = (Color)o;
        }
    }            
    return nearest_color;
}

/// <summary>
/// Returns an ArrayList filled with "WebColors"
/// </summary>
/// <returns>WebColors</returns>
/// <remarks></remarks>
private static List<Color> GetWebColors()
{
    List<string> listIgnore = new List<string>();
    listIgnore.Add("transparent");

    Type color = (typeof(Color));
    PropertyInfo[] propertyInfos = color.GetProperties(BindingFlags.Public | BindingFlags.Static);
    List<Color> colors = new List<Color>();

    foreach (PropertyInfo pi in propertyInfos)
    {
        if (pi.PropertyType.Equals(typeof(Color)))
        {
            Color c = (Color)pi.GetValue((object)(typeof(Color)), null);
            if (listIgnore.Contains(c.Name.ToLower()))
            continue;
            colors.Add(c);
        }
    }
    return colors;
}
Dere_2929
  • 365
  • 3
  • 9
0

I think you should use some image processing libraries like Imagemagick or Opencv.There are methods for extracting pixel colors in these libraries.You just need to link these libraries to you project. i have used imagemagick for extracting major colors from images, but that was a cpp application.I think .net interfece is also there for imagemagick

Thanks

0

I think If you do not want use some libs, you should use HSL instead of the RGB for for calculations http://en.wikipedia.org/wiki/HSL_and_HSV