diff --git a/src/ImageSharp/Colors/Spaces/CieLab.cs b/src/ImageSharp/Colors/Spaces/CieLab.cs new file mode 100644 index 0000000000..e5edfcc793 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/CieLab.cs @@ -0,0 +1,187 @@ +// +// 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 LAB 1976 color. + /// + /// + public struct CieLab : IColorVector, IEquatable, IAlmostEquatable + { + /// + /// D50 standard illuminant. + /// Used when reference white is not specified explicitly. + /// + public static readonly CieXyz DefaultWhitePoint = Illuminants.D50; + + /// + /// Represents a that has L, A, B values set to zero. + /// + public static readonly CieLab Empty = default(CieLab); + + /// + /// The backing vector for SIMD support. + /// + private readonly Vector3 backingVector; + + /// + /// Initializes a new instance of the struct. + /// + /// The lightness dimension. + /// The a (green - magenta) component. + /// The b (blue - yellow) component. + /// Uses as white point. + public CieLab(float l, float a, float b) + : this(new Vector3(l, a, b), DefaultWhitePoint) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The lightness dimension. + /// The a (green - magenta) component. + /// The b (blue - yellow) component. + /// The reference white point. + public CieLab(float l, float a, float b, CieXyz whitePoint) + : this(new Vector3(l, a, b), whitePoint) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the l, a, b components. + /// Uses as white point. + public CieLab(Vector3 vector) + : this(vector, DefaultWhitePoint) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the l a b components. + /// The reference white point. + public CieLab(Vector3 vector, CieXyz whitePoint) + : this() + { + this.backingVector = vector; + this.WhitePoint = whitePoint; + } + + public CieXyz WhitePoint { get; } + + /// + /// 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. + /// A value ranging from -100 to 100. Negative is green, positive magenta. + /// + public float A => this.backingVector.Y; + + /// + /// Gets the b color component. + /// A value ranging from -100 to 100. 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.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 ==(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 int GetHashCode() + { + return this.backingVector.GetHashCode(); + } + + /// + 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 override bool Equals(object obj) + { + if (obj is CieLab) + { + return this.Equals((CieLab)obj); + } + + return false; + } + + /// + public bool Equals(CieLab other) + { + return this.backingVector.Equals(other.backingVector); + } + + /// + public bool AlmostEquals(CieLab 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 new file mode 100644 index 0000000000..2e4a73e2da --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/CieXyz.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 1931 color + /// + /// + public struct CieXyz : IColorVector, IEquatable, IAlmostEquatable + { + /// + /// Represents a that has Y, Cb, and Cr values set to zero. + /// + public static readonly CieXyz Empty = default(CieXyz); + + /// + /// The backing vector for SIMD support. + /// + private readonly Vector3 backingVector; + + /// + /// Initializes a new instance of the struct. + /// + /// X is a mix (a linear combination) of cone response curves chosen to be nonnegative + /// The y luminance component. + /// Z is quasi-equal to blue stimulation, or the S cone of the human eye. + public CieXyz(float x, float y, float z) + : this(new Vector3(x, y, z)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the x, y, z components. + public CieXyz(Vector3 vector) + : this() + { + // Not clamping as documentation about this space seems to indicate "usual" ranges + this.backingVector = vector; + } + + /// + /// Gets the Y luminance component. + /// A value usually ranging between 0 and 1. + /// + public float X => this.backingVector.X; + + /// + /// Gets the Cb chroma component. + /// A value usually ranging between 0 and 1. + /// + public float Y => this.backingVector.Y; + + /// + /// Gets the Cr chroma component. + /// A value usually ranging between 0 and 1. + /// + public float Z => 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 ==(CieXyz left, CieXyz 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 !=(CieXyz left, CieXyz right) + { + return !left.Equals(right); + } + + /// + public override int GetHashCode() + { + return this.backingVector.GetHashCode(); + } + + /// + public override string ToString() + { + if (this.IsEmpty) + { + return "CieXyz [ Empty ]"; + } + + return $"CieXyz [ X={this.X:#0.##}, Y={this.Y:#0.##}, Z={this.Z:#0.##} ]"; + } + + /// + public override bool Equals(object obj) + { + if (obj is CieXyz) + { + return this.Equals((CieXyz)obj); + } + + return false; + } + + /// + public bool Equals(CieXyz other) + { + return this.backingVector.Equals(other.backingVector); + } + + /// + public bool AlmostEquals(CieXyz 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/Conversion/CieConstants.cs b/src/ImageSharp/Colors/Spaces/Conversion/CieConstants.cs new file mode 100644 index 0000000000..2134ea214d --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/CieConstants.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Conversion +{ + /// + /// Constants use for Cie conversion calculations + /// + /// + internal static class CieConstants + { + /// + /// 216F / 24389F + /// + public const float Epsilon = 0.008856452F; + + /// + /// 24389F / 27F + /// + public const float Kappa = 903.2963F; + } +} diff --git a/src/ImageSharp/Colors/Spaces/Conversion/ColorConverter.Adapt.cs b/src/ImageSharp/Colors/Spaces/Conversion/ColorConverter.Adapt.cs new file mode 100644 index 0000000000..fc768d3ea1 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorConverter.Adapt.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Conversion +{ + using System; + using Spaces; + + /// + /// Converts between color spaces ensuring that the color is adapted using chromatic adaptation. + /// + public partial class ColorConverter + { + /// + /// Performs chromatic adaptation of given XYZ color. + /// Target white point is . + /// + public CieXyz Adapt(CieXyz color, CieXyz sourceWhitePoint) + { + Guard.NotNull(color, nameof(color)); + Guard.NotNull(sourceWhitePoint, nameof(sourceWhitePoint)); + + if (!this.IsChromaticAdaptationPerformed) + { + throw new InvalidOperationException("Cannot perform chromatic adaptation, provide a chromatic adaptation method and white point."); + } + + return this.ChromaticAdaptation.Transform(color, sourceWhitePoint, this.WhitePoint); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/ColorConverter.CieLab.cs b/src/ImageSharp/Colors/Spaces/Conversion/ColorConverter.CieLab.cs new file mode 100644 index 0000000000..8ccefc5326 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorConverter.CieLab.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Conversion +{ + using Implementation; + using Spaces; + + /// + /// Converts between color spaces ensuring that the color is adapted using chromatic adaptation. + /// + public partial class ColorConverter + { + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public CieLab ToCieLab(CieXyz color) + { + Guard.NotNull(color, nameof(color)); + + // Adaptation + CieXyz adapted = !this.WhitePoint.Equals(this.TargetLabWhitePoint) && this.IsChromaticAdaptationPerformed + ? this.ChromaticAdaptation.Transform(color, this.WhitePoint, this.TargetLabWhitePoint) + : color; + + // Conversion + CieXyzToCieLabConverter converter = new CieXyzToCieLabConverter(this.TargetLabWhitePoint); + return converter.Convert(adapted); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/ColorConverter.CieXyz.cs b/src/ImageSharp/Colors/Spaces/Conversion/ColorConverter.CieXyz.cs new file mode 100644 index 0000000000..66f3b25e77 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorConverter.CieXyz.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Conversion +{ + using Implementation; + using Spaces; + + /// + /// Converts between color spaces ensuring that the color is adapted using chromatic adaptation. + /// + public partial class ColorConverter + { + private static readonly CieLabToCieXyzConverter CieLabToCieXyzConverter = new CieLabToCieXyzConverter(); + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public CieXyz ToCieXyz(CieLab color) + { + Guard.NotNull(color, nameof(color)); + + // Conversion + + CieXyz unadapted = CieLabToCieXyzConverter.Convert(color); + + // Adaptation + CieXyz adapted = color.WhitePoint.Equals(this.WhitePoint) || !this.IsChromaticAdaptationPerformed + ? unadapted + : this.Adapt(unadapted, color.WhitePoint); + + return adapted; + } + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public CieXyz ToCieXyz(Lms color) + { + Guard.NotNull(color, nameof(color)); + + // Conversion + return this.cachedCieXyzAndLmsConverter.Convert(color); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/ColorConverter.Lms.cs b/src/ImageSharp/Colors/Spaces/Conversion/ColorConverter.Lms.cs new file mode 100644 index 0000000000..12d4ca9432 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorConverter.Lms.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Conversion +{ + using Implementation; + using Spaces; + + /// + /// Converts between color spaces ensuring that the color is adapted using chromatic adaptation. + /// + public partial class ColorConverter + { + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public Lms ToLms(CieXyz color) + { + Guard.NotNull(color, nameof(color)); + + // Conversion + return this.cachedCieXyzAndLmsConverter.Convert(color); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/ColorConverter.cs b/src/ImageSharp/Colors/Spaces/Conversion/ColorConverter.cs new file mode 100644 index 0000000000..93d04e7e19 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorConverter.cs @@ -0,0 +1,81 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Conversion +{ + using System.Numerics; + using Implementation; + using Spaces; + + /// + /// Converts between color spaces ensuring that the color is adapted using chromatic adaptation. + /// + public partial class ColorConverter + { + private Matrix4x4 transformationMatrix; + private CieXyzAndLmsConverter cachedCieXyzAndLmsConverter; + + /// + /// The default whitepoint used for converting to CieLab + /// + public static readonly CieXyz DefaultWhitePoint = Illuminants.D65; + + /// + /// Initializes a new instance of the class. + /// + public ColorConverter() + { + // Note the order here this is important. + this.WhitePoint = DefaultWhitePoint; + this.LmsAdaptationMatrix = CieXyzAndLmsConverter.DefaultTransformationMatrix; + this.ChromaticAdaptation = new VonKriesChromaticAdaptation(this.cachedCieXyzAndLmsConverter, this.cachedCieXyzAndLmsConverter); + this.TargetLabWhitePoint = CieLab.DefaultWhitePoint; + } + + /// + /// Gets or sets the white point used for chromatic adaptation in conversions from/to XYZ color space. + /// When null, no adaptation will be performed. + /// + public CieXyz WhitePoint { get; set; } + + /// + /// Gets or sets the white point used *when creating* Lab/LChab colors. (Lab/LChab colors on the input already contain the white point information) + /// Defaults to: . + /// + public CieXyz TargetLabWhitePoint { get; set; } + + /// + /// Gets or sets the chromatic adaptation method used. When null, no adaptation will be performed. + /// + public IChromaticAdaptation ChromaticAdaptation { get; set; } + + /// + /// Gets or sets transformation matrix used in conversion to , + /// also used in the default Von Kries Chromatic Adaptation method. + /// + public Matrix4x4 LmsAdaptationMatrix + { + get { return this.transformationMatrix; } + set + { + this.transformationMatrix = value; + + if (this.cachedCieXyzAndLmsConverter == null) + { + this.cachedCieXyzAndLmsConverter = new CieXyzAndLmsConverter(value); + } + else + { + this.cachedCieXyzAndLmsConverter.TransformationMatrix = value; + } + } + } + + /// + /// Gets a value indicating whether chromatic adaptation has been performed. + /// + private bool IsChromaticAdaptationPerformed => this.ChromaticAdaptation != null; + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/IChromaticAdaptation.cs b/src/ImageSharp/Colors/Spaces/Conversion/IChromaticAdaptation.cs new file mode 100644 index 0000000000..d97d1bd1c6 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/IChromaticAdaptation.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Conversion +{ + using Spaces; + + /// + /// Chromatic adaptation. + /// A linear transformation of a source color (XS, YS, ZS) into a destination color (XD, YD, ZD) by a linear transformation [M] + /// which is dependent on the source reference white (XWS, YWS, ZWS) and the destination reference white (XWD, YWD, ZWD). + /// + public interface IChromaticAdaptation + { + /// + /// Performs a linear transformation of a source color in to the destination color. + /// + /// Doesn't crop the resulting color space coordinates (e. g. allows negative values for XYZ coordinates). + /// The source color. + /// The source white point. + /// The target white point. + /// The + CieXyz Transform(CieXyz sourceColor, CieXyz sourceWhitePoint, CieXyz targetWhitePoint); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/IColorConversion.cs b/src/ImageSharp/Colors/Spaces/Conversion/IColorConversion.cs new file mode 100644 index 0000000000..ad653da947 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/IColorConversion.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Conversion +{ + /// + /// Converts color between two color spaces. + /// + /// The input color type. + /// The result color type. + public interface IColorConversion + { + /// + /// Performs the conversion from the input to an instance of the output type. + /// + /// The input color instance. + /// The + TResult Convert(T input); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieLab/CieLabToCieXyzConverter.cs b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieLab/CieLabToCieXyzConverter.cs new file mode 100644 index 0000000000..1d9ab6269d --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieLab/CieLabToCieXyzConverter.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Conversion.Implementation +{ + using System; + using Spaces; + + /// + /// Converts from to . + /// + public class CieLabToCieXyzConverter : IColorConversion + { + /// + public CieXyz Convert(CieLab input) + { + Guard.NotNull(input, nameof(input)); + + // Conversion algorithm described here: http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html + float l = input.L, a = input.A, b = input.B; + float fy = (l + 16) / 116F; + float fx = a / 500F + fy; + float fz = fy - b / 200F; + + float fx3 = (float)Math.Pow(fx, 3D); + float fz3 = (float)Math.Pow(fz, 3D); + + float xr = fx3 > CieConstants.Epsilon ? fx3 : (116F * fx - 16F) / CieConstants.Kappa; + float yr = l > CieConstants.Kappa * CieConstants.Epsilon ? (float)Math.Pow((l + 16F) / 116F, 3D) : l / CieConstants.Kappa; + float zr = fz3 > CieConstants.Epsilon ? fz3 : (116F * fz - 16F) / CieConstants.Kappa; + + float wx = input.WhitePoint.X, wy = input.WhitePoint.Y, wz = input.WhitePoint.Z; + + // Avoids XYZ coordinates out range (restricted by 0 and XYZ reference white) + xr = xr.Clamp(0, 1F); + yr = yr.Clamp(0, 1F); + zr = zr.Clamp(0, 1F); + + float x = xr * wx; + float y = yr * wy; + float z = zr * wz; + + return new CieXyz(x, y, z); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieLab/CieXyzToCieLabConverter.cs b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieLab/CieXyzToCieLabConverter.cs new file mode 100644 index 0000000000..ddd7c4bb2e --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieLab/CieXyzToCieLabConverter.cs @@ -0,0 +1,59 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Conversion.Implementation +{ + using System; + using Spaces; + + /// + /// Converts from to . + /// + public class CieXyzToCieLabConverter : IColorConversion + { + /// + /// Initializes a new instance of the class. + /// + public CieXyzToCieLabConverter() + : this(CieLab.DefaultWhitePoint) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The target reference lab white point + public CieXyzToCieLabConverter(CieXyz labWhitePoint) + { + this.LabWhitePoint = labWhitePoint; + } + + /// + /// Gets the target reference whitepoint. When not set, is used. + /// + public CieXyz LabWhitePoint { get; } + + /// + public CieLab Convert(CieXyz input) + { + Guard.NotNull(input, nameof(input)); + + // Conversion algorithm described here: http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html + float wx = this.LabWhitePoint.X, wy = this.LabWhitePoint.Y, wz = this.LabWhitePoint.Z; + + float xr = input.X / wx, yr = input.Y / wy, zr = input.Z / wz; + + float fx = xr > CieConstants.Epsilon ? (float)Math.Pow(xr, 0.333333333333333D) : (CieConstants.Kappa * xr + 16F) / 116F; + float fy = yr > CieConstants.Epsilon ? (float)Math.Pow(yr, 0.333333333333333D) : (CieConstants.Kappa * yr + 16F) / 116F; + float fz = zr > CieConstants.Epsilon ? (float)Math.Pow(zr, 0.333333333333333D) : (CieConstants.Kappa * zr + 16F) / 116F; + + float l = (116F * fy) - 16F; + float a = 500F * (fx - fy); + float b = 200F * (fy - fz); + + return new CieLab(l, a, b, this.LabWhitePoint); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Lms/CieXyzAndLmsConverter.cs b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Lms/CieXyzAndLmsConverter.cs new file mode 100644 index 0000000000..69899e0dbd --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Lms/CieXyzAndLmsConverter.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +using System.Numerics; + +namespace ImageSharp.Colors.Conversion.Implementation +{ + using Spaces; + + public class CieXyzAndLmsConverter : IColorConversion, IColorConversion + { + /// + /// Default transformation matrix used, when no other is set. (Bradford) + /// + /// + public static readonly Matrix4x4 DefaultTransformationMatrix = LmsAdaptationMatrix.Bradford; + + private Matrix4x4 inverseTransformationMatrix; + private Matrix4x4 transformationMatrix; + + /// + /// Transformation matrix used for the conversion (definition of the cone response domain). + /// + /// + public Matrix4x4 TransformationMatrix + { + get { return this.transformationMatrix; } + internal set + { + this.transformationMatrix = value; + Matrix4x4.Invert(this.transformationMatrix, out this.inverseTransformationMatrix); + } + } + + /// + /// Initializes a new instance of the class. + /// + public CieXyzAndLmsConverter() + : this(DefaultTransformationMatrix) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Definition of the cone response domain (see ), + /// if not set will be used. + /// + public CieXyzAndLmsConverter(Matrix4x4 transformationMatrix) + { + this.TransformationMatrix = transformationMatrix; + } + + /// + public Lms Convert(CieXyz input) + { + Guard.NotNull(input, nameof(input)); + + Vector3 vector = Vector3.Transform(input.Vector, this.transformationMatrix); + return new Lms(vector); + } + + /// + public CieXyz Convert(Lms input) + { + Vector3 vector = Vector3.Transform(input.Vector, this.inverseTransformationMatrix); + return new CieXyz(vector); + } + } +} diff --git a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Lms/LmsAdaptationMatrix.cs b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Lms/LmsAdaptationMatrix.cs new file mode 100644 index 0000000000..fe3ded527a --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Lms/LmsAdaptationMatrix.cs @@ -0,0 +1,101 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +// ReSharper disable InconsistentNaming +namespace ImageSharp.Colors.Conversion.Implementation +{ + using System.Numerics; + + /// + /// AdaptionMatrix3X3 used for transformation from XYZ to LMS, defining the cone response domain. + /// Used in + /// + /// + /// AdaptionMatrix3X3 data obtained from: + /// Two New von Kries Based Chromatic Adaptation Transforms Found by Numerical Optimization + /// S. Bianco, R. Schettini + /// DISCo, Department of Informatics, Systems and Communication, University of Milan-Bicocca, viale Sarca 336, 20126 Milan, Italy + /// http://www.ivl.disco.unimib.it/papers2003/CRA-CAT.pdf + /// + public static class LmsAdaptationMatrix + { + /// + /// Von Kries chromatic adaptation transform matrix (Hunt-Pointer-Estevez adjusted for D65) + /// + public static readonly Matrix4x4 VonKriesHPEAdjusted + = new Matrix4x4() + { + M11 = 0.40024F, M12 = 0.7076F, M13 = -0.08081F, + M21 = -0.2263F, M22 = 1.16532F, M23 = 0.0457F, + M31 = 0, M32 = 0, M33 = 0.91822F, + M44 = 1F // Important for inverse transforms. + }; + + /// + /// Von Kries chromatic adaptation transform matrix (Hunt-Pointer-Estevez for equal energy) + /// + public static readonly Matrix4x4 VonKriesHPE + = new Matrix4x4 + { + M11 = 0.3897F, M12 = 0.6890F, M13 = -0.0787F, + M21 = -0.2298F, M22 = 1.1834F, M23 = 0.0464F, + M31 = 0, M32 = 0, M33 = 1F, + M44 = 1F + }; + + /// + /// XYZ scaling chromatic adaptation transform matrix + /// + public static readonly Matrix4x4 XYZScaling = Matrix4x4.Identity; + + /// + /// Bradford chromatic adaptation transform matrix (used in CMCCAT97) + /// + public static readonly Matrix4x4 Bradford + = new Matrix4x4 + { + M11 = 0.8951F, M12 = 0.2664F, M13 = -0.1614F, + M21 = -0.7502F, M22 = 1.7135F, M23 = 0.0367F, + M31 = 0.0389F, M32 = -0.0685F, M33 = 1.0296F, + M44 = 1F + }; + + /// + /// Spectral sharpening and the Bradford transform + /// + public static readonly Matrix4x4 BradfordSharp + = new Matrix4x4 + { + M11 = 1.2694F, M12 = -0.0988F, M13 = -0.1706F, + M21 = -0.8364F, M22 = 1.8006F, M23 = 0.0357F, + M31 = 0.0297F, M32 = -0.0315F, M33 = 1.0018F, + M44 = 1F + }; + + /// + /// CMCCAT2000 (fitted from all available color data sets) + /// + public static readonly Matrix4x4 CMCCAT2000 + = new Matrix4x4 + { + M11 = 0.7982F, M12 = 0.3389F, M13 = -0.1371F, + M21 = -0.5918F, M22 = 1.5512F, M23 = 0.0406F, + M31 = 0.0008F, M32 = 0.239F, M33 = 0.9753F, + M44 = 1F + }; + + /// + /// CAT02 (optimized for minimizing CIELAB differences) + /// + public static readonly Matrix4x4 CAT02 + = new Matrix4x4 + { + M11 = 0.7328F, M12 = 0.4296F, M13 = -0.1624F, + M21 = -0.7036F, M22 = 1.6975F, M23 = 0.0061F, + M31 = 0.0030F, M32 = 0.0136F, M33 = 0.9834F, + M44 = 1F + }; + } +} diff --git a/src/ImageSharp/Colors/Spaces/Conversion/VonKriesChromaticAdaptation.cs b/src/ImageSharp/Colors/Spaces/Conversion/VonKriesChromaticAdaptation.cs new file mode 100644 index 0000000000..a5b92d06db --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/VonKriesChromaticAdaptation.cs @@ -0,0 +1,91 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Conversion +{ + using System.Numerics; + + using Implementation; + using Spaces; + + /// + /// Basic implementation of the von Kries chromatic adaptation model + /// + /// + /// Transformation described here: + /// http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html + /// + public class VonKriesChromaticAdaptation : IChromaticAdaptation + { + private readonly IColorConversion conversionToLms; + + private readonly IColorConversion conversionToXyz; + + /// + /// Initializes a new instance of the class. + /// + public VonKriesChromaticAdaptation() + : this(new CieXyzAndLmsConverter()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The transformation matrix used for the conversion (definition of the cone response domain). + /// + /// + public VonKriesChromaticAdaptation(Matrix4x4 transformationMatrix) + : this(new CieXyzAndLmsConverter(transformationMatrix)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + private VonKriesChromaticAdaptation(CieXyzAndLmsConverter converter) + : this(converter, converter) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The color converter. + /// The color converter. + public VonKriesChromaticAdaptation(IColorConversion conversionToLms, IColorConversion conversionToCieXyz) + { + Guard.NotNull(conversionToLms, nameof(conversionToLms)); + Guard.NotNull(conversionToCieXyz, nameof(conversionToCieXyz)); + + this.conversionToLms = conversionToLms; + this.conversionToXyz = conversionToCieXyz; + } + + /// + public CieXyz Transform(CieXyz sourceColor, CieXyz sourceWhitePoint, CieXyz targetWhitePoint) + { + Guard.NotNull(sourceColor, nameof(sourceColor)); + Guard.NotNull(sourceWhitePoint, nameof(sourceWhitePoint)); + Guard.NotNull(targetWhitePoint, nameof(targetWhitePoint)); + + if (sourceWhitePoint.Equals(targetWhitePoint)) + { + return sourceColor; + } + + Lms sourceColorLms = this.conversionToLms.Convert(sourceColor); + Lms sourceWhitePointLms = this.conversionToLms.Convert(sourceWhitePoint); + Lms targetWhitePointLms = this.conversionToLms.Convert(targetWhitePoint); + + Vector3 vector = new Vector3(targetWhitePointLms.L / sourceWhitePointLms.L, targetWhitePointLms.M / sourceWhitePointLms.M, targetWhitePointLms.S / sourceWhitePointLms.S); + + Lms targetColorLms = new Lms(Vector3.Multiply(vector, sourceColorLms.Vector)); + return this.conversionToXyz.Convert(targetColorLms); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/IAlmostEquatable.cs b/src/ImageSharp/Colors/Spaces/IAlmostEquatable.cs new file mode 100644 index 0000000000..dc3dc57f0b --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/IAlmostEquatable.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Spaces +{ + using System; + + /// + /// Defines a generalized method that a value type or class implements to create + /// a type-specific method for determining approximate equality of instances. + /// + /// The type of objects to compare. + /// The object specifying the type to specify precision with. + public interface IAlmostEquatable + where TPrecision : struct, IComparable + { + /// + /// Indicates whether the current object is equal to another object of the same type + /// when compared to the specified precision level. + /// + /// An object to compare with this object. + /// The object specifying the level of precision. + /// + /// true if the current object is equal to the other parameter; otherwise, false. + /// + bool AlmostEquals(TColor other, TPrecision precision); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/IColorVector.cs b/src/ImageSharp/Colors/Spaces/IColorVector.cs new file mode 100644 index 0000000000..10ad9d30f4 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/IColorVector.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Spaces +{ + using System.Numerics; + + /// + /// Color represented as a vector in its color space + /// + public interface IColorVector + { + /// + /// The vector representation of the color + /// + Vector3 Vector { get; } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/ICompanding.cs b/src/ImageSharp/Colors/Spaces/ICompanding.cs new file mode 100644 index 0000000000..f20946b4fb --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/ICompanding.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Spaces +{ + /// + /// Pair of companding functions for . + /// Used for conversion to and backwards. + /// See also: + /// + public interface ICompanding + { + /// + /// Companded channel is made linear with respect to the energy. + /// + /// + /// For more info see: + /// http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html + /// + float InverseCompanding(float channel); + + /// + /// Uncompanded channel (linear) is made nonlinear (depends on the RGB color system). + /// + /// + /// For more info see: + /// http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html + /// + float Companding(float channel); + } +} diff --git a/src/ImageSharp/Colors/Spaces/IRgbWorkingSpace.cs b/src/ImageSharp/Colors/Spaces/IRgbWorkingSpace.cs new file mode 100644 index 0000000000..cc759753bf --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/IRgbWorkingSpace.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Spaces +{ + /// + /// Encasulates the RGB working color space + /// + public interface IRgbWorkingSpace + { + /// + /// Gets the reference white of the color space + /// + CieXyz WhitePoint { get; } + + /// + /// Chromaticity coordinates of the primaries + /// + // RGBPrimariesChromaticityCoordinates ChromaticityCoordinates { get; } + + /// + /// The companding function associated with the RGB color system. + /// Used for conversion to XYZ and backwards. + /// See this for more information: + /// http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html + /// http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html + /// + ICompanding Companding { get; } + } +} diff --git a/src/ImageSharp/Colors/Spaces/Illuminants.cs b/src/ImageSharp/Colors/Spaces/Illuminants.cs new file mode 100644 index 0000000000..51ef4d8d95 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Illuminants.cs @@ -0,0 +1,71 @@ +namespace ImageSharp.Colors.Spaces +{ + /// + /// The well known standard illuminants. + /// Standard illuminants provide a basis for comparing images or colors recorded under different lighting + /// + /// + /// Coefficients taken from: + /// http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html + ///
+ /// Descriptions taken from: + /// http://en.wikipedia.org/wiki/Standard_illuminant + ///
+ public static class Illuminants + { + /// + /// Incandescent / Tungsten + /// + public static readonly CieXyz A = new CieXyz(1.09850F, 1F, 0.35585F); + + /// + /// Direct sunlight at noon (obsoleteF) + /// + public static readonly CieXyz B = new CieXyz(0.99072F, 1F, 0.85223F); + + /// + /// Average / North sky Daylight (obsoleteF) + /// + public static readonly CieXyz C = new CieXyz(0.98074F, 1F, 1.18232F); + + /// + /// Horizon Light. ICC profile PCS + /// + public static readonly CieXyz D50 = new CieXyz(0.96422F, 1F, 0.82521F); + + /// + /// Mid-morning / Mid-afternoon Daylight + /// + public static readonly CieXyz D55 = new CieXyz(0.95682F, 1F, 0.92149F); + + /// + /// Noon Daylight: TelevisionF, sRGB color space + /// + public static readonly CieXyz D65 = new CieXyz(0.95047F, 1F, 1.08883F); + + /// + /// North sky Daylight + /// + public static readonly CieXyz D75 = new CieXyz(0.94972F, 1F, 1.22638F); + + /// + /// Equal energy + /// + public static readonly CieXyz E = new CieXyz(1F, 1F, 1F); + + /// + /// Cool White Fluorescent + /// + public static readonly CieXyz F2 = new CieXyz(0.99186F, 1F, 0.67393F); + + /// + /// D65 simulatorF, Daylight simulator + /// + public static readonly CieXyz F7 = new CieXyz(0.95041F, 1F, 1.08747F); + + /// + /// Philips TL84F, Ultralume 40 + /// + public static readonly CieXyz F11 = new CieXyz(1.00962F, 1F, 0.64350F); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Lms.cs b/src/ImageSharp/Colors/Spaces/Lms.cs new file mode 100644 index 0000000000..75ad6710b0 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Lms.cs @@ -0,0 +1,156 @@ +// +// 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; + + /// + /// LMS is a color space represented by the response of the three types of cones of the human eye, + /// named after their responsivity (sensitivity) at long, medium and short wavelengths. + /// + /// + public struct Lms : IColorVector, IEquatable, IAlmostEquatable + { + /// + /// Represents a that has Y, Cb, and Cr values set to zero. + /// + public static readonly Lms Empty = default(Lms); + + /// + /// The backing vector for SIMD support. + /// + private readonly Vector3 backingVector; + + /// + /// Initializes a new instance of the struct. + /// + /// L represents the responsivity at long wavelengths. + /// M represents the responsivity at medium wavelengths. + /// S represents the responsivity at short wavelengths. + public Lms(float l, float m, float s) + : this(new Vector3(l, m, s)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the x, y, z components. + public Lms(Vector3 vector) + : this() + { + // Not clamping as documentation about this space seems to indicate "usual" ranges + this.backingVector = vector; + } + + /// + /// Gets the L long component. + /// A value usually ranging between -1 and 1. + /// + public float L => this.backingVector.X; + + /// + /// Gets the M medium component. + /// A value usually ranging between -1 and 1. + /// + public float M => this.backingVector.Y; + + /// + /// Gets the S short component. + /// A value usually ranging between -1 and 1. + /// + public float S => 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 ==(Lms left, Lms 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 !=(Lms left, Lms right) + { + return !left.Equals(right); + } + + /// + public override int GetHashCode() + { + return this.backingVector.GetHashCode(); + } + + /// + public override string ToString() + { + if (this.IsEmpty) + { + return "Lms [ Empty ]"; + } + + return $"Lms [ L={this.L:#0.##}, M={this.M:#0.##}, S={this.S:#0.##} ]"; + } + + /// + public override bool Equals(object obj) + { + if (obj is Lms) + { + return this.Equals((Lms)obj); + } + + return false; + } + + /// + public bool Equals(Lms other) + { + return this.backingVector.Equals(other.backingVector); + } + + /// + public bool AlmostEquals(Lms 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/tests/ImageSharp.Tests/Colors/Colorspaces/CieXyzAndCieLabConversionTest.cs b/tests/ImageSharp.Tests/Colors/Colorspaces/CieXyzAndCieLabConversionTest.cs new file mode 100644 index 0000000000..5be2f27acf --- /dev/null +++ b/tests/ImageSharp.Tests/Colors/Colorspaces/CieXyzAndCieLabConversionTest.cs @@ -0,0 +1,72 @@ +namespace ImageSharp.Tests +{ + using ImageSharp.Colors.Conversion; + using System.Collections.Generic; + using ImageSharp.Colors.Spaces; + using Xunit; + + /// + /// Tests - conversions. + /// + /// + /// Test data generated using: + /// http://www.brucelindbloom.com/index.html?ColorCalculator.html + /// + public class CieXyzAndCieLabConversionTest + { + private static readonly IEqualityComparer FloatComparerLabPrecision = new ApproximateFloatComparer(4); + private static readonly IEqualityComparer FloatComparerXyzPrecision = new ApproximateFloatComparer(6); + + /// + /// Tests conversion from to (). + /// + [Theory] + [InlineData(100, 0, 0, 0.95047, 1, 1.08883)] + [InlineData(0, 0, 0, 0, 0, 0)] + [InlineData(0, 431.0345, 0, 0.95047, 0, 0)] + [InlineData(100, -431.0345, 172.4138, 0, 1, 0)] + [InlineData(0, 0, -172.4138, 0, 0, 1.08883)] + [InlineData(45.6398, 39.8753, 35.2091, 0.216938, 0.150041, 0.048850)] + [InlineData(77.1234, -40.1235, 78.1120, 0.358530, 0.517372, 0.076273)] + [InlineData(10, -400, 20, 0, 0.011260, 0)] + public void Convert_Lab_to_XYZ(float l, float a, float b, float x, float y, float z) + { + // Arrange + CieLab input = new CieLab(l, a, b, Illuminants.D65); + ColorConverter converter = new ColorConverter { WhitePoint = Illuminants.D65, TargetLabWhitePoint = Illuminants.D65 }; + + // Act + CieXyz output = converter.ToCieXyz(input); + + // Assert + Assert.Equal(output.X, x, FloatComparerXyzPrecision); + Assert.Equal(output.Y, y, FloatComparerXyzPrecision); + Assert.Equal(output.Z, z, FloatComparerXyzPrecision); + } + + /// + /// Tests conversion from () to . + /// + [Theory] + [InlineData(0.95047, 1, 1.08883, 100, 0, 0)] + [InlineData(0, 0, 0, 0, 0, 0)] + [InlineData(0.95047, 0, 0, 0, 431.0345, 0)] + [InlineData(0, 1, 0, 100, -431.0345, 172.4138)] + [InlineData(0, 0, 1.08883, 0, 0, -172.4138)] + [InlineData(0.216938, 0.150041, 0.048850, 45.6398, 39.8753, 35.2091)] + public void Convert_XYZ_to_Lab(float x, float y, float z, float l, float a, float b) + { + // Arrange + CieXyz input = new CieXyz(x, y, z); + ColorConverter converter = new ColorConverter { WhitePoint = Illuminants.D65, TargetLabWhitePoint = Illuminants.D65 }; + + // Act + CieLab output = converter.ToCieLab(input); + + // Assert + Assert.Equal(output.L, l, FloatComparerLabPrecision); + Assert.Equal(output.A, a, FloatComparerLabPrecision); + Assert.Equal(output.B, b, FloatComparerLabPrecision); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Colors/Colorspaces/CieXyzAndLmsConversionTest.cs b/tests/ImageSharp.Tests/Colors/Colorspaces/CieXyzAndLmsConversionTest.cs new file mode 100644 index 0000000000..d60b72c6d9 --- /dev/null +++ b/tests/ImageSharp.Tests/Colors/Colorspaces/CieXyzAndLmsConversionTest.cs @@ -0,0 +1,68 @@ +namespace ImageSharp.Tests +{ + using ImageSharp.Colors.Conversion; + using System.Collections.Generic; + using ImageSharp.Colors.Spaces; + using Xunit; + + /// + /// Tests - conversions. + /// + /// + /// Test data generated using original colorful library. + /// + public class CieXyzAndLmsConversionTest + { + private static readonly IEqualityComparer FloatComparer = new ApproximateFloatComparer(6); + + /// + /// Tests conversion from () to . + /// + [Theory] + [InlineData(0.941428535, 1.040417467, 1.089532651, 0.95047, 1, 1.08883)] + [InlineData(0, 0, 0, 0, 0, 0)] + [InlineData(0.850765697, -0.713042594, 0.036973283, 0.95047, 0, 0)] + [InlineData(0.2664, 1.7135, -0.0685, 0, 1, 0)] + [InlineData(-0.175737162, 0.039960061, 1.121059368, 0, 0, 1.08883)] + [InlineData(0.2262677362, 0.0961411609, 0.0484570397, 0.216938, 0.150041, 0.048850)] + public void Convert_Lms_to_CieXyz(float l, float m, float s, float x, float y, float z) + { + // Arrange + Lms input = new Lms(x, y, z); + ColorConverter converter = new ColorConverter(); + + // Act + CieXyz output = converter.ToCieXyz(input); + + // Assert + Assert.Equal(output.X, l, FloatComparer); + Assert.Equal(output.Y, m, FloatComparer); + Assert.Equal(output.Z, s, FloatComparer); + } + + /// + /// Tests conversion from () to . + /// + [Theory] + [InlineData(0.95047, 1, 1.08883, 0.941428535, 1.040417467, 1.089532651)] + [InlineData(0, 0, 0, 0, 0, 0)] + [InlineData(0.95047, 0, 0, 0.850765697, -0.713042594, 0.036973283)] + [InlineData(0, 1, 0, 0.2664, 1.7135, -0.0685)] + [InlineData(0, 0, 1.08883, -0.175737162, 0.039960061, 1.121059368)] + [InlineData(0.216938, 0.150041, 0.048850, 0.2262677362, 0.0961411609, 0.0484570397)] + public void Convert_CieXyz_to_Lms(float x, float y, float z, float l, float m, float s) + { + // Arrange + CieXyz input = new CieXyz(x, y, z); + ColorConverter converter = new ColorConverter(); + + // Act + Lms output = converter.ToLms(input); + + // Assert + Assert.Equal(output.L, l, FloatComparer); + Assert.Equal(output.M, m, FloatComparer); + Assert.Equal(output.S, s, FloatComparer); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Colors/Colorspaces/ColorConverterAdaptTest.cs b/tests/ImageSharp.Tests/Colors/Colorspaces/ColorConverterAdaptTest.cs new file mode 100644 index 0000000000..b9476ce291 --- /dev/null +++ b/tests/ImageSharp.Tests/Colors/Colorspaces/ColorConverterAdaptTest.cs @@ -0,0 +1,37 @@ +using ImageSharp.Colors.Conversion; +using ImageSharp.Colors.Conversion.Implementation; +using ImageSharp.Colors.Spaces; + +namespace ImageSharp.Tests +{ + using System.Collections.Generic; + using Xunit; + + public class ColorConverterAdaptTest + { + private static readonly IEqualityComparer FloatComparer = new ApproximateFloatComparer(4); + + [Theory] + [InlineData(0, 0, 0, 0, 0, 0)] + [InlineData(0.5, 0.5, 0.5, 0.507233, 0.500000, 0.378943)] + public void Adapt_CieXyz_D65_To_D50_XyzScaling(float x1, float y1, float z1, float x2, float y2, float z2) + { + // Arrange + CieXyz input = new CieXyz(x1, y1, z1); + CieXyz expectedOutput = new CieXyz(x2, y2, z2); + ColorConverter converter = new ColorConverter + { + ChromaticAdaptation = new VonKriesChromaticAdaptation(LmsAdaptationMatrix.XYZScaling), + WhitePoint = Illuminants.D50 + }; + + // Action + CieXyz output = converter.Adapt(input, Illuminants.D65); + + // Assert + Assert.Equal(output.X, expectedOutput.X, FloatComparer); + Assert.Equal(output.Y, expectedOutput.Y, FloatComparer); + Assert.Equal(output.Z, expectedOutput.Z, FloatComparer); + } + } +} \ No newline at end of file