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;