Skip to content

Commit 8bc794a

Browse files
Add CieLch
1 parent 3d23690 commit 8bc794a

5 files changed

Lines changed: 313 additions & 4 deletions

File tree

src/ImageSharp/ColorProfiles/CieLab.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,7 @@ public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
155155

156156
CieXyz whitePoint = options.WhitePoint;
157157
Vector3 wxyz = new(whitePoint.X, whitePoint.Y, whitePoint.Z);
158-
159-
// Avoids XYZ coordinates out range (restricted by 0 and XYZ reference white)
160-
Vector3 xyzr = Vector3.Clamp(new Vector3(xr, yr, zr), Vector3.Zero, Vector3.One);
158+
Vector3 xyzr = new(xr, yr, zr);
161159

162160
return new(xyzr * wxyz);
163161
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Numerics;
5+
using System.Runtime.CompilerServices;
6+
7+
namespace SixLabors.ImageSharp.ColorProfiles;
8+
9+
/// <summary>
10+
/// Represents the CIE L*C*h°, cylindrical form of the CIE L*a*b* 1976 color.
11+
/// <see href="https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC"/>
12+
/// </summary>
13+
public readonly struct CieLch : IColorProfile<CieLch, CieLab>
14+
{
15+
/// <summary>
16+
/// D50 standard illuminant.
17+
/// Used when reference white is not specified explicitly.
18+
/// </summary>
19+
public static readonly CieXyz DefaultWhitePoint = Illuminants.D50;
20+
21+
private static readonly Vector3 Min = new(0, -200, 0);
22+
private static readonly Vector3 Max = new(100, 200, 360);
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="CieLch"/> struct.
26+
/// </summary>
27+
/// <param name="l">The lightness dimension.</param>
28+
/// <param name="c">The chroma, relative saturation.</param>
29+
/// <param name="h">The hue in degrees.</param>
30+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
31+
public CieLch(float l, float c, float h)
32+
: this(new Vector3(l, c, h))
33+
{
34+
}
35+
36+
/// <summary>
37+
/// Initializes a new instance of the <see cref="CieLch"/> struct.
38+
/// </summary>
39+
/// <param name="vector">The vector representing the l, c, h components.</param>
40+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
41+
public CieLch(Vector3 vector)
42+
{
43+
vector = Vector3.Clamp(vector, Min, Max);
44+
this.L = vector.X;
45+
this.C = vector.Y;
46+
this.H = vector.Z;
47+
}
48+
49+
/// <summary>
50+
/// Gets the lightness dimension.
51+
/// <remarks>A value ranging between 0 (black), 100 (diffuse white) or higher (specular white).</remarks>
52+
/// </summary>
53+
public readonly float L { get; }
54+
55+
/// <summary>
56+
/// Gets the a chroma component.
57+
/// <remarks>A value ranging from 0 to 200.</remarks>
58+
/// </summary>
59+
public readonly float C { get; }
60+
61+
/// <summary>
62+
/// Gets the h° hue component in degrees.
63+
/// <remarks>A value ranging from 0 to 360.</remarks>
64+
/// </summary>
65+
public readonly float H { get; }
66+
67+
/// <summary>
68+
/// Compares two <see cref="CieLch"/> objects for equality.
69+
/// </summary>
70+
/// <param name="left">The <see cref="CieLch"/> on the left side of the operand.</param>
71+
/// <param name="right">The <see cref="CieLch"/> on the right side of the operand.</param>
72+
/// <returns>
73+
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
74+
/// </returns>
75+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
76+
public static bool operator ==(CieLch left, CieLch right) => left.Equals(right);
77+
78+
/// <summary>
79+
/// Compares two <see cref="CieLch"/> objects for inequality
80+
/// </summary>
81+
/// <param name="left">The <see cref="CieLch"/> on the left side of the operand.</param>
82+
/// <param name="right">The <see cref="CieLch"/> on the right side of the operand.</param>
83+
/// <returns>
84+
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
85+
/// </returns>
86+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
87+
public static bool operator !=(CieLch left, CieLch right) => !left.Equals(right);
88+
89+
/// <inheritdoc/>
90+
public override int GetHashCode()
91+
=> HashCode.Combine(this.L, this.C, this.H);
92+
93+
/// <inheritdoc/>
94+
public override string ToString() => FormattableString.Invariant($"CieLch({this.L:#0.##}, {this.C:#0.##}, {this.H:#0.##})");
95+
96+
/// <inheritdoc/>
97+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
98+
public override bool Equals(object? obj) => obj is CieLch other && this.Equals(other);
99+
100+
/// <inheritdoc/>
101+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
102+
public bool Equals(CieLch other)
103+
=> this.L.Equals(other.L)
104+
&& this.C.Equals(other.C)
105+
&& this.H.Equals(other.H);
106+
107+
/// <inheritdoc/>
108+
public static CieLch FromProfileConnectingSpace(ColorConversionOptions options, in CieLab source)
109+
{
110+
// Conversion algorithm described here:
111+
// https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC
112+
float l = source.L, a = source.A, b = source.B;
113+
float c = MathF.Sqrt((a * a) + (b * b));
114+
float hRadians = MathF.Atan2(b, a);
115+
float hDegrees = GeometryUtilities.RadianToDegree(hRadians);
116+
117+
// Wrap the angle round at 360.
118+
hDegrees %= 360;
119+
120+
// Make sure it's not negative.
121+
while (hDegrees < 0)
122+
{
123+
hDegrees += 360;
124+
}
125+
126+
return new CieLch(l, c, hDegrees);
127+
}
128+
129+
/// <inheritdoc/>
130+
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieLab> source, Span<CieLch> destination)
131+
{
132+
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
133+
134+
for (int i = 0; i < source.Length; i++)
135+
{
136+
CieLab lab = source[i];
137+
destination[i] = FromProfileConnectingSpace(options, in lab);
138+
}
139+
}
140+
141+
/// <inheritdoc/>
142+
public CieLab ToProfileConnectingSpace(ColorConversionOptions options)
143+
{
144+
// Conversion algorithm described here:
145+
// https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC
146+
float l = this.L, c = this.C, hDegrees = this.H;
147+
float hRadians = GeometryUtilities.DegreeToRadian(hDegrees);
148+
149+
float a = c * MathF.Cos(hRadians);
150+
float b = c * MathF.Sin(hRadians);
151+
152+
return new CieLab(l, a, b);
153+
}
154+
155+
/// <inheritdoc/>
156+
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieLch> source, Span<CieLab> destination)
157+
{
158+
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
159+
160+
for (int i = 0; i < source.Length; i++)
161+
{
162+
CieLch lch = source[i];
163+
destination[i] = lch.ToProfileConnectingSpace(options);
164+
}
165+
}
166+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Buffers;
5+
using SixLabors.ImageSharp.Memory;
6+
7+
namespace SixLabors.ImageSharp.ColorProfiles;
8+
9+
internal static class ColorProfileConverterExtensionsCieXyzCieLab
10+
{
11+
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, TFrom source)
12+
where TFrom : struct, IColorProfile<TFrom, CieXyz>
13+
where TTo : struct, IColorProfile<TTo, CieLab>
14+
{
15+
ColorConversionOptions options = converter.Options;
16+
17+
// Convert to input PCS
18+
CieXyz pcsFrom = source.ToProfileConnectingSpace(options);
19+
20+
// Adapt to target white point
21+
VonKriesChromaticAdaptation.Transform(options, in pcsFrom);
22+
23+
// Convert between PCS
24+
CieLab pcsTo = CieLab.FromProfileConnectingSpace(options, in pcsFrom);
25+
26+
// Convert to output from PCS
27+
return TTo.FromProfileConnectingSpace(options, pcsTo);
28+
}
29+
30+
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
31+
where TFrom : struct, IColorProfile<TFrom, CieXyz>
32+
where TTo : struct, IColorProfile<TTo, CieLab>
33+
{
34+
ColorConversionOptions options = converter.Options;
35+
36+
// Convert to input PCS.
37+
using IMemoryOwner<CieXyz> pcsFromOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
38+
Span<CieXyz> pcsFrom = pcsFromOwner.GetSpan();
39+
TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
40+
41+
// Adapt to target white point
42+
VonKriesChromaticAdaptation.Transform(options, pcsFrom, pcsFrom);
43+
44+
// Convert between PCS.
45+
using IMemoryOwner<CieLab> pcsToOwner = options.MemoryAllocator.Allocate<CieLab>(source.Length);
46+
Span<CieLab> pcsTo = pcsToOwner.GetSpan();
47+
CieLab.FromProfileConnectionSpace(options, pcsFrom, pcsTo);
48+
49+
// Convert to output from PCS
50+
TTo.FromProfileConnectionSpace(options, pcsTo, destination);
51+
}
52+
}

tests/ImageSharp.Tests/ColorProfiles/ApproximateColorProfileComparer.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ namespace SixLabors.ImageSharp.Tests.ColorProfiles;
1212
internal readonly struct ApproximateColorProfileComparer :
1313
IEqualityComparer<CieLab>,
1414
IEqualityComparer<CieXyz>,
15-
IEqualityComparer<Lms>
15+
IEqualityComparer<Lms>,
16+
IEqualityComparer<CieLch>
1617
{
1718
private readonly float epsilon;
1819

@@ -28,12 +29,16 @@ namespace SixLabors.ImageSharp.Tests.ColorProfiles;
2829

2930
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);
3031

32+
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);
33+
3134
public int GetHashCode([DisallowNull] CieLab obj) => obj.GetHashCode();
3235

3336
public int GetHashCode([DisallowNull] CieXyz obj) => obj.GetHashCode();
3437

3538
public int GetHashCode([DisallowNull] Lms obj) => obj.GetHashCode();
3639

40+
public int GetHashCode([DisallowNull] CieLch obj) => obj.GetHashCode();
41+
3742
private bool Equals(float x, float y)
3843
{
3944
float d = x - y;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using SixLabors.ImageSharp.ColorProfiles;
5+
6+
namespace SixLabors.ImageSharp.Tests.ColorProfiles;
7+
8+
/// <summary>
9+
/// Tests <see cref="CieLab"/>-<see cref="CieLch"/> conversions.
10+
/// </summary>
11+
/// <remarks>
12+
/// Test data generated using:
13+
/// <see href="http://www.brucelindbloom.com/index.html?ColorCalculator.html"/>
14+
/// </remarks>
15+
public class CieLabAndCieLchConversionTests
16+
{
17+
private static readonly ApproximateColorProfileComparer Comparer = new(.0001f);
18+
19+
[Theory]
20+
[InlineData(0, 0, 0, 0, 0, 0)]
21+
[InlineData(54.2917, 106.8391, 40.8526, 54.2917, 80.8125, 69.8851)]
22+
[InlineData(100, 0, 0, 100, 0, 0)]
23+
[InlineData(100, 50, 180, 100, -50, 0)]
24+
[InlineData(10, 36.0555, 56.3099, 10, 20, 30)]
25+
[InlineData(10, 36.0555, 123.6901, 10, -20, 30)]
26+
[InlineData(10, 36.0555, 303.6901, 10, 20, -30)]
27+
[InlineData(10, 36.0555, 236.3099, 10, -20, -30)]
28+
public void Convert_Lch_to_Lab(float l, float c, float h, float l2, float a, float b)
29+
{
30+
// Arrange
31+
CieLch input = new(l, c, h);
32+
CieLab expected = new(l2, a, b);
33+
ColorConversionOptions options = new() { WhitePoint = Illuminants.D50, TargetWhitePoint = Illuminants.D50 };
34+
ColorProfileConverter converter = new(options);
35+
36+
Span<CieLch> inputSpan = new CieLch[5];
37+
inputSpan.Fill(input);
38+
39+
Span<CieLab> actualSpan = new CieLab[5];
40+
41+
// Act
42+
CieLab actual = converter.Convert<CieLch, CieLab>(input);
43+
converter.Convert<CieLch, CieLab>(inputSpan, actualSpan);
44+
45+
// Assert
46+
Assert.Equal(expected, actual, Comparer);
47+
48+
for (int i = 0; i < actualSpan.Length; i++)
49+
{
50+
Assert.Equal(expected, actualSpan[i], Comparer);
51+
}
52+
}
53+
54+
[Theory]
55+
[InlineData(0, 0, 0, 0, 0, 0)]
56+
[InlineData(54.2917, 80.8125, 69.8851, 54.2917, 106.8391, 40.8526)]
57+
[InlineData(100, 0, 0, 100, 0, 0)]
58+
[InlineData(100, -50, 0, 100, 50, 180)]
59+
[InlineData(10, 20, 30, 10, 36.0555, 56.3099)]
60+
[InlineData(10, -20, 30, 10, 36.0555, 123.6901)]
61+
[InlineData(10, 20, -30, 10, 36.0555, 303.6901)]
62+
[InlineData(10, -20, -30, 10, 36.0555, 236.3099)]
63+
public void Convert_Lab_to_Lch(float l, float a, float b, float l2, float c, float h)
64+
{
65+
// Arrange
66+
CieLab input = new(l, a, b);
67+
CieLch expected = new(l2, c, h);
68+
ColorConversionOptions options = new() { WhitePoint = Illuminants.D50, TargetWhitePoint = Illuminants.D50 };
69+
ColorProfileConverter converter = new(options);
70+
71+
Span<CieLab> inputSpan = new CieLab[5];
72+
inputSpan.Fill(input);
73+
74+
Span<CieLch> actualSpan = new CieLch[5];
75+
76+
// Act
77+
CieLch actual = converter.Convert<CieLab, CieLch>(input);
78+
converter.Convert<CieLab, CieLch>(inputSpan, actualSpan);
79+
80+
// Assert
81+
Assert.Equal(expected, actual, Comparer);
82+
83+
for (int i = 0; i < actualSpan.Length; i++)
84+
{
85+
Assert.Equal(expected, actualSpan[i], Comparer);
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)