mirror of https://github.com/SixLabors/ImageSharp
19 changed files with 754 additions and 37 deletions
@ -0,0 +1,209 @@ |
|||
// <copyright file="CieLch.cs" company="James Jackson-South">
|
|||
// Copyright (c) James Jackson-South and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
// </copyright>
|
|||
|
|||
namespace ImageSharp.Colors.Spaces |
|||
{ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Numerics; |
|||
|
|||
/// <summary>
|
|||
/// Represents the CIE L*C*h°, cylindrical form of the CIE L*a*b* 1976 color.
|
|||
/// <see href="https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC"/>
|
|||
/// </summary>
|
|||
public struct CieLch : IColorVector, IEquatable<CieLch>, IAlmostEquatable<CieLch, float> |
|||
{ |
|||
/// <summary>
|
|||
/// D50 standard illuminant.
|
|||
/// Used when reference white is not specified explicitly.
|
|||
/// </summary>
|
|||
public static readonly CieXyz DefaultWhitePoint = Illuminants.D50; |
|||
|
|||
/// <summary>
|
|||
/// Represents a <see cref="CieLch"/> that has L, C, H values set to zero.
|
|||
/// </summary>
|
|||
public static readonly CieLch Empty = default(CieLch); |
|||
|
|||
/// <summary>
|
|||
/// The backing vector for SIMD support.
|
|||
/// </summary>
|
|||
private readonly Vector3 backingVector; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="CieLch"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="l">The lightness dimension.</param>
|
|||
/// <param name="c">The chroma, relative saturation.</param>
|
|||
/// <param name="h">The hue in degrees.</param>
|
|||
/// <remarks>Uses <see cref="DefaultWhitePoint"/> as white point.</remarks>
|
|||
public CieLch(float l, float c, float h) |
|||
: this(new Vector3(l, c, h), DefaultWhitePoint) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="CieLch"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="l">The lightness dimension.</param>
|
|||
/// <param name="c">The chroma, relative saturation.</param>
|
|||
/// <param name="h">The hue in degrees.</param>
|
|||
/// <param name="whitePoint">The reference white point. <see cref="Illuminants"/></param>
|
|||
public CieLch(float l, float c, float h, CieXyz whitePoint) |
|||
: this(new Vector3(l, c, h), whitePoint) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="CieLch"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="vector">The vector representing the l, c, h components.</param>
|
|||
/// <remarks>Uses <see cref="DefaultWhitePoint"/> as white point.</remarks>
|
|||
public CieLch(Vector3 vector) |
|||
: this(vector, DefaultWhitePoint) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="CieLch"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="vector">The vector representing the l, c, h components.</param>
|
|||
/// <param name="whitePoint">The reference white point. <see cref="Illuminants"/></param>
|
|||
public CieLch(Vector3 vector, CieXyz whitePoint) |
|||
: this() |
|||
{ |
|||
this.backingVector = vector; |
|||
this.WhitePoint = whitePoint; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the reference white point of this color
|
|||
/// </summary>
|
|||
public CieXyz WhitePoint { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the lightness dimension.
|
|||
/// <remarks>A value ranging between 0 (black), 100 (diffuse white) or higher (specular white).</remarks>
|
|||
/// </summary>
|
|||
public float L => this.backingVector.X; |
|||
|
|||
/// <summary>
|
|||
/// Gets the a chroma component.
|
|||
/// <remarks>A value ranging from 0 to 100.</remarks>
|
|||
/// </summary>
|
|||
public float C => this.backingVector.Y; |
|||
|
|||
/// <summary>
|
|||
/// Gets the h° hue component in degrees.
|
|||
/// <remarks>A value ranging from 0 to 360.</remarks>
|
|||
/// </summary>
|
|||
public float H => this.backingVector.Z; |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether this <see cref="CieLch"/> is empty.
|
|||
/// </summary>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public bool IsEmpty => this.Equals(Empty); |
|||
|
|||
/// <inheritdoc />
|
|||
public Vector3 Vector => this.backingVector; |
|||
|
|||
/// <summary>
|
|||
/// Compares two <see cref="CieLch"/> objects for equality.
|
|||
/// </summary>
|
|||
/// <param name="left">
|
|||
/// The <see cref="CieLch"/> on the left side of the operand.
|
|||
/// </param>
|
|||
/// <param name="right">
|
|||
/// The <see cref="CieLch"/> on the right side of the operand.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
|
|||
/// </returns>
|
|||
public static bool operator ==(CieLch left, CieLch right) |
|||
{ |
|||
return left.Equals(right); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compares two <see cref="CieLch"/> objects for inequality
|
|||
/// </summary>
|
|||
/// <param name="left">
|
|||
/// The <see cref="CieLch"/> on the left side of the operand.
|
|||
/// </param>
|
|||
/// <param name="right">
|
|||
/// The <see cref="CieLch"/> on the right side of the operand.
|
|||
/// </param>
|
|||
/// <returns>
|
|||
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
|
|||
/// </returns>
|
|||
public static bool operator !=(CieLch left, CieLch right) |
|||
{ |
|||
return !left.Equals(right); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int GetHashCode() |
|||
{ |
|||
return this.backingVector.GetHashCode(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override string ToString() |
|||
{ |
|||
if (this.IsEmpty) |
|||
{ |
|||
return "CieLch [Empty]"; |
|||
} |
|||
|
|||
return $"CieLch [ L={this.L:#0.##}, C={this.C:#0.##}, H={this.H:#0.##}]"; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
if (obj is CieLch) |
|||
{ |
|||
return this.Equals((CieLch)obj); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool Equals(CieLch other) |
|||
{ |
|||
return this.backingVector.Equals(other.backingVector); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool AlmostEquals(CieLch other, float precision) |
|||
{ |
|||
Vector3 result = Vector3.Abs(this.backingVector - other.backingVector); |
|||
|
|||
return result.X <= precision |
|||
&& result.Y <= precision |
|||
&& result.Z <= precision; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Computes the saturation of the color (chroma normalized by lightness)
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// A value ranging from 0 to 100.
|
|||
/// </remarks>
|
|||
/// <returns>The <see cref="float"/></returns>
|
|||
public float Saturation() |
|||
{ |
|||
float result = 100 * (this.C / this.L); |
|||
|
|||
if (float.IsNaN(result)) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,101 @@ |
|||
// <copyright file="ColorSpaceConverter.cs" company="James Jackson-South">
|
|||
// Copyright (c) James Jackson-South and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
// </copyright>
|
|||
|
|||
namespace ImageSharp.Colors.Spaces.Conversion |
|||
{ |
|||
using ImageSharp.Colors.Spaces.Conversion.Implementation.CieLch; |
|||
|
|||
/// <summary>
|
|||
/// Converts between color spaces ensuring that the color is adapted using chromatic adaptation.
|
|||
/// </summary>
|
|||
public partial class ColorSpaceConverter |
|||
{ |
|||
/// <summary>
|
|||
/// The converter for converting between CieLab to CieLch.
|
|||
/// </summary>
|
|||
private static readonly CieLabToCieLchConverter CieLabToCieLchConverter = new CieLabToCieLchConverter(); |
|||
|
|||
/// <summary>
|
|||
/// Converts a <see cref="CieXyz"/> into a <see cref="CieLch"/>
|
|||
/// </summary>
|
|||
/// <param name="color">The color to convert.</param>
|
|||
/// <returns>The <see cref="CieLch"/></returns>
|
|||
public CieLch ToCieLch(CieXyz color) |
|||
{ |
|||
Guard.NotNull(color, nameof(color)); |
|||
|
|||
CieLab labColor = this.ToCieLab(color); |
|||
return this.ToCieLch(labColor); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a <see cref="Rgb"/> into a <see cref="CieLch"/>
|
|||
/// </summary>
|
|||
/// <param name="color">The color to convert.</param>
|
|||
/// <returns>The <see cref="CieLch"/></returns>
|
|||
public CieLch ToCieLch(Rgb color) |
|||
{ |
|||
Guard.NotNull(color, nameof(color)); |
|||
|
|||
CieLab labColor = this.ToCieLab(color); |
|||
return this.ToCieLch(labColor); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a <see cref="LinearRgb"/> into a <see cref="CieLch"/>
|
|||
/// </summary>
|
|||
/// <param name="color">The color to convert.</param>
|
|||
/// <returns>The <see cref="CieLch"/></returns>
|
|||
public CieLch ToCieLch(LinearRgb color) |
|||
{ |
|||
Guard.NotNull(color, nameof(color)); |
|||
|
|||
CieLab labColor = this.ToCieLab(color); |
|||
return this.ToCieLch(labColor); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a <see cref="CieLab"/> into a <see cref="CieLch"/>
|
|||
/// </summary>
|
|||
/// <param name="color">The color to convert.</param>
|
|||
/// <returns>The <see cref="CieLch"/></returns>
|
|||
public CieLch ToCieLch(CieLab color) |
|||
{ |
|||
Guard.NotNull(color, nameof(color)); |
|||
|
|||
// Adaptation
|
|||
CieLab adapted = this.IsChromaticAdaptationPerformed ? this.Adapt(color) : color; |
|||
|
|||
// Conversion
|
|||
return CieLabToCieLchConverter.Convert(adapted); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a <see cref="Lms"/> into a <see cref="CieLch"/>
|
|||
/// </summary>
|
|||
/// <param name="color">The color to convert.</param>
|
|||
/// <returns>The <see cref="CieLch"/></returns>
|
|||
public CieLch ToCieLch(Lms color) |
|||
{ |
|||
Guard.NotNull(color, nameof(color)); |
|||
|
|||
CieLab labColor = this.ToCieLab(color); |
|||
return this.ToCieLch(labColor); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a <see cref="HunterLab"/> into a <see cref="CieLch"/>
|
|||
/// </summary>
|
|||
/// <param name="color">The color to convert.</param>
|
|||
/// <returns>The <see cref="CieLch"/></returns>
|
|||
public CieLch ToCieLch(HunterLab color) |
|||
{ |
|||
Guard.NotNull(color, nameof(color)); |
|||
|
|||
CieLab labColor = this.ToCieLab(color); |
|||
return this.ToCieLch(labColor); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
// <copyright file="CieLchToCieLabConverter.cs" company="James Jackson-South">
|
|||
// Copyright (c) James Jackson-South and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
// </copyright>
|
|||
|
|||
namespace ImageSharp.Colors.Spaces.Conversion.Implementation.CieLch |
|||
{ |
|||
using ImageSharp.Colors.Spaces; |
|||
|
|||
/// <summary>
|
|||
/// Converts from <see cref="CieLch"/> to <see cref="CieLab"/>.
|
|||
/// </summary>
|
|||
public class CieLchToCieLabConverter : IColorConversion<CieLch, CieLab> |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public CieLab Convert(CieLch input) |
|||
{ |
|||
DebugGuard.NotNull(input, nameof(input)); |
|||
|
|||
// Conversion algorithm described here: https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC
|
|||
float l = input.L, c = input.C, hDegrees = input.H; |
|||
float hRadians = MathF.DegreeToRadian(hDegrees); |
|||
|
|||
float a = c * MathF.Cos(hRadians); |
|||
float b = c * MathF.Sin(hRadians); |
|||
|
|||
return new CieLab(l, a, b, input.WhitePoint); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
// <copyright file="CieLabToCieLchConverter.cs" company="James Jackson-South">
|
|||
// Copyright (c) James Jackson-South and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
// </copyright>
|
|||
|
|||
namespace ImageSharp.Colors.Spaces.Conversion.Implementation.CieLch |
|||
{ |
|||
using ImageSharp.Colors.Spaces; |
|||
|
|||
/// <summary>
|
|||
/// Converts from <see cref="CieLab"/> to <see cref="CieLch"/>.
|
|||
/// </summary>
|
|||
internal class CieLabToCieLchConverter : IColorConversion<CieLab, CieLch> |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public CieLch Convert(CieLab input) |
|||
{ |
|||
DebugGuard.NotNull(input, nameof(input)); |
|||
|
|||
// Conversion algorithm described here: https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC
|
|||
float l = input.L, a = input.A, b = input.B; |
|||
float c = MathF.Sqrt((a * a) + (b * b)); |
|||
float hRadians = MathF.Atan2(b, a); |
|||
float hDegrees = MathF.RadianToDegree(hRadians); |
|||
|
|||
if (hDegrees > 360) |
|||
{ |
|||
hDegrees -= 360; |
|||
} |
|||
else if (hDegrees < 0) |
|||
{ |
|||
hDegrees += 360; |
|||
} |
|||
|
|||
return new CieLch(l, c, hDegrees, input.WhitePoint); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
namespace ImageSharp.Tests.Colors.Colorspaces |
|||
{ |
|||
using System.Collections.Generic; |
|||
using ImageSharp.Colors.Spaces; |
|||
using ImageSharp.Colors.Spaces.Conversion; |
|||
|
|||
using Xunit; |
|||
|
|||
/// <summary>
|
|||
/// Tests <see cref="CieLab"/>-<see cref="CieLch"/> conversions.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Test data generated using:
|
|||
/// <see href="http://www.brucelindbloom.com/index.html?ColorCalculator.html"/>
|
|||
/// </remarks>
|
|||
public class CieLabAndCieLchConversionTests |
|||
{ |
|||
private static readonly IEqualityComparer<float> FloatRoundingComparer = new FloatRoundingComparer(4); |
|||
|
|||
private static readonly ColorSpaceConverter Converter = new ColorSpaceConverter(); |
|||
|
|||
/// <summary>
|
|||
/// Tests conversion from <see cref="CieLch"/> to <see cref="CieLab"/>.
|
|||
/// </summary>
|
|||
[Theory] |
|||
[InlineData(0, 0, 0, 0, 0, 0)] |
|||
[InlineData(54.2917, 106.8391, 40.8526, 54.2917, 80.8125, 69.8851)] |
|||
[InlineData(100, 0, 0, 100, 0, 0)] |
|||
[InlineData(100, 50, 180, 100, -50, 0)] |
|||
[InlineData(10, 36.0555, 56.3099, 10, 20, 30)] |
|||
[InlineData(10, 36.0555, 56.3099, 10, 20, 30)] |
|||
[InlineData(10, 36.0555, 123.6901, 10, -20, 30)] |
|||
[InlineData(10, 36.0555, 303.6901, 10, 20, -30)] |
|||
[InlineData(10, 36.0555, 236.3099, 10, -20, -30)] |
|||
public void Convert_Lch_to_Lab(float l, float c, float h, float l2, float a, float b) |
|||
{ |
|||
// Arrange
|
|||
CieLch input = new CieLch(l, c, h); |
|||
|
|||
// Act
|
|||
CieLab output = Converter.ToCieLab(input); |
|||
|
|||
// Assert
|
|||
Assert.Equal(l2, output.L, FloatRoundingComparer); |
|||
Assert.Equal(a, output.A, FloatRoundingComparer); |
|||
Assert.Equal(b, output.B, FloatRoundingComparer); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tests conversion from <see cref="LabColor"/> to <see cref="LChabColor"/>.
|
|||
/// </summary>
|
|||
[Theory] |
|||
[InlineData(0, 0, 0, 0, 0, 0)] |
|||
[InlineData(54.2917, 80.8125, 69.8851, 54.2917, 106.8391, 40.8526)] |
|||
[InlineData(100, 0, 0, 100, 0, 0)] |
|||
[InlineData(100, -50, 0, 100, 50, 180)] |
|||
[InlineData(10, 20, 30, 10, 36.0555, 56.3099)] |
|||
[InlineData(10, 20, 30, 10, 36.0555, 56.3099)] |
|||
[InlineData(10, -20, 30, 10, 36.0555, 123.6901)] |
|||
[InlineData(10, 20, -30, 10, 36.0555, 303.6901)] |
|||
[InlineData(10, -20, -30, 10, 36.0555, 236.3099)] |
|||
public void Convert_Lab_to_LCHab(float l, float a, float b, float l2, float c, float h) |
|||
{ |
|||
// Arrange
|
|||
CieLab input = new CieLab(l, a, b); |
|||
|
|||
// Act
|
|||
CieLch output = Converter.ToCieLch(input); |
|||
|
|||
// Assert
|
|||
Assert.Equal(l2, output.L, FloatRoundingComparer); |
|||
Assert.Equal(c, output.C, FloatRoundingComparer); |
|||
Assert.Equal(h, output.H, FloatRoundingComparer); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue