diff --git a/src/ImageSharp/Colors/Spaces/CieXyy.cs b/src/ImageSharp/Colors/Spaces/CieXyy.cs new file mode 100644 index 000000000..cf33c1473 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/CieXyy.cs @@ -0,0 +1,155 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Spaces +{ + using System; + using System.ComponentModel; + using System.Numerics; + + /// + /// Represents an CIE xyY 1931 color + /// + /// + public struct CieXyy : IColorVector, IEquatable, IAlmostEquatable + { + /// + /// Represents a that has X, Y, and Y values set to zero. + /// + public static readonly CieXyy Empty = default(CieXyy); + + /// + /// The backing vector for SIMD support. + /// + private readonly Vector3 backingVector; + + /// + /// Initializes a new instance of the struct. + /// + /// The x chroma component. + /// The y chroma component. + /// The y luminance component. + public CieXyy(float x, float y, float yl) + : this(new Vector3(x, y, yl)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the x, y, Y components. + public CieXyy(Vector3 vector) + : this() + { + // Not clamping as documentation about this space seems to indicate "usual" ranges + this.backingVector = vector; + } + + /// + /// Gets the X chrominance component. + /// A value usually ranging between 0 and 1. + /// + public float X => this.backingVector.X; + + /// + /// Gets the Y chrominance component. + /// A value usually ranging between 0 and 1. + /// + public float Y => this.backingVector.Y; + + /// + /// Gets the Y luminance component. + /// A value usually ranging between 0 and 1. + /// + public float Yl => this.backingVector.Z; + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => this.Equals(Empty); + + /// + public Vector3 Vector => this.backingVector; + + /// + /// 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 ==(CieXyy left, CieXyy 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 !=(CieXyy left, CieXyy right) + { + return !left.Equals(right); + } + + /// + public override int GetHashCode() + { + return this.backingVector.GetHashCode(); + } + + /// + public override string ToString() + { + if (this.IsEmpty) + { + return "CieXyy [ Empty ]"; + } + + return $"CieXyy [ X={this.X:#0.##}, Y={this.Y:#0.##}, Yl={this.Yl:#0.##} ]"; + } + + /// + public override bool Equals(object obj) + { + if (obj is CieXyy) + { + return this.Equals((CieXyy)obj); + } + + return false; + } + + /// + public bool Equals(CieXyy other) + { + return this.backingVector.Equals(other.backingVector); + } + + /// + public bool AlmostEquals(CieXyy other, float precision) + { + Vector3 result = Vector3.Abs(this.backingVector - other.backingVector); + + return result.X <= precision + && result.Y <= precision + && result.Z <= precision; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/CieXyz.cs b/src/ImageSharp/Colors/Spaces/CieXyz.cs index 2e4a73e2d..9bf68a36d 100644 --- a/src/ImageSharp/Colors/Spaces/CieXyz.cs +++ b/src/ImageSharp/Colors/Spaces/CieXyz.cs @@ -10,13 +10,13 @@ namespace ImageSharp.Colors.Spaces using System.Numerics; /// - /// Represents an CIE 1931 color - /// + /// Represents an CIE XYZ 1931 color + /// /// public struct CieXyz : IColorVector, IEquatable, IAlmostEquatable { /// - /// Represents a that has Y, Cb, and Cr values set to zero. + /// Represents a that has X, Y, and Z values set to zero. /// public static readonly CieXyz Empty = default(CieXyz); @@ -48,19 +48,19 @@ namespace ImageSharp.Colors.Spaces } /// - /// Gets the Y luminance component. + /// Gets the X component. A mix (a linear combination) of cone response curves chosen to be nonnegative. /// A value usually ranging between 0 and 1. /// public float X => this.backingVector.X; /// - /// Gets the Cb chroma component. + /// Gets the Y luminance component. /// A value usually ranging between 0 and 1. /// public float Y => this.backingVector.Y; /// - /// Gets the Cr chroma component. + /// Gets the Z component. Quasi-equal to blue stimulation, or the S cone response /// A value usually ranging between 0 and 1. /// public float Z => this.backingVector.Z; diff --git a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.CieLab.cs b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.CieLab.cs index 418366401..dfc5fbe4a 100644 --- a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.CieLab.cs +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.CieLab.cs @@ -90,6 +90,19 @@ namespace ImageSharp.Colors.Spaces.Conversion return this.ToCieLab(xyzColor); } + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public CieLab ToCieLab(CieXyy color) + { + Guard.NotNull(color, nameof(color)); + + CieXyz xyzColor = this.ToCieXyz(color); + return this.ToCieLab(xyzColor); + } + /// /// Converts a into a /// diff --git a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.CieXyy.cs b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.CieXyy.cs new file mode 100644 index 000000000..692b13f94 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.CieXyy.cs @@ -0,0 +1,113 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Spaces.Conversion +{ + using ImageSharp.Colors.Spaces.Conversion.Implementation.CieXyy; + + /// + /// Converts between color spaces ensuring that the color is adapted using chromatic adaptation. + /// + public partial class ColorSpaceConverter + { + private static readonly CieXyzAndCieXyyConverter CieXyzAndCieXyyConverter = new CieXyzAndCieXyyConverter(); + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public CieXyy ToCieXyy(CieXyz color) + { + Guard.NotNull(color, nameof(color)); + + return CieXyzAndCieXyyConverter.Convert(color); + } + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public CieXyy ToCieXyy(CieLab color) + { + Guard.NotNull(color, nameof(color)); + + CieXyz xyzColor = this.ToCieXyz(color); + + return this.ToCieXyy(xyzColor); + } + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public CieXyy ToCieXyy(CieLch color) + { + Guard.NotNull(color, nameof(color)); + + CieXyz xyzColor = this.ToCieXyz(color); + + return this.ToCieXyy(xyzColor); + } + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public CieXyy ToCieXyy(HunterLab color) + { + Guard.NotNull(color, nameof(color)); + + CieXyz xyzColor = this.ToCieXyz(color); + + return this.ToCieXyy(xyzColor); + } + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public CieXyy ToCieXyy(LinearRgb color) + { + Guard.NotNull(color, nameof(color)); + + CieXyz xyzColor = this.ToCieXyz(color); + + return this.ToCieXyy(xyzColor); + } + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public CieXyy ToCieXyy(Rgb color) + { + Guard.NotNull(color, nameof(color)); + + CieXyz xyzColor = this.ToCieXyz(color); + + return this.ToCieXyy(xyzColor); + } + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public CieXyy ToCieXyy(Lms color) + { + Guard.NotNull(color, nameof(color)); + + CieXyz xyzColor = this.ToCieXyz(color); + + return this.ToCieXyy(xyzColor); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.CieXyz.cs b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.CieXyz.cs index b5a708dec..c04f1093b 100644 --- a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.CieXyz.cs +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.CieXyz.cs @@ -41,6 +41,19 @@ namespace ImageSharp.Colors.Spaces.Conversion return adapted; } + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public CieXyz ToCieXyz(CieXyy color) + { + Guard.NotNull(color, nameof(color)); + + // Conversion + return CieXyzAndCieXyyConverter.Convert(color); + } + /// /// Converts a into a /// diff --git a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.HunterLab.cs b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.HunterLab.cs index 33fad16c7..961b43fd2 100644 --- a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.HunterLab.cs +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.HunterLab.cs @@ -94,5 +94,18 @@ namespace ImageSharp.Colors.Spaces.Conversion CieXyz xyzColor = this.ToCieXyz(color); return this.ToHunterLab(xyzColor); } + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public HunterLab ToHunterLab(CieXyy color) + { + Guard.NotNull(color, nameof(color)); + + CieXyz xyzColor = this.ToCieXyz(color); + return this.ToHunterLab(xyzColor); + } } } \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.LinearRgb.cs b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.LinearRgb.cs index 1cf577d11..c7389918e 100644 --- a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.LinearRgb.cs +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.LinearRgb.cs @@ -100,6 +100,19 @@ namespace ImageSharp.Colors.Spaces.Conversion return this.ToLinearRgb(xyzColor); } + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public LinearRgb ToLinearRgb(CieXyy color) + { + Guard.NotNull(color, nameof(color)); + + CieXyz xyzColor = this.ToCieXyz(color); + return this.ToLinearRgb(xyzColor); + } + /// /// Gets the correct converter for the given rgb working space. /// diff --git a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.Lms.cs b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.Lms.cs index de9f765ce..74a6dd639 100644 --- a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.Lms.cs +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.Lms.cs @@ -89,5 +89,18 @@ namespace ImageSharp.Colors.Spaces.Conversion CieXyz xyzColor = this.ToCieXyz(color); return this.ToLms(xyzColor); } + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public Lms ToLms(CieXyy color) + { + Guard.NotNull(color, nameof(color)); + + CieXyz xyzColor = this.ToCieXyz(color); + return this.ToLms(xyzColor); + } } } \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.Rgb.cs b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.Rgb.cs index 879f915dc..50b79bd2b 100644 --- a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.Rgb.cs +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.Rgb.cs @@ -94,5 +94,18 @@ namespace ImageSharp.Colors.Spaces.Conversion CieXyz xyzColor = this.ToCieXyz(color); return this.ToRgb(xyzColor); } + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public Rgb ToRgb(CieXyy color) + { + Guard.NotNull(color, nameof(color)); + + CieXyz xyzColor = this.ToCieXyz(color); + return this.ToRgb(xyzColor); + } } } \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieXyy/CieXyzAndCieXyyConverter.cs b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieXyy/CieXyzAndCieXyyConverter.cs new file mode 100644 index 000000000..dedb95ff8 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieXyy/CieXyzAndCieXyyConverter.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Spaces.Conversion.Implementation.CieXyy +{ + using ImageSharp.Colors.Spaces; + + /// + /// Color converter between CIE XYZ and CIE xyY + /// for formulas. + /// + internal class CieXyzAndCieXyyConverter : IColorConversion, IColorConversion + { + /// + public CieXyy Convert(CieXyz input) + { + DebugGuard.NotNull(input, nameof(input)); + + float x = input.X / (input.X + input.Y + input.Z); + float y = input.Y / (input.X + input.Y + input.Z); + + if (float.IsNaN(x) || float.IsNaN(y)) + { + return new CieXyy(0, 0, input.Y); + } + + return new CieXyy(x, y, input.Y); + } + + /// + public CieXyz Convert(CieXyy input) + { + DebugGuard.NotNull(input, nameof(input)); + + if (MathF.Abs(input.Y) < Constants.Epsilon) + { + return new CieXyz(0, 0, input.Yl); + } + + float x = (input.X * input.Yl) / input.Y; + float y = input.Yl; + float z = ((1 - input.X - input.Y) * y) / input.Y; + + return new CieXyz(x, y, z); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index a19bed604..fe37a26eb 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -33,6 +33,9 @@ + + + All diff --git a/tests/ImageSharp.Tests/Colors/Colorspaces/CieXyzAndCieXyyConversionTest.cs b/tests/ImageSharp.Tests/Colors/Colorspaces/CieXyzAndCieXyyConversionTest.cs new file mode 100644 index 000000000..bf892c9cc --- /dev/null +++ b/tests/ImageSharp.Tests/Colors/Colorspaces/CieXyzAndCieXyyConversionTest.cs @@ -0,0 +1,61 @@ +namespace ImageSharp.Tests.Colors.Colorspaces +{ + using System.Collections.Generic; + + using ImageSharp.Colors.Spaces; + using ImageSharp.Colors.Spaces.Conversion; + + using Xunit; + + /// + /// Tests - conversions. + /// + /// + /// Test data generated using: + /// + /// + public class CieXyzAndCieXyyConversionTest + { + private static readonly IEqualityComparer FloatRoundingComparer = new FloatRoundingComparer(4); + + private static readonly ColorSpaceConverter Converter = new ColorSpaceConverter(); + + [Theory] + [InlineData(0.436075, 0.222504, 0.013932, 0.648427, 0.330856, 0.222504)] + [InlineData(0.964220, 1.000000, 0.825210, 0.345669, 0.358496, 1.000000)] + [InlineData(0.434119, 0.356820, 0.369447, 0.374116, 0.307501, 0.356820)] + [InlineData(0, 0, 0, 0.538842, 0.000000, 0.000000)] + public void Convert_xyY_to_XYZ(float xyzX, float xyzY, float xyzZ, float x, float y, float yl) + { + // Arrange + CieXyy input = new CieXyy(x, y, yl); + + // Act + CieXyz output = Converter.ToCieXyz(input); + + // Assert + Assert.Equal(xyzX, output.X, FloatRoundingComparer); + Assert.Equal(xyzY, output.Y, FloatRoundingComparer); + Assert.Equal(xyzZ, output.Z, FloatRoundingComparer); + } + + [Theory] + [InlineData(0.436075, 0.222504, 0.013932, 0.648427, 0.330856, 0.222504)] + [InlineData(0.964220, 1.000000, 0.825210, 0.345669, 0.358496, 1.000000)] + [InlineData(0.434119, 0.356820, 0.369447, 0.374116, 0.307501, 0.356820)] + [InlineData(0.231809, 0, 0.077528, 0.749374, 0.000000, 0.000000)] + public void Convert_XYZ_to_xyY(float xyzX, float xyzY, float xyzZ, float x, float y, float yl) + { + // Arrange + CieXyz input = new CieXyz(xyzX, xyzY, xyzZ); + + // Act + CieXyy output = Converter.ToCieXyy(input); + + // Assert + Assert.Equal(x, output.X, FloatRoundingComparer); + Assert.Equal(y, output.Y, FloatRoundingComparer); + Assert.Equal(yl, output.Yl, FloatRoundingComparer); + } + } +} diff --git a/tests/ImageSharp.Tests/Colors/Colorspaces/ColorSpaceEqualityTests.cs b/tests/ImageSharp.Tests/Colors/Colorspaces/ColorSpaceEqualityTests.cs index 1ea4d3ce5..c82fc6d80 100644 --- a/tests/ImageSharp.Tests/Colors/Colorspaces/ColorSpaceEqualityTests.cs +++ b/tests/ImageSharp.Tests/Colors/Colorspaces/ColorSpaceEqualityTests.cs @@ -16,26 +16,49 @@ namespace ImageSharp.Tests.Colors /// public class ColorSpaceEqualityTests { + public static readonly TheoryData EmptyData = + new TheoryData + { + CieLab.Empty, + CieLch.Empty, + CieXyz.Empty, + CieXyy.Empty, + HunterLab.Empty, + Lms.Empty, + LinearRgb.Empty, + Rgb.Empty, + }; + public static readonly TheoryData EqualityData = - new TheoryData() - { + new TheoryData + { { new CieLab(Vector3.One), new CieLab(Vector3.One), typeof(CieLab) }, { new CieLch(Vector3.One), new CieLch(Vector3.One), typeof(CieLch) }, { new CieXyz(Vector3.One), new CieXyz(Vector3.One), typeof(CieXyz) }, + { new CieXyy(Vector3.One), new CieXyy(Vector3.One), typeof(CieXyy) }, + { new HunterLab(Vector3.One), new HunterLab(Vector3.One), typeof(HunterLab) }, + { new Lms(Vector3.One), new Lms(Vector3.One), typeof(Lms) }, + { new LinearRgb(Vector3.One), new LinearRgb(Vector3.One), typeof(LinearRgb) }, + { new Rgb(Vector3.One), new Rgb(Vector3.One), typeof(Rgb) }, }; public static readonly TheoryData NotEqualityDataNulls = - new TheoryData() - { + new TheoryData + { // Valid object against null { new CieLab(Vector3.One), null, typeof(CieLab) }, { new CieLch(Vector3.One), null, typeof(CieLch) }, { new CieXyz(Vector3.One), null, typeof(CieXyz) }, + { new CieXyy(Vector3.One), null, typeof(CieXyy) }, + { new HunterLab(Vector3.One), null, typeof(HunterLab) }, + { new Lms(Vector3.One), null, typeof(Lms) }, + { new LinearRgb(Vector3.One), null, typeof(LinearRgb) }, + { new Rgb(Vector3.One), null, typeof(Rgb) }, }; public static readonly TheoryData NotEqualityDataDifferentObjects = - new TheoryData() - { + new TheoryData + { // Valid objects of different types but not equal { new CieLab(Vector3.One), new CieLch(Vector3.Zero), null }, { new CieXyz(Vector3.One), new HunterLab(Vector3.Zero), null }, @@ -44,17 +67,22 @@ namespace ImageSharp.Tests.Colors }; public static readonly TheoryData NotEqualityData = - new TheoryData() - { + new TheoryData + { // Valid objects of the same type but not equal { new CieLab(Vector3.One), new CieLab(Vector3.Zero), typeof(CieLab) }, { new CieLch(Vector3.One), new CieLch(Vector3.Zero), typeof(CieLch) }, { new CieXyz(Vector3.One), new CieXyz(Vector3.Zero), typeof(CieXyz) }, + { new CieXyy(Vector3.One), new CieXyy(Vector3.Zero), typeof(CieXyy) }, + { new HunterLab(Vector3.One), new HunterLab(Vector3.Zero), typeof(HunterLab) }, + { new Lms(Vector3.One), new Lms(Vector3.Zero), typeof(Lms) }, + { new LinearRgb(Vector3.One), new LinearRgb(Vector3.Zero), typeof(LinearRgb) }, + { new Rgb(Vector3.One), new Rgb(Vector3.Zero), typeof(Rgb) }, }; public static readonly TheoryData AlmostEqualsData = - new TheoryData() - { + new TheoryData + { { new CieLab(0F, 0F, 0F), new CieLab(0F, 0F, 0F), typeof(CieLab), 0F }, { new CieLab(0F, 0F, 0F), new CieLab(0F, 0F, 0F), typeof(CieLab), .001F }, { new CieLab(0F, 0F, 0F), new CieLab(0F, 0F, 0F), typeof(CieLab), .0001F }, @@ -69,8 +97,8 @@ namespace ImageSharp.Tests.Colors }; public static readonly TheoryData AlmostNotEqualsData = - new TheoryData() - { + new TheoryData + { { new CieLab(0F, 0F, 0F), new CieLab(0.1F, 0F, 0F), typeof(CieLab), .001F }, { new CieLab(0F, 0F, 0F), new CieLab(0F, 0.1F, 0F), typeof(CieLab), .001F }, { new CieLab(0F, 0F, 0F), new CieLab(0F, 0F, 0.1F), typeof(CieLab), .001F }, @@ -79,6 +107,17 @@ namespace ImageSharp.Tests.Colors { new CieXyz(380F, 380F, 380F), new CieXyz(380F, 380F, 380.1F), typeof(CieXyz), .001F }, }; + [Theory] + [MemberData(nameof(EmptyData))] + public void Equality(IColorVector color) + { + // Act + bool equal = color.Vector.Equals(Vector3.Zero); + + // Assert + Assert.True(equal); + } + [Theory] [MemberData(nameof(EqualityData))] public void Equality(object first, object second, Type type)