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