diff --git a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.CieXyz.cs b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.CieXyz.cs index 46bcfbc1ac..01a9680611 100644 --- a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.CieXyz.cs +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.CieXyz.cs @@ -7,6 +7,7 @@ namespace ImageSharp.Colors.Spaces.Conversion { using ImageSharp.Colors.Spaces; using ImageSharp.Colors.Spaces.Conversion.Implementation.CieLab; + using ImageSharp.Colors.Spaces.Conversion.Implementation.Rgb; /// /// Converts between color spaces ensuring that the color is adapted using chromatic adaptation. @@ -15,6 +16,8 @@ namespace ImageSharp.Colors.Spaces.Conversion { private static readonly CieLabToCieXyzConverter CieLabToCieXyzConverter = new CieLabToCieXyzConverter(); + private LinearRgbToCieXyzConverter linearRgbToCieXyzConverter; + /// /// Converts a into a /// @@ -47,5 +50,53 @@ namespace ImageSharp.Colors.Spaces.Conversion // Conversion return this.cachedCieXyzAndLmsConverter.Convert(color); } + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public CieXyz ToCieXyz(Rgb color) + { + Guard.NotNull(color, nameof(color)); + + // Conversion + LinearRgb linear = RgbToLinearRgbConverter.Convert(color); + return this.ToCieXyz(linear); + } + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public CieXyz ToCieXyz(LinearRgb color) + { + Guard.NotNull(color, nameof(color)); + + // Conversion + LinearRgbToCieXyzConverter converter = this.GetLinearRgbToCieXyzConverter(color.WorkingSpace); + CieXyz unadapted = converter.Convert(color); + + // Adaptation + return color.WorkingSpace.WhitePoint.Equals(this.WhitePoint) || !this.IsChromaticAdaptationPerformed + ? unadapted + : this.Adapt(unadapted, color.WorkingSpace.WhitePoint); + } + + /// + /// Gets the correct converter for the given rgb working space. + /// + /// The source working space + /// The + private LinearRgbToCieXyzConverter GetLinearRgbToCieXyzConverter(IRgbWorkingSpace workingSpace) + { + if (this.linearRgbToCieXyzConverter != null && this.linearRgbToCieXyzConverter.SourceWorkingSpace.Equals(workingSpace)) + { + return this.linearRgbToCieXyzConverter; + } + + return this.linearRgbToCieXyzConverter = new LinearRgbToCieXyzConverter(workingSpace); + } } } \ 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 new file mode 100644 index 0000000000..3724efaf2c --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.LinearRgb.cs @@ -0,0 +1,66 @@ +// +// 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.Rgb; + + /// + /// Converts between color spaces ensuring that the color is adapted using chromatic adaptation. + /// + public partial class ColorSpaceConverter + { + private static readonly RgbToLinearRgbConverter RgbToLinearRgbConverter = new RgbToLinearRgbConverter(); + + private CieXyzToLinearRgbConverter cieXyzToLinearRgbConverter; + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public LinearRgb ToLinearRgb(Rgb color) + { + Guard.NotNull(color, nameof(color)); + + // Conversion + return RgbToLinearRgbConverter.Convert(color); + } + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public LinearRgb ToLinearRgb(CieXyz color) + { + Guard.NotNull(color, nameof(color)); + + // Adaptation + CieXyz adapted = this.TargetRgbWorkingSpace.WhitePoint.Equals(this.WhitePoint) || !this.IsChromaticAdaptationPerformed + ? color + : this.ChromaticAdaptation.Transform(color, this.WhitePoint, this.TargetRgbWorkingSpace.WhitePoint); + + // Conversion + CieXyzToLinearRgbConverter xyzConverter = this.GetCieXyxToLinearRgbConverter(this.TargetRgbWorkingSpace); + return xyzConverter.Convert(adapted); + } + + /// + /// Gets the correct converter for the given rgb working space. + /// + /// The target working space + /// The + private CieXyzToLinearRgbConverter GetCieXyxToLinearRgbConverter(IRgbWorkingSpace workingSpace) + { + if (this.cieXyzToLinearRgbConverter != null && this.cieXyzToLinearRgbConverter.TargetWorkingSpace.Equals(workingSpace)) + { + return this.cieXyzToLinearRgbConverter; + } + + return this.cieXyzToLinearRgbConverter = new CieXyzToLinearRgbConverter(workingSpace); + } + } +} \ 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 new file mode 100644 index 0000000000..b6a7dd86d4 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.Rgb.cs @@ -0,0 +1,46 @@ +// +// 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.Rgb; + + /// + /// Converts between color spaces ensuring that the color is adapted using chromatic adaptation. + /// + public partial class ColorSpaceConverter + { + private static readonly LinearRgbToRgbConverter LinearRgbToRgbConverter = new LinearRgbToRgbConverter(); + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public Rgb ToRgb(LinearRgb color) + { + Guard.NotNull(color, nameof(color)); + + // Conversion + return LinearRgbToRgbConverter.Convert(color); + } + + /// + /// Converts a into a + /// + /// The color to convert. + /// The + public Rgb ToRgb(CieXyz color) + { + Guard.NotNull(color, nameof(color)); + + // Conversion + LinearRgb linear = this.ToLinearRgb(color); + + // Compand + return this.ToRgb(linear); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.cs b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.cs index bb61843716..459a4f474a 100644 --- a/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.cs +++ b/src/ImageSharp/Colors/Spaces/Conversion/ColorSpaceConverter.cs @@ -33,6 +33,7 @@ namespace ImageSharp.Colors.Spaces.Conversion this.LmsAdaptationMatrix = CieXyzAndLmsConverter.DefaultTransformationMatrix; this.ChromaticAdaptation = new VonKriesChromaticAdaptation(this.cachedCieXyzAndLmsConverter, this.cachedCieXyzAndLmsConverter); this.TargetLabWhitePoint = CieLab.DefaultWhitePoint; + this.TargetRgbWorkingSpace = Rgb.DefaultWorkingSpace; } /// @@ -47,6 +48,12 @@ namespace ImageSharp.Colors.Spaces.Conversion /// public CieXyz TargetLabWhitePoint { get; set; } + /// + /// Gets or sets the target working space used *when creating* RGB colors. (RGB colors on the input already contain the working space information) + /// Defaults to: . + /// + public IRgbWorkingSpace TargetRgbWorkingSpace { get; set; } + /// /// Gets or sets the chromatic adaptation method used. When null, no adaptation will be performed. /// diff --git a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieLab/CieLabToCieXyzConverter.cs b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieLab/CieLabToCieXyzConverter.cs index 47fe803a8d..c312424968 100644 --- a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieLab/CieLabToCieXyzConverter.cs +++ b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieLab/CieLabToCieXyzConverter.cs @@ -10,12 +10,12 @@ namespace ImageSharp.Colors.Spaces.Conversion.Implementation.CieLab /// /// Converts from to . /// - public class CieLabToCieXyzConverter : IColorConversion + internal class CieLabToCieXyzConverter : IColorConversion { /// public CieXyz Convert(CieLab input) { - Guard.NotNull(input, nameof(input)); + DebugGuard.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; diff --git a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieLab/CieXyzToCieLabConverter.cs b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieLab/CieXyzToCieLabConverter.cs index 2b80ff1b61..ef01e9c025 100644 --- a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieLab/CieXyzToCieLabConverter.cs +++ b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/CieLab/CieXyzToCieLabConverter.cs @@ -10,7 +10,7 @@ namespace ImageSharp.Colors.Spaces.Conversion.Implementation.CieLab /// /// Converts from to . /// - public class CieXyzToCieLabConverter : IColorConversion + internal class CieXyzToCieLabConverter : IColorConversion { /// /// Initializes a new instance of the class. @@ -37,7 +37,7 @@ namespace ImageSharp.Colors.Spaces.Conversion.Implementation.CieLab /// public CieLab Convert(CieXyz input) { - Guard.NotNull(input, nameof(input)); + DebugGuard.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; diff --git a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Lms/CieXyzAndLmsConverter.cs b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Lms/CieXyzAndLmsConverter.cs index c169e60114..cc88095748 100644 --- a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Lms/CieXyzAndLmsConverter.cs +++ b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Lms/CieXyzAndLmsConverter.cs @@ -12,7 +12,7 @@ namespace ImageSharp.Colors.Spaces.Conversion.Implementation.Lms /// /// Color converter between CIE XYZ and LMS /// - public class CieXyzAndLmsConverter : IColorConversion, IColorConversion + internal class CieXyzAndLmsConverter : IColorConversion, IColorConversion { /// /// Default transformation matrix used, when no other is set. (Bradford) @@ -44,7 +44,7 @@ namespace ImageSharp.Colors.Spaces.Conversion.Implementation.Lms } /// - /// Gets transformation matrix used for the conversion (definition of the cone response domain). + /// Gets or sets the transformation matrix used for the conversion (definition of the cone response domain). /// /// public Matrix4x4 TransformationMatrix @@ -54,7 +54,7 @@ namespace ImageSharp.Colors.Spaces.Conversion.Implementation.Lms return this.transformationMatrix; } - internal set + set { this.transformationMatrix = value; Matrix4x4.Invert(this.transformationMatrix, out this.inverseTransformationMatrix); @@ -64,7 +64,7 @@ namespace ImageSharp.Colors.Spaces.Conversion.Implementation.Lms /// public Lms Convert(CieXyz input) { - Guard.NotNull(input, nameof(input)); + DebugGuard.NotNull(input, nameof(input)); Vector3 vector = Vector3.Transform(input.Vector, this.transformationMatrix); return new Lms(vector); @@ -73,6 +73,8 @@ namespace ImageSharp.Colors.Spaces.Conversion.Implementation.Lms /// public CieXyz Convert(Lms input) { + DebugGuard.NotNull(input, nameof(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 index 1c06425e75..97fb6eebc0 100644 --- a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Lms/LmsAdaptationMatrix.cs +++ b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Lms/LmsAdaptationMatrix.cs @@ -25,7 +25,7 @@ namespace ImageSharp.Colors.Spaces.Conversion.Implementation.Lms /// Von Kries chromatic adaptation transform matrix (Hunt-Pointer-Estevez adjusted for D65) /// public static readonly Matrix4x4 VonKriesHPEAdjusted - = new Matrix4x4() + = new Matrix4x4 { M11 = 0.40024F, M12 = 0.7076F, M13 = -0.08081F, M21 = -0.2263F, M22 = 1.16532F, M23 = 0.0457F, diff --git a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Rgb/CieXyzToLinearRgbConverter.cs b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Rgb/CieXyzToLinearRgbConverter.cs new file mode 100644 index 0000000000..5f6ec8bd68 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Rgb/CieXyzToLinearRgbConverter.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Spaces.Conversion.Implementation.Rgb +{ + using System.Numerics; + + using Rgb = ImageSharp.Colors.Spaces.Rgb; + + /// + /// Color converter between CieXyz and LinearRgb + /// + internal class CieXyzToLinearRgbConverter : LinearRgbAndCieXyzConverterBase, IColorConversion + { + private readonly Matrix4x4 conversionMatrix; + + /// + /// Initializes a new instance of the class. + /// + public CieXyzToLinearRgbConverter() + : this(Rgb.DefaultWorkingSpace) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The target working space. + public CieXyzToLinearRgbConverter(IRgbWorkingSpace workingSpace) + { + this.TargetWorkingSpace = workingSpace; + this.conversionMatrix = GetRgbToCieXyzMatrix(workingSpace); + } + + /// + /// Gets the target working space + /// + public IRgbWorkingSpace TargetWorkingSpace { get; } + + /// + public LinearRgb Convert(CieXyz input) + { + DebugGuard.NotNull(input, nameof(input)); + + Vector3 vector = Vector3.Transform(input.Vector, this.conversionMatrix); + return new LinearRgb(vector, this.TargetWorkingSpace); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Rgb/LinearRgbAndCieXyzConverterBase.cs b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Rgb/LinearRgbAndCieXyzConverterBase.cs new file mode 100644 index 0000000000..67bd024cf8 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Rgb/LinearRgbAndCieXyzConverterBase.cs @@ -0,0 +1,68 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Spaces.Conversion.Implementation.Rgb +{ + using System.Numerics; + + /// + /// Provides base methods for converting between Rgb and CieXyz color spaces. + /// + internal abstract class LinearRgbAndCieXyzConverterBase + { + /// + /// Geturns the correct matrix to convert between the Rgb and CieXyz color space. + /// + /// The Rgb working space. + /// The based on the chromaticity and working space. + public static Matrix4x4 GetRgbToCieXyzMatrix(IRgbWorkingSpace workingSpace) + { + DebugGuard.NotNull(workingSpace, nameof(workingSpace)); + + RgbPrimariesChromaticityCoordinates chromaticity = workingSpace.ChromaticityCoordinates; + + float xr = chromaticity.R.X; + float xg = chromaticity.G.X; + float xb = chromaticity.B.X; + float yr = chromaticity.R.Y; + float yg = chromaticity.G.Y; + float yb = chromaticity.B.Y; + + float mXr = xr / yr; + const float Yr = 1; + float mZr = (1 - xr - yr) / yr; + + float mXg = xg / yg; + const float Yg = 1; + float mZg = (1 - xg - yg) / yg; + + float mXb = xb / yb; + const float Yb = 1; + float mZb = (1 - xb - yb) / yb; + + Matrix4x4 xyzMatrix = new Matrix4x4 + { + M11 = mXr, M12 = mXg, M13 = mXb, + M21 = Yr, M22 = Yg, M23 = Yb, + M31 = mZr, M32 = mZg, M33 = mZb, + M44 = 1F + }; + + Matrix4x4 inverseXyzMatrix; + Matrix4x4.Invert(xyzMatrix, out inverseXyzMatrix); + + Vector3 vector = Vector3.Transform(workingSpace.WhitePoint.Vector, inverseXyzMatrix); + + // TODO: Is there a built in method for this? + return new Matrix4x4 + { + M11 = vector.X * mXr, M12 = vector.Y * mXg, M13 = vector.Z * mXb, + M21 = vector.X * Yr, M22 = vector.Y * Yg, M23 = vector.Z * Yb, + M31 = vector.X * mZr, M32 = vector.Y * mZg, M33 = vector.Z * mZb, + M44 = 1F + }; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Rgb/LinearRgbToCieXyzConverter.cs b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Rgb/LinearRgbToCieXyzConverter.cs new file mode 100644 index 0000000000..9f5b3439db --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Rgb/LinearRgbToCieXyzConverter.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Spaces.Conversion.Implementation.Rgb +{ + using System.Numerics; + + using Rgb = ImageSharp.Colors.Spaces.Rgb; + + /// + /// Color converter between LinearRgb and CieXyz + /// + internal class LinearRgbToCieXyzConverter : LinearRgbAndCieXyzConverterBase, IColorConversion + { + private readonly Matrix4x4 conversionMatrix; + + /// + /// Initializes a new instance of the class. + /// + public LinearRgbToCieXyzConverter() + : this(Rgb.DefaultWorkingSpace) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The target working space. + public LinearRgbToCieXyzConverter(IRgbWorkingSpace workingSpace) + { + this.SourceWorkingSpace = workingSpace; + this.conversionMatrix = GetRgbToCieXyzMatrix(workingSpace); + } + + /// + /// Gets the source working space + /// + public IRgbWorkingSpace SourceWorkingSpace { get; } + + /// + public CieXyz Convert(LinearRgb input) + { + DebugGuard.NotNull(input, nameof(input)); + Guard.IsTrue(input.WorkingSpace.Equals(this.SourceWorkingSpace), nameof(input.WorkingSpace), "Input and source working spaces must be equal."); + + Vector3 vector = Vector3.Transform(input.Vector, this.conversionMatrix); + return new CieXyz(vector); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Rgb/LinearRgbToRgbConverter.cs b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Rgb/LinearRgbToRgbConverter.cs new file mode 100644 index 0000000000..dd3bd12554 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Rgb/LinearRgbToRgbConverter.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Spaces.Conversion.Implementation.Rgb +{ + using System.Numerics; + + using Rgb = ImageSharp.Colors.Spaces.Rgb; + + /// + /// Color converter between LinearRgb and Rgb + /// + public class LinearRgbToRgbConverter : IColorConversion + { + /// + public Rgb Convert(LinearRgb input) + { + DebugGuard.NotNull(input, nameof(input)); + + Vector3 vector = input.Vector; + vector.X = input.WorkingSpace.Companding.Compress(vector.X); + vector.Y = input.WorkingSpace.Companding.Compress(vector.Y); + vector.Z = input.WorkingSpace.Companding.Compress(vector.Z); + + return new Rgb(vector, input.WorkingSpace); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Rgb/RgbToLinearRgbConverter.cs b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Rgb/RgbToLinearRgbConverter.cs new file mode 100644 index 0000000000..3e1e00ee61 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Conversion/Implementation/Rgb/RgbToLinearRgbConverter.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Colors.Spaces.Conversion.Implementation.Rgb +{ + using System.Numerics; + + using Rgb = ImageSharp.Colors.Spaces.Rgb; + + /// + /// Color converter between Rgb and LinearRgb + /// + public class RgbToLinearRgbConverter : IColorConversion + { + /// + public LinearRgb Convert(Rgb input) + { + Guard.NotNull(input, nameof(input)); + + Vector3 vector = input.Vector; + vector.X = input.WorkingSpace.Companding.Expand(vector.X); + vector.Y = input.WorkingSpace.Companding.Expand(vector.Y); + vector.Z = input.WorkingSpace.Companding.Expand(vector.Z); + + return new LinearRgb(vector, input.WorkingSpace); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Colors/Spaces/LinearRgb.cs b/src/ImageSharp/Colors/Spaces/LinearRgb.cs new file mode 100644 index 0000000000..049fc5c729 --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/LinearRgb.cs @@ -0,0 +1,175 @@ +// +// 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 linear Rgb color with specified working space + /// + public struct LinearRgb : IColorVector, IEquatable, IAlmostEquatable + { + /// + /// Represents a that has R, G, and B values set to zero. + /// + public static readonly LinearRgb Empty = default(LinearRgb); + + /// + /// The default LinearRgb working space + /// + private static readonly IRgbWorkingSpace DefaultWorkingSpace = RgbWorkingSpaces.SRgb; + + /// + /// The backing vector for SIMD support. + /// + private readonly Vector3 backingVector; + + /// + /// Initializes a new instance of the struct. + /// + /// The red component ranging between 0 and 1. + /// The green component ranging between 0 and 1. + /// The blue component ranging between 0 and 1. + public LinearRgb(float r, float g, float b) + : this(new Vector3(r, g, b)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the r, g, b components. + public LinearRgb(Vector3 vector) + : this(vector, DefaultWorkingSpace) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the r, g, b components. + /// The LinearRgb working space. + public LinearRgb(Vector3 vector, IRgbWorkingSpace workingSpace) + : this() + { + // Clamp to 0-1 range. + this.backingVector = Vector3.Clamp(vector, Vector3.Zero, Vector3.One); + this.WorkingSpace = workingSpace; + } + + /// + /// Gets the red component. + /// A value usually ranging between 0 and 1. + /// + public float R => this.backingVector.X; + + /// + /// Gets the green component. + /// A value usually ranging between 0 and 1. + /// + public float G => this.backingVector.Y; + + /// + /// Gets the blue component. + /// A value usually ranging between 0 and 1. + /// + public float B => this.backingVector.Z; + + /// + /// Gets the LinearRgb color space + /// + public IRgbWorkingSpace WorkingSpace { get; } + + /// + /// 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 ==(LinearRgb left, LinearRgb 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 !=(LinearRgb left, LinearRgb right) + { + return !left.Equals(right); + } + + /// + public override int GetHashCode() + { + return this.backingVector.GetHashCode(); + } + + /// + public override string ToString() + { + if (this.IsEmpty) + { + return "LinearRgb [ Empty ]"; + } + + return $"LinearRgb [ R={this.R:#0.##}, G={this.G:#0.##}, B={this.B:#0.##} ]"; + } + + /// + public override bool Equals(object obj) + { + if (obj is LinearRgb) + { + return this.Equals((LinearRgb)obj); + } + + return false; + } + + /// + public bool Equals(LinearRgb other) + { + return this.backingVector.Equals(other.backingVector); + } + + /// + public bool AlmostEquals(LinearRgb 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/Rgb.cs b/src/ImageSharp/Colors/Spaces/Rgb.cs new file mode 100644 index 0000000000..db71f8a02c --- /dev/null +++ b/src/ImageSharp/Colors/Spaces/Rgb.cs @@ -0,0 +1,175 @@ +// +// 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 RGB color with specified working space + /// + public struct Rgb : IColorVector, IEquatable, IAlmostEquatable + { + /// + /// Represents a that has R, G, and B values set to zero. + /// + public static readonly Rgb Empty = default(Rgb); + + /// + /// The default rgb working space + /// + internal static readonly IRgbWorkingSpace DefaultWorkingSpace = RgbWorkingSpaces.SRgb; + + /// + /// The backing vector for SIMD support. + /// + private readonly Vector3 backingVector; + + /// + /// Initializes a new instance of the struct. + /// + /// The red component ranging between 0 and 1. + /// The green component ranging between 0 and 1. + /// The blue component ranging between 0 and 1. + public Rgb(float r, float g, float b) + : this(new Vector3(r, g, b)) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the r, g, b components. + public Rgb(Vector3 vector) + : this(vector, DefaultWorkingSpace) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The vector representing the r, g, b components. + /// The rgb working space. + public Rgb(Vector3 vector, IRgbWorkingSpace workingSpace) + : this() + { + // Clamp to 0-1 range. + this.backingVector = Vector3.Clamp(vector, Vector3.Zero, Vector3.One); + this.WorkingSpace = workingSpace; + } + + /// + /// Gets the red component. + /// A value usually ranging between 0 and 1. + /// + public float R => this.backingVector.X; + + /// + /// Gets the green component. + /// A value usually ranging between 0 and 1. + /// + public float G => this.backingVector.Y; + + /// + /// Gets the blue component. + /// A value usually ranging between 0 and 1. + /// + public float B => this.backingVector.Z; + + /// + /// Gets the Rgb color space + /// + public IRgbWorkingSpace WorkingSpace { get; } + + /// + /// 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 ==(Rgb left, Rgb 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 !=(Rgb left, Rgb right) + { + return !left.Equals(right); + } + + /// + public override int GetHashCode() + { + return this.backingVector.GetHashCode(); + } + + /// + public override string ToString() + { + if (this.IsEmpty) + { + return "Rgb [ Empty ]"; + } + + return $"Rgb [ R={this.R:#0.##}, G={this.G:#0.##}, B={this.B:#0.##} ]"; + } + + /// + public override bool Equals(object obj) + { + if (obj is Rgb) + { + return this.Equals((Rgb)obj); + } + + return false; + } + + /// + public bool Equals(Rgb other) + { + return this.backingVector.Equals(other.backingVector); + } + + /// + public bool AlmostEquals(Rgb 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.Benchmarks/Color/ColorspaceConvert.cs b/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToLmsConvert.cs similarity index 87% rename from tests/ImageSharp.Benchmarks/Color/ColorspaceConvert.cs rename to tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToLmsConvert.cs index a18d5e7039..007f45f8e3 100644 --- a/tests/ImageSharp.Benchmarks/Color/ColorspaceConvert.cs +++ b/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToLmsConvert.cs @@ -9,7 +9,7 @@ using ColorSpaceConverter = ImageSharp.Colors.Spaces.Conversion.ColorSpaceConverter; - public class ColorspaceConvert + public class ColorspaceCieXyzToLmsConvert { private static readonly CieXyz CieXyz = new CieXyz(0.95047F, 1, 1.08883F); @@ -21,13 +21,13 @@ [Benchmark(Baseline = true, Description = "Colourful Convert")] - public LMSColor SystemDrawingColorEqual() + public LMSColor ColourfulConvert() { return ColourfulConverter.ToLMS(XYZColor); } [Benchmark(Description = "ImageSharp Convert")] - public Lms ColorEqual() + public Lms ColorSpaceConvert() { return ColorSpaceConverter.ToLms(CieXyz); } diff --git a/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToRgbConvert.cs b/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToRgbConvert.cs new file mode 100644 index 0000000000..d45267cff0 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Color/ColorspaceCieXyzToRgbConvert.cs @@ -0,0 +1,35 @@ +namespace ImageSharp.Benchmarks.Color +{ + using BenchmarkDotNet.Attributes; + + using Colourful; + using Colourful.Conversion; + + using ImageSharp.Colors.Spaces; + + using ColorSpaceConverter = ImageSharp.Colors.Spaces.Conversion.ColorSpaceConverter; + + public class ColorspaceCieXyzToRgbConvert + { + private static readonly CieXyz CieXyz = new CieXyz(0.95047F, 1, 1.08883F); + + private static readonly XYZColor XYZColor = new XYZColor(0.95047, 1, 1.08883); + + private static readonly ColorSpaceConverter ColorSpaceConverter = new ColorSpaceConverter(); + + private static readonly ColourfulConverter ColourfulConverter = new ColourfulConverter(); + + + [Benchmark(Baseline = true, Description = "Colourful Convert")] + public RGBColor ColourfulConvert() + { + return ColourfulConverter.ToRGB(XYZColor); + } + + [Benchmark(Description = "ImageSharp Convert")] + public Rgb ColorSpaceConvert() + { + return ColorSpaceConverter.ToRgb(CieXyz); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Colors/Colorspaces/RgbAndCieXyzConversionTest.cs b/tests/ImageSharp.Tests/Colors/Colorspaces/RgbAndCieXyzConversionTest.cs new file mode 100644 index 0000000000..b078c0a334 --- /dev/null +++ b/tests/ImageSharp.Tests/Colors/Colorspaces/RgbAndCieXyzConversionTest.cs @@ -0,0 +1,127 @@ +namespace ImageSharp.Tests +{ + using System.Collections.Generic; + using ImageSharp.Colors.Spaces; + using ImageSharp.Colors.Spaces.Conversion; + + using Xunit; + + /// + /// Tests - conversions. + /// + /// + /// Test data generated using: + /// http://www.brucelindbloom.com/index.html?ColorCalculator.html + /// + public class RgbAndCieXyzConversionTest + { + private static readonly IEqualityComparer FloatComparerPrecision = new ApproximateFloatComparer(6); + + /// + /// Tests conversion from () + /// to (default sRGB working space). + /// + [Theory] + [InlineData(0.96422, 1.00000, 0.82521, 1, 1, 1)] + [InlineData(0.00000, 1.00000, 0.00000, 0, 1, 0)] + [InlineData(0.96422, 0.00000, 0.00000, 1, 0, 0.292064)] + [InlineData(0.00000, 0.00000, 0.82521, 0, 0.181415, 1)] + [InlineData(0, 0, 0, 0, 0, 0)] + [InlineData(0.297676, 0.267854, 0.045504, 0.720315, 0.509999, 0.168112)] + public void Convert_XYZ_D50_to_SRGB(float x, float y, float z, float r, float g, float b) + { + // Arrange + CieXyz input = new CieXyz(x, y, z); + ColorSpaceConverter converter = new ColorSpaceConverter { WhitePoint = Illuminants.D50, TargetRgbWorkingSpace = RgbWorkingSpaces.SRgb }; + + // Act + Rgb output = converter.ToRgb(input); + + // Assert + Assert.Equal(output.WorkingSpace, Rgb.DefaultWorkingSpace); + Assert.Equal(output.R, r, FloatComparerPrecision); + Assert.Equal(output.G, g, FloatComparerPrecision); + Assert.Equal(output.B, b, FloatComparerPrecision); + } + + /// + /// Tests conversion + /// from () + /// to (default sRGB working space). + /// + [Theory] + [InlineData(0.950470, 1.000000, 1.088830, 1, 1, 1)] + [InlineData(0, 1.000000, 0, 0, 1, 0)] + [InlineData(0.950470, 0, 0, 1, 0, 0.254967)] + [InlineData(0, 0, 1.088830, 0, 0.235458, 1)] + [InlineData(0, 0, 0, 0, 0, 0)] + [InlineData(0.297676, 0.267854, 0.045504, 0.754903, 0.501961, 0.099998)] + public void Convert_XYZ_D65_to_SRGB(float x, float y, float z, float r, float g, float b) + { + // Arrange + CieXyz input = new CieXyz(x, y, z); + ColorSpaceConverter converter = new ColorSpaceConverter { WhitePoint = Illuminants.D65, TargetRgbWorkingSpace = RgbWorkingSpaces.SRgb }; + + // Act + Rgb output = converter.ToRgb(input); + + // Assert + Assert.Equal(output.WorkingSpace, Rgb.DefaultWorkingSpace); + Assert.Equal(output.R, r, FloatComparerPrecision); + Assert.Equal(output.G, g, FloatComparerPrecision); + Assert.Equal(output.B, b, FloatComparerPrecision); + } + + /// + /// Tests conversion from (default sRGB working space) + /// to (). + /// + [Theory] + [InlineData(1, 1, 1, 0.964220, 1.000000, 0.825210)] + [InlineData(0, 0, 0, 0, 0, 0)] + [InlineData(1, 0, 0, 0.436075, 0.222504, 0.013932)] + [InlineData(0, 1, 0, 0.385065, 0.716879, 0.097105)] + [InlineData(0, 0, 1, 0.143080, 0.060617, 0.714173)] + [InlineData(0.754902, 0.501961, 0.100000, 0.315757, 0.273323, 0.035506)] + public void Convert_SRGB_to_XYZ_D50(float r, float g, float b, float x, float y, float z) + { + // Arrange + Rgb input = new Rgb(r, g, b); + ColorSpaceConverter converter = new ColorSpaceConverter { WhitePoint = Illuminants.D50 }; + + // Act + CieXyz output = converter.ToCieXyz(input); + + // Assert + Assert.Equal(output.X, x, FloatComparerPrecision); + Assert.Equal(output.Y, y, FloatComparerPrecision); + Assert.Equal(output.Z, z, FloatComparerPrecision); + } + + /// + /// Tests conversion from (default sRGB working space) + /// to (). + /// + [Theory] + [InlineData(1, 1, 1, 0.950470, 1.000000, 1.088830)] + [InlineData(0, 0, 0, 0, 0, 0)] + [InlineData(1, 0, 0, 0.412456, 0.212673, 0.019334)] + [InlineData(0, 1, 0, 0.357576, 0.715152, 0.119192)] + [InlineData(0, 0, 1, 0.180437, 0.072175, 0.950304)] + [InlineData(0.754902, 0.501961, 0.100000, 0.297676, 0.267854, 0.045504)] + public void Convert_SRGB_to_XYZ_D65(float r, float g, float b, float x, float y, float z) + { + // Arrange + Rgb input = new Rgb(r, g, b); + ColorSpaceConverter converter = new ColorSpaceConverter { WhitePoint = Illuminants.D65 }; + + // Act + CieXyz output = converter.ToCieXyz(input); + + // Assert + Assert.Equal(output.X, x, FloatComparerPrecision); + Assert.Equal(output.Y, y, FloatComparerPrecision); + Assert.Equal(output.Z, z, FloatComparerPrecision); + } + } +} \ No newline at end of file