diff --git a/src/ImageProcessor/Colors/Hsv.cs b/src/ImageProcessor/Colors/Hsv.cs
new file mode 100644
index 000000000..e76d1657c
--- /dev/null
+++ b/src/ImageProcessor/Colors/Hsv.cs
@@ -0,0 +1,284 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright © James South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+//
+// Represents a HSV (hue, saturation, value) color. Also known as HSB (hue, saturation, brightness).
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace ImageProcessor
+{
+ using System;
+ using System.ComponentModel;
+
+ ///
+ /// Represents a HSV (hue, saturation, value) color. Also known as HSB (hue, saturation, brightness).
+ ///
+ public struct Hsv : IEquatable
+ {
+ ///
+ /// Represents a that has H, S, and V values set to zero.
+ ///
+ public static readonly Hsv Empty = new Hsv();
+
+ ///
+ /// The epsilon for comparing floating point numbers.
+ ///
+ private const float Epsilon = 0.0001f;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The h hue component.
+ /// The s saturation component.
+ /// The v value (brightness) component.
+ public Hsv(float h, float s, float v)
+ {
+ this.H = h.Clamp(0, 360);
+ this.S = s.Clamp(0, 100);
+ this.V = v.Clamp(0, 100);
+ }
+
+ ///
+ /// Gets the H hue component.
+ /// A value ranging between 0 and 360.
+ ///
+ public float H { get; }
+
+ ///
+ /// Gets the S saturation component.
+ /// A value ranging between 0 and 100.
+ ///
+ public float S { get; }
+
+
+ ///
+ /// Gets the V value (brightness) component.
+ /// A value ranging between 0 and 100.
+ ///
+ public float V { get; }
+
+ ///
+ /// Gets a value indicating whether this is empty.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public bool IsEmpty => Math.Abs(this.H) < Epsilon
+ && Math.Abs(this.S) < Epsilon
+ && Math.Abs(this.V) < Epsilon;
+
+ ///
+ /// Compares two objects. The result specifies whether the values
+ /// of the , , and
+ /// properties of the two objects are equal.
+ ///
+ ///
+ /// The on the left side of the operand.
+ ///
+ ///
+ /// The on the right side of the operand.
+ ///
+ ///
+ /// True if the current left is equal to the parameter; otherwise, false.
+ ///
+ public static bool operator ==(Hsv left, Hsv right)
+ {
+ return left.Equals(right);
+ }
+
+ ///
+ /// Compares two objects. The result specifies whether the values
+ /// of the , , and
+ /// properties of the two objects are unequal.
+ ///
+ ///
+ /// The on the left side of the operand.
+ ///
+ ///
+ /// The on the right side of the operand.
+ ///
+ ///
+ /// True if the current left is equal to the parameter; otherwise, false.
+ ///
+ public static bool operator !=(Hsv left, Hsv right)
+ {
+ return !left.Equals(right);
+ }
+
+ ///
+ /// Allows the implicit conversion of an instance of to a
+ /// .
+ ///
+ ///
+ /// The instance of to convert.
+ ///
+ ///
+ /// An instance of .
+ ///
+ public static implicit operator Hsv(Bgra color)
+ {
+ float max = Math.Max(color.R, Math.Max(color.G, color.B));
+ float min = Math.Min(color.R, Math.Min(color.G, color.B));
+ float delta = max - min;
+
+ if (Math.Abs(max) < Epsilon)
+ {
+ return new Hsv(0, 0, 0);
+ }
+
+ float h = 0.0F;
+ float s;
+ float v;
+
+ if (Math.Abs(delta) < Epsilon) { h = 0; }
+ else if (Math.Abs(color.R - max) < Epsilon) { h = (color.G - color.B) / delta; }
+ else if (Math.Abs(color.G - max) < Epsilon) { h = 2 + (color.B - color.R) / delta; }
+ else if (Math.Abs(color.B - max) < Epsilon) { h = 4 + (color.R - color.G) / delta; }
+
+ h *= 60;
+ if (h < 0.0) { h += 360; }
+
+ s = delta / max;
+ v = max / 255F;
+ return new Hsv(h, s, v);
+ }
+
+ ///
+ /// Allows the implicit conversion of an instance of to a
+ /// .
+ ///
+ ///
+ /// The instance of to convert.
+ ///
+ ///
+ /// An instance of .
+ ///
+ public static implicit operator Bgra(Hsv color)
+ {
+ if (Math.Abs(color.S) < Epsilon)
+ {
+ byte component = (byte)(color.V * 255);
+ return new Bgra(component, component, component, 255);
+ }
+
+ float h = (Math.Abs(color.H - 360) < Epsilon) ? 0 : color.H / 60;
+ int i = (int)(Math.Truncate(h));
+ float f = h - i;
+
+ float p = color.V * (1.0f - color.S);
+ float q = color.V * (1.0f - (color.S * f));
+ float t = color.V * (1.0f - (color.S * (1.0f - f)));
+
+ float r, g, b;
+ switch (i)
+ {
+ case 0:
+ r = color.V;
+ g = t;
+ b = p;
+ break;
+
+ case 1:
+ r = q;
+ g = color.V;
+ b = p;
+ break;
+
+ case 2:
+ r = p;
+ g = color.V;
+ b = t;
+ break;
+
+ case 3:
+ r = p;
+ g = q;
+ b = color.V;
+ break;
+
+ case 4:
+ r = t;
+ g = p;
+ b = color.V;
+ break;
+
+ default:
+ r = color.V;
+ g = p;
+ b = q;
+ break;
+ }
+
+ return new Bgra((byte)b, (byte)g, (byte)r);
+ }
+
+ ///
+ /// Indicates whether this instance and a specified object are equal.
+ ///
+ ///
+ /// true if and this instance are the same type and represent the same value; otherwise, false.
+ ///
+ /// Another object to compare to.
+ public override bool Equals(object obj)
+ {
+ if (obj is Hsv)
+ {
+ Hsv color = (Hsv)obj;
+
+ return Math.Abs(this.H - color.H) < Epsilon
+ && Math.Abs(this.S - color.S) < Epsilon
+ && Math.Abs(this.V - color.V) < Epsilon;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Returns the hash code for this instance.
+ ///
+ ///
+ /// A 32-bit signed integer that is the hash code for this instance.
+ ///
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hashCode = this.H.GetHashCode();
+ hashCode = (hashCode * 397) ^ this.S.GetHashCode();
+ hashCode = (hashCode * 397) ^ this.V.GetHashCode();
+ return hashCode;
+ }
+ }
+
+ ///
+ /// Returns the fully qualified type name of this instance.
+ ///
+ ///
+ /// A containing a fully qualified type name.
+ ///
+ public override string ToString()
+ {
+ if (this.IsEmpty)
+ {
+ return "Hsv [ Empty ]";
+ }
+
+ return $"Hsv [ H={this.H:#0.##}, S={this.S:#0.##}, V={this.V:#0.##} ]";
+ }
+
+ ///
+ /// Indicates whether the current object is equal to another object of the same type.
+ ///
+ ///
+ /// True if the current object is equal to the parameter; otherwise, false.
+ ///
+ /// An object to compare with this object.
+ public bool Equals(Hsv other)
+ {
+ return this.H.Equals(other.H)
+ && this.S.Equals(other.S)
+ && this.V.Equals(other.V);
+ }
+ }
+}
diff --git a/src/ImageProcessor/Colors/YCbCr.cs b/src/ImageProcessor/Colors/YCbCr.cs
index 37be0765b..0c4a1b67b 100644
--- a/src/ImageProcessor/Colors/YCbCr.cs
+++ b/src/ImageProcessor/Colors/YCbCr.cs
@@ -27,24 +27,6 @@ namespace ImageProcessor
///
public static readonly YCbCr Empty = new YCbCr();
- ///
- /// Holds the Y luminance component.
- /// A value ranging between 0 and 255.
- ///
- public float Y { get; }
-
- ///
- /// Holds the Cb chroma component.
- /// A value ranging between 0 and 255.
- ///
- public float Cb { get; }
-
- ///
- /// Holds the Cr chroma component.
- /// A value ranging between 0 and 255.
- ///
- public float Cr { get; }
-
///
/// The epsilon for comparing floating point numbers.
///
@@ -63,6 +45,24 @@ namespace ImageProcessor
this.Cr = cr.Clamp(0, 255);
}
+ ///
+ /// Gets the Y luminance component.
+ /// A value ranging between 0 and 255.
+ ///
+ public float Y { get; }
+
+ ///
+ /// Gets the Cb chroma component.
+ /// A value ranging between 0 and 255.
+ ///
+ public float Cb { get; }
+
+ ///
+ /// Gets the Cr chroma component.
+ /// A value ranging between 0 and 255.
+ ///
+ public float Cr { get; }
+
///
/// Gets a value indicating whether this is empty.
///
@@ -203,10 +203,10 @@ namespace ImageProcessor
{
if (this.IsEmpty)
{
- return "YCbCrColor [ Empty ]";
+ return "YCbCr [ Empty ]";
}
- return $"YCbCrColor [ Y={this.Y:#0.##}, Cb={this.Cb:#0.##}, Cr={this.Cr:#0.##} ]";
+ return $"YCbCr [ Y={this.Y:#0.##}, Cb={this.Cb:#0.##}, Cr={this.Cr:#0.##} ]";
}
///
diff --git a/src/ImageProcessor/ImageProcessor.csproj b/src/ImageProcessor/ImageProcessor.csproj
index 8ef33006e..f74fc83d5 100644
--- a/src/ImageProcessor/ImageProcessor.csproj
+++ b/src/ImageProcessor/ImageProcessor.csproj
@@ -37,9 +37,10 @@
-
+
+
diff --git a/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs b/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs
index 1b6c994b8..788e33a60 100644
--- a/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs
+++ b/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs
@@ -23,42 +23,44 @@ namespace ImageProcessor.Tests
/// Tests the implicit conversion from to .
///
[Fact]
- [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Reviewed. Suppression is OK here.")]
- public void ColorToYCbCrColor()
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation",
+ Justification = "Reviewed. Suppression is OK here.")]
+ public void BgrToYCbCr()
{
// White
Bgra color = new Bgra(255, 255, 255, 255);
- YCbCr yCbCrColor = color;
+ YCbCr yCbCr = color;
- Assert.Equal(255, yCbCrColor.Y);
- Assert.Equal(128, yCbCrColor.Cb);
- Assert.Equal(128, yCbCrColor.Cr);
+ Assert.Equal(255, yCbCr.Y);
+ Assert.Equal(128, yCbCr.Cb);
+ Assert.Equal(128, yCbCr.Cr);
// Black
Bgra color2 = new Bgra(0, 0, 0, 255);
- YCbCr yCbCrColor2 = color2;
- Assert.Equal(0, yCbCrColor2.Y);
- Assert.Equal(128, yCbCrColor2.Cb);
- Assert.Equal(128, yCbCrColor2.Cr);
+ YCbCr yCbCr2 = color2;
+ Assert.Equal(0, yCbCr2.Y);
+ Assert.Equal(128, yCbCr2.Cb);
+ Assert.Equal(128, yCbCr2.Cr);
// Grey
Bgra color3 = new Bgra(128, 128, 128, 255);
- YCbCr yCbCrColor3 = color3;
- Assert.Equal(128, yCbCrColor3.Y);
- Assert.Equal(128, yCbCrColor3.Cb);
- Assert.Equal(128, yCbCrColor3.Cr);
+ YCbCr yCbCr3 = color3;
+ Assert.Equal(128, yCbCr3.Y);
+ Assert.Equal(128, yCbCr3.Cb);
+ Assert.Equal(128, yCbCr3.Cr);
}
///
/// Tests the implicit conversion from to .
///
[Fact]
- [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Reviewed. Suppression is OK here.")]
- public void YCbCrColorToColor()
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation",
+ Justification = "Reviewed. Suppression is OK here.")]
+ public void YCbCrToBgr()
{
// White
- YCbCr yCbCrColor = new YCbCr(255, 128, 128);
- Bgra color = yCbCrColor;
+ YCbCr yCbCr = new YCbCr(255, 128, 128);
+ Bgra color = yCbCr;
Assert.Equal(255, color.B);
Assert.Equal(255, color.G);
@@ -66,8 +68,8 @@ namespace ImageProcessor.Tests
Assert.Equal(255, color.A);
// Black
- YCbCr yCbCrColor2 = new YCbCr(0, 128, 128);
- Bgra color2 = yCbCrColor2;
+ YCbCr yCbCr2 = new YCbCr(0, 128, 128);
+ Bgra color2 = yCbCr2;
Assert.Equal(0, color2.B);
Assert.Equal(0, color2.G);
@@ -75,13 +77,38 @@ namespace ImageProcessor.Tests
Assert.Equal(255, color2.A);
// Grey
- YCbCr yCbCrColor3 = new YCbCr(128, 128, 128);
- Bgra color3 = yCbCrColor3;
+ YCbCr yCbCr3 = new YCbCr(128, 128, 128);
+ Bgra color3 = yCbCr3;
Assert.Equal(128, color3.B);
Assert.Equal(128, color3.G);
Assert.Equal(128, color3.R);
Assert.Equal(255, color3.A);
}
+
+ ///
+ /// Tests the implicit conversion from to .
+ ///
+ [Fact]
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation",
+ Justification = "Reviewed. Suppression is OK here.")]
+ public void BgrToHsv()
+ {
+ // White
+ Bgra color = new Bgra(255, 255, 255, 255);
+ Hsv hsv = color;
+
+ Assert.Equal(0, hsv.H);
+ Assert.Equal(0, hsv.S);
+ Assert.Equal(1, hsv.V);
+
+ // Dark moderate pink.
+ Bgra color2 = new Bgra(106, 64, 128, 255);
+ Hsv hsv2 = color2;
+
+ Assert.Equal(320.6, hsv2.H, 1);
+ Assert.Equal(50, hsv2.S, 1);
+ Assert.Equal(50.2, hsv2.V, 1);
+ }
}
}