RGB到HSL的转换及反向转换,计算问题

10

我正在尝试将RGB转换为HSL,并且我还想从HSL转换为RGB,我已经编写了一个类来实现它,但是如果我执行RGB->HSL->RGB以尝试这是否有效,我会得到不同的值。

示例: 如果您通过HSLColor.FromRGB(Colors.Green)创建HSLColor对象,然后执行Color ExpectedGreenHere = MyTestConversion.ToRGB(),则会得到与Colors.Green不同的颜色,而它是原始输入,因此出现了问题...

这是我使用的代码:

public class HSLColor
{
    public float Hue;
    public float Saturation;
    public float Luminosity;

    public HSLColor(float H, float S, float L)
    {
        Hue = H;
        Saturation = S;
        Luminosity = L;
    }

    public static HSLColor FromRGB(Color Clr)
    {
        return FromRGB(Clr.R, Clr.G, Clr.B);
    }

    public static HSLColor FromRGB(Byte R, Byte G, Byte B)
    {
        float _R = (R / 255f);
        float _G = (G / 255f);
        float _B = (B / 255f);

        float _Min = Math.Min(Math.Min(_R, _G), _B);
        float _Max = Math.Max(Math.Max(_R, _G), _B);
        float _Delta = _Max - _Min;

        float H = 0;
        float S = 0;
        float L = (float)((_Max + _Min) / 2.0f);

        if (_Delta != 0)
        {
            if (L < 0.5f)
            {
                S = (float)(_Delta / (_Max + _Min));
            }
            else
            {
                S = (float)(_Delta / (2.0f - _Max - _Min));
            }

            float _Delta_R = (float)(((_Max - _R) / 6.0f + (_Delta / 2.0f)) / _Delta);
            float _Delta_G = (float)(((_Max - _G) / 6.0f + (_Delta / 2.0f)) / _Delta);
            float _Delta_B = (float)(((_Max - _B) / 6.0f + (_Delta / 2.0f)) / _Delta);

            if (_R == _Max)
            {
                H = _Delta_B - _Delta_G;
            }
            else if (_G == _Max)
            {
                H = (1.0f / 3.0f) + _Delta_R - _Delta_B;
            }
            else if (_B == _Max)
            {
                H = (2.0f / 3.0f) + _Delta_G - _Delta_R;
            }

            if (H < 0) H += 1.0f;
            if (H > 1) H -= 1.0f;
        }

        return new HSLColor(H, S, L);
    }

    private float Hue_2_RGB(float v1, float v2, float vH)
    {
        if (vH < 0) vH += 1;
        if (vH > 1) vH -= 1;
        if ((6 * vH) < 1) return (v1 + (v2 - v1) * 6 * vH);
        if ((2 * vH) < 1) return (v2);
        if ((3 * vH) < 2) return (v1 + (v2 - v1) * ((2 / 3) - vH) * 6);
        return (v1);
    }

    public Color ToRGB()
    {
        Color Clr = new Color();
        float var_1, var_2;

        if (Saturation == 0)
        {
            Clr.R = (Byte)(Luminosity * 255);
            Clr.G = (Byte)(Luminosity * 255);
            Clr.B = (Byte)(Luminosity * 255);
        }
        else
        {
            if (Luminosity < 0.5) var_2 = Luminosity * (1 + Saturation);
            else var_2 = (Luminosity + Saturation) - (Saturation * Luminosity);

            var_1 = 2 * Luminosity - var_2;

            Clr.R = (Byte)(255 * Hue_2_RGB(var_1, var_2, Hue + (1 / 3)));
            Clr.G = (Byte)(255 * Hue_2_RGB(var_1, var_2, Hue));
            Clr.B = (Byte)(255 * Hue_2_RGB(var_1, var_2, Hue - (1 / 3)));
        }

        return Clr;
    }
}

参考资料: EasyRGB 颜色数学


1
那问题到底是什么? - leppie
很好,你已经包含了你的代码,但是你还需要告诉我们你的输入、期望结果和实际结果。 - AakashM
@leppie 如果您将Colors.Green作为输入,然后将其转换回RGB,那么结果都不好,因为它不再是绿色了。 - Mervin
1
@Mervin:可能有一百万件事情出了问题。你有进行任何调查吗?首先,你是否查看了HSL值以确定问题是在RGB->HSL还是HSL->RGB方面? - tenfour
顺便说一句,System.Drawing.ColorGetHueGetSaturationGetBrightness 方法 - 但是没有办法返回到 RGB,需要注意。 - Lou
显示剩余2条评论
3个回答

22

除了精度问题以外,我认为你的实际算法是错误的。这应该是你的FromRGB:

    public static HSLColor FromRGB(Byte R, Byte G, Byte B)
    {
        float _R = (R / 255f);
        float _G = (G / 255f);
        float _B = (B / 255f);

        float _Min = Math.Min(Math.Min(_R, _G), _B);
        float _Max = Math.Max(Math.Max(_R, _G), _B);
        float _Delta = _Max - _Min;

        float H = 0;
        float S = 0;
        float L = (float)((_Max + _Min) / 2.0f);

        if (_Delta != 0)
        {
            if (L < 0.5f)
            {
                S = (float)(_Delta / (_Max + _Min));
            }
            else
            {
                S = (float)(_Delta / (2.0f - _Max - _Min));
            }


            if (_R == _Max)
            {
                H = (_G - _B) / _Delta;
            }
            else if (_G == _Max)
            {
                H = 2f + (_B - _R) / _Delta;
            }
            else if (_B == _Max)
            {
                H = 4f + (_R - _G) / _Delta;
            }
        }

        return new HSLColor(H, S, L);
    }

你需要理解的下一件事情是,我们将从0到255的整数RGB值转换为0到1的十进制值。我们得到的HSL因此需要转换为你习惯的常规度数/百分比/百分比。返回的H值应该在0到6之间,因此要将其转换为度数,只需乘以60即可。有时候H实际上可能是负数,所以如果是负数,只需加上360。

            //Convert to degrees
            H = H * 60f;
            if (H < 0) H += 360;

SL也需要乘以100,才能给出从0到100的百分比。

更新

这段代码可以将HSL转换为RGB。它假设HSL值仍处于其十进制格式。另外,我在下面的代码中使用了double而不是float以获得更好的精度。

    public Color ToRGB()
    {
        byte r, g, b;
        if (Saturation == 0)
        {
            r = (byte)Math.Round(Luminosity * 255d);
            g = (byte)Math.Round(Luminosity * 255d);
            b = (byte)Math.Round(Luminosity * 255d);
        }
        else
        {
            double t1, t2;
            double th = Hue / 6.0d;

            if (Luminosity < 0.5d)
            {
                t2 = Luminosity * (1d + Saturation);
            }
            else
            {
                t2 = (Luminosity + Saturation) - (Luminosity * Saturation);
            }
            t1 = 2d * Luminosity - t2;

            double tr, tg, tb;
            tr = th + (1.0d / 3.0d);
            tg = th;
            tb = th - (1.0d / 3.0d);

            tr = ColorCalc(tr, t1, t2);
            tg = ColorCalc(tg, t1, t2);
            tb = ColorCalc(tb, t1, t2);
            r = (byte)Math.Round(tr * 255d);
            g = (byte)Math.Round(tg * 255d);
            b = (byte)Math.Round(tb * 255d);
        }
        return Color.FromArgb(r, g, b);
    }
    private static double ColorCalc(double c, double t1, double t2)
    {

        if (c < 0) c += 1d;
        if (c > 1) c -= 1d;
        if (6.0d * c < 1.0d) return t1 + (t2 - t1) * 6.0d * c;
        if (2.0d * c < 1.0d) return t2;
        if (3.0d * c < 2.0d) return t1 + (t2 - t1) * (2.0d / 3.0d - c) * 6.0d;
        return t1;
    }

我尝试了你的建议,但是如果我输入例如 RGB(0,128,0),在转换成 HSL 后再转换回来,我得到的是 RGB(100,100,100)。 - Mervin
1
@Mervin,让我们分而治之。运行我给你的应该会得到HSL 2/1/0.25,即120° 100% 25%,对吗?如果是这样,我们知道这个方法可行。现在你只需要修复HSL转RGB的方程式。我现在没有时间去研究那部分,但这是我用来修复RGB到HSL的网站http://130.113.54.154/~monger/hsl-rgb.html。 - Chris Haas
它确实现在返回120、100、25,你给了我最有用的答案。我会去看看这个网站。 - Mervin
5
我只想指出你正在比较浮点数是否相等,这是极其不可预测的。这里有一篇好文章:http://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/简而言之,永远不要直接比较浮点数是否相等。使用“足够接近”的值(也称为epsilon):if (Math.Abs(a - b) < epsilon) Console.WriteLine("实际上,a等于b") - Jörgen Sigvardsson
1
@JörgenSigvardsson,我看到唯一的相等比较是通过 Math.Max()Math.Min() 进行的。这些函数能够返回与其输入值在任何方面不同的浮点数吗?还有一个零比较,但那只是为了避免除以零异常。 - Chris Haas
显示剩余2条评论

4

常见的错误。你遇到了它

public static HSLColor FromRGB(Byte R, Byte G, Byte B)
{
    float _R = (R / 255);
    float _G = (G / 255);
    float _B = (B / 255);
}

告诉我哪些 R 的值会导致 _R 不是 0。(提示:只有一个)。

编辑:在 ToRGB() 函数中,你也遇到了同样的问题,应该使用 1.0f/3.0f。


他已经得到了这个答案,只是复制了他的问题而没有使用它。http://stackoverflow.com/questions/4784040/rgb-to-hsl-hue-calculation-is-wrong/4784079#4784079 - Hans Passant
2
在编程中,对于TS的错误,直接给出答案并不是最好的方式,更好的方法是让TS自己发现错误并纠正。这样即使是简单的问题,TS也能从中学到教训,避免再次犯同样的错误 :)。因此,我认为这种方式更加有效。 - bastijn
@Hans Passant,这是一个不同的问题,因为这个问题也涉及到将颜色转换回RGB以正确遵循HSL和RGB标准。 - Mervin
1
@Mervin,有两件事。首先,你是否阅读、理解并执行了以“编辑”开头的那一行?其次,结果究竟不好在哪里?除非你提供输入、期望输出和实际输出,否则不能指望任何人重现你的问题。 - Peter Taylor

3
我看到你的代码存在以下问题:

在你的代码中,我发现了以下问题:

float _R = (R / 255);

您在这里实际上进行的是整数除法,因此您损失了大量精度。
尝试更改为:
float _R = (R / 255f);

(并且对于另外两行也是同样的情况)。

此外,为了提高精度,最好使用double而不是float。


1
考虑到输入和输出都具有8位精度,因此绝对没有理由优先选择double而不是float。float具有足够的精度来获得完美的8位结果。当然,所有的数学错误和编程错误都必须得到纠正,并且从float到double的转换需要使用四舍五入而不是截断。例如,如果您使用定点数学进行此操作,只要您精心选择使用的定点常量,它们只需要具有约8-10位的精度--远少于float的24位或double的53位。 - Bruce Dawson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接