Search code examples
c#colorsmodelsrgbhsl

C# RGB to HSL one way conversion algorithm


The following is my RGB to HSL one way color conversion method, written in C#. It is closely based on code from the open source program, "Meazure" (which is the tool I am using for my project, thus I need my HSL color to be formatted the same as it is in Meazure).

    public String rgbToHsl(int r, int g, int b)
    {
        double h, s, l;
        double rDecimal = r / 255;
        double gDecimal = g / 255;
        double bDecimal = b / 255;
        double cMin = Math.Min(r, Math.Min(g, b));
        double cMax = Math.Max(r, Math.Max(g, b));
        double delta = cMax - cMin;

        l = (cMax + cMin) / 2;

        if (cMax == cMin)
        {
            s = 0;
            h = 0;  // It's really undefined
        }
        else
        {
            if (l < .5)
            {
                s = delta / (cMax + cMin);
            }
            else
            {
                s = delta / (2 - cMax - cMin);
            }

            if (r == cMax)
            {
                h = (g - b) / delta;
            }
            else if (g == cMax)
            {
                h = 2 + (b - r) / delta;
            }
            else
            {
                h = 4 + (r - g) / delta;
            }

            h /= 6;

            if (h < 0)
            {
                h += 1;
            }
        }

        return h.ToString().PadLeft(3, '0') + s.ToString().PadLeft(3, '0') + l.ToString().PadLeft(3, '0');
    }

The following is the open source, C++ Meazure code I used as a reference.

void MeaColors::RGBtoHSL(COLORREF rgb, HSL& hsl)
{
    double h, s, l;
    double r = GetRValue(rgb) / 255.0;
    double g = GetGValue(rgb) / 255.0;
    double b = GetBValue(rgb) / 255.0;
    double cmax = Max(r, Max(g, b));
    double cmin = Min(r, Min(g, b));

    l = (cmax + cmin) / 2.0;
    if (MEA_DBL_EQL(cmax, cmin)) {
        s = 0.0;
        h = 0.0; // it's really undefined
    } else {
        if (l < 0.5) {
            s = (cmax - cmin) / (cmax + cmin);
        } else {
           s = (cmax - cmin) / (2.0 - cmax - cmin);
        }
        double delta = cmax - cmin;

        if (MEA_DBL_EQL(r, cmax)) {
            h = (g - b) / delta;
        } else if (MEA_DBL_EQL(g, cmax)) {
            h = 2.0 + (b - r) / delta;
        } else {
            h = 4.0 + (r - g) / delta;
        }
        h /= 6.0;

        if (h < 0.0) {
            h += 1.0;
        }
    }

    hsl.hue = h;
    hsl.lightness = l;
    hsl.saturation = s;
}

The problem is that my method does not output the expected values. It does compile and run, without crashing. However, for the input RGB value 214, 219, 233, my method produces the HSL value .6, 228, 70, while the expected value, obtained by measuring the same pixel in HSL format using Meazure, is 149, 72, 210. I noticed a few similar questions on this site, but none with a functional solution, at least in the format desired here.

This is my code that calls my method. It happens to convert 25 RGB values from a single input, by building a 5x5 box centered around the input pixel (just mentioned it to avoid confusion).

            Bitmap screenShot = takeScreenShot();
            const int squareSideSize = 5;
            Color[] firstPixelSquare = new Color[(int)Math.Pow(squareSideSize, 2)];
            hslColor[] hslFirstPixelSquare = new hslColor[(int)Math.Pow(squareSideSize, 2)];

            for (int hOffset = -2, i = 0; hOffset <= 2; hOffset++, i += squareSideSize)
            {
                for (int vOffset = -2, j = i; vOffset <= 2; vOffset++, j++)
                {
                    firstPixelSquare[j] = screenShot.GetPixel((int)numericUpDownX1.Value + hOffset, (int)numericUpDownY1.Value + vOffset);
                    hslFirstPixelSquare[j].h = Convert.ToDouble(rgbToHsl(firstPixelSquare[j].R, firstPixelSquare[j].G, firstPixelSquare[j].B).Substring(0, 3));
                    hslFirstPixelSquare[j].s = Convert.ToDouble(rgbToHsl(firstPixelSquare[j].R, firstPixelSquare[j].G, firstPixelSquare[j].B).Substring(3, 3));
                    hslFirstPixelSquare[j].l = Convert.ToDouble(rgbToHsl(firstPixelSquare[j].R, firstPixelSquare[j].G, firstPixelSquare[j].B).Substring(6, 3));
                }
            }

Solution

  • There are two problems with your original algorithm :

    The variables declared here are never used :

        double rDecimal = r / 255;
        double gDecimal = g / 255;
        double bDecimal = b / 255;
    

    Either switch the r for rDecimal everywhere, or simply rename the input to continue using r, g, b variables :

    public String RgbToHsl(int rInput, int gInput, int bInput)
    {
        double h, s, l;
        double r = rInput / 255;
        double g = gInput / 255;
        double b = bInput / 255;
    

    The second problem is in this segment too. The inputs are ints and you're dividing by 255 (which is an int too). This results in an integer division, which will yield 0 each time. You need to divide by 255.0 to force a double division:

    public String RgbToHsl(int rInput, int gInput, int bInput)
    {
        double h, s, l;
        double r = rInput / 255.0;
        double g = gInput / 255.0;
        double b = bInput / 255.0;
    

    Once that's done, you need to convert the result from a [0,1] interval to the one you get from your tool. The closest I've been able to go is by multiplying the result by 239 and then rounding. It's probably possible to find the real pattern/exact value from more input/output examples...

    h = Math.Round(h * 239);
    s = Math.Round(s * 239);
    l = Math.Round(l * 239);
    
    return h.ToString().PadLeft(3, '0') + s.ToString().PadLeft(3, '0') + l.ToString().PadLeft(3, '0');
    }