mirror of https://github.com/SixLabors/ImageSharp
5 changed files with 313 additions and 4 deletions
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue