Skip to content

Commit 3703cb1

Browse files
Wire up Rgb working spaces
1 parent 6ad5e02 commit 3703cb1

27 files changed

Lines changed: 1445 additions & 36 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.ColorProfiles;
5+
6+
/// <summary>
7+
/// Enumerate the possible sources of the white point used in chromatic adaptation.
8+
/// </summary>
9+
public enum ChromaticAdaptionWhitePointSource
10+
{
11+
/// <summary>
12+
/// The white point of the source color space.
13+
/// </summary>
14+
WhitePoint,
15+
16+
/// <summary>
17+
/// The white point of the source working space.
18+
/// </summary>
19+
RgbWorkingSpace
20+
}

src/ImageSharp/ColorProfiles/CieLab.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,7 @@ public static void ToProfileConnectionSpace(ColorConversionOptions options, Read
172172
destination[i] = lab.ToProfileConnectingSpace(options);
173173
}
174174
}
175+
176+
/// <inheritdoc/>
177+
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() => ChromaticAdaptionWhitePointSource.WhitePoint;
175178
}

src/ImageSharp/ColorProfiles/CieLch.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,7 @@ public static void ToProfileConnectionSpace(ColorConversionOptions options, Read
163163
destination[i] = lch.ToProfileConnectingSpace(options);
164164
}
165165
}
166+
167+
/// <inheritdoc/>
168+
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() => ChromaticAdaptionWhitePointSource.WhitePoint;
166169
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Runtime.CompilerServices;
5+
6+
// ReSharper disable CompareOfFloatsByEqualityOperator
7+
namespace SixLabors.ImageSharp.ColorProfiles;
8+
9+
/// <summary>
10+
/// Represents the coordinates of CIEXY chromaticity space.
11+
/// </summary>
12+
public readonly struct CieXyChromaticityCoordinates : IEquatable<CieXyChromaticityCoordinates>
13+
{
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="CieXyChromaticityCoordinates"/> struct.
16+
/// </summary>
17+
/// <param name="x">Chromaticity coordinate x (usually from 0 to 1)</param>
18+
/// <param name="y">Chromaticity coordinate y (usually from 0 to 1)</param>
19+
[MethodImpl(InliningOptions.ShortMethod)]
20+
public CieXyChromaticityCoordinates(float x, float y)
21+
{
22+
this.X = x;
23+
this.Y = y;
24+
}
25+
26+
/// <summary>
27+
/// Gets the chromaticity X-coordinate.
28+
/// </summary>
29+
/// <remarks>
30+
/// Ranges usually from 0 to 1.
31+
/// </remarks>
32+
public readonly float X { get; }
33+
34+
/// <summary>
35+
/// Gets the chromaticity Y-coordinate
36+
/// </summary>
37+
/// <remarks>
38+
/// Ranges usually from 0 to 1.
39+
/// </remarks>
40+
public readonly float Y { get; }
41+
42+
/// <summary>
43+
/// Compares two <see cref="CieXyChromaticityCoordinates"/> objects for equality.
44+
/// </summary>
45+
/// <param name="left">The <see cref="CieXyChromaticityCoordinates"/> on the left side of the operand.</param>
46+
/// <param name="right">The <see cref="CieXyChromaticityCoordinates"/> on the right side of the operand.</param>
47+
/// <returns>
48+
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
49+
/// </returns>
50+
[MethodImpl(InliningOptions.ShortMethod)]
51+
public static bool operator ==(CieXyChromaticityCoordinates left, CieXyChromaticityCoordinates right)
52+
=> left.Equals(right);
53+
54+
/// <summary>
55+
/// Compares two <see cref="CieXyChromaticityCoordinates"/> objects for inequality
56+
/// </summary>
57+
/// <param name="left">The <see cref="CieXyChromaticityCoordinates"/> on the left side of the operand.</param>
58+
/// <param name="right">The <see cref="CieXyChromaticityCoordinates"/> on the right side of the operand.</param>
59+
/// <returns>
60+
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
61+
/// </returns>
62+
[MethodImpl(InliningOptions.ShortMethod)]
63+
public static bool operator !=(CieXyChromaticityCoordinates left, CieXyChromaticityCoordinates right)
64+
=> !left.Equals(right);
65+
66+
/// <inheritdoc />
67+
[MethodImpl(InliningOptions.ShortMethod)]
68+
public override int GetHashCode()
69+
=> HashCode.Combine(this.X, this.Y);
70+
71+
/// <inheritdoc/>
72+
public override string ToString()
73+
=> FormattableString.Invariant($"CieXyChromaticityCoordinates({this.X:#0.##}, {this.Y:#0.##})");
74+
75+
/// <inheritdoc/>
76+
public override bool Equals(object? obj)
77+
=> obj is CieXyChromaticityCoordinates other && this.Equals(other);
78+
79+
/// <inheritdoc/>
80+
[MethodImpl(InliningOptions.ShortMethod)]
81+
public bool Equals(CieXyChromaticityCoordinates other)
82+
=> this.X.Equals(other.X) && this.Y.Equals(other.Y);
83+
}

src/ImageSharp/ColorProfiles/CieXyz.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,7 @@ public static void ToProfileConnectionSpace(ColorConversionOptions options, Read
121121
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
122122
source.CopyTo(destination[..source.Length]);
123123
}
124+
125+
/// <inheritdoc/>
126+
public static ChromaticAdaptionWhitePointSource GetChromaticAdaptionWhitePointSource() => ChromaticAdaptionWhitePointSource.WhitePoint;
124127
}

src/ImageSharp/ColorProfiles/ColorConversionOptions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the Six Labors Split License.
33

44
using System.Numerics;
5+
using SixLabors.ColorProfiles;
6+
using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
57
using SixLabors.ImageSharp.Memory;
68

79
namespace SixLabors.ImageSharp.ColorProfiles;
@@ -33,6 +35,16 @@ public class ColorConversionOptions
3335
/// </summary>
3436
public CieXyz TargetWhitePoint { get; init; } = Illuminants.D50;
3537

38+
/// <summary>
39+
/// Gets the source working space used for companding in conversions from/to XYZ color space.
40+
/// </summary>
41+
public RgbWorkingSpace RgbWorkingSpace { get; init; } = RgbWorkingSpaces.SRgb;
42+
43+
/// <summary>
44+
/// Gets the destination working space used for companding in conversions from/to XYZ color space.
45+
/// </summary>
46+
public RgbWorkingSpace TargetRgbWorkingSpace { get; init; } = RgbWorkingSpaces.SRgb;
47+
3648
/// <summary>
3749
/// Gets the transformation matrix used in conversion to perform chromatic adaptation.
3850
/// </summary>

src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, TFro
2121
CieXyz pcsTo = pcsFrom.ToProfileConnectingSpace(options);
2222

2323
// Adapt to target white point
24-
VonKriesChromaticAdaptation.Transform(options, in pcsTo);
24+
VonKriesChromaticAdaptation.Transform<TFrom, TTo>(options, in pcsTo);
2525

2626
// Convert to output from PCS
2727
return TTo.FromProfileConnectingSpace(options, pcsTo);
@@ -44,7 +44,7 @@ public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, Rea
4444
CieLab.ToProfileConnectionSpace(options, pcsFrom, pcsTo);
4545

4646
// Adapt to target white point
47-
VonKriesChromaticAdaptation.Transform(options, pcsTo, pcsTo);
47+
VonKriesChromaticAdaptation.Transform<TFrom, TTo>(options, pcsTo, pcsTo);
4848

4949
// Convert to output from PCS
5050
TTo.FromProfileConnectionSpace(options, pcsTo, destination);

src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

44
using System.Buffers;
@@ -18,7 +18,7 @@ public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, TFro
1818
CieXyz pcsFrom = source.ToProfileConnectingSpace(options);
1919

2020
// Adapt to target white point
21-
VonKriesChromaticAdaptation.Transform(options, in pcsFrom);
21+
VonKriesChromaticAdaptation.Transform<TFrom, TTo>(options, in pcsFrom);
2222

2323
// Convert between PCS
2424
CieLab pcsTo = CieLab.FromProfileConnectingSpace(options, in pcsFrom);
@@ -39,7 +39,7 @@ public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, Rea
3939
TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
4040

4141
// Adapt to target white point
42-
VonKriesChromaticAdaptation.Transform(options, pcsFrom, pcsFrom);
42+
VonKriesChromaticAdaptation.Transform<TFrom, TTo>(options, pcsFrom, pcsFrom);
4343

4444
// Convert between PCS.
4545
using IMemoryOwner<CieLab> pcsToOwner = options.MemoryAllocator.Allocate<CieLab>(source.Length);

src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, TFro
1818
CieXyz pcsFrom = source.ToProfileConnectingSpace(options);
1919

2020
// Adapt to target white point
21-
VonKriesChromaticAdaptation.Transform(options, in pcsFrom);
21+
VonKriesChromaticAdaptation.Transform<TFrom, TTo>(options, in pcsFrom);
2222

2323
// Convert between PCS
2424
CieXyz pcsTo = CieXyz.FromProfileConnectingSpace(options, in pcsFrom);
@@ -39,7 +39,7 @@ public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, Rea
3939
TFrom.ToProfileConnectionSpace(options, source, pcsFrom);
4040

4141
// Adapt to target white point
42-
VonKriesChromaticAdaptation.Transform(options, pcsFrom, pcsFrom);
42+
VonKriesChromaticAdaptation.Transform<TFrom, TTo>(options, pcsFrom, pcsFrom);
4343

4444
// Convert between PCS.
4545
using IMemoryOwner<CieXyz> pcsToOwner = options.MemoryAllocator.Allocate<CieXyz>(source.Length);
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Collections.Concurrent;
5+
using System.Numerics;
6+
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
8+
using System.Runtime.Intrinsics;
9+
using System.Runtime.Intrinsics.X86;
10+
11+
namespace SixLabors.ImageSharp.ColorProfiles.Companding;
12+
13+
/// <summary>
14+
/// Companding utilities that allow the accelerated compression-expansion of color channels.
15+
/// </summary>
16+
public static class CompandingUtilities
17+
{
18+
private const int Length = Scale + 2; // 256kb @ 16bit precision.
19+
private const int Scale = (1 << 16) - 1;
20+
private static readonly ConcurrentDictionary<(Type, double), Lazy<float[]>> LookupTables = new();
21+
22+
/// <summary>
23+
/// Lazily creates and stores a companding lookup table using the given function and modifier.
24+
/// </summary>
25+
/// <typeparam name="T">The type of companding function.</typeparam>
26+
/// <param name="compandingFunction">The companding function.</param>
27+
/// <param name="modifier">A modifier to pass to the function.</param>
28+
/// <returns>The <see cref="float"/> array.</returns>
29+
public static Lazy<float[]> GetLookupTable<T>(Func<double, double, double> compandingFunction, double modifier = 0)
30+
=> LookupTables.GetOrAdd((typeof(T), modifier), args => new(() => CreateLookupTableImpl(compandingFunction, args.Item2), true));
31+
32+
/// <summary>
33+
/// Creates a companding lookup table using the given function.
34+
/// </summary>
35+
/// <param name="compandingFunction">The companding function.</param>
36+
/// <param name="modifier">A modifier to pass to the function.</param>
37+
/// <returns>The <see cref="float"/> array.</returns>
38+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
39+
private static float[] CreateLookupTableImpl(Func<double, double, double> compandingFunction, double modifier = 0)
40+
{
41+
float[] result = new float[Length];
42+
43+
for (int i = 0; i < result.Length; i++)
44+
{
45+
double d = (double)i / Scale;
46+
d = compandingFunction(d, modifier);
47+
result[i] = (float)d;
48+
}
49+
50+
return result;
51+
}
52+
53+
/// <summary>
54+
/// Performs the companding operation on the given vectors using the given table.
55+
/// </summary>
56+
/// <param name="vectors">The span of vectors.</param>
57+
/// <param name="table">The lookup table.</param>
58+
public static void Compand(Span<Vector4> vectors, float[] table)
59+
{
60+
DebugGuard.MustBeGreaterThanOrEqualTo(table.Length, Length, nameof(table));
61+
62+
if (Avx2.IsSupported && vectors.Length >= 2)
63+
{
64+
CompandAvx2(vectors, table);
65+
66+
if (Numerics.Modulo2(vectors.Length) != 0)
67+
{
68+
// Vector4 fits neatly in pairs. Any overlap has to be equal to 1.
69+
ref Vector4 last = ref MemoryMarshal.GetReference(vectors[^1..]);
70+
last = Compand(last, table);
71+
}
72+
}
73+
else
74+
{
75+
CompandScalar(vectors, table);
76+
}
77+
}
78+
79+
/// <summary>
80+
/// Performs the companding operation on the given vector using the given table.
81+
/// </summary>
82+
/// <param name="vector">The vector.</param>
83+
/// <param name="table">The lookup table.</param>
84+
/// <returns>The <see cref="Vector4"/></returns>
85+
public static Vector4 Compand(Vector4 vector, float[] table)
86+
{
87+
DebugGuard.MustBeGreaterThanOrEqualTo(table.Length, Length, nameof(table));
88+
89+
Vector4 zero = Vector4.Zero;
90+
Vector4 scale = new(Scale);
91+
92+
Vector4 multiplied = Numerics.Clamp(vector * Scale, zero, scale);
93+
94+
float f0 = multiplied.X;
95+
float f1 = multiplied.Y;
96+
float f2 = multiplied.Z;
97+
98+
uint i0 = (uint)f0;
99+
uint i1 = (uint)f1;
100+
uint i2 = (uint)f2;
101+
102+
// Alpha is already a linear representation of opacity so we do not want to convert it.
103+
vector.X = Numerics.Lerp(table[i0], table[i0 + 1], f0 - (int)i0);
104+
vector.Y = Numerics.Lerp(table[i1], table[i1 + 1], f1 - (int)i1);
105+
vector.Z = Numerics.Lerp(table[i2], table[i2 + 1], f2 - (int)i2);
106+
107+
return vector;
108+
}
109+
110+
private static unsafe void CompandAvx2(Span<Vector4> vectors, float[] table)
111+
{
112+
fixed (float* tablePointer = &MemoryMarshal.GetArrayDataReference(table))
113+
{
114+
Vector256<float> scale = Vector256.Create((float)Scale);
115+
Vector256<float> zero = Vector256<float>.Zero;
116+
Vector256<int> offset = Vector256.Create(1);
117+
118+
// Divide by 2 as 4 elements per Vector4 and 8 per Vector256<float>
119+
ref Vector256<float> vectorsBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(vectors));
120+
ref Vector256<float> vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length / 2u);
121+
122+
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast))
123+
{
124+
Vector256<float> multiplied = Avx.Multiply(scale, vectorsBase);
125+
multiplied = Avx.Min(Avx.Max(zero, multiplied), scale);
126+
127+
Vector256<int> truncated = Avx.ConvertToVector256Int32WithTruncation(multiplied);
128+
Vector256<float> truncatedF = Avx.ConvertToVector256Single(truncated);
129+
130+
Vector256<float> low = Avx2.GatherVector256(tablePointer, truncated, sizeof(float));
131+
Vector256<float> high = Avx2.GatherVector256(tablePointer, Avx2.Add(truncated, offset), sizeof(float));
132+
133+
// Alpha is already a linear representation of opacity so we do not want to convert it.
134+
Vector256<float> companded = Numerics.Lerp(low, high, Avx.Subtract(multiplied, truncatedF));
135+
vectorsBase = Avx.Blend(companded, vectorsBase, Numerics.BlendAlphaControl);
136+
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
137+
}
138+
}
139+
}
140+
141+
private static unsafe void CompandScalar(Span<Vector4> vectors, float[] table)
142+
{
143+
fixed (float* tablePointer = &MemoryMarshal.GetArrayDataReference(table))
144+
{
145+
Vector4 zero = Vector4.Zero;
146+
Vector4 scale = new(Scale);
147+
ref Vector4 vectorsBase = ref MemoryMarshal.GetReference(vectors);
148+
ref Vector4 vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length);
149+
150+
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast))
151+
{
152+
Vector4 multiplied = Numerics.Clamp(vectorsBase * Scale, zero, scale);
153+
154+
float f0 = multiplied.X;
155+
float f1 = multiplied.Y;
156+
float f2 = multiplied.Z;
157+
158+
uint i0 = (uint)f0;
159+
uint i1 = (uint)f1;
160+
uint i2 = (uint)f2;
161+
162+
// Alpha is already a linear representation of opacity so we do not want to convert it.
163+
vectorsBase.X = Numerics.Lerp(tablePointer[i0], tablePointer[i0 + 1], f0 - (int)i0);
164+
vectorsBase.Y = Numerics.Lerp(tablePointer[i1], tablePointer[i1 + 1], f1 - (int)i1);
165+
vectorsBase.Z = Numerics.Lerp(tablePointer[i2], tablePointer[i2 + 1], f2 - (int)i2);
166+
167+
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
168+
}
169+
}
170+
}
171+
}

0 commit comments

Comments
 (0)