Skip to content

Commit e24146b

Browse files
Add Add LUV and LCHLUV
1 parent ad6e16d commit e24146b

7 files changed

Lines changed: 517 additions & 8 deletions

File tree

src/ImageSharp/ColorProfiles/CieConstants.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ internal static class CieConstants
1212
/// <summary>
1313
/// 216F / 24389F
1414
/// </summary>
15-
public const float Epsilon = 0.008856452F;
15+
public const float Epsilon = 216f / 24389f;
1616

1717
/// <summary>
1818
/// 24389F / 27F
1919
/// </summary>
20-
public const float Kappa = 903.2963F;
20+
public const float Kappa = 24389f / 27f;
2121
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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*u*v* 1976 color.
11+
/// <see href="https://en.wikipedia.org/wiki/CIELAB_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC"/>
12+
/// </summary>
13+
public readonly struct CieLchuv : IColorProfile<CieLchuv, CieXyz>
14+
{
15+
private static readonly Vector3 Min = new(0, -200, 0);
16+
private static readonly Vector3 Max = new(100, 200, 360);
17+
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="CieLchuv"/> struct.
20+
/// </summary>
21+
/// <param name="l">The lightness dimension.</param>
22+
/// <param name="c">The chroma, relative saturation.</param>
23+
/// <param name="h">The hue in degrees.</param>
24+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
25+
public CieLchuv(float l, float c, float h)
26+
: this(new Vector3(l, c, h))
27+
{
28+
}
29+
30+
/// <summary>
31+
/// Initializes a new instance of the <see cref="CieLchuv"/> struct.
32+
/// </summary>
33+
/// <param name="vector">The vector representing the l, c, h components.</param>
34+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
35+
public CieLchuv(Vector3 vector)
36+
: this()
37+
{
38+
vector = Vector3.Clamp(vector, Min, Max);
39+
this.L = vector.X;
40+
this.C = vector.Y;
41+
this.H = vector.Z;
42+
}
43+
44+
/// <summary>
45+
/// Gets the lightness dimension.
46+
/// <remarks>A value ranging between 0 (black), 100 (diffuse white) or higher (specular white).</remarks>
47+
/// </summary>
48+
public readonly float L { get; }
49+
50+
/// <summary>
51+
/// Gets the a chroma component.
52+
/// <remarks>A value ranging from 0 to 200.</remarks>
53+
/// </summary>
54+
public readonly float C { get; }
55+
56+
/// <summary>
57+
/// Gets the h° hue component in degrees.
58+
/// <remarks>A value ranging from 0 to 360.</remarks>
59+
/// </summary>
60+
public readonly float H { get; }
61+
62+
/// <summary>
63+
/// Gets the reference white point of this color
64+
/// </summary>
65+
public readonly CieXyz WhitePoint { get; }
66+
67+
/// <summary>
68+
/// Compares two <see cref="CieLchuv"/> objects for equality.
69+
/// </summary>
70+
/// <param name="left">The <see cref="CieLchuv"/> on the left side of the operand.</param>
71+
/// <param name="right">The <see cref="CieLchuv"/> 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+
public static bool operator ==(CieLchuv left, CieLchuv right) => left.Equals(right);
76+
77+
/// <summary>
78+
/// Compares two <see cref="CieLchuv"/> objects for inequality
79+
/// </summary>
80+
/// <param name="left">The <see cref="CieLchuv"/> on the left side of the operand.</param>
81+
/// <param name="right">The <see cref="CieLchuv"/> on the right side of the operand.</param>
82+
/// <returns>
83+
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
84+
/// </returns>
85+
public static bool operator !=(CieLchuv left, CieLchuv right) => !left.Equals(right);
86+
87+
/// <inheritdoc/>
88+
public static CieLchuv FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
89+
{
90+
CieLuv luv = CieLuv.FromProfileConnectingSpace(options, source);
91+
92+
// Conversion algorithm described here:
93+
// https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_.28CIELCH.29
94+
float l = luv.L, u = luv.U, v = luv.V;
95+
float c = MathF.Sqrt((u * u) + (v * v));
96+
float hRadians = MathF.Atan2(v, u);
97+
float hDegrees = GeometryUtilities.RadianToDegree(hRadians);
98+
99+
// Wrap the angle round at 360.
100+
hDegrees %= 360;
101+
102+
// Make sure it's not negative.
103+
while (hDegrees < 0)
104+
{
105+
hDegrees += 360;
106+
}
107+
108+
return new CieLchuv(l, c, hDegrees);
109+
}
110+
111+
/// <inheritdoc/>
112+
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieLchuv> destination)
113+
{
114+
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
115+
for (int i = 0; i < source.Length; i++)
116+
{
117+
CieXyz xyz = source[i];
118+
destination[i] = FromProfileConnectingSpace(options, in xyz);
119+
}
120+
}
121+
122+
/// <inheritdoc/>
123+
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
124+
{
125+
// Conversion algorithm described here:
126+
// https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_.28CIELCH.29
127+
float l = this.L, c = this.C, hDegrees = this.H;
128+
float hRadians = GeometryUtilities.DegreeToRadian(hDegrees);
129+
130+
float u = c * MathF.Cos(hRadians);
131+
float v = c * MathF.Sin(hRadians);
132+
133+
CieLuv luv = new(l, u, v);
134+
return luv.ToProfileConnectingSpace(options);
135+
}
136+
137+
/// <inheritdoc/>
138+
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieLchuv> source, Span<CieXyz> destination)
139+
{
140+
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
141+
for (int i = 0; i < source.Length; i++)
142+
{
143+
CieLchuv lch = source[i];
144+
destination[i] = lch.ToProfileConnectingSpace(options);
145+
}
146+
}
147+
148+
/// <inheritdoc/>
149+
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource()
150+
=> ChromaticAdaptionWhitePointSource.WhitePoint;
151+
152+
/// <inheritdoc/>
153+
public override int GetHashCode()
154+
=> HashCode.Combine(this.L, this.C, this.H, this.WhitePoint);
155+
156+
/// <inheritdoc/>
157+
public override string ToString()
158+
=> FormattableString.Invariant($"CieLchuv({this.L:#0.##}, {this.C:#0.##}, {this.H:#0.##})");
159+
160+
/// <inheritdoc/>
161+
public override bool Equals(object? obj)
162+
=> obj is CieLchuv other && this.Equals(other);
163+
164+
/// <inheritdoc/>
165+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
166+
public bool Equals(CieLchuv other)
167+
=> this.L.Equals(other.L)
168+
&& this.C.Equals(other.C)
169+
&& this.H.Equals(other.H)
170+
&& this.WhitePoint.Equals(other.WhitePoint);
171+
172+
/// <summary>
173+
/// Computes the saturation of the color (chroma normalized by lightness)
174+
/// </summary>
175+
/// <remarks>
176+
/// A value ranging from 0 to 100.
177+
/// </remarks>
178+
/// <returns>The <see cref="float"/></returns>
179+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
180+
public float Saturation()
181+
{
182+
float result = 100 * (this.C / this.L);
183+
184+
if (float.IsNaN(result))
185+
{
186+
return 0;
187+
}
188+
189+
return result;
190+
}
191+
}

0 commit comments

Comments
 (0)