From 9aeed10da9825c9cdd2fbb62e9b2efe5a6830ebd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 9 Sep 2026 10:02:43 +1000 Subject: [PATCH] Add selectable ICC lookup-table interpolation Expose automatic, trilinear, and tetrahedral interpolation through the shared color conversion options. Preserve multilinear behavior for explicit callers and use that option in the existing Unicolour comparisons. Verified with 63 Release/net11 ICC conversion and calculator tests. --- .../ColorProfiles/ColorConversionOptions.cs | 5 + .../ColorProfileConverterExtensionsIcc.cs | 12 +- .../Icc/Calculators/ClutCalculator.cs | 463 +++++++++--------- .../Icc/Calculators/LutABCalculator.cs | 21 +- .../Icc/Calculators/LutEntryCalculator.cs | 12 +- ...ons.cs => IccConverterBase.Conversions.cs} | 44 +- ...ccConverterbase.cs => IccConverterBase.cs} | 5 +- .../Icc/IccDataToDataConverter.cs | 5 +- .../Icc/IccDataToPcsConverter.cs | 5 +- .../Icc/IccPcsToDataConverter.cs | 5 +- .../ColorProfiles/Icc/IccPcsToPcsConverter.cs | 5 +- .../ColorProfiles/IccInterpolationMethod.cs | 25 + .../Icc/Calculators/ClutCalculatorTests.cs | 84 +++- .../Icc/Calculators/LutABCalculatorTests.cs | 4 +- .../Calculators/LutEntryCalculatorTests.cs | 4 +- .../Icc/ColorProfileConverterTests.Icc.cs | 6 +- 16 files changed, 417 insertions(+), 288 deletions(-) rename src/ImageSharp/ColorProfiles/Icc/{IccConverterbase.Conversions.cs => IccConverterBase.Conversions.cs} (75%) rename src/ImageSharp/ColorProfiles/Icc/{IccConverterbase.cs => IccConverterBase.cs} (88%) create mode 100644 src/ImageSharp/ColorProfiles/IccInterpolationMethod.cs 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;