From b68715af5751bcb2d2a32dbbadf5dfce23e1edde Mon Sep 17 00:00:00 2001 From: James South Date: Mon, 4 May 2015 21:21:55 +0100 Subject: [PATCH] Add HSV color Former-commit-id: 335da75d1e70ee70be838cd80524cc27d3d4d084 Former-commit-id: b9de3784cf6e4b64bb8d461564efbc48b1663b4c Former-commit-id: caab867d4d031398f3029354978426aea46cc1bb --- src/ImageProcessor/Colors/Hsv.cs | 284 ++++++++++++++++++ src/ImageProcessor/Colors/YCbCr.cs | 40 +-- src/ImageProcessor/ImageProcessor.csproj | 3 +- .../Colors/ColorConversionTests.cs | 71 +++-- 4 files changed, 355 insertions(+), 43 deletions(-) create mode 100644 src/ImageProcessor/Colors/Hsv.cs 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); + } } }