Search code examples
c#.netcolorsrgbcolor-space

Colors Harmony Theory and Algorithm, compute complementary, triad, tetratic, etc


currently working on a application and trying to find colors(complementary, split-Complementary, analogous, triad, tetratic, square, etc...) from a provided base colors.

What i am doing right now:

  1. Convert RGB Color to HSV
  2. Adjust the H value to get a color around the 360 degrees wheel(S and V values are untouched)
  3. Convert HSV back to RGB

Here is an example for Triad(hsv object represent the base color):

colors.Add(new HSVData() { h = hsv.h + 120, s = hsv.s, v = hsv.v });
colors.Add(new HSVData() { h = hsv.h - 120, s = hsv.s, v = hsv.v });

And for Square:

colors.Add(new HSVData() { h = hsv.h + 90, s = hsv.s, v = hsv.v });
colors.Add(new HSVData() { h = hsv.h + 180, s = hsv.s, v = hsv.v });
colors.Add(new HSVData() { h = hsv.h + 270, s = hsv.s, v = hsv.v });

RGB to HSV:

public static HSVData RGBtoHSV(RGBResult RGB)
{
    double min;
    double max;
    double delta;

    double r = (double)RGB.r / 255;
    double g = (double)RGB.g / 255;
    double b = (double)RGB.b / 255;

    double h;
    double s;
    double v;

    min = Math.Min(Math.Min(r, g), b);
    max = Math.Max(Math.Max(r, g), b);
    v = max;
    delta = max - min;
    if (max == 0 || delta == 0)
    {
        s = 0;
        h = 0;
    }
    else
    {
        s = delta / max;
        if (r == max)
        {
            // Between Yellow and Magenta
            h = (g - b) / delta;
        }
        else if (g == max)
        {
            // Between Cyan and Yellow
            h = 2 + (b - r) / delta;
        }
        else
        {
            // Between Magenta and Cyan
            h = 4 + (r - g) / delta;
        } 
    }

    h *= 60;
    if (h < 0)
    {
        h += 360;
    }

    return new HSVData()
    {
        h = (int)(h / 360 * 255),
        s = (int)(s * 255),
        v = (int)(v * 255)
    };
}

HSV to RGB:

public static Color ConvertHsvToRgb(float h, float s, float v)
{
    byte MAX = 255;

    h = h / 360;
    if (s > 0)
    {
        if (h >= 1)
            h = 0;
        h = 6 * h;
        int hueFloor = (int)Math.Floor(h);
        byte a = (byte)Math.Round(MAX * v * (1.0 - s));
        byte b = (byte)Math.Round(MAX * v * (1.0 - (s * (h - hueFloor))));
        byte c = (byte)Math.Round(MAX * v * (1.0 - (s * (1.0 - (h - hueFloor)))));
        byte d = (byte)Math.Round(MAX * v);

        switch (hueFloor)
        {
            case 0: return Color.FromArgb(MAX, d, c, a);
            case 1: return Color.FromArgb(MAX, b, d, a);
            case 2: return Color.FromArgb(MAX, a, d, c);
            case 3: return Color.FromArgb(MAX, a, b, d);
            case 4: return Color.FromArgb(MAX, c, a, d);
            case 5: return Color.FromArgb(MAX, d, a, b);
            default: return Color.FromArgb(0, 0, 0, 0);
        }
    }
    else
    {
        byte d = (byte)(v * MAX);
        return Color.FromArgb(255, d, d, d);
    }
}

The colors I am getting are wrong according to many online colors tools! Should I be using HSL intead of HSV? What am I doing wrong?

Online Tools compared with:

http://colorschemedesigner.com/

http://www.colorsontheweb.com/colorwizard.asp

Thanks in advance!


Solution

  • What range of values are you expecting to get in the ConvertHsvToRgb method? It looks to me like it is:

    0 <= h <= 360
    0 <= s <= 1.0
    0 <= v <= 1.0
    

    You don't show how you are calling this method, but if you aren't passing values in these ranges, you won't get the correct conversion. You probably want to include a way to normalize the hue to 0 - 360 if you plan to subtract hues, like you do in the triad.

    I think your conversions are correct except you should not be converting your h,s,v values to integers; keep them as doubles, in the ranges shown above.

    public static HSVData RGBtoHSV(Color RGB)
    {
        double r = (double)RGB.R / 255;
        double g = (double)RGB.G / 255;
        double b = (double)RGB.B / 255;
    
        double h;
        double s;
        double v;
    
        double min = Math.Min(Math.Min(r, g), b);
        double max = Math.Max(Math.Max(r, g), b);
        v = max;
        double delta = max - min;
        if (max == 0 || delta == 0)
        {
            s = 0;
            h = 0;
        }
        else
        {
            s = delta / max;
            if (r == max)
            {
                // Between Yellow and Magenta
                h = (g - b) / delta;
            }
            else if (g == max)
            {
                // Between Cyan and Yellow
                h = 2 + (b - r) / delta;
            }
            else
            {
                // Between Magenta and Cyan
                h = 4 + (r - g) / delta;
            }
    
        }
    
        h *= 60;
        if (h < 0)
        {
            h += 360;
        }
    
        return new HSVData()
        {
            h = h,
            s = s,
            v = v
        };
    }
    

    Now you can pass these h,s,v valuew directly into the ConvertHsvToRgb method. I have changed the arguments to double, validating the saturation and value inputs, and normalized the hue.

    public static Color ConvertHsvToRgb(double h, double s, double v)
    {
        Debug.Assert(0.0 <= s && s <= 1.0);
        Debug.Assert(0.0 <= v && v <= 1.0);
    
        // normalize the hue:
        while (h < 0)
            h += 360;
        while (h > 360)
            h -= 360;
    
        h = h / 360;
    
        byte MAX = 255;
    
        if (s > 0)
        {
            if (h >= 1)
                h = 0;
            h = 6 * h;
            int hueFloor = (int)Math.Floor(h);
            byte a = (byte)Math.Round(MAX * v * (1.0 - s));
            byte b = (byte)Math.Round(MAX * v * (1.0 - (s * (h - hueFloor))));
            byte c = (byte)Math.Round(MAX * v * (1.0 - (s * (1.0 - (h - hueFloor)))));
            byte d = (byte)Math.Round(MAX * v);
    
            switch (hueFloor)
            {
                case 0: return Color.FromArgb(MAX, d, c, a);
                case 1: return Color.FromArgb(MAX, b, d, a);
                case 2: return Color.FromArgb(MAX, a, d, c);
                case 3: return Color.FromArgb(MAX, a, b, d);
                case 4: return Color.FromArgb(MAX, c, a, d);
                case 5: return Color.FromArgb(MAX, d, a, b);
                default: return Color.FromArgb(0, 0, 0, 0);
            }
        }
        else
        {
            byte d = (byte)(v * MAX);
            return Color.FromArgb(255, d, d, d);
        }
    }
    

    Based on my tests, these two methods will now give "round-trip" conversions for any color from RGB to HSV and back.

    For the "triad" you are are adjusting +/- 120 degrees from the original color. So for example, if you start with Red as your base color, the +/- 120 degree colors are Green and Blue. These conversions appear to work correctly:

    HSVData hsv = HSVData.RGBtoHSV(Color.FromArgb(255, 0, 0));
    HSVData hsv2 = new HSVData() { h = hsv.h + 120, s = hsv.s, v = hsv.v };
    HSVData hsv3 = new HSVData() { h = hsv.h - 120 , s = hsv.s, v = hsv.v };
    
    Color red = HSVData.ConvertHsvToRgb(hsv.h, hsv.s, hsv.v);
    Color green = HSVData.ConvertHsvToRgb(hsv2.h, hsv2.s, hsv2.v);
    Color blue = HSVData.ConvertHsvToRgb(hsv3.h, hsv3.s, hsv3.v);
    
    HSVData hsv4 = HSVData.RGBtoHSV(Color.YellowGreen);
    Color yellowGreen = HSVData.ConvertHsvToRgb(hsv4.h, hsv4.s, hsv4.v);