Skip to content

Commit fac508c

Browse files
Can translate between profiles.
1 parent f44b761 commit fac508c

18 files changed

Lines changed: 1212 additions & 2 deletions
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.ColorProfiles;
5+
6+
/// <summary>
7+
/// Constants use for Cie conversion calculations
8+
/// <see href="http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html"/>
9+
/// </summary>
10+
internal static class CieConstants
11+
{
12+
/// <summary>
13+
/// 216F / 24389F
14+
/// </summary>
15+
public const float Epsilon = 0.008856452F;
16+
17+
/// <summary>
18+
/// 24389F / 27F
19+
/// </summary>
20+
public const float Kappa = 903.2963F;
21+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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 a CIE L*a*b* 1976 color.
11+
/// <see href="https://en.wikipedia.org/wiki/Lab_color_space"/>
12+
/// </summary>
13+
public readonly struct CieLab : IProfileConnectingSpace<CieLab, CieXyz>
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+
/// <summary>
22+
/// Initializes a new instance of the <see cref="CieLab"/> struct.
23+
/// </summary>
24+
/// <param name="l">The lightness dimension.</param>
25+
/// <param name="a">The a (green - magenta) component.</param>
26+
/// <param name="b">The b (blue - yellow) component.</param>
27+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
28+
public CieLab(float l, float a, float b)
29+
: this(new Vector3(l, a, b))
30+
{
31+
}
32+
33+
/// <summary>
34+
/// Initializes a new instance of the <see cref="CieLab"/> struct.
35+
/// </summary>
36+
/// <param name="vector">The vector representing the l, a, b components.</param>
37+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
38+
public CieLab(Vector3 vector)
39+
: this()
40+
{
41+
// Not clamping as documentation about this space only indicates "usual" ranges
42+
this.L = vector.X;
43+
this.A = vector.Y;
44+
this.B = vector.Z;
45+
}
46+
47+
/// <summary>
48+
/// Gets the lightness dimension.
49+
/// <remarks>A value usually ranging between 0 (black), 100 (diffuse white) or higher (specular white).</remarks>
50+
/// </summary>
51+
public readonly float L { get; }
52+
53+
/// <summary>
54+
/// Gets the a color component.
55+
/// <remarks>A value usually ranging from -100 to 100. Negative is green, positive magenta.</remarks>
56+
/// </summary>
57+
public readonly float A { get; }
58+
59+
/// <summary>
60+
/// Gets the b color component.
61+
/// <remarks>A value usually ranging from -100 to 100. Negative is blue, positive is yellow</remarks>
62+
/// </summary>
63+
public readonly float B { get; }
64+
65+
/// <summary>
66+
/// Compares two <see cref="CieLab"/> objects for equality.
67+
/// </summary>
68+
/// <param name="left">The <see cref="CieLab"/> on the left side of the operand.</param>
69+
/// <param name="right">The <see cref="CieLab"/> on the right side of the operand.</param>
70+
/// <returns>
71+
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
72+
/// </returns>
73+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
74+
public static bool operator ==(CieLab left, CieLab right) => left.Equals(right);
75+
76+
/// <summary>
77+
/// Compares two <see cref="CieLab"/> objects for inequality
78+
/// </summary>
79+
/// <param name="left">The <see cref="CieLab"/> on the left side of the operand.</param>
80+
/// <param name="right">The <see cref="CieLab"/> on the right side of the operand.</param>
81+
/// <returns>
82+
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
83+
/// </returns>
84+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
85+
public static bool operator !=(CieLab left, CieLab right) => !left.Equals(right);
86+
87+
/// <inheritdoc/>
88+
public override int GetHashCode() => HashCode.Combine(this.L, this.A, this.B);
89+
90+
/// <inheritdoc/>
91+
public override string ToString() => FormattableString.Invariant($"CieLab({this.L:#0.##}, {this.A:#0.##}, {this.B:#0.##})");
92+
93+
/// <inheritdoc/>
94+
public override bool Equals(object? obj) => obj is CieLab other && this.Equals(other);
95+
96+
/// <inheritdoc/>
97+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
98+
public bool Equals(CieLab other) =>
99+
this.L.Equals(other.L)
100+
&& this.A.Equals(other.A)
101+
&& this.B.Equals(other.B);
102+
103+
/// <inheritdoc/>
104+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
105+
public static CieLab FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
106+
{
107+
// Conversion algorithm described here: http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html
108+
CieXyz whitePoint = options.TargetWhitePoint;
109+
float wx = whitePoint.X, wy = whitePoint.Y, wz = whitePoint.Z;
110+
111+
float xr = source.X / wx, yr = source.Y / wy, zr = source.Z / wz;
112+
113+
const float inv116 = 1 / 116F;
114+
115+
float fx = xr > CieConstants.Epsilon ? MathF.Pow(xr, 0.3333333F) : ((CieConstants.Kappa * xr) + 16F) * inv116;
116+
float fy = yr > CieConstants.Epsilon ? MathF.Pow(yr, 0.3333333F) : ((CieConstants.Kappa * yr) + 16F) * inv116;
117+
float fz = zr > CieConstants.Epsilon ? MathF.Pow(zr, 0.3333333F) : ((CieConstants.Kappa * zr) + 16F) * inv116;
118+
119+
float l = (116F * fy) - 16F;
120+
float a = 500F * (fx - fy);
121+
float b = 200F * (fy - fz);
122+
123+
return new CieLab(l, a, b);
124+
}
125+
126+
/// <inheritdoc/>
127+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
128+
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieLab> destination)
129+
{
130+
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
131+
132+
for (int i = 0; i < source.Length; i++)
133+
{
134+
CieXyz xyz = source[i];
135+
destination[i] = FromProfileConnectingSpace(options, in xyz);
136+
}
137+
}
138+
139+
/// <inheritdoc/>
140+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
141+
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
142+
{
143+
// Conversion algorithm described here: http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html
144+
float l = this.L, a = this.A, b = this.B;
145+
float fy = (l + 16) / 116F;
146+
float fx = (a / 500F) + fy;
147+
float fz = fy - (b / 200F);
148+
149+
float fx3 = Numerics.Pow3(fx);
150+
float fz3 = Numerics.Pow3(fz);
151+
152+
float xr = fx3 > CieConstants.Epsilon ? fx3 : ((116F * fx) - 16F) / CieConstants.Kappa;
153+
float yr = l > CieConstants.Kappa * CieConstants.Epsilon ? Numerics.Pow3((l + 16F) / 116F) : l / CieConstants.Kappa;
154+
float zr = fz3 > CieConstants.Epsilon ? fz3 : ((116F * fz) - 16F) / CieConstants.Kappa;
155+
156+
CieXyz whitePoint = options.WhitePoint;
157+
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);
161+
162+
return new(xyzr * wxyz);
163+
}
164+
165+
/// <inheritdoc/>
166+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
167+
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieLab> source, Span<CieXyz> destination)
168+
{
169+
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
170+
171+
for (int i = 0; i < source.Length; i++)
172+
{
173+
CieLab lab = source[i];
174+
destination[i] = lab.ToProfileConnectingSpace(options);
175+
}
176+
}
177+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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 an CIE XYZ 1931 color
11+
/// <see href="https://en.wikipedia.org/wiki/CIE_1931_color_space#Definition_of_the_CIE_XYZ_color_space"/>
12+
/// </summary>
13+
public readonly struct CieXyz : IProfileConnectingSpace<CieXyz, CieXyz>
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="CieXyz"/> struct.
17+
/// </summary>
18+
/// <param name="x">X is a mix (a linear combination) of cone response curves chosen to be nonnegative</param>
19+
/// <param name="y">The y luminance component.</param>
20+
/// <param name="z">Z is quasi-equal to blue stimulation, or the S cone of the human eye.</param>
21+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
22+
public CieXyz(float x, float y, float z)
23+
: this(new Vector3(x, y, z))
24+
{
25+
}
26+
27+
/// <summary>
28+
/// Initializes a new instance of the <see cref="CieXyz"/> struct.
29+
/// </summary>
30+
/// <param name="vector">The vector representing the x, y, z components.</param>
31+
public CieXyz(Vector3 vector)
32+
: this()
33+
{
34+
// Not clamping as documentation about this space only indicates "usual" ranges
35+
this.X = vector.X;
36+
this.Y = vector.Y;
37+
this.Z = vector.Z;
38+
}
39+
40+
/// <summary>
41+
/// Gets the X component. A mix (a linear combination) of cone response curves chosen to be nonnegative.
42+
/// <remarks>A value usually ranging between 0 and 1.</remarks>
43+
/// </summary>
44+
public float X { get; }
45+
46+
/// <summary>
47+
/// Gets the Y luminance component.
48+
/// <remarks>A value usually ranging between 0 and 1.</remarks>
49+
/// </summary>
50+
public float Y { get; }
51+
52+
/// <summary>
53+
/// Gets the Z component. Quasi-equal to blue stimulation, or the S cone response.
54+
/// <remarks>A value usually ranging between 0 and 1.</remarks>
55+
/// </summary>
56+
public float Z { get; }
57+
58+
/// <summary>
59+
/// Compares two <see cref="CieXyz"/> objects for equality.
60+
/// </summary>
61+
/// <param name="left">The <see cref="CieXyz"/> on the left side of the operand.</param>
62+
/// <param name="right">The <see cref="CieXyz"/> on the right side of the operand.</param>
63+
/// <returns>
64+
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
65+
/// </returns>
66+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
67+
public static bool operator ==(CieXyz left, CieXyz right) => left.Equals(right);
68+
69+
/// <summary>
70+
/// Compares two <see cref="CieXyz"/> objects for inequality.
71+
/// </summary>
72+
/// <param name="left">The <see cref="CieXyz"/> on the left side of the operand.</param>
73+
/// <param name="right">The <see cref="CieXyz"/> on the right side of the operand.</param>
74+
/// <returns>
75+
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
76+
/// </returns>
77+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
78+
public static bool operator !=(CieXyz left, CieXyz right) => !left.Equals(right);
79+
80+
/// <summary>
81+
/// Returns a new <see cref="Vector3"/> representing this instance.
82+
/// </summary>
83+
/// <returns>The <see cref="Vector3"/>.</returns>
84+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
85+
public Vector3 ToVector3() => new(this.X, this.Y, this.Z);
86+
87+
/// <inheritdoc/>
88+
public override int GetHashCode() => HashCode.Combine(this.X, this.Y, this.Z);
89+
90+
/// <inheritdoc/>
91+
public override string ToString() => FormattableString.Invariant($"CieXyz({this.X:#0.##}, {this.Y:#0.##}, {this.Z:#0.##})");
92+
93+
/// <inheritdoc/>
94+
public override bool Equals(object? obj) => obj is CieXyz other && this.Equals(other);
95+
96+
/// <inheritdoc/>
97+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
98+
public bool Equals(CieXyz other)
99+
=> this.X.Equals(other.X)
100+
&& this.Y.Equals(other.Y)
101+
&& this.Z.Equals(other.Z);
102+
103+
/// <inheritdoc/>
104+
public static CieXyz FromProfileConnectingSpace(ColorConversionOptions options, in CieXyz source)
105+
=> new(source.X, source.Y, source.Z);
106+
107+
/// <inheritdoc/>
108+
public static void FromProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieXyz> destination)
109+
{
110+
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
111+
source.CopyTo(destination[..source.Length]);
112+
}
113+
114+
/// <inheritdoc/>
115+
public CieXyz ToProfileConnectingSpace(ColorConversionOptions options)
116+
=> new(this.X, this.Y, this.Z);
117+
118+
/// <inheritdoc/>
119+
public static void ToProfileConnectionSpace(ColorConversionOptions options, ReadOnlySpan<CieXyz> source, Span<CieXyz> destination)
120+
{
121+
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
122+
source.CopyTo(destination[..source.Length]);
123+
}
124+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Numerics;
5+
using SixLabors.ImageSharp.Memory;
6+
7+
namespace SixLabors.ImageSharp.ColorProfiles;
8+
9+
/// <summary>
10+
/// Provides options for color profile conversion.
11+
/// </summary>
12+
public class ColorConversionOptions
13+
{
14+
private Matrix4x4 adaptationMatrix;
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="ColorConversionOptions"/> class.
18+
/// </summary>
19+
public ColorConversionOptions() => this.AdaptationMatrix = LmsAdaptationMatrix.Bradford;
20+
21+
/// <summary>
22+
/// Gets the memory allocator.
23+
/// </summary>
24+
public MemoryAllocator MemoryAllocator { get; init; } = MemoryAllocator.Default;
25+
26+
/// <summary>
27+
/// Gets the source white point used for chromatic adaptation in conversions from/to XYZ color space.
28+
/// </summary>
29+
public CieXyz WhitePoint { get; init; } = Illuminants.D50;
30+
31+
/// <summary>
32+
/// Gets the destination white point used for chromatic adaptation in conversions from/to XYZ color space.
33+
/// </summary>
34+
public CieXyz TargetWhitePoint { get; init; } = Illuminants.D50;
35+
36+
/// <summary>
37+
/// Gets the transformation matrix used in conversion to perform chromatic adaptation.
38+
/// </summary>
39+
public Matrix4x4 AdaptationMatrix
40+
{
41+
get => this.adaptationMatrix;
42+
init
43+
{
44+
this.adaptationMatrix = value;
45+
Matrix4x4.Invert(value, out Matrix4x4 inverted);
46+
this.InverseAdaptationMatrix = inverted;
47+
}
48+
}
49+
50+
internal Matrix4x4 InverseAdaptationMatrix { get; private set; }
51+
}

0 commit comments

Comments
 (0)