Is there a built-in C#/.NET System API for HSV to RGB?
Asked Answered
R

6

42

Is there an API built into the .NET framework for converting HSV to RGB? I didn't see a method in System.Drawing.Color for this, but it seems surprising that there wouldn't be one in the platform.

Rhizopod answered 26/8, 2009 at 15:8 Comment(0)
O
28

I don't think there's a method doing this in the .NET framework.
Check out Converting HSV to RGB colour using C#

This is the implementation code,

void HsvToRgb(double h, double S, double V, out int r, out int g, out int b)
{    
  double H = h;
  while (H < 0) { H += 360; };
  while (H >= 360) { H -= 360; };
  double R, G, B;
  if (V <= 0)
    { R = G = B = 0; }
  else if (S <= 0)
  {
    R = G = B = V;
  }
  else
  {
    double hf = H / 60.0;
    int i = (int)Math.Floor(hf);
    double f = hf - i;
    double pv = V * (1 - S);
    double qv = V * (1 - S * f);
    double tv = V * (1 - S * (1 - f));
    switch (i)
    {

      // Red is the dominant color

      case 0:
        R = V;
        G = tv;
        B = pv;
        break;

      // Green is the dominant color

      case 1:
        R = qv;
        G = V;
        B = pv;
        break;
      case 2:
        R = pv;
        G = V;
        B = tv;
        break;

      // Blue is the dominant color

      case 3:
        R = pv;
        G = qv;
        B = V;
        break;
      case 4:
        R = tv;
        G = pv;
        B = V;
        break;

      // Red is the dominant color

      case 5:
        R = V;
        G = pv;
        B = qv;
        break;

      // Just in case we overshoot on our math by a little, we put these here. Since its a switch it won't slow us down at all to put these here.

      case 6:
        R = V;
        G = tv;
        B = pv;
        break;
      case -1:
        R = V;
        G = pv;
        B = qv;
        break;

      // The color is not defined, we should throw an error.

      default:
        //LFATAL("i Value error in Pixel conversion, Value is %d", i);
        R = G = B = V; // Just pretend its black/white
        break;
    }
  }
  r = Clamp((int)(R * 255.0));
  g = Clamp((int)(G * 255.0));
  b = Clamp((int)(B * 255.0));
}

/// <summary>
/// Clamp a value to 0-255
/// </summary>
int Clamp(int i)
{
  if (i < 0) return 0;
  if (i > 255) return 255;
  return i;
}
Overanxious answered 26/8, 2009 at 15:13 Comment(8)
Thanks for that method. Weird that Color has .GetHue(), .GetSaturation() and .GetBrightness(), but no inverse method like .fromHSB().Horatia
Indeed... its a very strange omission, imo.Rhizopod
Why not return a Color object instead of using out for three separate values?Coral
The code is posted from the provided link and is not mine.Overanxious
@FizzledOut: Maybe because like this, the code can directly be used for SWF Color, WPF Color, Gdk# Color, and others.Nursery
If you start with for example, System.Drawing.Color.FromArgb(255, 0, 0), use .GetHue() etc to emit that to HSV, then use this code to roundtrip back to a Color, you get rgb 127, 0, 0. .GetBrightness() returns 0.5, and this code interprets that to mean the dominant color is 255*.5 = 127. At least from my perspective that means this code does not work properly.Corrinnecorrival
I'm using a combination of the above code, and ColorToHSV given below. Works great.Eurus
Wouldn't be H = (H % 360 + 360) % 360; be way more efficient than those two while loops?Rewarding
W
71

There isn't a built-in method for doing this, but the calculations aren't terribly complex.
Also note that Color's GetHue(), GetSaturation() and GetBrightness() return HSL values, not HSV.

The following C# code converts between RGB and HSV using the algorithms described on Wikipedia.
I already posted this answer here, but I'll copy the code here for quick reference.

The ranges are 0 - 360 for hue, and 0 - 1 for saturation or value.

public static void ColorToHSV(Color color, out double hue, out double saturation, out double value)
{
    int max = Math.Max(color.R, Math.Max(color.G, color.B));
    int min = Math.Min(color.R, Math.Min(color.G, color.B));

    hue = color.GetHue();
    saturation = (max == 0) ? 0 : 1d - (1d * min / max);
    value = max / 255d;
}

public static Color ColorFromHSV(double hue, double saturation, double value)
{
    int hi = Convert.ToInt32(Math.Floor(hue / 60)) % 6;
    double f = hue / 60 - Math.Floor(hue / 60);

    value = value * 255;
    int v = Convert.ToInt32(value);
    int p = Convert.ToInt32(value * (1 - saturation));
    int q = Convert.ToInt32(value * (1 - f * saturation));
    int t = Convert.ToInt32(value * (1 - (1 - f) * saturation));

    if (hi == 0)
        return Color.FromArgb(255, v, t, p);
    else if (hi == 1)
        return Color.FromArgb(255, q, v, p);
    else if (hi == 2)
        return Color.FromArgb(255, p, v, t);
    else if (hi == 3)
        return Color.FromArgb(255, p, q, v);
    else if (hi == 4)
        return Color.FromArgb(255, t, p, v);
    else
        return Color.FromArgb(255, v, p, q);
}
Wilks answered 26/10, 2009 at 17:44 Comment(4)
Your ColorFromHSV might have something wrong with it, I was trying to rotate the hue 180 degrees using your code for an opposite color and it's not working too well. The accepted code gives a different color which seems correct to me.Eurus
I'm using your ColorToHSV function, however. It seems to work well.Eurus
@IsaacBolinger does not work well with negative hue, workd well for hue >= 0, but better to use hue between <0, 360) in your code.Entelechy
@IsaacBolinger I wanted the same and changing the hue line to the following worked perfectly for me: hue = (color.GetHue() + 180) % 360Elysium
O
28

I don't think there's a method doing this in the .NET framework.
Check out Converting HSV to RGB colour using C#

This is the implementation code,

void HsvToRgb(double h, double S, double V, out int r, out int g, out int b)
{    
  double H = h;
  while (H < 0) { H += 360; };
  while (H >= 360) { H -= 360; };
  double R, G, B;
  if (V <= 0)
    { R = G = B = 0; }
  else if (S <= 0)
  {
    R = G = B = V;
  }
  else
  {
    double hf = H / 60.0;
    int i = (int)Math.Floor(hf);
    double f = hf - i;
    double pv = V * (1 - S);
    double qv = V * (1 - S * f);
    double tv = V * (1 - S * (1 - f));
    switch (i)
    {

      // Red is the dominant color

      case 0:
        R = V;
        G = tv;
        B = pv;
        break;

      // Green is the dominant color

      case 1:
        R = qv;
        G = V;
        B = pv;
        break;
      case 2:
        R = pv;
        G = V;
        B = tv;
        break;

      // Blue is the dominant color

      case 3:
        R = pv;
        G = qv;
        B = V;
        break;
      case 4:
        R = tv;
        G = pv;
        B = V;
        break;

      // Red is the dominant color

      case 5:
        R = V;
        G = pv;
        B = qv;
        break;

      // Just in case we overshoot on our math by a little, we put these here. Since its a switch it won't slow us down at all to put these here.

      case 6:
        R = V;
        G = tv;
        B = pv;
        break;
      case -1:
        R = V;
        G = pv;
        B = qv;
        break;

      // The color is not defined, we should throw an error.

      default:
        //LFATAL("i Value error in Pixel conversion, Value is %d", i);
        R = G = B = V; // Just pretend its black/white
        break;
    }
  }
  r = Clamp((int)(R * 255.0));
  g = Clamp((int)(G * 255.0));
  b = Clamp((int)(B * 255.0));
}

/// <summary>
/// Clamp a value to 0-255
/// </summary>
int Clamp(int i)
{
  if (i < 0) return 0;
  if (i > 255) return 255;
  return i;
}
Overanxious answered 26/8, 2009 at 15:13 Comment(8)
Thanks for that method. Weird that Color has .GetHue(), .GetSaturation() and .GetBrightness(), but no inverse method like .fromHSB().Horatia
Indeed... its a very strange omission, imo.Rhizopod
Why not return a Color object instead of using out for three separate values?Coral
The code is posted from the provided link and is not mine.Overanxious
@FizzledOut: Maybe because like this, the code can directly be used for SWF Color, WPF Color, Gdk# Color, and others.Nursery
If you start with for example, System.Drawing.Color.FromArgb(255, 0, 0), use .GetHue() etc to emit that to HSV, then use this code to roundtrip back to a Color, you get rgb 127, 0, 0. .GetBrightness() returns 0.5, and this code interprets that to mean the dominant color is 255*.5 = 127. At least from my perspective that means this code does not work properly.Corrinnecorrival
I'm using a combination of the above code, and ColorToHSV given below. Works great.Eurus
Wouldn't be H = (H % 360 + 360) % 360; be way more efficient than those two while loops?Rewarding
A
13

It's not built in, but there's there's an open-source C# library called ColorMine which makes converting between color spaces pretty easy.

Rgb to Hsv:

var rgb = new Rgb {R = 123, G = 11, B = 7};
var hsv = rgb.To<Hsv>();

Hsv to Rgb:

var hsv = new Hsv { H = 360, S = .5, L = .17 }
var rgb = hsv.to<Rgb>();
Ainslee answered 20/7, 2013 at 19:45 Comment(2)
The ColorMine repository seems to have disappeared (404 on github as of 05 Aug 2018). Also, there doesn't seem to be a successor repository owned by Joe. However, I found ColorMinePortable which may be close enough.Kalie
Just doing a quick search, it looks like the user might have deleted their repo. It was forked by others though: github.com/hvalidi/ColorMineAffected
S
1

There is no built-in method (I couldn't find it), but here is code that might help you out. (above solutions didn't work for me)

/// <summary>
/// Converts HSV color values to RGB
/// </summary>
/// <param name="h">0 - 360</param>
/// <param name="s">0 - 100</param>
/// <param name="v">0 - 100</param>
/// <param name="r">0 - 255</param>
/// <param name="g">0 - 255</param>
/// <param name="b">0 - 255</param>
private void HSVToRGB(int h, int s, int v, out int r, out int g, out int b)
{
    var rgb = new int[3];

    var baseColor = (h + 60) % 360 / 120;
    var shift = (h + 60) % 360 - (120 * baseColor + 60 );
    var secondaryColor = (baseColor + (shift >= 0 ? 1 : -1) + 3) % 3;
    
    //Setting Hue
    rgb[baseColor] = 255;
    rgb[secondaryColor] = (int) ((Mathf.Abs(shift) / 60.0f) * 255.0f);
    
    //Setting Saturation
    for (var i = 0; i < 3; i++)
        rgb[i] += (int) ((255 - rgb[i]) * ((100 - s) / 100.0f));
    
    //Setting Value
    for (var i = 0; i < 3; i++)
        rgb[i] -= (int) (rgb[i] * (100-v) / 100.0f);

    r = rgb[0];
    g = rgb[1];
    b = rgb[2];
}
Stambul answered 29/1, 2022 at 12:38 Comment(1)
I confirm, this realization produces proper value.Brunet
A
0

I've searched the internet for better way to do this, but I can't find it.

This is a C# method to convert HSV to RGB, the method arguments are in range of 0-1, the output is in range of 0-255

private Color hsv2rgb (float h, float s, float v)
    {
        Func<float, int> f = delegate (float n)
        {
            float k = (n + h * 6) % 6;
            return (int)((v - (v * s * (Math.Max(0, Math.Min(Math.Min(k, 4 - k), 1))))) * 255);
        };
        return Color.FromArgb(f(5), f(3), f(1));
    }
Aret answered 5/2, 2022 at 7:47 Comment(0)
L
0

I have this complex class to convert colors which extending .NET System.Drawing.Color.

There is FromHsv method.

internal static class ColorHelper
{
    public static System.Drawing.Color ParseCssColor(string cssColor)
    {
        cssColor = cssColor.Trim().ToLowerInvariant();

        if (cssColor.StartsWith("rgb", StringComparison.Ordinal)) //rgb or rgba
        {
            int left = cssColor.IndexOf('(');
            int right = cssColor.IndexOf(')');

            if (left < 0 || right < 0)
            {
                throw new FormatException("rgba format error");
            }

            string noBrackets = cssColor.Substring(left + 1, right - left - 1);

            string[] parts = noBrackets.Split(',');

            int parseRgbValue(string value)
            {
                return value.IndexOf("%") == -1 ?
                    int.Parse(value, System.Globalization.CultureInfo.InvariantCulture) :
                    (int)Math.Round(255f * float.Parse(value.Replace("%", "", StringComparison.Ordinal), System.Globalization.CultureInfo.InvariantCulture) / 100, MidpointRounding.AwayFromZero);
            }

            int red = parseRgbValue(parts[0]);
            int green = parseRgbValue(parts[1]);
            int blue = parseRgbValue(parts[2]);

            if (parts.Length == 3)
            {
                return System.Drawing.Color.FromArgb(red, green, blue);
            }
            if (parts.Length == 4)
            {
                float alpha = float.Parse(parts[3], System.Globalization.CultureInfo.InvariantCulture);
                return FromRgb(red, green, blue, (int)Math.Round(255f * alpha));
            }
        }
        else if (cssColor.StartsWith("hsl", StringComparison.Ordinal)) //hsl or hsla
        {
            int left = cssColor.IndexOf('(');
            int right = cssColor.IndexOf(')');

            if (left < 0 || right < 0)
            {
                throw new FormatException("hsvl format error");
            }

            string noBrackets = cssColor.Substring(left + 1, right - left - 1);

            string[] parts = noBrackets.Split(',');

            float hue = float.Parse(parts[0], System.Globalization.CultureInfo.InvariantCulture);
            float saturationPercent = float.Parse(parts[1].Replace("%", "", StringComparison.Ordinal), System.Globalization.CultureInfo.InvariantCulture);
            float lightnessPercent = float.Parse(parts[2].Replace("%", "", StringComparison.Ordinal), System.Globalization.CultureInfo.InvariantCulture);

            if (parts.Length == 3)
            {
                return FromHsl(hue, saturationPercent, lightnessPercent);
            }
            if (parts.Length == 4)
            {
                float alpha = float.Parse(parts[3], System.Globalization.CultureInfo.InvariantCulture);
                return FromHsl(hue, saturationPercent, lightnessPercent, (int)Math.Round(255f * alpha));
            }
        }
        else if (cssColor.StartsWith("hsv", StringComparison.Ordinal)) //hsv or hsva
        {
            int left = cssColor.IndexOf('(');
            int right = cssColor.IndexOf(')');

            if (left < 0 || right < 0)
            {
                throw new FormatException("hsva format error");
            }

            string noBrackets = cssColor.Substring(left + 1, right - left - 1);

            string[] parts = noBrackets.Split(',');

            float hue = float.Parse(parts[0], System.Globalization.CultureInfo.InvariantCulture);
            float saturationPercent = float.Parse(parts[1].Replace("%", "", StringComparison.Ordinal), System.Globalization.CultureInfo.InvariantCulture);
            float valuePercent = float.Parse(parts[2].Replace("%", "", StringComparison.Ordinal), System.Globalization.CultureInfo.InvariantCulture);

            if (parts.Length == 3)
            {
                return FromHsv(hue, saturationPercent, valuePercent);
            }
            if (parts.Length == 4)
            {
                float alpha = float.Parse(parts[3], System.Globalization.CultureInfo.InvariantCulture);
                return FromHsv(hue, saturationPercent, valuePercent, (int)Math.Round(255f * alpha));
            }
        }

        //Fallback to ColorTranslator for hex format (#) or named colors, e.g. "Black", "White" etc.
        return System.Drawing.ColorTranslator.FromHtml(cssColor);
    }

    public static System.Drawing.Color FromRgb(int red, int green, int blue, int alpha = 255)
    {
        return System.Drawing.Color.FromArgb(alpha, red, green, blue);
    }

    public static System.Drawing.Color FromRgbPercent(float redPercent, float greenPercent, float bluePercent, int alpha = 255)
    {
        if (redPercent < 0 || redPercent > 100)
        {
            throw new ArgumentException($"Value of '{redPercent}' is not valid for '{nameof(redPercent)}'. '{nameof(redPercent)}' should be greater than or equal to 0 and less than or equal to 100.", nameof(redPercent));
        }
        if (greenPercent < 0 || greenPercent > 100)
        {
            throw new ArgumentException($"Value of '{greenPercent}' is not valid for '{nameof(greenPercent)}'. '{nameof(greenPercent)}' should be greater than or equal to 0 and less than or equal to 100.", nameof(greenPercent));
        }
        if (bluePercent < 0 || bluePercent > 100)
        {
            throw new ArgumentException($"Value of '{bluePercent}' is not valid for '{nameof(bluePercent)}'. '{nameof(bluePercent)}' should be greater than or equal to 0 and less than or equal to 100.", nameof(bluePercent));
        }

        int red = (int)Math.Round(255f * redPercent / 100, MidpointRounding.AwayFromZero);
        int green = (int)Math.Round(255f * greenPercent / 100, MidpointRounding.AwayFromZero);
        int blue = (int)Math.Round(255f * bluePercent / 100, MidpointRounding.AwayFromZero);

        return System.Drawing.Color.FromArgb(alpha, red, green, blue);
    }

    public static System.Drawing.Color FromHsl(float hue, float saturationPercent, float lightnessPercent, int alpha = 255)
    {
        if (hue < 0 || hue > 360)
        {
            throw new ArgumentException($"Value of '{hue}' is not valid for '{nameof(hue)}'. '{nameof(hue)}' should be greater than or equal to 0 and less than or equal to 360.", nameof(hue));
        }
        if (saturationPercent < 0 || saturationPercent > 100)
        {
            throw new ArgumentException($"Value of '{saturationPercent}' is not valid for '{nameof(saturationPercent)}'. '{nameof(saturationPercent)}' should be greater than or equal to 0 and less than or equal to 100.", nameof(saturationPercent));
        }
        if (lightnessPercent < 0 || lightnessPercent > 100)
        {
            throw new ArgumentException($"Value of '{lightnessPercent}' is not valid for '{nameof(lightnessPercent)}'. '{nameof(lightnessPercent)}' should be greater than or equal to 0 and less than or equal to 100.", nameof(lightnessPercent));
        }

        float saturation = saturationPercent / 100f;
        float lightness = lightnessPercent / 100f;

        hue /= 60f;
        float q = (!((double)lightness <= 0.5)) ? (lightness + saturation - (lightness * saturation)) : (lightness * (saturation + 1f));
        float p = (lightness * 2f) - q;

        float hueToRgb(float p, float q, float t)
        {
            if (t < 0f)
            {
                t += 6f;
            }
            if (t >= 6f)
            {
                t -= 6f;
            }

            if (t < 1f)
            {
                return ((q - p) * t) + p;
            }

            if (t < 3f)
            {
                return q;
            }

            if (t < 4f)
            {
                return ((q - p) * (4f - t)) + p;
            }

            return p;
        }

        return System.Drawing.Color.FromArgb(alpha,
            (int)Math.Round(255f * hueToRgb(p, q, hue + 2f), MidpointRounding.AwayFromZero),
            (int)Math.Round(255f * hueToRgb(p, q, hue), MidpointRounding.AwayFromZero),
            (int)Math.Round(255f * hueToRgb(p, q, hue - 2f), MidpointRounding.AwayFromZero));
    }

    public static System.Drawing.Color FromHsv(float hue, float saturationPercent, float valuePercent, int alpha = 255)
    {
        if (hue < 0 || hue > 360)
        {
            throw new ArgumentException($"Value of '{hue}' is not valid for '{nameof(hue)}'. '{nameof(hue)}' should be greater than or equal to 0 and less than or equal to 360.", nameof(hue));
        }
        if (saturationPercent < 0 || saturationPercent > 100)
        {
            throw new ArgumentException($"Value of '{saturationPercent}' is not valid for '{nameof(saturationPercent)}'. '{nameof(saturationPercent)}' should be greater than or equal to 0 and less than or equal to 100.", nameof(saturationPercent));
        }
        if (valuePercent < 0 || valuePercent > 100)
        {
            throw new ArgumentException($"Value of '{valuePercent}' is not valid for '{nameof(valuePercent)}'. '{nameof(valuePercent)}' should be greater than or equal to 0 and less than or equal to 100.", nameof(valuePercent));
        }

        float saturation = saturationPercent / 100f;
        float value = valuePercent / 100f;

        float lightness = value - (value * saturation / 2f);
        float hslSaturation = lightness == 0f || lightness == 1f ? 0f : (value - lightness) / Math.Min(lightness, 1f - lightness);

        return FromHsl(hue, hslSaturation * 100, lightness * 100, alpha);
    }

    public static (float hue, float saturationPercent, float lightnessPercent) ToHsl(this System.Drawing.Color color)
    {
        float red = color.R / 255f;
        float green = color.G / 255f;
        float blue = color.B / 255f;

        float vmax = new[] { red, green, blue }.Max();
        float vmin = new[] { red, green, blue }.Min();
        float d = vmax - vmin;

        float hue = 0f;
        if (d != 0)
        {
            if (vmax == red)
            {
                hue = (green - blue) / d;
            }
            if (vmax == green)
            {
                hue = ((blue - red) / d) + 2f;
            }
            if (vmax == blue)
            {
                hue = ((red - green) / d) + 4f;
            }
        }

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

        float lightness = (vmin + vmax) / 2f;
        float saturation = vmin == vmax ? 0f : lightness > 0.5f ? d / (2f - vmax - vmin) : d / (vmax + vmin);

        return (hue: (int)Math.Round(hue, MidpointRounding.AwayFromZero), (int)Math.Round(saturation * 100d, MidpointRounding.AwayFromZero), lightnessPercent: (int)Math.Round(lightness * 100d, MidpointRounding.AwayFromZero));
    }

    public static (float hue, float saturationPercent, float valuePercent) ToHsv(this System.Drawing.Color color)
    {
        (float hue, float saturationPercent, float lightnessPercent) = ToHsl(color);

        float saturation = saturationPercent / 100f;
        float lightness = lightnessPercent / 100f;

        float value = lightness + (saturation * Math.Min(lightness, 1f - lightness));
        float hsvSaturation = value == 0f ? 0f : 2f - (2f * lightness / value);

        return (hue, saturationPercent: (int)Math.Round(hsvSaturation * 100d, MidpointRounding.AwayFromZero), valuePercent: (int)Math.Round(value * 100d, MidpointRounding.AwayFromZero));
    }

    public static string ToHexCssString(this System.Drawing.Color color)
    {
        if (color.A == byte.MaxValue)
        {
            return string.Format("#{0:X2}{1:X2}{2:X2}", color.R, color.G, color.B).ToLowerInvariant();
        }

        return string.Format("#{0:X2}{1:X2}{2:X2}{3:X2}", color.A, color.R, color.G, color.B).ToLowerInvariant();
    }

    public static string ToRgbCssString(this System.Drawing.Color color)
    {
        if (color.A == byte.MaxValue)
        {
            return string.Format("rgb({0}, {1}, {2})", color.R, color.G, color.B);
        }

        return string.Format("rgba({0}, {1}, {2}, {3})", color.R, color.G, color.B, Math.Round((decimal)color.A / 255, 2, MidpointRounding.AwayFromZero).ToString(System.Globalization.CultureInfo.InvariantCulture));
    }

    public static string ToHslCssString(this System.Drawing.Color color)
    {
        (float hue, float saturationPercent, float lightnessPercent) = ToHsl(color);

        if (color.A == byte.MaxValue)
        {
            return string.Format("hsl({0}, {1}%, {2}%)", hue, saturationPercent, lightnessPercent);
        }

        return string.Format("hsla({0}, {1}%, {2}%, {3})", hue, saturationPercent, lightnessPercent, Math.Round((decimal)color.A / 255, 2, MidpointRounding.AwayFromZero).ToString(System.Globalization.CultureInfo.InvariantCulture));
    }

    public static string ToHsvCssString(this System.Drawing.Color color)
    {
        (float hue, float saturationPercent, float valuePercent) = ToHsv(color);

        if (color.A == byte.MaxValue)
        {
            return string.Format("hsv({0}, {1}%, {2}%)", hue, saturationPercent, valuePercent);
        }

        return string.Format("hsva({0}, {1}%, {2}%, {3})", hue, saturationPercent, valuePercent, Math.Round((decimal)color.A / 255, 2, MidpointRounding.AwayFromZero).ToString(System.Globalization.CultureInfo.InvariantCulture));
    }
}
Levasseur answered 9/12, 2023 at 11:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.