Skip to content

Commit a196758

Browse files
committed
Merge branch 'icc-performance' into 'main'
Improve CLUT speed See merge request Wacton/Unicolour!103
2 parents d34e188 + 8562573 commit a196758

8 files changed

Lines changed: 111 additions & 77 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![GitLab](https://badgen.net/static/gitlab/source/ff1493?icon=gitlab)](https://gitlab.com/Wacton/Unicolour)
44
[![NuGet](https://badgen.net/nuget/v/Wacton.Unicolour?icon)](https://www.nuget.org/packages/Wacton.Unicolour/)
55
[![pipeline status](https://gitlab.com/Wacton/Unicolour/badges/main/pipeline.svg)](https://gitlab.com/Wacton/Unicolour/-/commits/main)
6-
[![tests passed](https://badgen.net/static/tests/237,247/green/)](https://gitlab.com/Wacton/Unicolour/-/pipelines)
6+
[![tests passed](https://badgen.net/static/tests/237,251/green/)](https://gitlab.com/Wacton/Unicolour/-/pipelines)
77
[![coverage report](https://gitlab.com/Wacton/Unicolour/badges/main/coverage.svg)](https://gitlab.com/Wacton/Unicolour/-/pipelines)
88

99
Unicolour is the most comprehensive .NET library for working with colour:

Unicolour.Readme/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![GitLab](https://badgen.net/static/gitlab/source/ff1493?icon=gitlab)](https://gitlab.com/Wacton/Unicolour)
44
[![NuGet](https://badgen.net/nuget/v/Wacton.Unicolour?icon)](https://www.nuget.org/packages/Wacton.Unicolour/)
55
[![pipeline status](https://gitlab.com/Wacton/Unicolour/badges/main/pipeline.svg)](https://gitlab.com/Wacton/Unicolour/-/commits/main)
6-
[![tests passed](https://badgen.net/static/tests/237,247/green/)](https://gitlab.com/Wacton/Unicolour/-/pipelines)
6+
[![tests passed](https://badgen.net/static/tests/237,251/green/)](https://gitlab.com/Wacton/Unicolour/-/pipelines)
77
[![coverage report](https://gitlab.com/Wacton/Unicolour/badges/main/coverage.svg)](https://gitlab.com/Wacton/Unicolour/-/pipelines)
88

99
Unicolour is the most comprehensive .NET library for working with colour:

Unicolour.Tests/PigmentTests.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,24 @@ public void NoConcentration()
226226
double[] concentrations = [-0.5, 0.0];
227227
AssertReflectance(pigments, concentrations, expected: null, expectedXyzNaN: true);
228228
}
229+
230+
[Test]
231+
public void EmptyPigments()
232+
{
233+
Pigment[] pigments = [];
234+
double[] concentrations = [1.0];
235+
AssertReflectance(pigments, concentrations, expected: null, expectedXyzNaN: true);
236+
}
237+
238+
[Test]
239+
public void EmptyConcentration()
240+
{
241+
Pigment pigment1 = new(400, 10, [0.5, 0.5, 0.5, 0.5, 0.5], [0.5, 0.5, 0.5, 0.5, 0.5], k1, k2);
242+
Pigment pigment2 = new(400, 10, [0.5, 0.5, 0.5, 0.5, 0.5], [0.5, 0.5, 0.5, 0.5, 0.5], k1, k2);
243+
Pigment[] pigments = [pigment1, pigment2];
244+
double[] concentrations = [];
245+
AssertReflectance(pigments, concentrations, expected: null, expectedXyzNaN: true);
246+
}
229247

230248
private static readonly Configuration ConfigWithIlluminantSpd = TestUtils.D50Config; // contains D50 SPD (as well as precalculated D65 white point)
231249
private static readonly Configuration ConfigWithoutIlluminantSpd = new(xyzConfig: new(new WhitePoint(96.422, 100.000, 82.521))); // D50 white point only

Unicolour.Tests/SpectralJsTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,20 @@ public void MixNoConcentration()
107107
TestUtils.AssertTriplet<Xyz>(colour, new(double.NaN, double.NaN, double.NaN), 0);
108108
}
109109

110+
[Test]
111+
public void MixEmptyPigments()
112+
{
113+
var colour = SpectralJs.Mix([], [1.0]);
114+
TestUtils.AssertTriplet<Xyz>(colour, new(double.NaN, double.NaN, double.NaN), 0);
115+
}
116+
117+
[Test]
118+
public void MixEmptyConcentration()
119+
{
120+
var colour = SpectralJs.Mix([Red], []);
121+
TestUtils.AssertTriplet<Xyz>(colour, new(double.NaN, double.NaN, double.NaN), 0);
122+
}
123+
110124
[Test]
111125
public void PaletteTwo() => AssertPalette(Blue, Yellow, 2, [BlueToYellowHex.First(), BlueToYellowHex.Last()]);
112126

Unicolour/Icc/Clut.cs

Lines changed: 70 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
namespace Wacton.Unicolour.Icc;
44

5+
// multidimensional lookup
6+
// e.g. with 4 input channels, 25 grid points, 3 output channels = 5D-CLUT of [25, 25, 25, 25, 3]
57
internal class Clut
68
{
7-
private readonly Array clutGrid;
9+
private readonly double[] values;
10+
private readonly List<int[]> inputBinaryVectors;
811

912
internal int InputChannels { get; }
1013
internal int GridPoints { get; }
@@ -13,39 +16,11 @@ internal class Clut
1316
// NOTE: # of values should = gridPoints ^ inputChannels * outputChannels (e.g. 37 ^ 3 * 4 = 202,612)
1417
internal Clut(double[] values, int inputChannels, int gridPoints, int outputChannels)
1518
{
19+
this.values = values;
1620
InputChannels = inputChannels;
1721
GridPoints = gridPoints;
1822
OutputChannels = outputChannels;
19-
clutGrid = InitialiseClutGrid(values);
20-
}
21-
22-
private Array InitialiseClutGrid(double[] values)
23-
{
24-
// e.g. CLUT grid with 4 input channels, 25 grid points, 3 output channels
25-
// = 5D-CLUT of [25, 25, 25, 25, 3]
26-
var dimensionLengths = new List<int>();
27-
for (var i = 0; i < InputChannels; i++)
28-
{
29-
dimensionLengths.Add(GridPoints);
30-
}
31-
32-
dimensionLengths.Add(OutputChannels);
33-
34-
var grid = Array.CreateInstance(typeof(double), dimensionLengths.ToArray());
35-
var inputGridCoordinates = GenerateVectorsOfBaseN(InputChannels, GridPoints);
36-
foreach (var inputGridCoordinate in inputGridCoordinates)
37-
{
38-
for (var outputChannel = 0; outputChannel < OutputChannels; outputChannel++)
39-
{
40-
var index = GetIndex(inputGridCoordinate, outputChannel);
41-
42-
var indexes = inputGridCoordinate.ToList();
43-
indexes.Add(outputChannel);
44-
grid.SetValue(values[index], indexes.ToArray());
45-
}
46-
}
47-
48-
return grid;
23+
inputBinaryVectors = GenerateVectorsOfBaseN(inputChannels, 2);
4924
}
5025

5126
/*
@@ -71,36 +46,70 @@ private Array InitialiseClutGrid(double[] values)
7146
*/
7247
internal double[] Lookup(double[] clutInputs)
7348
{
74-
var clutIndexes = clutInputs.Select(clutInput => new ClutIndex(clutInput, GridPoints)).ToList();
49+
var clutIndexes = clutInputs.Select(clutInput => new ClutIndex(clutInput, GridPoints)).ToArray();
7550

76-
var binaryVectors = GenerateVectorsOfBaseN(InputChannels, 2);
77-
var weightedOutputs = new List<double[]>();
78-
foreach (var binaryVector in binaryVectors)
51+
var result = new double[OutputChannels];
52+
foreach (var inputBinaryVector in inputBinaryVectors)
7953
{
80-
var inputChannelIndexes = new List<int>();
81-
var distanceComponents = new List<double>();
82-
for (var i = 0; i < binaryVector.Length; i++)
54+
var inputIndexes = new int[InputChannels];
55+
var distances = new double[InputChannels];
56+
for (var i = 0; i < InputChannels; i++)
8357
{
84-
var binary = binaryVector[i];
58+
var binary = inputBinaryVector[i];
8559
var clutIndex = clutIndexes[i];
86-
inputChannelIndexes.Add(binary == 0 ? clutIndex.Lower : clutIndex.Upper);
87-
distanceComponents.Add(binary == 0 ? clutIndex.DistanceToLower : clutIndex.DistanceToUpper);
60+
inputIndexes[i] = binary == 0 ? clutIndex.Lower : clutIndex.Upper;
61+
distances[i] = binary == 0 ? clutIndex.DistanceToLower : clutIndex.DistanceToUpper;
8862
}
8963

90-
var output = GetOutput(inputChannelIndexes.ToArray());
91-
var distance = distanceComponents.Aggregate((accumulate, item) => accumulate * item);
92-
var weightedOutput = output.Select(x => x * distance).ToArray();
93-
weightedOutputs.Add(weightedOutput);
64+
var output = GetOutput(inputIndexes);
65+
var distance = Product(distances);
66+
for (var i = 0; i < OutputChannels; i++)
67+
{
68+
result[i] += output[i] * distance;
69+
}
9470
}
9571

96-
var outputComponents = Enumerable.Range(0, OutputChannels).Select(outputChannel => weightedOutputs.Select(x => x[outputChannel])).ToArray();
97-
var outputSummed = outputComponents.Select(x => x.Sum()).ToArray();
98-
return outputSummed;
72+
return result;
73+
}
74+
75+
private double[] GetOutput(int[] inputIndexes)
76+
{
77+
var output = new double[OutputChannels];
78+
var outputIndex = GetOutputIndex(inputIndexes);
79+
for (var i = 0; i < OutputChannels; i++)
80+
{
81+
output[i] = values[outputIndex + i];
82+
}
83+
84+
return output;
9985
}
86+
87+
/*
88+
* e.g. Fogra39 CMYK -> LAB: 4 input channels, 25 grid points, 3 output channels
89+
* index = (cIndex * 25^3 * 3) + (mIndex * 25^2 * 3) + (yIndex * 25^1 * 3) + (kIndex * 25^0 * 3)
90+
* L = index + 0, A = index + 1, B = index + 2
91+
*
92+
* e.g. Fogra55 CMYKOGV -> LAB: 7 input channels, 5 grid points, 3 output channels
93+
* index = (cIndex * 5^6 * 3) + (mIndex * 5^5 * 3) + (yIndex * 5^4 * 3) + (kIndex * 5^3 * 3) +
94+
* (oIndex * 5^2 * 3) + (gIndex * 5^1 * 3) + (vIndex * 5^0 * 3)
95+
* L = index + 0, A = index + 1, B = index + 2
96+
*/
97+
private int GetOutputIndex(int[] gridIndexes)
98+
{
99+
var outputIndex = 0;
100+
for (var i = 0; i < gridIndexes.Length; i++)
101+
{
102+
var gridIndex = gridIndexes[i];
103+
var power = gridIndexes.Length - 1 - i;
104+
outputIndex += gridIndex * Power(GridPoints, power) * OutputChannels;
105+
}
100106

107+
return outputIndex;
108+
}
109+
101110
private static List<int[]> GenerateVectorsOfBaseN(int n, int @base)
102111
{
103-
var totalVectors = (int)Math.Pow(@base, n);
112+
var totalVectors = Power(@base, n);
104113

105114
var vectors = new List<int[]>();
106115
for (var i = 0; i < totalVectors; i++)
@@ -123,39 +132,27 @@ private static List<int[]> GenerateVectorsOfBaseN(int n, int @base)
123132
return vectors;
124133
}
125134

126-
/*
127-
* e.g. Fogra39 CMYK -> LAB: 4 input channels, 25 grid points, 3 output channels
128-
* index = (cGrid * 25^3 * 3) + (mGrid * 25^2 * 3) + (yGrid * 25^1 * 3) + (kGrid * 25^0 * 3)
129-
*
130-
* e.g. Fogra55 CMYKOGV -> LAB: 7 input channels, 5 grid points, 3 output channels
131-
* index = (cGrid * 5^6 * 3) + (mGrid * 5^5 * 3) + (yGrid * 5^4 * 3) + (kGrid * 5^3 * 3) +
132-
* (oGrid * 5^2 * 3) + (gGrid * 5^1 * 3) + (vGrid * 5^0 * 3)
133-
*/
134-
private int GetIndex(int[] gridPointInputs, int outputChannel)
135+
// avoiding .Aggregate((accumulate, item) => accumulate * item) to improve performance
136+
private static double Product(double[] values)
135137
{
136-
var index = 0;
137-
for (var gridPointInputIndex = 0; gridPointInputIndex < gridPointInputs.Length; gridPointInputIndex++)
138+
var result = 1.0;
139+
foreach (var value in values)
138140
{
139-
var gridPointInput = gridPointInputs[gridPointInputIndex];
140-
var power = gridPointInputs.Length - 1 - gridPointInputIndex;
141-
index += gridPointInput * (int)Math.Pow(GridPoints, power) * OutputChannels;
141+
result *= value;
142142
}
143143

144-
index += outputChannel;
145-
return index;
144+
return result;
146145
}
147-
148-
private double[] GetOutput(int[] inputChannelIndexes)
146+
147+
private static int Power(int number, int exponent)
149148
{
150-
var output = new double[OutputChannels];
151-
for (var i = 0; i < OutputChannels; i++)
149+
var result = 1;
150+
for (var i = 1; i <= exponent; i++)
152151
{
153-
var indexes = inputChannelIndexes.Concat(new[] { i }).ToArray();
154-
var value = (double)clutGrid.GetValue(indexes);
155-
output[i] = value;
152+
result *= number;
156153
}
157154

158-
return output;
155+
return result;
159156
}
160157

161158
private class ClutIndex

Unicolour/Pigment.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ public Pigment(int startWavelength, int wavelengthInterval, double[] k, double[]
3939

4040
internal static SpectralCoefficients? GetReflectance(Pigment[] pigments, double[] weights)
4141
{
42+
if (!pigments.Any())
43+
{
44+
return null;
45+
}
46+
4247
var examplePigment = pigments.First();
4348
var kubelkaMunk = examplePigment.KubelkaMunk;
4449
var wavelengths = examplePigment.Wavelengths;

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![GitLab](https://badgen.net/static/gitlab/source/ff1493?icon=gitlab)](https://gitlab.com/Wacton/Unicolour)
44
[![NuGet](https://badgen.net/nuget/v/Wacton.Unicolour?icon)](https://www.nuget.org/packages/Wacton.Unicolour/)
55
[![pipeline status](https://gitlab.com/Wacton/Unicolour/badges/main/pipeline.svg)](https://gitlab.com/Wacton/Unicolour/-/commits/main)
6-
[![tests passed](https://badgen.net/static/tests/237,247/green/)](https://gitlab.com/Wacton/Unicolour/-/pipelines)
6+
[![tests passed](https://badgen.net/static/tests/237,251/green/)](https://gitlab.com/Wacton/Unicolour/-/pipelines)
77
[![coverage report](https://gitlab.com/Wacton/Unicolour/badges/main/coverage.svg)](https://gitlab.com/Wacton/Unicolour/-/pipelines)
88

99
Unicolour is the most comprehensive .NET library for working with colour:

docs/README_us.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![GitLab](https://badgen.net/static/gitlab/source/ff1493?icon=gitlab)](https://gitlab.com/Wacton/Unicolour)
44
[![NuGet](https://badgen.net/nuget/v/Wacton.Unicolour?icon)](https://www.nuget.org/packages/Wacton.Unicolour/)
55
[![pipeline status](https://gitlab.com/Wacton/Unicolour/badges/main/pipeline.svg)](https://gitlab.com/Wacton/Unicolour/-/commits/main)
6-
[![tests passed](https://badgen.net/static/tests/237,247/green/)](https://gitlab.com/Wacton/Unicolour/-/pipelines)
6+
[![tests passed](https://badgen.net/static/tests/237,251/green/)](https://gitlab.com/Wacton/Unicolour/-/pipelines)
77
[![coverage report](https://gitlab.com/Wacton/Unicolour/badges/main/coverage.svg)](https://gitlab.com/Wacton/Unicolour/-/pipelines)
88

99
Unicolour is the most comprehensive .NET library for working with color:

0 commit comments

Comments
 (0)