Jim
Jim

Reputation: 191

Find closest match to an RGB color value in a color swatch

In a WPF usercontrol I'm writing I have 3 sliders representing RGB with values from 0 - 255. Standard stuff ! Also displayed is a color swatch

Displayed Color Swatch - imgur.com/69kEtdZ

Base class is

    private class SwatchPixels
    {
        public Point P { get; set; }
        public int X { get; set; }
        public int Y { get; set; }

        public int R { get; set; }
        public int G { get; set; }
        public int B { get; set; }
        public int A { get; set; }

    }

private List<SwatchPixels> _points = new List<SwatchPixels>();

Now what I want to do is (what I have sorta works but no accurate enough !)

Load the List with all of the RGB values of the pixels in the color swatch and secondly find the closest pixel color to a supplied set of RGB values. I can then move an ellipse to the x,y location of that pixel. Reason I'm wanting this of course is that a color swatch generally will not contain all 16 million plus colors !

So if somebody can suggest a better method I would be most appreciative !

The swatch sits in a canvas object of the same size.

 <Canvas Name="CanvasImage" Grid.Row="0" Grid.Column="0" Grid.RowSpan="3" Width="150" Height="150"
         HorizontalAlignment="Center" Background="Transparent" VerticalAlignment="Top" Margin="2" 
         MouseMove="CanvasImage_MouseMove" MouseDown="CanvasImage_MouseDown" MouseUp="CanvasImage_MouseUp">

          <Ellipse Name="EllipsePixel" Width="8" Height="8" Stroke="Black" Fill="White" 
                   Canvas.Left="0" Canvas.Top="0"/>
 </Canvas>

Method to load all of the pixel values

private List<SwatchPixels> FindAllPixelLocations()
{
    // http://stackoverflow.com/questions/1176910/finding-specific-pixel-colors-of-a-bitmapimage

    var img = new BitmapImage(new Uri(@"Resources/Cws.png", UriKind.RelativeOrAbsolute));
    Image Ti = new Image();
    Ti.Source = img;

    ImageSource ims = Ti.Source;
    BitmapImage bitmapImage = (BitmapImage)ims;

    try
    {
        int stride = bitmapImage.PixelWidth * 4;
        int size = bitmapImage.PixelHeight * stride;  // stride
        pixels = new byte[size];
        bitmapImage.CopyPixels(pixels, stride, 0);

        for (int y = 0; y < bitmapImage.PixelHeight; y++)
        {
            for (int x = 0; x < bitmapImage.PixelWidth; x++)
            {
                int index = y * stride + 4 * x;
                byte red = pixels[index];
                byte green = pixels[index + 1];
                byte blue = pixels[index + 2];
                byte alpha = pixels[index + 3];

                var swatchPixels = new SwatchPixels
                {
                    X = x,
                    Y = y,
                    P = new Point(x, y),
                    R = red,
                    G = green,
                    B = blue,
                    A = alpha
                };

                _points.Add(swatchPixels);

            }
        }
    }
    catch (Exception e)
    {
        MessageBox.Show(e.Message);
    }

    return _points;
}

The find a point method follows (the bit I'm not happy with !)

private const int initVariance = 40;
private const int dropPercent = 5;

    private SwatchPixels FindXy(string a, string r, string g, string b)
    {
        var iR = 0;
        var iG = 0;
        var iB = 0;

        if (RgbDec.IsChecked == true)
        {
            iR = Convert.ToByte(r);
            iG = Convert.ToByte(g);
            iB = Convert.ToByte(b);
        }
        else
        {
            // Number in hex format so convert to dec first

            iR = Convert.ToByte(int.Parse(r, NumberStyles.HexNumber));
            iG = Convert.ToByte(int.Parse(g, NumberStyles.HexNumber));
            iB = Convert.ToByte(int.Parse(b, NumberStyles.HexNumber));
        }

        List<SwatchPixels> closepoints = new List<SwatchPixels>();
        List<SwatchPixels> prevCp = new List<SwatchPixels>(closepoints);

        var v = Convert.ToInt32(initVariance);

        foreach (var p in _points)
        {
            if ((iR >= p.R - v && iR <= p.R + v) &&
                (iG >= p.G - v && iG <= p.G + v) &&
                (iB >= p.B - v && iB <= p.B + v))
            {
                closepoints.Add(p);
            }
        }

        var c = closepoints.Count;

        while (c > 10)
        {
            v = v - (v * dropPercent / 100);
            closepoints = LoopAgain(closepoints, v, iR, iG, iB);
            c = closepoints.Count;
            //Variance.Text += c.ToString() + Environment.NewLine;
            if (c == 0)
            {
                closepoints = new List<SwatchPixels>(prevCp);
                break;
            }
            else
            {
                prevCp = new List<SwatchPixels>(closepoints);
            }
        }

        if (c > 1)
            return closepoints[0];

        return null;
    }

    private List<SwatchPixels> LoopAgain(IEnumerable<SwatchPixels> cpoints, int v, int iR, int iG, int iB)
    {
        var closepoints = new List<SwatchPixels>();

        foreach (var p in cpoints)
        {
            if ((iR >= p.R - v && iR <= p.R + v) &&
                                    (iG >= p.G - v && iG <= p.G + v) &&
                                    (iB >= p.B - v && iB <= p.B + v))
            {
                closepoints.Add(p);
            }
        }

        return closepoints;
    }

Here is my modified ColorCanvas. Changes include individual color rectangles for RGB, apply variances to a chosen color and use different color models for entry. Also you can copy the Hex contents to the clipboard with 1 click.

New ColorCanvas

Upvotes: 1

Views: 1216

Answers (2)

SnowballTwo
SnowballTwo

Reputation: 539

I am not totally sure, but let's see if i can help you:

Your "Color swatch" looks mostly, but not totally like a control to pick the HUE component of a HSL color selector, except the strange shadow. This also means there is only a subset of RGB in it, which you already know.

Now i see two main disadvantages in your picture:

  • when a certain color is given, it is impossible to determine whether to place the ellipse near the center or near the border, since the colors are the SAME at all radii.

  • the swatch starts at green and ends at blue. a Hue-Selector usualle starts at red and ends at red: The following one is from Inkscape:

hue-bar

Generally, you can easily extract the HSL components from an RGB-Color, for example with the following function:

public static void FromRGB(byte ARed, byte AGreen, byte ABlue, out double AHue, out double ASaturation, out double AValue)
    {
        double h = 0;
        double s = 0;

        double r = (double)ARed / 255;
        double g = (double)AGreen / 255;
        double b = (double)ABlue / 255;

        double max = Math.Max(r, Math.Max(g, b));
        double min = Math.Min(r, Math.Min(g, b));

        if (r == g && g == b) h = 0;

        if (r == max) h = 60.0 * ((g - b) / (max - min));
        else if (g == max) h = 60.0 * (2.0 + (b - r) / (max - min));
        else if (b == max) h = 60.0 * (4.0 + (r - g) / (max - min));

        if (max != 0) s = (max - min) / max;

        AHue = h;
        ASaturation = s;
        AValue = max;

    }

Let's take a look at another color selector, which looks almost like yours:

hsl

This selector has some advantages:

  • It stores the hue AND the saturation, which allows you to place the ellipse exactly at one certain position
  • It is round, which makes calculations a bit easier

To answer your actual question:

You will find the best matching pixel in your selector when you convert your RGB to HSV, set the saturation and value components to maximum and convert it back to RGB.

Hope it helps you a bit...

EDIT

As requested i'll provide an example about how to make an own rgb picker image:

if (w <= 0 || h <= 0) return;

        WriteableBitmap wb = new WriteableBitmap(w, h, 96, 96, PixelFormats.Rgb24, null);

        byte[] pixels = new byte[wb.PixelWidth * wb.PixelHeight * 3];

        Color base_color = GetBaseColorFromHue(Hue * 1.5);

        for (int y = 0; y < h; y++)
        {
            double pos_v = (double)y / (double)h;

            for (int x = 0; x < w; x++)
            {
                double pos_s = (double)x / (double)w;

                pixels[(y * wb.PixelWidth + x) * 3] = (byte)((base_color.R * (pos_s) + 255 * (1 - pos_s)) * (1.0 - pos_v));
                pixels[(y * wb.PixelWidth + x) * 3 + 1] = (byte)((base_color.G * (pos_s) + 255 * (1 - pos_s)) * (1.0 - pos_v));
                pixels[(y * wb.PixelWidth + x) * 3 + 2] = (byte)((base_color.B * (pos_s) + 255 * (1 - pos_s)) * (1.0 - pos_v));
            }
        }

        wb.WritePixels(new Int32Rect(0, 0, wb.PixelWidth, wb.PixelHeight), pixels, wb.PixelWidth * wb.Format.BitsPerPixel / 8, 0);

Upvotes: 0

Pablo Caballero
Pablo Caballero

Reputation: 338

Don't reinvent the wheel, try with Extended WPF Toolkit™ Community Edition, you have two controls ColorPicker and ColorCanvas.

Upvotes: 1

Related Questions