diff --git a/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs b/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs
index 882d246a76..a0fcd92df3 100644
--- a/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs
+++ b/src/ImageSharp/ColorProfiles/ColorConversionOptions.cs
@@ -73,6 +73,11 @@ public class ColorConversionOptions
///
public IccProfile? TargetIccProfile { get; init; }
+ ///
+ /// Gets the interpolation method used for ICC color lookup tables. Defaults to .
+ ///
+ public IccInterpolationMethod IccInterpolationMethod { get; init; }
+
///
/// Gets the transformation matrix used in conversion to perform chromatic adaptation.
/// for further information. Default is Bradford.
diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
index 424f4ec377..4c0023ebe4 100644
--- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
@@ -75,8 +75,8 @@ internal static class ColorProfileConverterExtensionsIcc
throw new InvalidOperationException("Target ICC profile is missing.");
}
- ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true);
- ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false);
+ ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true, converter.Options.IccInterpolationMethod);
+ ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false, converter.Options.IccInterpolationMethod);
ColorProfileConverter pcsConverter = new(new ColorConversionOptions
{
@@ -142,8 +142,8 @@ internal static class ColorProfileConverterExtensionsIcc
Guard.MustBeGreaterThanOrEqualTo(source.Length, destination.Length, nameof(destination));
- ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true);
- ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false);
+ ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true, converter.Options.IccInterpolationMethod);
+ ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false, converter.Options.IccInterpolationMethod);
ColorProfileConverter pcsConverter = new(new ColorConversionOptions
{
@@ -692,10 +692,10 @@ internal static class ColorProfileConverterExtensionsIcc
{
private readonly IccProfile profile;
- internal ConversionParams(IccProfile profile, bool toPcs)
+ internal ConversionParams(IccProfile profile, bool toPcs, IccInterpolationMethod interpolationMethod)
{
this.profile = profile;
- this.Converter = toPcs ? new IccDataToPcsConverter(profile) : new IccPcsToDataConverter(profile);
+ this.Converter = toPcs ? new IccDataToPcsConverter(profile, interpolationMethod) : new IccPcsToDataConverter(profile, interpolationMethod);
}
internal IccConverterBase Converter { get; }
diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs
index 82d475e578..90daea93da 100644
--- a/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs
+++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs
@@ -8,51 +8,39 @@ namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators;
///
/// Implements interpolation methods for color profile lookup tables.
-/// Adapted from ICC Reference implementation:
-/// https://github.com/InternationalColorConsortium/DemoIccMAX/blob/79ecb74135ad47bac7d42692905a079839b7e105/IccProfLib/IccTagLut.cpp
///
internal class ClutCalculator : IVector4Calculator
{
+ private readonly bool useTrilinearInterpolation;
private readonly int inputCount;
private readonly int outputCount;
private readonly float[] lut;
private readonly byte[] gridPointCount;
private readonly byte[] maxGridPoint;
- private readonly int[] indexFactor;
private readonly int[] dimSize;
- private readonly int nodeCount;
- private readonly float[][] nodes;
- private readonly float[] g;
- private readonly uint[] ig;
- private readonly float[] s;
- private readonly float[] df;
- private readonly uint[] nPower;
- private int n000;
- private int n001;
- private int n010;
- private int n011;
- private int n100;
- private int n101;
- private int n110;
- private int n111;
- private int n1000;
-
- public ClutCalculator(IccClut clut)
+ private const int LowerCorner = 0;
+ private readonly int n001;
+ private readonly int n010;
+ private readonly int n011;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The table to evaluate.
+ /// Whether tables use multilinear interpolation.
+ public ClutCalculator(IccClut clut, bool useTrilinearInterpolation)
{
Guard.NotNull(clut, nameof(clut));
- Guard.MustBeGreaterThan(clut.InputChannelCount, 0, nameof(clut.InputChannelCount));
- Guard.MustBeGreaterThan(clut.OutputChannelCount, 0, nameof(clut.OutputChannelCount));
+ // This calculator consumes and produces Vector4 values. A table may describe
+ // more channels, but it cannot be evaluated through this four-channel contract.
+ Guard.MustBeBetweenOrEqualTo(clut.InputChannelCount, 1, 4, nameof(clut.InputChannelCount));
+ Guard.MustBeBetweenOrEqualTo(clut.OutputChannelCount, 1, 4, nameof(clut.OutputChannelCount));
+
+ this.useTrilinearInterpolation = useTrilinearInterpolation;
this.inputCount = clut.InputChannelCount;
this.outputCount = clut.OutputChannelCount;
- this.g = new float[this.inputCount];
- this.ig = new uint[this.inputCount];
- this.s = new float[this.inputCount];
- this.nPower = new uint[16];
this.lut = clut.Values;
- this.nodeCount = (int)Math.Pow(2, clut.InputChannelCount);
- this.df = new float[this.nodeCount];
- this.nodes = new float[this.nodeCount][];
this.dimSize = new int[this.inputCount];
this.gridPointCount = clut.GridPointCount;
this.maxGridPoint = new byte[this.inputCount];
@@ -67,9 +55,15 @@ internal class ClutCalculator : IVector4Calculator
this.dimSize[i] = this.dimSize[i + 1] * this.gridPointCount[i + 1];
}
- this.indexFactor = this.CalculateIndexFactor();
+ this.n001 = this.dimSize[0];
+ if (this.inputCount == 2)
+ {
+ this.n010 = this.dimSize[1];
+ this.n011 = this.n001 + this.n010;
+ }
}
+ ///
public unsafe Vector4 Calculate(Vector4 value)
{
Vector4 result = default;
@@ -82,95 +76,30 @@ internal class ClutCalculator : IVector4Calculator
this.Interpolate2d((float*)&value, (float*)&result);
break;
case 3:
- this.Interpolate3d((float*)&value, (float*)&result);
- break;
- case 4:
- this.Interpolate4d((float*)&value, (float*)&result);
- break;
- default:
- this.InterpolateNd((float*)&value, (float*)&result);
- break;
- }
-
- return result;
- }
+ if (this.useTrilinearInterpolation)
+ {
+ this.Interpolate3d((float*)&value, (float*)&result);
+ }
+ else
+ {
+ this.InterpolateTetrahedral((float*)&value, (float*)&result);
+ }
- private int[] CalculateIndexFactor()
- {
- int[] factors = new int[16];
- switch (this.inputCount)
- {
- case 1:
- factors[0] = this.n000 = 0;
- factors[1] = this.n001 = this.dimSize[0];
- break;
- case 2:
- factors[0] = this.n000 = 0;
- factors[1] = this.n001 = this.dimSize[0];
- factors[2] = this.n010 = this.dimSize[1];
- factors[3] = this.n011 = this.n001 + this.n010;
- break;
- case 3:
- factors[0] = this.n000 = 0;
- factors[1] = this.n001 = this.dimSize[0];
- factors[2] = this.n010 = this.dimSize[1];
- factors[3] = this.n011 = this.n001 + this.n010;
- factors[4] = this.n100 = this.dimSize[2];
- factors[5] = this.n101 = this.n100 + this.n001;
- factors[6] = this.n110 = this.n100 + this.n010;
- factors[7] = this.n111 = this.n110 + this.n001;
break;
case 4:
- factors[0] = 0;
- factors[1] = this.n001 = this.dimSize[0];
- factors[2] = this.n010 = this.dimSize[1];
- factors[3] = factors[2] + factors[1];
- factors[4] = this.n100 = this.dimSize[2];
- factors[5] = factors[4] + factors[1];
- factors[6] = factors[4] + factors[2];
- factors[7] = factors[4] + factors[3];
- factors[8] = this.n1000 = this.dimSize[3];
- factors[9] = factors[8] + factors[1];
- factors[10] = factors[8] + factors[2];
- factors[11] = factors[8] + factors[3];
- factors[12] = factors[8] + factors[4];
- factors[13] = factors[8] + factors[5];
- factors[14] = factors[8] + factors[6];
- factors[15] = factors[8] + factors[7];
- break;
- default:
- // Initialize ND interpolation variables.
- factors[0] = 0;
- int count;
- for (count = 0; count < this.inputCount; count++)
+ if (this.useTrilinearInterpolation)
{
- this.nPower[count] = (uint)(1 << (this.inputCount - 1 - count));
+ this.Interpolate4d((float*)&value, (float*)&result);
}
-
- uint[] nPower = [0, 1];
- count = 0;
- int nFlag = 1;
- for (uint j = 1; j < this.nodeCount; j++)
+ else
{
- if (j == nPower[1])
- {
- factors[j] = this.dimSize[count];
- nPower[0] = (uint)(1 << count);
- count++;
- nPower[1] = (uint)(1 << count);
- nFlag = 1;
- }
- else
- {
- factors[j] = factors[nPower[0]] + factors[nFlag];
- nFlag++;
- }
+ this.InterpolateTetrahedral((float*)&value, (float*)&result);
}
break;
}
- return factors;
+ return result;
}
///
@@ -182,7 +111,7 @@ internal class ClutCalculator : IVector4Calculator
{
byte mx = this.maxGridPoint[0];
- float x = UnitClip(srcPixel[0]) * mx;
+ float x = Numerics.Clamp(srcPixel[0], 0F, 1F) * mx;
uint ix = (uint)x;
@@ -206,7 +135,7 @@ internal class ClutCalculator : IVector4Calculator
int offset = 0;
for (i = 0; i < this.outputCount; i++)
{
- destPixel[i] = (float)((p[offset + this.n000] * dF0) + (p[offset + this.n001] * dF1));
+ destPixel[i] = (float)((p[offset + LowerCorner] * dF0) + (p[offset + this.n001] * dF1));
offset++;
}
}
@@ -221,8 +150,8 @@ internal class ClutCalculator : IVector4Calculator
byte mx = this.maxGridPoint[0];
byte my = this.maxGridPoint[1];
- float x = UnitClip(srcPixel[0]) * mx;
- float y = UnitClip(srcPixel[1]) * my;
+ float x = Numerics.Clamp(srcPixel[0], 0F, 1F) * mx;
+ float y = Numerics.Clamp(srcPixel[1], 0F, 1F) * my;
uint ix = (uint)x;
uint iy = (uint)y;
@@ -257,25 +186,29 @@ internal class ClutCalculator : IVector4Calculator
int offset = 0;
for (i = 0; i < this.outputCount; i++)
{
- destPixel[i] = (float)((p[offset + this.n000] * dF0) + (p[offset + this.n001] * dF1) + (p[offset + this.n010] * dF2) + (p[offset + this.n011] * dF3));
+ destPixel[i] = (float)((p[offset + LowerCorner] * dF0) + (p[offset + this.n001] * dF1) + (p[offset + this.n010] * dF2) + (p[offset + this.n011] * dF3));
offset++;
}
}
///
- /// Three dimensional interpolation function.
+ /// Interpolates a three-channel table independently along each axis.
///
/// The input pixel values, which will be interpolated.
/// The interpolated output pixels.
private unsafe void Interpolate3d(float* srcPixel, float* destPixel)
{
+ int xStride = this.dimSize[0];
+ int yStride = this.dimSize[1];
+ int zStride = this.dimSize[2];
+
byte mx = this.maxGridPoint[0];
byte my = this.maxGridPoint[1];
byte mz = this.maxGridPoint[2];
- float x = UnitClip(srcPixel[0]) * mx;
- float y = UnitClip(srcPixel[1]) * my;
- float z = UnitClip(srcPixel[2]) * mz;
+ float x = Numerics.Clamp(srcPixel[0], 0F, 1F) * mx;
+ float y = Numerics.Clamp(srcPixel[1], 0F, 1F) * my;
+ float z = Numerics.Clamp(srcPixel[2], 0F, 1F) * mz;
uint ix = (uint)x;
uint iy = (uint)y;
@@ -307,9 +240,10 @@ internal class ClutCalculator : IVector4Calculator
float nt = (float)(1.0 - t);
float nu = (float)(1.0 - u);
- Span p = this.lut.AsSpan((int)((ix * this.n001) + (iy * this.n010) + (iz * this.n100)));
+ Span p = this.lut.AsSpan((int)((ix * xStride) + (iy * yStride) + (iz * zStride)));
- // Normalize grid units
+ // The eight corner weights are products of the independent axis fractions.
+ // This tensor-product blend is used for Lab-indexed output tables.
float dF0 = ns * nt * nu;
float dF1 = ns * nt * u;
float dF2 = ns * t * nu;
@@ -322,51 +256,61 @@ internal class ClutCalculator : IVector4Calculator
int offset = 0;
for (int i = 0; i < this.outputCount; i++)
{
- destPixel[i] = (float)((p[offset + this.n000] * dF0) +
- (p[offset + this.n001] * dF1) +
- (p[offset + this.n010] * dF2) +
- (p[offset + this.n011] * dF3) +
- (p[offset + this.n100] * dF4) +
- (p[offset + this.n101] * dF5) +
- (p[offset + this.n110] * dF6) +
- (p[offset + this.n111] * dF7));
+ destPixel[i] = (float)((p[offset + 0] * dF0) +
+ (p[offset + xStride] * dF1) +
+ (p[offset + yStride] * dF2) +
+ (p[offset + (xStride + yStride)] * dF3) +
+ (p[offset + zStride] * dF4) +
+ (p[offset + (xStride + zStride)] * dF5) +
+ (p[offset + (yStride + zStride)] * dF6) +
+ (p[offset + (xStride + yStride + zStride)] * dF7));
offset++;
}
}
///
- /// Four dimensional interpolation function.
+ /// Interpolates three-channel tables or blends tetrahedral slices of four-channel tables.
///
/// The input pixel values, which will be interpolated.
/// The interpolated output pixels.
- private unsafe void Interpolate4d(float* srcPixel, float* destPixel)
+ private unsafe void InterpolateTetrahedral(float* srcPixel, float* destPixel)
{
- byte mw = this.maxGridPoint[0];
- byte mx = this.maxGridPoint[1];
- byte my = this.maxGridPoint[2];
- byte mz = this.maxGridPoint[3];
+ int dimension = this.inputCount - 3;
+ int tableOffset = 0;
+ int sliceStride = 0;
+ float fraction = 0F;
+ if (this.inputCount == 4)
+ {
+ float position = Numerics.Clamp(srcPixel[0], 0F, 1F) * this.maxGridPoint[0];
+ int lowerSlice = (int)position;
+ fraction = position - lowerSlice;
+ tableOffset = lowerSlice * this.dimSize[0];
+ sliceStride = lowerSlice == this.maxGridPoint[0] ? 0 : this.dimSize[0];
+ srcPixel++;
+ }
- float w = UnitClip(srcPixel[0]) * mw;
- float x = UnitClip(srcPixel[1]) * mx;
- float y = UnitClip(srcPixel[2]) * my;
- float z = UnitClip(srcPixel[3]) * mz;
+ // Adjacent slices have the same grid and input coordinates. Compute their cell
+ // and tetrahedron once; only the first-axis offset differs between the slices.
+ int xStride = this.dimSize[dimension];
+ int yStride = this.dimSize[dimension + 1];
+ int zStride = this.dimSize[dimension + 2];
+
+ byte mx = this.maxGridPoint[dimension];
+ byte my = this.maxGridPoint[dimension + 1];
+ byte mz = this.maxGridPoint[dimension + 2];
+
+ float x = Numerics.Clamp(srcPixel[0], 0F, 1F) * mx;
+ float y = Numerics.Clamp(srcPixel[1], 0F, 1F) * my;
+ float z = Numerics.Clamp(srcPixel[2], 0F, 1F) * mz;
- uint iw = (uint)w;
uint ix = (uint)x;
uint iy = (uint)y;
uint iz = (uint)z;
- float v = w - iw;
float u = x - ix;
float t = y - iy;
float s = z - iz;
- if (iw == mw)
- {
- iw--;
- v = 1.0f;
- }
-
if (ix == mx)
{
ix--;
@@ -385,122 +329,161 @@ internal class ClutCalculator : IVector4Calculator
s = 1.0f;
}
- float ns = (float)(1.0 - s);
- float nt = (float)(1.0 - t);
- float nu = (float)(1.0 - u);
- float nv = (float)(1.0 - v);
-
- Span p = this.lut.AsSpan((int)((iw * this.n001) + (ix * this.n010) + (iy * this.n100) + (iz * this.n1000)));
-
- // Normalize grid units.
- float[] dF =
- [
- ns * nt * nu * nv,
- ns * nt * nu * v,
- ns * nt * u * nv,
- ns * nt * u * v,
- ns * t * nu * nv,
- ns * t * nu * v,
- ns * t * u * nv,
- ns * t * u * v,
- s * nt * nu * nv,
- s * nt * nu * v,
- s * nt * u * nv,
- s * nt * u * v,
- s * t * nu * nv,
- s * t * nu * v,
- s * t * u * nv,
- s * t * u * v,
- ];
+ // The fractional coordinates select one of six tetrahedra sharing the cell's
+ // lower and upper corners. Walking the axes from largest fraction to smallest
+ // identifies the two intermediate vertices. Choose once for all output channels.
+ int firstVertex;
+ int secondVertex;
+ float firstWeight;
+ float secondWeight;
+ float thirdWeight;
- int offset = 0;
- for (int i = 0; i < this.outputCount; i++)
+ if (u >= t)
{
- float pv = 0.0f;
- for (int j = 0; j < 16; j++)
+ if (t >= s)
{
- pv += p[offset + this.indexFactor[j]] * dF[j];
+ firstVertex = xStride;
+ secondVertex = xStride + yStride;
+ firstWeight = u;
+ secondWeight = t;
+ thirdWeight = s;
}
-
- destPixel[i] = pv;
- offset++;
- }
- }
-
- ///
- /// Generic N-dimensional interpolation function.
- ///
- /// The input pixel values, which will be interpolated.
- /// The interpolated output pixels.
- private unsafe void InterpolateNd(float* srcPixel, float* destPixel)
- {
- int index = 0;
- for (int i = 0; i < this.inputCount; i++)
- {
- this.g[i] = UnitClip(srcPixel[i]) * this.maxGridPoint[i];
- this.ig[i] = (uint)this.g[i];
- this.s[this.inputCount - 1 - i] = this.g[i] - this.ig[i];
- if (this.ig[i] == this.maxGridPoint[i])
+ else if (u >= s)
{
- this.ig[i]--;
- this.s[this.inputCount - 1 - i] = 1.0f;
+ firstVertex = xStride;
+ secondVertex = xStride + zStride;
+ firstWeight = u;
+ secondWeight = s;
+ thirdWeight = t;
+ }
+ else
+ {
+ firstVertex = zStride;
+ secondVertex = xStride + zStride;
+ firstWeight = s;
+ secondWeight = u;
+ thirdWeight = t;
}
-
- index += (int)this.ig[i] * this.dimSize[i];
}
-
- Span p = this.lut.AsSpan(index);
- float[] temp = new float[2];
- bool nFlag = false;
-
- for (int i = 0; i < this.nodeCount; i++)
+ else if (u >= s)
+ {
+ firstVertex = yStride;
+ secondVertex = xStride + yStride;
+ firstWeight = t;
+ secondWeight = u;
+ thirdWeight = s;
+ }
+ else if (t >= s)
{
- this.df[i] = 1.0f;
+ firstVertex = yStride;
+ secondVertex = yStride + zStride;
+ firstWeight = t;
+ secondWeight = s;
+ thirdWeight = u;
+ }
+ else
+ {
+ firstVertex = zStride;
+ secondVertex = yStride + zStride;
+ firstWeight = s;
+ secondWeight = t;
+ thirdWeight = u;
}
- for (int i = 0; i < this.inputCount; i++)
+ ReadOnlySpan cell = this.lut.AsSpan(tableOffset + (int)((ix * xStride) + (iy * yStride) + (iz * zStride)));
+
+ // Interpolate along the tetrahedron's three edges. Sorted fractions give vertex
+ // weights 1-first, first-second, second-third, and third, which sum to one.
+ // An input at the upper boundary uses the preceding cell with fraction one;
+ // equal fractions give a shared face or edge the same value from either side.
+ int upperVertex = xStride + yStride + zStride;
+ if (this.inputCount == 3)
{
- temp[0] = 1.0f - this.s[i];
- temp[1] = this.s[i];
- index = (int)this.nPower[i];
- for (int j = 0; j < this.nodeCount; j++)
+ for (int i = 0; i < this.outputCount; i++)
{
- this.df[j] *= temp[nFlag ? 1 : 0];
- if ((j + 1) % index == 0)
- {
- nFlag = !nFlag;
- }
+ float lower = cell[i];
+ float first = cell[i + firstVertex];
+ float second = cell[i + secondVertex];
+ float upper = cell[i + upperVertex];
+ destPixel[i] = lower
+ + ((first - lower) * firstWeight)
+ + ((second - first) * secondWeight)
+ + ((upper - second) * thirdWeight);
}
-
- nFlag = false;
}
-
- int offset = 0;
- for (int i = 0; i < this.outputCount; i++)
+ else
{
- float pv = 0;
- for (int j = 0; j < this.nodeCount; j++)
+ // Evaluate corresponding vertices in both slices and immediately blend the
+ // channel results. At the upper boundary both slices address the same cell.
+ ReadOnlySpan upperCell = cell[sliceStride..];
+ for (int i = 0; i < this.outputCount; i++)
{
- pv += p[offset + this.indexFactor[j]] * this.df[j];
+ float lower = cell[i];
+ float first = cell[i + firstVertex];
+ float second = cell[i + secondVertex];
+ float upper = cell[i + upperVertex];
+ float lowerValue = lower
+ + ((first - lower) * firstWeight)
+ + ((second - first) * secondWeight)
+ + ((upper - second) * thirdWeight);
+
+ lower = upperCell[i];
+ first = upperCell[i + firstVertex];
+ second = upperCell[i + secondVertex];
+ upper = upperCell[i + upperVertex];
+ float upperValue = lower
+ + ((first - lower) * firstWeight)
+ + ((second - first) * secondWeight)
+ + ((upper - second) * thirdWeight);
+
+ destPixel[i] = lowerValue + ((upperValue - lowerValue) * fraction);
}
-
- destPixel[i] = pv;
- offset++;
}
}
- private static float UnitClip(float v)
+ ///
+ /// Interpolates the sixteen corners surrounding a four-channel input.
+ ///
+ /// The normalized input channels.
+ /// The interpolated output channels, initially zero.
+ private unsafe void Interpolate4d(float* srcPixel, float* destPixel)
{
- if (v < 0)
- {
- return 0;
- }
-
- if (v > 1.0)
+ // Each lane holds one input axis. At the upper boundary, the lower and upper
+ // corner share an index, so a zero stride keeps every lookup inside the table.
+ Vector4 position = Numerics.Clamp(new Vector4(srcPixel[0], srcPixel[1], srcPixel[2], srcPixel[3]), Vector4.Zero, Vector4.One)
+ * new Vector4(this.maxGridPoint[0], this.maxGridPoint[1], this.maxGridPoint[2], this.maxGridPoint[3]);
+
+ int w = (int)position.X;
+ int x = (int)position.Y;
+ int y = (int)position.Z;
+ int z = (int)position.W;
+ Vector4 fraction = position - new Vector4(w, x, y, z);
+ Vector4 inverse = Vector4.One - fraction;
+ int offset = (w * this.dimSize[0]) + (x * this.dimSize[1]) + (y * this.dimSize[2]) + (z * this.dimSize[3]);
+ int dw = w == this.maxGridPoint[0] ? 0 : this.dimSize[0];
+ int dx = x == this.maxGridPoint[1] ? 0 : this.dimSize[1];
+ int dy = y == this.maxGridPoint[2] ? 0 : this.dimSize[2];
+ int dz = z == this.maxGridPoint[3] ? 0 : this.dimSize[3];
+
+ // The low bit selects the first axis. Multiply weights from the last axis
+ // to the first, and reuse each corner's weight across all output channels.
+ for (int corner = 0; corner < 16; corner++)
{
- return 1.0f;
+ float weight = ((corner & 8) == 0 ? inverse.W : fraction.W)
+ * ((corner & 4) == 0 ? inverse.Z : fraction.Z)
+ * ((corner & 2) == 0 ? inverse.Y : fraction.Y)
+ * ((corner & 1) == 0 ? inverse.X : fraction.X);
+
+ int index = offset
+ + ((corner & 1) == 0 ? 0 : dw)
+ + ((corner & 2) == 0 ? 0 : dx)
+ + ((corner & 4) == 0 ? 0 : dy)
+ + ((corner & 8) == 0 ? 0 : dz);
+
+ for (int channel = 0; channel < this.outputCount; channel++)
+ {
+ destPixel[channel] += this.lut[index + channel] * weight;
+ }
}
-
- return v;
}
}
diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs
index 10ac6e596f..c430b769b9 100644
--- a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs
+++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs
@@ -21,10 +21,11 @@ internal partial class LutABCalculator : IVector4Calculator
/// Initializes a new instance of the class for an ICC mAB transform.
///
/// The parsed A-to-B LUT entry.
- public LutABCalculator(IccLutAToBTagDataEntry entry)
+ /// Whether a three-channel table uses trilinear interpolation.
+ public LutABCalculator(IccLutAToBTagDataEntry entry, bool useTrilinearInterpolation)
{
Guard.NotNull(entry, nameof(entry));
- this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues);
+ this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues, useTrilinearInterpolation);
this.type = CalculationType.AtoB;
}
@@ -32,10 +33,11 @@ internal partial class LutABCalculator : IVector4Calculator
/// Initializes a new instance of the class for an ICC mBA transform.
///
/// The parsed B-to-A LUT entry.
- public LutABCalculator(IccLutBToATagDataEntry entry)
+ /// Whether a three-channel table uses trilinear interpolation.
+ public LutABCalculator(IccLutBToATagDataEntry entry, bool useTrilinearInterpolation)
{
Guard.NotNull(entry, nameof(entry));
- this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues);
+ this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues, useTrilinearInterpolation);
this.type = CalculationType.BtoA;
}
@@ -117,7 +119,14 @@ internal partial class LutABCalculator : IVector4Calculator
///
/// The tag entry classes already validate channel continuity, so this method only materializes the available stages.
///
- private void Init(IccTagDataEntry[] curveA, IccTagDataEntry[] curveB, IccTagDataEntry[] curveM, Vector3? matrix3x1, Matrix4x4? matrix3x3, IccClut clut)
+ private void Init(
+ IccTagDataEntry[] curveA,
+ IccTagDataEntry[] curveB,
+ IccTagDataEntry[] curveM,
+ Vector3? matrix3x1,
+ Matrix4x4? matrix3x3,
+ IccClut clut,
+ bool useTrilinearInterpolation)
{
bool hasACurve = curveA != null;
bool hasBCurve = curveB != null;
@@ -152,7 +161,7 @@ internal partial class LutABCalculator : IVector4Calculator
if (hasClut)
{
- this.clutCalculator = new ClutCalculator(clut);
+ this.clutCalculator = new ClutCalculator(clut, useTrilinearInterpolation);
}
}
}
diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs
index c97578ee3f..aa8839aa94 100644
--- a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs
+++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs
@@ -16,17 +16,17 @@ internal class LutEntryCalculator : IVector4Calculator
private Matrix4x4 matrix;
private bool doTransform;
- public LutEntryCalculator(IccLut8TagDataEntry lut)
+ public LutEntryCalculator(IccLut8TagDataEntry lut, bool useTrilinearInterpolation)
{
Guard.NotNull(lut, nameof(lut));
- this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix);
+ this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix, useTrilinearInterpolation);
this.Is16Bit = false;
}
- public LutEntryCalculator(IccLut16TagDataEntry lut)
+ public LutEntryCalculator(IccLut16TagDataEntry lut, bool useTrilinearInterpolation)
{
Guard.NotNull(lut, nameof(lut));
- this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix);
+ this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix, useTrilinearInterpolation);
this.Is16Bit = true;
}
@@ -57,11 +57,11 @@ internal class LutEntryCalculator : IVector4Calculator
return value;
}
- private void Init(IccLut[] inputCurve, IccLut[] outputCurve, IccClut clut, Matrix4x4 matrix)
+ private void Init(IccLut[] inputCurve, IccLut[] outputCurve, IccClut clut, Matrix4x4 matrix, bool useTrilinearInterpolation)
{
this.inputCurve = InitLut(inputCurve);
this.outputCurve = InitLut(outputCurve);
- this.clutCalculator = new ClutCalculator(clut);
+ this.clutCalculator = new ClutCalculator(clut, useTrilinearInterpolation);
this.matrix = matrix;
this.doTransform = !matrix.IsIdentity && inputCurve.Length == 3;
diff --git a/src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs b/src/ImageSharp/ColorProfiles/Icc/IccConverterBase.Conversions.cs
similarity index 75%
rename from src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs
rename to src/ImageSharp/ColorProfiles/Icc/IccConverterBase.Conversions.cs
index 5875b74f13..ef9f6312a2 100644
--- a/src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs
+++ b/src/ImageSharp/ColorProfiles/Icc/IccConverterBase.Conversions.cs
@@ -24,7 +24,8 @@ internal abstract partial class IccConverterBase
/// True if the conversion is to the Profile Connection Space.
/// The wanted rendering intent. Can be ignored if not available.
/// Invalid conversion method.
- protected void Init(IccProfile profile, bool toPcs, IccRenderingIntent renderingIntent)
+ /// The interpolation method used for color lookup tables.
+ protected void Init(IccProfile profile, bool toPcs, IccRenderingIntent renderingIntent, IccInterpolationMethod interpolationMethod)
=> this.calculator = GetConversionMethod(profile, renderingIntent) switch
{
ConversionMethod.D0 => toPcs ?
@@ -40,28 +41,45 @@ internal abstract partial class IccConverterBase
InitD(profile, IccProfileTag.DToB3) :
InitD(profile, IccProfileTag.BToD3),
ConversionMethod.A0 => toPcs ?
- InitA(profile, IccProfileTag.AToB0) :
- InitA(profile, IccProfileTag.BToA0),
+ InitA(profile, IccProfileTag.AToB0, interpolationMethod) :
+ InitA(profile, IccProfileTag.BToA0, interpolationMethod),
ConversionMethod.A1 => toPcs ?
- InitA(profile, IccProfileTag.AToB1) :
- InitA(profile, IccProfileTag.BToA1),
+ InitA(profile, IccProfileTag.AToB1, interpolationMethod) :
+ InitA(profile, IccProfileTag.BToA1, interpolationMethod),
ConversionMethod.A2 => toPcs ?
- InitA(profile, IccProfileTag.AToB2) :
- InitA(profile, IccProfileTag.BToA2),
+ InitA(profile, IccProfileTag.AToB2, interpolationMethod) :
+ InitA(profile, IccProfileTag.BToA2, interpolationMethod),
ConversionMethod.ColorTrc => InitColorTrc(profile, toPcs),
ConversionMethod.GrayTrc => InitGrayTrc(profile, toPcs),
_ => throw new InvalidIccProfileException("Invalid conversion method."),
};
- private static IVector4Calculator InitA(IccProfile profile, IccProfileTag tag)
- => GetTag(profile, tag) switch
+ ///
+ /// Creates a LUT calculator with interpolation selected for its input color space and direction.
+ ///
+ /// The profile containing the table.
+ /// The transform tag to evaluate.
+ /// The configured table calculator.
+ /// The requested interpolation method.
+ private static IVector4Calculator InitA(IccProfile profile, IccProfileTag tag, IccInterpolationMethod interpolationMethod)
+ {
+ // Lab-indexed output and linking tables use independent-axis interpolation.
+ // Device-to-PCS tables use tetrahedra in their final three input dimensions.
+ bool useTrilinearInterpolation = interpolationMethod == IccInterpolationMethod.Trilinear
+ || (interpolationMethod == IccInterpolationMethod.Auto
+ && profile.Header.ProfileConnectionSpace == IccColorSpaceType.CieLab
+ && (tag is IccProfileTag.BToA0 or IccProfileTag.BToA1 or IccProfileTag.BToA2
+ || profile.Header.Class is IccProfileClass.DeviceLink or IccProfileClass.Abstract));
+
+ return GetTag(profile, tag) switch
{
- IccLut8TagDataEntry lut8 => new LutEntryCalculator(lut8),
- IccLut16TagDataEntry lut16 => new LutEntryCalculator(lut16),
- IccLutAToBTagDataEntry lutAtoB => new LutABCalculator(lutAtoB),
- IccLutBToATagDataEntry lutBtoA => new LutABCalculator(lutBtoA),
+ IccLut8TagDataEntry lut8 => new LutEntryCalculator(lut8, useTrilinearInterpolation),
+ IccLut16TagDataEntry lut16 => new LutEntryCalculator(lut16, useTrilinearInterpolation),
+ IccLutAToBTagDataEntry lutAtoB => new LutABCalculator(lutAtoB, useTrilinearInterpolation),
+ IccLutBToATagDataEntry lutBtoA => new LutABCalculator(lutBtoA, useTrilinearInterpolation),
_ => throw new InvalidIccProfileException($"Invalid entry {tag}."),
};
+ }
private static IVector4Calculator InitD(IccProfile profile, IccProfileTag tag)
{
diff --git a/src/ImageSharp/ColorProfiles/Icc/IccConverterbase.cs b/src/ImageSharp/ColorProfiles/Icc/IccConverterBase.cs
similarity index 88%
rename from src/ImageSharp/ColorProfiles/Icc/IccConverterbase.cs
rename to src/ImageSharp/ColorProfiles/Icc/IccConverterBase.cs
index d9976dc2ac..49af0726ff 100644
--- a/src/ImageSharp/ColorProfiles/Icc/IccConverterbase.cs
+++ b/src/ImageSharp/ColorProfiles/Icc/IccConverterBase.cs
@@ -18,10 +18,11 @@ internal abstract partial class IccConverterBase
///
/// The ICC profile to use for the conversions
/// True if the conversion is to the profile connection space (PCS); False if the conversion is to the data space
- protected IccConverterBase(IccProfile profile, bool toPcs)
+ /// The interpolation method used for color lookup tables.
+ protected IccConverterBase(IccProfile profile, bool toPcs, IccInterpolationMethod interpolationMethod)
{
Guard.NotNull(profile, nameof(profile));
- this.Init(profile, toPcs, profile.Header.RenderingIntent);
+ this.Init(profile, toPcs, profile.Header.RenderingIntent, interpolationMethod);
}
///
diff --git a/src/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs b/src/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs
index cb4d89bb53..5a75288924 100644
--- a/src/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs
+++ b/src/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs
@@ -15,8 +15,9 @@ internal class IccDataToDataConverter : IccConverterBase
/// Initializes a new instance of the class.
///
/// The ICC profile to use for the conversions
- public IccDataToDataConverter(IccProfile profile)
- : base(profile, true) // toPCS is true because in this case the PCS space is also a data space
+ /// The interpolation method used for color lookup tables.
+ public IccDataToDataConverter(IccProfile profile, IccInterpolationMethod interpolationMethod)
+ : base(profile, true, interpolationMethod) // toPCS is true because in this case the PCS space is also a data space
{
}
}
diff --git a/src/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs b/src/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs
index 6e95d3cb32..15fcf9b526 100644
--- a/src/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs
+++ b/src/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs
@@ -15,8 +15,9 @@ internal class IccDataToPcsConverter : IccConverterBase
/// Initializes a new instance of the class.
///
/// The ICC profile to use for the conversions
- public IccDataToPcsConverter(IccProfile profile)
- : base(profile, true)
+ /// The interpolation method used for color lookup tables.
+ public IccDataToPcsConverter(IccProfile profile, IccInterpolationMethod interpolationMethod)
+ : base(profile, true, interpolationMethod)
{
}
}
diff --git a/src/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs b/src/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs
index d29517fca2..0fb10591be 100644
--- a/src/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs
+++ b/src/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs
@@ -15,8 +15,9 @@ internal class IccPcsToDataConverter : IccConverterBase
/// Initializes a new instance of the class.
///
/// The ICC profile to use for the conversions
- public IccPcsToDataConverter(IccProfile profile)
- : base(profile, false)
+ /// The interpolation method used for color lookup tables.
+ public IccPcsToDataConverter(IccProfile profile, IccInterpolationMethod interpolationMethod)
+ : base(profile, false, interpolationMethod)
{
}
}
diff --git a/src/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs b/src/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs
index 30b44ca75c..0a6b964fe7 100644
--- a/src/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs
+++ b/src/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs
@@ -15,8 +15,9 @@ internal class IccPcsToPcsConverter : IccConverterBase
/// Initializes a new instance of the class.
///
/// The ICC profile to use for the conversions
- public IccPcsToPcsConverter(IccProfile profile)
- : base(profile, true)
+ /// The interpolation method used for color lookup tables.
+ public IccPcsToPcsConverter(IccProfile profile, IccInterpolationMethod interpolationMethod)
+ : base(profile, true, interpolationMethod)
{
}
}
diff --git a/src/ImageSharp/ColorProfiles/IccInterpolationMethod.cs b/src/ImageSharp/ColorProfiles/IccInterpolationMethod.cs
new file mode 100644
index 0000000000..6196762f88
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/IccInterpolationMethod.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+///
+/// Defines the interpolation method for ICC color lookup tables.
+///
+public enum IccInterpolationMethod
+{
+ ///
+ /// Selects trilinear interpolation for Lab output and Lab device-link or abstract profiles, and tetrahedral interpolation otherwise.
+ ///
+ Auto,
+
+ ///
+ /// Uses trilinear interpolation for three input channels and multilinear interpolation for four input channels.
+ ///
+ Trilinear,
+
+ ///
+ /// Uses tetrahedral interpolation for three input channels and linearly blends tetrahedral results for four input channels.
+ ///
+ Tetrahedral
+}
diff --git a/tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/ClutCalculatorTests.cs b/tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/ClutCalculatorTests.cs
index 249e7f4ed1..553d99adb6 100644
--- a/tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/ClutCalculatorTests.cs
+++ b/tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/ClutCalculatorTests.cs
@@ -18,10 +18,92 @@ public class ClutCalculatorTests
[MemberData(nameof(IccConversionDataClut.ClutConversionTestData), MemberType = typeof(IccConversionDataClut))]
internal void ClutCalculator_WithClut_ReturnsResult(IccClut lut, Vector4 input, Vector4 expected)
{
- ClutCalculator calculator = new(lut);
+ ClutCalculator calculator = new(lut, false);
Vector4 result = calculator.Calculate(input);
VectorAssert.Equal(expected, result, 4);
}
+
+ [Theory]
+ [InlineData(0.75F, 0.5F, 0.25F, 0.25F)]
+ [InlineData(0.75F, 0.25F, 0.5F, 0.25F)]
+ [InlineData(0.5F, 0.25F, 0.75F, 0.25F)]
+ [InlineData(0.5F, 0.75F, 0.25F, 0.25F)]
+ [InlineData(0.25F, 0.75F, 0.5F, 0.25F)]
+ [InlineData(0.25F, 0.5F, 0.75F, 0.25F)]
+ [InlineData(0.5F, 0.5F, 0.25F, 0.25F)]
+ [InlineData(0.25F, 0.5F, 0.5F, 0.25F)]
+ [InlineData(0.5F, 0.25F, 0.5F, 0.25F)]
+ [InlineData(0.5F, 0.5F, 0.5F, 0.5F)]
+ [InlineData(0F, 0F, 0F, 0F)]
+ [InlineData(1F, 1F, 1F, 1F)]
+ [InlineData(-0.25F, 0.5F, 0.75F, 0F)]
+ [InlineData(1.25F, 0.5F, 0.75F, 0.5F)]
+ public void ThreeChannelsInterpolateTetrahedra(float x, float y, float z, float expected)
+ {
+ // Only the upper corner is nonzero. Its tetrahedral weight is the smallest
+ // coordinate, whereas trilinear interpolation would multiply all three.
+ // Complementary output channels also detect incorrect table strides.
+ IccClut table = new(
+ [0F, 1F, 0F, 1F, 0F, 1F, 0F, 1F, 0F, 1F, 0F, 1F, 0F, 1F, 1F, 0F],
+ [2, 2, 2],
+ IccClutDataType.Float,
+ 2);
+
+ ClutCalculator calculator = new(table, false);
+ Vector4 actual = calculator.Calculate(new Vector4(x, y, z, 0F));
+ Assert.Equal(new Vector4(expected, 1F - expected, 0F, 0F), actual);
+ }
+
+ [Theory]
+ [InlineData(0F, 0.25F)]
+ [InlineData(0.5F, 0.3125F)]
+ [InlineData(1F, 0.375F)]
+ [InlineData(-0.5F, 0.25F)]
+ [InlineData(1.5F, 0.375F)]
+ public void FourChannelsBlendTetrahedralSlices(float first, float expected)
+ {
+ // The lower slice evaluates to min(x,y,z); the upper slice evaluates to
+ // 0.25 + 0.5 * min(x,y,z). The fourth-dimensional blend is independently
+ // determined by the first coordinate, including clipping at either boundary.
+ IccClut table = new(
+ [0F, 0F, 0F, 0F, 0F, 0F, 0F, 1F, 0.25F, 0.25F, 0.25F, 0.25F, 0.25F, 0.25F, 0.25F, 0.75F],
+ [2, 2, 2, 2],
+ IccClutDataType.Float,
+ 1);
+
+ ClutCalculator calculator = new(table, false);
+ Vector4 actual = calculator.Calculate(new Vector4(first, 0.75F, 0.5F, 0.25F));
+ Assert.Equal(new Vector4(expected, 0F, 0F, 0F), actual);
+ }
+
+ [Theory]
+ [InlineData(0.25F, 0.25F)]
+ [InlineData(0.75F, 0.5F)]
+ public void ThreeChannelsUseUnequalGridStrides(float y, float expected)
+ {
+ // The middle axis has two cells while the other axes have one. Each cell
+ // has a different upper value, exposing both incorrect strides and offsets.
+ IccClut table = new(
+ [0F, 0F, 0F, 0F, 0F, 0F, 0F, 0F, 0F, 0.5F, 0F, 1F],
+ [2, 3, 2],
+ IccClutDataType.Float,
+ 1);
+
+ ClutCalculator calculator = new(table, false);
+ Vector4 actual = calculator.Calculate(new Vector4(0.75F, y, 0.5F, 0F));
+ Assert.Equal(new Vector4(expected, 0F, 0F, 0F), actual);
+ }
+
+ [Fact]
+ public void ThreeChannelsUseTrilinearWhenSelected()
+ {
+ // Independent-axis interpolation gives the upper corner the product of
+ // the fractions, rather than the minimum used by a tetrahedral table.
+ IccClut table = new([0F, 0F, 0F, 0F, 0F, 0F, 0F, 1F], [2, 2, 2], IccClutDataType.Float, 1);
+ ClutCalculator calculator = new(table, true);
+ Vector4 actual = calculator.Calculate(new Vector4(0.75F, 0.5F, 0.25F, 0F));
+ Assert.Equal(new Vector4(0.09375F, 0F, 0F, 0F), actual);
+ }
}
diff --git a/tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/LutABCalculatorTests.cs b/tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/LutABCalculatorTests.cs
index de2b4f5fae..7c70266868 100644
--- a/tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/LutABCalculatorTests.cs
+++ b/tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/LutABCalculatorTests.cs
@@ -18,7 +18,7 @@ public class LutABCalculatorTests
[MemberData(nameof(IccConversionDataLutAB.LutAToBConversionTestData), MemberType = typeof(IccConversionDataLutAB))]
internal void LutABCalculator_WithLutAToB_ReturnsResult(IccLutAToBTagDataEntry lut, Vector4 input, Vector4 expected)
{
- LutABCalculator calculator = new(lut);
+ LutABCalculator calculator = new(lut, false);
Vector4 result = calculator.Calculate(input);
@@ -29,7 +29,7 @@ public class LutABCalculatorTests
[MemberData(nameof(IccConversionDataLutAB.LutBToAConversionTestData), MemberType = typeof(IccConversionDataLutAB))]
internal void LutABCalculator_WithLutBToA_ReturnsResult(IccLutBToATagDataEntry lut, Vector4 input, Vector4 expected)
{
- LutABCalculator calculator = new(lut);
+ LutABCalculator calculator = new(lut, false);
Vector4 result = calculator.Calculate(input);
diff --git a/tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/LutEntryCalculatorTests.cs b/tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/LutEntryCalculatorTests.cs
index 14f1386eb8..befc39a872 100644
--- a/tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/LutEntryCalculatorTests.cs
+++ b/tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/LutEntryCalculatorTests.cs
@@ -18,7 +18,7 @@ public class LutEntryCalculatorTests
[MemberData(nameof(IccConversionDataLutEntry.Lut8ConversionTestData), MemberType = typeof(IccConversionDataLutEntry))]
internal void LutEntryCalculator_WithLut8_ReturnsResult(IccLut8TagDataEntry lut, Vector4 input, Vector4 expected)
{
- LutEntryCalculator calculator = new(lut);
+ LutEntryCalculator calculator = new(lut, false);
Vector4 result = calculator.Calculate(input);
@@ -29,7 +29,7 @@ public class LutEntryCalculatorTests
[MemberData(nameof(IccConversionDataLutEntry.Lut16ConversionTestData), MemberType = typeof(IccConversionDataLutEntry))]
internal void LutEntryCalculator_WithLut16_ReturnsResult(IccLut16TagDataEntry lut, Vector4 input, Vector4 expected)
{
- LutEntryCalculator calculator = new(lut);
+ LutEntryCalculator calculator = new(lut, false);
Vector4 result = calculator.Calculate(input);
diff --git a/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs b/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs
index c171b725a7..86a2f3b3a0 100644
--- a/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs
+++ b/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs
@@ -154,7 +154,8 @@ public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper)
ColorProfileConverter converter = new(new ColorConversionOptions
{
SourceIccProfile = TestIccProfiles.GetProfile(sourceProfile),
- TargetIccProfile = TestIccProfiles.GetProfile(targetProfile)
+ TargetIccProfile = TestIccProfiles.GetProfile(targetProfile),
+ IccInterpolationMethod = IccInterpolationMethod.Trilinear
});
IccColorSpaceType sourceDataSpace = converter.Options.SourceIccProfile!.Header.DataColorSpace;
@@ -178,7 +179,8 @@ public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper)
ColorProfileConverter converter = new(new ColorConversionOptions
{
SourceIccProfile = TestIccProfiles.GetProfile(sourceProfile),
- TargetIccProfile = TestIccProfiles.GetProfile(targetProfile)
+ TargetIccProfile = TestIccProfiles.GetProfile(targetProfile),
+ IccInterpolationMethod = IccInterpolationMethod.Trilinear
});
IccColorSpaceType sourceDataSpace = converter.Options.SourceIccProfile!.Header.DataColorSpace;