Browse Source

Add CieLch

pull/2739/head
James Jackson-South 2 years ago
parent
commit
8bc794af65
  1. 4
      src/ImageSharp/ColorProfiles/CieLab.cs
  2. 166
      src/ImageSharp/ColorProfiles/CieLch.cs
  3. 52
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs
  4. 7
      tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs
  5. 88
      tests/ImageSharp.Tests/ColorProfiles/CieLabAndCieLchConversionTests.cs

4
src/ImageSharp/ColorProfiles/CieLab.cs

@ -155,9 +155,7 @@ public readonly struct CieLab : IProfileConnectingSpace<CieLab, CieXyz>
CieXyz whitePoint = options.WhitePoint;
Vector3 wxyz = new(whitePoint.X, whitePoint.Y, whitePoint.Z);
// Avoids XYZ coordinates out range (restricted by 0 and XYZ reference white)
Vector3 xyzr = Vector3.Clamp(new Vector3(xr, yr, zr), Vector3.Zero, Vector3.One);
Vector3 xyzr = new(xr, yr, zr);
return new(xyzr * wxyz);
}

166
src/ImageSharp/ColorProfiles/CieLch.cs

@ -0,0 +1,166 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.ColorProfiles;
/// <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 readonly struct CieLch : IColorProfile<CieLch, CieLab>
{
/// <summary>
/// D50 standard illuminant.
/// Used when reference white is not specified explicitly.
/// </summary>
public static readonly CieXyz DefaultWhitePoint = Illuminants.D50;
private static readonly Vector3 Min = new(0, -200, 0);
private static readonly Vector3 Max = new(100, 200, 360);
/// <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>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLch(float l, float c, float h)
: this(new Vector3(l, c, h))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CieLch"/> struct.
/// </summary>
/// <param name="vector">The vector representing the l, c, h components.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CieLch(Vector3 vector)
{
vector = Vector3.Clamp(vector, Min, Max);
this.L = vector.X;
this.C = vector.Y;
this.H = vector.Z;
}
/// <summary>
/// Gets the lightness dimension.
/// <remarks>A value ranging between 0 (black), 100 (diffuse white) or higher (specular white).</remarks>
/// </summary>
public readonly float L { get; }
/// <summary>
/// Gets the a chroma component.
/// <remarks>A value ranging from 0 to 200.</remarks>
/// </summary>
public readonly float C { get; }
/// <summary>
/// Gets the h° hue component in degrees.
/// <remarks>A value ranging from 0 to 360.</remarks>
/// </summary>
public readonly float H { get; }
/// <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>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(CieLch left, CieLch right) => 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>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(CieLch left, CieLch right) => !left.Equals(right);
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine(this.L, this.C, this.H);
/// <inheritdoc/>
public override string ToString() => FormattableString.Invariant($"CieLch({this.L:#0.##}, {this.C:#0.##}, {this.H:#0.##})");
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool Equals(object? obj) => obj is CieLch other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(CieLch other)
=> this.L.Equals(other.L)
&& this.C.Equals(other.C)
&& this.H.Equals(other.H);
/// <inheritdoc/>
public static CieLch FromProfileConnectingSpace(ColorConversionOptions options, in CieLab source)
{
// Conversion algorithm described here:
// https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC
float l = source.L, a = source.A, b = source.B;
float c = MathF.Sqrt((a * a) + (b * b));
float hRadians = MathF.Atan2(b, a);
float hDegrees = GeometryUtilities.RadianToDegree(hRadians);
// Wrap the angle round at 360.
hDegrees %= 360;
// Make sure it's not negative.
while (hDegrees < 0)
{
hDegrees += 360;
}
return new CieLch(l, c, hDegrees);
}
/// <inheritdoc/>
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieLab> source, Span<CieLch> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieLab lab = source[i];
destination[i] = FromProfileConnectingSpace(options, in lab);
}
}
/// <inheritdoc/>
public CieLab ToProfileConnectingSpace(ColorConversionOptions options)
{
// Conversion algorithm described here:
// https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC
float l = this.L, c = this.C, hDegrees = this.H;
float hRadians = GeometryUtilities.DegreeToRadian(hDegrees);
float a = c * MathF.Cos(hRadians);
float b = c * MathF.Sin(hRadians);
return new CieLab(l, a, b);
}
/// <inheritdoc/>
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieLch> source, Span<CieLab> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
CieLch lch = source[i];
destination[i] = lch.ToProfileConnectingSpace(options);
}
}
}

52
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs

@ -0,0 +1,52 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsCieXyzCieLab
{
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, TFrom source)
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, CieLab>
{
ColorConversionOptions options = converter.Options;
// Convert to input PCS
CieXyz pcsFrom = source.ToProfileConnectingSpace(options);
// Adapt to target white point
VonKriesChromaticAdaptation.Transform(options, in pcsFrom);
// Convert between PCS
CieLab pcsTo = CieLab.FromProfileConnectingSpace(options, in pcsFrom);
// Convert to output from PCS
return TTo.FromProfileConnectingSpace(options, pcsTo);
}
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, CieLab>
{
ColorConversionOptions options = converter.Options;
// Convert to input PCS.
using IMemoryOwner<CieXyz> pcsFromOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
Span<CieXyz> pcsFrom = pcsFromOwner.GetSpan();
TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
// Adapt to target white point
VonKriesChromaticAdaptation.Transform(options, pcsFrom, pcsFrom);
// Convert between PCS.
using IMemoryOwner<CieLab> pcsToOwner = options.MemoryAllocator.Allocate<CieLab>(source.Length);
Span<CieLab> pcsTo = pcsToOwner.GetSpan();
CieLab.FromProfileConnectionSpace(options, pcsFrom, pcsTo);
// Convert to output from PCS
TTo.FromProfileConnectionSpace(options, pcsTo, destination);
}
}

7
tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs

@ -12,7 +12,8 @@ namespace SixLabors.ImageSharp.Tests.ColorProfiles;
internal readonly struct ApproximateColorProfileComparer :
IEqualityComparer<CieLab>,
IEqualityComparer<CieXyz>,
IEqualityComparer<Lms>
IEqualityComparer<Lms>,
IEqualityComparer<CieLch>
{
private readonly float epsilon;
@ -28,12 +29,16 @@ internal readonly struct ApproximateColorProfileComparer :
public bool Equals(Lms x, Lms y) => this.Equals(x.L, y.L) && this.Equals(x.M, y.M) && this.Equals(x.S, y.S);
public bool Equals(CieLch x, CieLch y) => this.Equals(x.L, y.L) && this.Equals(x.C, y.C) && this.Equals(x.H, y.H);
public int GetHashCode([DisallowNull] CieLab obj) => obj.GetHashCode();
public int GetHashCode([DisallowNull] CieXyz obj) => obj.GetHashCode();
public int GetHashCode([DisallowNull] Lms obj) => obj.GetHashCode();
public int GetHashCode([DisallowNull] CieLch obj) => obj.GetHashCode();
private bool Equals(float x, float y)
{
float d = x - y;

88
tests/ImageSharp.Tests/ColorProfiles/CieLabAndCieLchConversionTests.cs

@ -0,0 +1,88 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.ColorProfiles;
namespace SixLabors.ImageSharp.Tests.ColorProfiles;
/// <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 ApproximateColorProfileComparer Comparer = new(.0001f);
[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, 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(l, c, h);
CieLab expected = new(l2, a, b);
ColorConversionOptions options = new() { WhitePoint = Illuminants.D50, TargetWhitePoint = Illuminants.D50 };
ColorProfileConverter converter = new(options);
Span<CieLch> inputSpan = new CieLch[5];
inputSpan.Fill(input);
Span<CieLab> actualSpan = new CieLab[5];
// Act
CieLab actual = converter.Convert<CieLch, CieLab>(input);
converter.Convert<CieLch, CieLab>(inputSpan, actualSpan);
// Assert
Assert.Equal(expected, actual, Comparer);
for (int i = 0; i < actualSpan.Length; i++)
{
Assert.Equal(expected, actualSpan[i], Comparer);
}
}
[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, 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_Lch(float l, float a, float b, float l2, float c, float h)
{
// Arrange
CieLab input = new(l, a, b);
CieLch expected = new(l2, c, h);
ColorConversionOptions options = new() { WhitePoint = Illuminants.D50, TargetWhitePoint = Illuminants.D50 };
ColorProfileConverter converter = new(options);
Span<CieLab> inputSpan = new CieLab[5];
inputSpan.Fill(input);
Span<CieLch> actualSpan = new CieLch[5];
// Act
CieLch actual = converter.Convert<CieLab, CieLch>(input);
converter.Convert<CieLab, CieLch>(inputSpan, actualSpan);
// Assert
Assert.Equal(expected, actual, Comparer);
for (int i = 0; i < actualSpan.Length; i++)
{
Assert.Equal(expected, actualSpan[i], Comparer);
}
}
}
Loading…
Cancel
Save