diff --git a/src/ImageProcessor/Colors/ColorTransforms.cs b/src/ImageProcessor/Colors/ColorTransforms.cs index 5c7dc99ef..e085f35e8 100644 --- a/src/ImageProcessor/Colors/ColorTransforms.cs +++ b/src/ImageProcessor/Colors/ColorTransforms.cs @@ -173,6 +173,42 @@ namespace ImageProcessor return new Color(r, g, b); } + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// The instance of to convert. + /// + /// An instance of . + /// + public static implicit operator Color(CieLab cieLabColor) + { + // First convert back to XYZ... + float y = (cieLabColor.L + 16F) / 116F; + float x = cieLabColor.A / 500F + y; + float z = y - cieLabColor.B / 200F; + + float x3 = x * x * x; + float y3 = y * y * y; + float z3 = z * z * z; + + x = (x3 > 0.008856F) ? x3 : (x - 16F / 116F) / 7.787F; + y = (cieLabColor.L > 0.008856F * 903.3F) ? y3 : (cieLabColor.L / 903.3F); + z = (z3 > 0.008856F) ? z3 : (z - 16F / 116F) / 7.787F; + + x *= 0.95047F; + //y *= 1F; + z *= 1.08883F; + + // Then XYZ to RGB (multiplication by 100 was done above already) + + float r = (x * 3.2406F) + (y * -1.5372F) + (z * -0.4986F); + float g = (x * -0.9689F) + (y * 1.8758F) + (z * 0.0415F); + float b = (x * 0.0557F) + (y * -0.2040F) + (z * 1.0570F); + + return Color.Compand(new Color(r, g, b)); + } + /// /// Gets the color component from the given values. /// diff --git a/src/ImageProcessor/Colors/Colorspaces/CieLab.cs b/src/ImageProcessor/Colors/Colorspaces/CieLab.cs new file mode 100644 index 000000000..6098d40f8 --- /dev/null +++ b/src/ImageProcessor/Colors/Colorspaces/CieLab.cs @@ -0,0 +1,214 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessor +{ + using System; + using System.ComponentModel; + using System.Numerics; + + /// + /// Represents an CIE LAB 1976 color. + /// + public struct CieLab : IEquatable + { + /// + /// Represents a that has L, A, B values set to zero. + /// + public static readonly CieLab Empty = default(CieLab); + + /// + /// The epsilon for comparing floating point numbers. + /// + private const float Epsilon = 0.0001f; + + /// + /// The backing vector for SIMD support. + /// + private Vector3 backingVector; + + /// + /// Initializes a new instance of the struct. + /// + /// The lightness dimension. + /// The a (green - magenta) component. + /// The b (blue - yellow) component. + public CieLab(float l, float a, float b) + : this() + { + this.backingVector.X = ClampL(l); + this.backingVector.Y = ClampAB(a); + this.backingVector.Z = ClampAB(b); + } + + /// + /// Gets the lightness dimension. + /// A value ranging between 0 (black), 100 (diffuse white) or higher (specular white). + /// + public float L => this.backingVector.X; + + /// + /// Gets the a color component. + /// Negative is green, positive magenta. + /// + public float A => this.backingVector.Y; + + /// + /// Gets the b color component. + /// Negative is blue, positive is yellow + /// + public float B => this.backingVector.Z; + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => this.backingVector.Equals(default(Vector4)); + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// + /// The instance of to convert. + /// + /// + /// An instance of . + /// + public static implicit operator CieLab(Color color) + { + // First convert to CIE XYZ + color = Color.InverseCompand(color.Limited); + + float x = (color.R * 0.4124F) + (color.G * 0.3576F) + (color.B * 0.1805F); + float y = (color.R * 0.2126F) + (color.G * 0.7152F) + (color.B * 0.0722F); + float z = (color.R * 0.0193F) + (color.G * 0.1192F) + (color.B * 0.9505F); + + // Now to LAB + x /= 0.95047F; + //y /= 1F; + z /= 1.08883F; + + x = x > 0.008856F ? (float) Math.Pow(x, 1F / 3F) : (903.3F * x + 16F) / 116F; + y = y > 0.008856F ? (float) Math.Pow(y, 1F / 3F) : (903.3F * y + 16F) / 116F; + z = z > 0.008856F ? (float) Math.Pow(z, 1F / 3F) : (903.3F * z + 16F) / 116F; + + float l = Math.Max(0, (116F * y) - 16F); + float a = 500F * (x - y); + float b = 200F * (y - z); + + return new CieLab(l, a, b); + } + + /// + /// Compares two objects for equality. + /// + /// + /// 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 ==(CieLab left, CieLab right) + { + return left.Equals(right); + } + + /// + /// Compares two objects for inequality + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + public static bool operator !=(CieLab left, CieLab right) + { + return !left.Equals(right); + } + + /// + public override bool Equals(object obj) + { + if (obj is CieLab) + { + CieLab color = (CieLab)obj; + + return this.backingVector == color.backingVector; + } + + return false; + } + + /// + public override int GetHashCode() + { + return GetHashCode(this); + } + + /// + public override string ToString() + { + if (this.IsEmpty) + { + return "CieLab [Empty]"; + } + + return $"CieLab [ L={this.L:#0.##}, A={this.A:#0.##}, B={this.B:#0.##}]"; + } + + /// + public bool Equals(CieLab other) + { + return this.backingVector.Equals(other.backingVector); + } + + /// + /// Checks the range for lightness. + /// + /// + /// The value to check. + /// + /// + /// The sanitized . + /// + private static float ClampL(float value) + { + return value.Clamp(0, 100); + } + + /// + /// Checks the range for components A or B. + /// + /// + /// The value to check. + /// + /// + /// The sanitized . + /// + private static float ClampAB(float value) + { + return value.Clamp(-100, 100); + } + + /// + /// Returns the hash code for this instance. + /// + /// + /// The instance of to return the hash code for. + /// + /// + /// A 32-bit signed integer that is the hash code for this instance. + /// + private static int GetHashCode(CieLab color) => color.backingVector.GetHashCode(); + } +} diff --git a/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs b/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs index 6551b17c7..b219cd123 100644 --- a/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs +++ b/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs @@ -334,5 +334,85 @@ namespace ImageProcessor.Tests Assert.Equal(color4, (Color)cmyk4); } } + + /// + /// Tests the implicit conversion from to . + /// Comparison values obtained from + /// http://colormine.org/convert/rgb-to-lab + /// + [Fact] + public void ColorToCieLab() + { + // White + Color color = new Color(1, 1, 1); + CieLab cielab = color; + + Assert.Equal(100, cielab.L, 3); + Assert.Equal(0.005, cielab.A, 3); + Assert.Equal(-0.010, cielab.B, 3); + + // Black + Color color2 = new Color(0, 0, 0); + CieLab cielab2 = color2; + Assert.Equal(0, cielab2.L, 3); + Assert.Equal(0, cielab2.A, 3); + Assert.Equal(0, cielab2.B, 3); + + //// Grey + Color color3 = new Color(128 / 255f, 128 / 255f, 128 / 255f); + CieLab cielab3 = color3; + Assert.Equal(53.585, cielab3.L, 3); + Assert.Equal(0.003, cielab3.A, 3); + Assert.Equal(-0.006, cielab3.B, 3); + + //// Cyan + Color color4 = new Color(0, 1, 1); + CieLab cielab4 = color4; + Assert.Equal(91.117, cielab4.L, 3); + Assert.Equal(-48.080, cielab4.A, 3); + Assert.Equal(-14.138, cielab4.B, 3); + } + + /// + /// Tests the implicit conversion from to . + /// + /// Comparison values obtained from + /// http://colormine.org/convert/rgb-to-lab + [Fact] + public void CieLabToColor() + { + // Dark moderate pink. + CieLab cielab = new CieLab(36.5492f, 33.3173f, -12.0615f); + Color color = cielab; + + Assert.Equal(color.R, 128 / 255f, 3); + Assert.Equal(color.G, 64 / 255f, 3); + Assert.Equal(color.B, 106 / 255f, 3); + + // Ochre + CieLab cielab2 = new CieLab(58.1758f, 27.3399f, 56.8240f); + Color color2 = cielab2; + + Assert.Equal(color2.R, 204 / 255f, 3); + Assert.Equal(color2.G, 119 / 255f, 3); + Assert.Equal(color2.B, 34 / 255f, 3); + + //// White + CieLab cielab3 = new CieLab(0, 0, 0); + Color color3 = cielab3; + + Assert.Equal(color3.R, 0f, 3); + Assert.Equal(color3.G, 0f, 3); + Assert.Equal(color3.B, 0f, 3); + + //// Check others. + Random random = new Random(0); + for (int i = 0; i < 1000; i++) + { + Color color4 = new Color(random.Next(1), random.Next(1), random.Next(1)); + CieLab cielab4 = color4; + Assert.Equal(color4, (Color)cielab4); + } + } } }