Browse Source

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.
pull/2633/head
James Jackson-South 3 weeks ago
parent
commit
9aeed10da9
  1. 5
      src/ImageSharp/ColorProfiles/ColorConversionOptions.cs
  2. 12
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
  3. 463
      src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs
  4. 21
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs
  5. 12
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs
  6. 44
      src/ImageSharp/ColorProfiles/Icc/IccConverterBase.Conversions.cs
  7. 5
      src/ImageSharp/ColorProfiles/Icc/IccConverterBase.cs
  8. 5
      src/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs
  9. 5
      src/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs
  10. 5
      src/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs
  11. 5
      src/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs
  12. 25
      src/ImageSharp/ColorProfiles/IccInterpolationMethod.cs
  13. 84
      tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/ClutCalculatorTests.cs
  14. 4
      tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/LutABCalculatorTests.cs
  15. 4
      tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/LutEntryCalculatorTests.cs
  16. 6
      tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs

5
src/ImageSharp/ColorProfiles/ColorConversionOptions.cs

@ -73,6 +73,11 @@ public class ColorConversionOptions
/// </summary> /// </summary>
public IccProfile? TargetIccProfile { get; init; } public IccProfile? TargetIccProfile { get; init; }
/// <summary>
/// Gets the interpolation method used for ICC color lookup tables. Defaults to <see cref="IccInterpolationMethod.Auto"/>.
/// </summary>
public IccInterpolationMethod IccInterpolationMethod { get; init; }
/// <summary> /// <summary>
/// Gets the transformation matrix used in conversion to perform chromatic adaptation. /// Gets the transformation matrix used in conversion to perform chromatic adaptation.
/// <see cref="KnownChromaticAdaptationMatrices"/> for further information. Default is Bradford. /// <see cref="KnownChromaticAdaptationMatrices"/> for further information. Default is Bradford.

12
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

@ -75,8 +75,8 @@ internal static class ColorProfileConverterExtensionsIcc
throw new InvalidOperationException("Target ICC profile is missing."); throw new InvalidOperationException("Target ICC profile is missing.");
} }
ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true); ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true, converter.Options.IccInterpolationMethod);
ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false); ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false, converter.Options.IccInterpolationMethod);
ColorProfileConverter pcsConverter = new(new ColorConversionOptions ColorProfileConverter pcsConverter = new(new ColorConversionOptions
{ {
@ -142,8 +142,8 @@ internal static class ColorProfileConverterExtensionsIcc
Guard.MustBeGreaterThanOrEqualTo(source.Length, destination.Length, nameof(destination)); Guard.MustBeGreaterThanOrEqualTo(source.Length, destination.Length, nameof(destination));
ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true); ConversionParams sourceParams = new(converter.Options.SourceIccProfile, toPcs: true, converter.Options.IccInterpolationMethod);
ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false); ConversionParams targetParams = new(converter.Options.TargetIccProfile, toPcs: false, converter.Options.IccInterpolationMethod);
ColorProfileConverter pcsConverter = new(new ColorConversionOptions ColorProfileConverter pcsConverter = new(new ColorConversionOptions
{ {
@ -692,10 +692,10 @@ internal static class ColorProfileConverterExtensionsIcc
{ {
private readonly IccProfile profile; private readonly IccProfile profile;
internal ConversionParams(IccProfile profile, bool toPcs) internal ConversionParams(IccProfile profile, bool toPcs, IccInterpolationMethod interpolationMethod)
{ {
this.profile = profile; 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; } internal IccConverterBase Converter { get; }

463
src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs

@ -8,51 +8,39 @@ namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators;
/// <summary> /// <summary>
/// Implements interpolation methods for color profile lookup tables. /// Implements interpolation methods for color profile lookup tables.
/// Adapted from ICC Reference implementation:
/// https://github.com/InternationalColorConsortium/DemoIccMAX/blob/79ecb74135ad47bac7d42692905a079839b7e105/IccProfLib/IccTagLut.cpp
/// </summary> /// </summary>
internal class ClutCalculator : IVector4Calculator internal class ClutCalculator : IVector4Calculator
{ {
private readonly bool useTrilinearInterpolation;
private readonly int inputCount; private readonly int inputCount;
private readonly int outputCount; private readonly int outputCount;
private readonly float[] lut; private readonly float[] lut;
private readonly byte[] gridPointCount; private readonly byte[] gridPointCount;
private readonly byte[] maxGridPoint; private readonly byte[] maxGridPoint;
private readonly int[] indexFactor;
private readonly int[] dimSize; private readonly int[] dimSize;
private readonly int nodeCount; private const int LowerCorner = 0;
private readonly float[][] nodes; private readonly int n001;
private readonly float[] g; private readonly int n010;
private readonly uint[] ig; private readonly int n011;
private readonly float[] s;
private readonly float[] df; /// <summary>
private readonly uint[] nPower; /// Initializes a new instance of the <see cref="ClutCalculator"/> class.
private int n000; /// </summary>
private int n001; /// <param name="clut">The table to evaluate.</param>
private int n010; /// <param name="useTrilinearInterpolation">Whether tables use multilinear interpolation.</param>
private int n011; public ClutCalculator(IccClut clut, bool useTrilinearInterpolation)
private int n100;
private int n101;
private int n110;
private int n111;
private int n1000;
public ClutCalculator(IccClut clut)
{ {
Guard.NotNull(clut, nameof(clut)); 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.inputCount = clut.InputChannelCount;
this.outputCount = clut.OutputChannelCount; 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.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.dimSize = new int[this.inputCount];
this.gridPointCount = clut.GridPointCount; this.gridPointCount = clut.GridPointCount;
this.maxGridPoint = new byte[this.inputCount]; 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.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;
}
} }
/// <inheritdoc/>
public unsafe Vector4 Calculate(Vector4 value) public unsafe Vector4 Calculate(Vector4 value)
{ {
Vector4 result = default; Vector4 result = default;
@ -82,95 +76,30 @@ internal class ClutCalculator : IVector4Calculator
this.Interpolate2d((float*)&value, (float*)&result); this.Interpolate2d((float*)&value, (float*)&result);
break; break;
case 3: case 3:
this.Interpolate3d((float*)&value, (float*)&result); if (this.useTrilinearInterpolation)
break; {
case 4: this.Interpolate3d((float*)&value, (float*)&result);
this.Interpolate4d((float*)&value, (float*)&result); }
break; else
default: {
this.InterpolateNd((float*)&value, (float*)&result); this.InterpolateTetrahedral((float*)&value, (float*)&result);
break; }
}
return 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; break;
case 4: case 4:
factors[0] = 0; if (this.useTrilinearInterpolation)
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++)
{ {
this.nPower[count] = (uint)(1 << (this.inputCount - 1 - count)); this.Interpolate4d((float*)&value, (float*)&result);
} }
else
uint[] nPower = [0, 1];
count = 0;
int nFlag = 1;
for (uint j = 1; j < this.nodeCount; j++)
{ {
if (j == nPower[1]) this.InterpolateTetrahedral((float*)&value, (float*)&result);
{
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++;
}
} }
break; break;
} }
return factors; return result;
} }
/// <summary> /// <summary>
@ -182,7 +111,7 @@ internal class ClutCalculator : IVector4Calculator
{ {
byte mx = this.maxGridPoint[0]; byte mx = this.maxGridPoint[0];
float x = UnitClip(srcPixel[0]) * mx; float x = Numerics.Clamp(srcPixel[0], 0F, 1F) * mx;
uint ix = (uint)x; uint ix = (uint)x;
@ -206,7 +135,7 @@ internal class ClutCalculator : IVector4Calculator
int offset = 0; int offset = 0;
for (i = 0; i < this.outputCount; i++) 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++; offset++;
} }
} }
@ -221,8 +150,8 @@ internal class ClutCalculator : IVector4Calculator
byte mx = this.maxGridPoint[0]; byte mx = this.maxGridPoint[0];
byte my = this.maxGridPoint[1]; byte my = this.maxGridPoint[1];
float x = UnitClip(srcPixel[0]) * mx; float x = Numerics.Clamp(srcPixel[0], 0F, 1F) * mx;
float y = UnitClip(srcPixel[1]) * my; float y = Numerics.Clamp(srcPixel[1], 0F, 1F) * my;
uint ix = (uint)x; uint ix = (uint)x;
uint iy = (uint)y; uint iy = (uint)y;
@ -257,25 +186,29 @@ internal class ClutCalculator : IVector4Calculator
int offset = 0; int offset = 0;
for (i = 0; i < this.outputCount; i++) 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++; offset++;
} }
} }
/// <summary> /// <summary>
/// Three dimensional interpolation function. /// Interpolates a three-channel table independently along each axis.
/// </summary> /// </summary>
/// <param name="srcPixel">The input pixel values, which will be interpolated.</param> /// <param name="srcPixel">The input pixel values, which will be interpolated.</param>
/// <param name="destPixel">The interpolated output pixels.</param> /// <param name="destPixel">The interpolated output pixels.</param>
private unsafe void Interpolate3d(float* srcPixel, float* destPixel) 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 mx = this.maxGridPoint[0];
byte my = this.maxGridPoint[1]; byte my = this.maxGridPoint[1];
byte mz = this.maxGridPoint[2]; byte mz = this.maxGridPoint[2];
float x = UnitClip(srcPixel[0]) * mx; float x = Numerics.Clamp(srcPixel[0], 0F, 1F) * mx;
float y = UnitClip(srcPixel[1]) * my; float y = Numerics.Clamp(srcPixel[1], 0F, 1F) * my;
float z = UnitClip(srcPixel[2]) * mz; float z = Numerics.Clamp(srcPixel[2], 0F, 1F) * mz;
uint ix = (uint)x; uint ix = (uint)x;
uint iy = (uint)y; uint iy = (uint)y;
@ -307,9 +240,10 @@ internal class ClutCalculator : IVector4Calculator
float nt = (float)(1.0 - t); float nt = (float)(1.0 - t);
float nu = (float)(1.0 - u); float nu = (float)(1.0 - u);
Span<float> p = this.lut.AsSpan((int)((ix * this.n001) + (iy * this.n010) + (iz * this.n100))); Span<float> 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 dF0 = ns * nt * nu;
float dF1 = ns * nt * u; float dF1 = ns * nt * u;
float dF2 = ns * t * nu; float dF2 = ns * t * nu;
@ -322,51 +256,61 @@ internal class ClutCalculator : IVector4Calculator
int offset = 0; int offset = 0;
for (int i = 0; i < this.outputCount; i++) for (int i = 0; i < this.outputCount; i++)
{ {
destPixel[i] = (float)((p[offset + this.n000] * dF0) + destPixel[i] = (float)((p[offset + 0] * dF0) +
(p[offset + this.n001] * dF1) + (p[offset + xStride] * dF1) +
(p[offset + this.n010] * dF2) + (p[offset + yStride] * dF2) +
(p[offset + this.n011] * dF3) + (p[offset + (xStride + yStride)] * dF3) +
(p[offset + this.n100] * dF4) + (p[offset + zStride] * dF4) +
(p[offset + this.n101] * dF5) + (p[offset + (xStride + zStride)] * dF5) +
(p[offset + this.n110] * dF6) + (p[offset + (yStride + zStride)] * dF6) +
(p[offset + this.n111] * dF7)); (p[offset + (xStride + yStride + zStride)] * dF7));
offset++; offset++;
} }
} }
/// <summary> /// <summary>
/// Four dimensional interpolation function. /// Interpolates three-channel tables or blends tetrahedral slices of four-channel tables.
/// </summary> /// </summary>
/// <param name="srcPixel">The input pixel values, which will be interpolated.</param> /// <param name="srcPixel">The input pixel values, which will be interpolated.</param>
/// <param name="destPixel">The interpolated output pixels.</param> /// <param name="destPixel">The interpolated output pixels.</param>
private unsafe void Interpolate4d(float* srcPixel, float* destPixel) private unsafe void InterpolateTetrahedral(float* srcPixel, float* destPixel)
{ {
byte mw = this.maxGridPoint[0]; int dimension = this.inputCount - 3;
byte mx = this.maxGridPoint[1]; int tableOffset = 0;
byte my = this.maxGridPoint[2]; int sliceStride = 0;
byte mz = this.maxGridPoint[3]; 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; // Adjacent slices have the same grid and input coordinates. Compute their cell
float x = UnitClip(srcPixel[1]) * mx; // and tetrahedron once; only the first-axis offset differs between the slices.
float y = UnitClip(srcPixel[2]) * my; int xStride = this.dimSize[dimension];
float z = UnitClip(srcPixel[3]) * mz; 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 ix = (uint)x;
uint iy = (uint)y; uint iy = (uint)y;
uint iz = (uint)z; uint iz = (uint)z;
float v = w - iw;
float u = x - ix; float u = x - ix;
float t = y - iy; float t = y - iy;
float s = z - iz; float s = z - iz;
if (iw == mw)
{
iw--;
v = 1.0f;
}
if (ix == mx) if (ix == mx)
{ {
ix--; ix--;
@ -385,122 +329,161 @@ internal class ClutCalculator : IVector4Calculator
s = 1.0f; s = 1.0f;
} }
float ns = (float)(1.0 - s); // The fractional coordinates select one of six tetrahedra sharing the cell's
float nt = (float)(1.0 - t); // lower and upper corners. Walking the axes from largest fraction to smallest
float nu = (float)(1.0 - u); // identifies the two intermediate vertices. Choose once for all output channels.
float nv = (float)(1.0 - v); int firstVertex;
int secondVertex;
Span<float> p = this.lut.AsSpan((int)((iw * this.n001) + (ix * this.n010) + (iy * this.n100) + (iz * this.n1000))); float firstWeight;
float secondWeight;
// Normalize grid units. float thirdWeight;
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,
];
int offset = 0; if (u >= t)
for (int i = 0; i < this.outputCount; i++)
{ {
float pv = 0.0f; if (t >= s)
for (int j = 0; j < 16; j++)
{ {
pv += p[offset + this.indexFactor[j]] * dF[j]; firstVertex = xStride;
secondVertex = xStride + yStride;
firstWeight = u;
secondWeight = t;
thirdWeight = s;
} }
else if (u >= s)
destPixel[i] = pv;
offset++;
}
}
/// <summary>
/// Generic N-dimensional interpolation function.
/// </summary>
/// <param name="srcPixel">The input pixel values, which will be interpolated.</param>
/// <param name="destPixel">The interpolated output pixels.</param>
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])
{ {
this.ig[i]--; firstVertex = xStride;
this.s[this.inputCount - 1 - i] = 1.0f; 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];
} }
else if (u >= s)
Span<float> p = this.lut.AsSpan(index); {
float[] temp = new float[2]; firstVertex = yStride;
bool nFlag = false; secondVertex = xStride + yStride;
firstWeight = t;
for (int i = 0; i < this.nodeCount; i++) 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<float> 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]; for (int i = 0; i < this.outputCount; i++)
temp[1] = this.s[i];
index = (int)this.nPower[i];
for (int j = 0; j < this.nodeCount; j++)
{ {
this.df[j] *= temp[nFlag ? 1 : 0]; float lower = cell[i];
if ((j + 1) % index == 0) float first = cell[i + firstVertex];
{ float second = cell[i + secondVertex];
nFlag = !nFlag; float upper = cell[i + upperVertex];
} destPixel[i] = lower
+ ((first - lower) * firstWeight)
+ ((second - first) * secondWeight)
+ ((upper - second) * thirdWeight);
} }
nFlag = false;
} }
else
int offset = 0;
for (int i = 0; i < this.outputCount; i++)
{ {
float pv = 0; // Evaluate corresponding vertices in both slices and immediately blend the
for (int j = 0; j < this.nodeCount; j++) // channel results. At the upper boundary both slices address the same cell.
ReadOnlySpan<float> 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) /// <summary>
/// Interpolates the sixteen corners surrounding a four-channel input.
/// </summary>
/// <param name="srcPixel">The normalized input channels.</param>
/// <param name="destPixel">The interpolated output channels, initially zero.</param>
private unsafe void Interpolate4d(float* srcPixel, float* destPixel)
{ {
if (v < 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.
return 0; 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]);
if (v > 1.0) 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;
} }
} }

21
src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs

@ -21,10 +21,11 @@ internal partial class LutABCalculator : IVector4Calculator
/// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mAB</c> transform. /// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mAB</c> transform.
/// </summary> /// </summary>
/// <param name="entry">The parsed A-to-B LUT entry.</param> /// <param name="entry">The parsed A-to-B LUT entry.</param>
public LutABCalculator(IccLutAToBTagDataEntry entry) /// <param name="useTrilinearInterpolation">Whether a three-channel table uses trilinear interpolation.</param>
public LutABCalculator(IccLutAToBTagDataEntry entry, bool useTrilinearInterpolation)
{ {
Guard.NotNull(entry, nameof(entry)); 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; this.type = CalculationType.AtoB;
} }
@ -32,10 +33,11 @@ internal partial class LutABCalculator : IVector4Calculator
/// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mBA</c> transform. /// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mBA</c> transform.
/// </summary> /// </summary>
/// <param name="entry">The parsed B-to-A LUT entry.</param> /// <param name="entry">The parsed B-to-A LUT entry.</param>
public LutABCalculator(IccLutBToATagDataEntry entry) /// <param name="useTrilinearInterpolation">Whether a three-channel table uses trilinear interpolation.</param>
public LutABCalculator(IccLutBToATagDataEntry entry, bool useTrilinearInterpolation)
{ {
Guard.NotNull(entry, nameof(entry)); 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; this.type = CalculationType.BtoA;
} }
@ -117,7 +119,14 @@ internal partial class LutABCalculator : IVector4Calculator
/// <remarks> /// <remarks>
/// The tag entry classes already validate channel continuity, so this method only materializes the available stages. /// The tag entry classes already validate channel continuity, so this method only materializes the available stages.
/// </remarks> /// </remarks>
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 hasACurve = curveA != null;
bool hasBCurve = curveB != null; bool hasBCurve = curveB != null;
@ -152,7 +161,7 @@ internal partial class LutABCalculator : IVector4Calculator
if (hasClut) if (hasClut)
{ {
this.clutCalculator = new ClutCalculator(clut); this.clutCalculator = new ClutCalculator(clut, useTrilinearInterpolation);
} }
} }
} }

12
src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs

@ -16,17 +16,17 @@ internal class LutEntryCalculator : IVector4Calculator
private Matrix4x4 matrix; private Matrix4x4 matrix;
private bool doTransform; private bool doTransform;
public LutEntryCalculator(IccLut8TagDataEntry lut) public LutEntryCalculator(IccLut8TagDataEntry lut, bool useTrilinearInterpolation)
{ {
Guard.NotNull(lut, nameof(lut)); 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; this.Is16Bit = false;
} }
public LutEntryCalculator(IccLut16TagDataEntry lut) public LutEntryCalculator(IccLut16TagDataEntry lut, bool useTrilinearInterpolation)
{ {
Guard.NotNull(lut, nameof(lut)); 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; this.Is16Bit = true;
} }
@ -57,11 +57,11 @@ internal class LutEntryCalculator : IVector4Calculator
return value; 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.inputCurve = InitLut(inputCurve);
this.outputCurve = InitLut(outputCurve); this.outputCurve = InitLut(outputCurve);
this.clutCalculator = new ClutCalculator(clut); this.clutCalculator = new ClutCalculator(clut, useTrilinearInterpolation);
this.matrix = matrix; this.matrix = matrix;
this.doTransform = !matrix.IsIdentity && inputCurve.Length == 3; this.doTransform = !matrix.IsIdentity && inputCurve.Length == 3;

44
src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs → src/ImageSharp/ColorProfiles/Icc/IccConverterBase.Conversions.cs

@ -24,7 +24,8 @@ internal abstract partial class IccConverterBase
/// <param name="toPcs">True if the conversion is to the Profile Connection Space.</param> /// <param name="toPcs">True if the conversion is to the Profile Connection Space.</param>
/// <param name="renderingIntent">The wanted rendering intent. Can be ignored if not available.</param> /// <param name="renderingIntent">The wanted rendering intent. Can be ignored if not available.</param>
/// <exception cref="InvalidIccProfileException">Invalid conversion method.</exception> /// <exception cref="InvalidIccProfileException">Invalid conversion method.</exception>
protected void Init(IccProfile profile, bool toPcs, IccRenderingIntent renderingIntent) /// <param name="interpolationMethod">The interpolation method used for color lookup tables.</param>
protected void Init(IccProfile profile, bool toPcs, IccRenderingIntent renderingIntent, IccInterpolationMethod interpolationMethod)
=> this.calculator = GetConversionMethod(profile, renderingIntent) switch => this.calculator = GetConversionMethod(profile, renderingIntent) switch
{ {
ConversionMethod.D0 => toPcs ? ConversionMethod.D0 => toPcs ?
@ -40,28 +41,45 @@ internal abstract partial class IccConverterBase
InitD(profile, IccProfileTag.DToB3) : InitD(profile, IccProfileTag.DToB3) :
InitD(profile, IccProfileTag.BToD3), InitD(profile, IccProfileTag.BToD3),
ConversionMethod.A0 => toPcs ? ConversionMethod.A0 => toPcs ?
InitA(profile, IccProfileTag.AToB0) : InitA(profile, IccProfileTag.AToB0, interpolationMethod) :
InitA(profile, IccProfileTag.BToA0), InitA(profile, IccProfileTag.BToA0, interpolationMethod),
ConversionMethod.A1 => toPcs ? ConversionMethod.A1 => toPcs ?
InitA(profile, IccProfileTag.AToB1) : InitA(profile, IccProfileTag.AToB1, interpolationMethod) :
InitA(profile, IccProfileTag.BToA1), InitA(profile, IccProfileTag.BToA1, interpolationMethod),
ConversionMethod.A2 => toPcs ? ConversionMethod.A2 => toPcs ?
InitA(profile, IccProfileTag.AToB2) : InitA(profile, IccProfileTag.AToB2, interpolationMethod) :
InitA(profile, IccProfileTag.BToA2), InitA(profile, IccProfileTag.BToA2, interpolationMethod),
ConversionMethod.ColorTrc => InitColorTrc(profile, toPcs), ConversionMethod.ColorTrc => InitColorTrc(profile, toPcs),
ConversionMethod.GrayTrc => InitGrayTrc(profile, toPcs), ConversionMethod.GrayTrc => InitGrayTrc(profile, toPcs),
_ => throw new InvalidIccProfileException("Invalid conversion method."), _ => throw new InvalidIccProfileException("Invalid conversion method."),
}; };
private static IVector4Calculator InitA(IccProfile profile, IccProfileTag tag) /// <summary>
=> GetTag(profile, tag) switch /// Creates a LUT calculator with interpolation selected for its input color space and direction.
/// </summary>
/// <param name="profile">The profile containing the table.</param>
/// <param name="tag">The transform tag to evaluate.</param>
/// <returns>The configured table calculator.</returns>
/// <param name="interpolationMethod">The requested interpolation method.</param>
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), IccLut8TagDataEntry lut8 => new LutEntryCalculator(lut8, useTrilinearInterpolation),
IccLut16TagDataEntry lut16 => new LutEntryCalculator(lut16), IccLut16TagDataEntry lut16 => new LutEntryCalculator(lut16, useTrilinearInterpolation),
IccLutAToBTagDataEntry lutAtoB => new LutABCalculator(lutAtoB), IccLutAToBTagDataEntry lutAtoB => new LutABCalculator(lutAtoB, useTrilinearInterpolation),
IccLutBToATagDataEntry lutBtoA => new LutABCalculator(lutBtoA), IccLutBToATagDataEntry lutBtoA => new LutABCalculator(lutBtoA, useTrilinearInterpolation),
_ => throw new InvalidIccProfileException($"Invalid entry {tag}."), _ => throw new InvalidIccProfileException($"Invalid entry {tag}."),
}; };
}
private static IVector4Calculator InitD(IccProfile profile, IccProfileTag tag) private static IVector4Calculator InitD(IccProfile profile, IccProfileTag tag)
{ {

5
src/ImageSharp/ColorProfiles/Icc/IccConverterbase.cs → src/ImageSharp/ColorProfiles/Icc/IccConverterBase.cs

@ -18,10 +18,11 @@ internal abstract partial class IccConverterBase
/// </summary> /// </summary>
/// <param name="profile">The ICC profile to use for the conversions</param> /// <param name="profile">The ICC profile to use for the conversions</param>
/// <param name="toPcs">True if the conversion is to the profile connection space (PCS); False if the conversion is to the data space</param> /// <param name="toPcs">True if the conversion is to the profile connection space (PCS); False if the conversion is to the data space</param>
protected IccConverterBase(IccProfile profile, bool toPcs) /// <param name="interpolationMethod">The interpolation method used for color lookup tables.</param>
protected IccConverterBase(IccProfile profile, bool toPcs, IccInterpolationMethod interpolationMethod)
{ {
Guard.NotNull(profile, nameof(profile)); Guard.NotNull(profile, nameof(profile));
this.Init(profile, toPcs, profile.Header.RenderingIntent); this.Init(profile, toPcs, profile.Header.RenderingIntent, interpolationMethod);
} }
/// <summary> /// <summary>

5
src/ImageSharp/ColorProfiles/Icc/IccDataToDataConverter.cs

@ -15,8 +15,9 @@ internal class IccDataToDataConverter : IccConverterBase
/// Initializes a new instance of the <see cref="IccDataToDataConverter"/> class. /// Initializes a new instance of the <see cref="IccDataToDataConverter"/> class.
/// </summary> /// </summary>
/// <param name="profile">The ICC profile to use for the conversions</param> /// <param name="profile">The ICC profile to use for the conversions</param>
public IccDataToDataConverter(IccProfile profile) /// <param name="interpolationMethod">The interpolation method used for color lookup tables.</param>
: base(profile, true) // toPCS is true because in this case the PCS space is also a data space 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
{ {
} }
} }

5
src/ImageSharp/ColorProfiles/Icc/IccDataToPcsConverter.cs

@ -15,8 +15,9 @@ internal class IccDataToPcsConverter : IccConverterBase
/// Initializes a new instance of the <see cref="IccDataToPcsConverter"/> class. /// Initializes a new instance of the <see cref="IccDataToPcsConverter"/> class.
/// </summary> /// </summary>
/// <param name="profile">The ICC profile to use for the conversions</param> /// <param name="profile">The ICC profile to use for the conversions</param>
public IccDataToPcsConverter(IccProfile profile) /// <param name="interpolationMethod">The interpolation method used for color lookup tables.</param>
: base(profile, true) public IccDataToPcsConverter(IccProfile profile, IccInterpolationMethod interpolationMethod)
: base(profile, true, interpolationMethod)
{ {
} }
} }

5
src/ImageSharp/ColorProfiles/Icc/IccPcsToDataConverter.cs

@ -15,8 +15,9 @@ internal class IccPcsToDataConverter : IccConverterBase
/// Initializes a new instance of the <see cref="IccPcsToDataConverter"/> class. /// Initializes a new instance of the <see cref="IccPcsToDataConverter"/> class.
/// </summary> /// </summary>
/// <param name="profile">The ICC profile to use for the conversions</param> /// <param name="profile">The ICC profile to use for the conversions</param>
public IccPcsToDataConverter(IccProfile profile) /// <param name="interpolationMethod">The interpolation method used for color lookup tables.</param>
: base(profile, false) public IccPcsToDataConverter(IccProfile profile, IccInterpolationMethod interpolationMethod)
: base(profile, false, interpolationMethod)
{ {
} }
} }

5
src/ImageSharp/ColorProfiles/Icc/IccPcsToPcsConverter.cs

@ -15,8 +15,9 @@ internal class IccPcsToPcsConverter : IccConverterBase
/// Initializes a new instance of the <see cref="IccPcsToPcsConverter"/> class. /// Initializes a new instance of the <see cref="IccPcsToPcsConverter"/> class.
/// </summary> /// </summary>
/// <param name="profile">The ICC profile to use for the conversions</param> /// <param name="profile">The ICC profile to use for the conversions</param>
public IccPcsToPcsConverter(IccProfile profile) /// <param name="interpolationMethod">The interpolation method used for color lookup tables.</param>
: base(profile, true) public IccPcsToPcsConverter(IccProfile profile, IccInterpolationMethod interpolationMethod)
: base(profile, true, interpolationMethod)
{ {
} }
} }

25
src/ImageSharp/ColorProfiles/IccInterpolationMethod.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.ColorProfiles;
/// <summary>
/// Defines the interpolation method for ICC color lookup tables.
/// </summary>
public enum IccInterpolationMethod
{
/// <summary>
/// Selects trilinear interpolation for Lab output and Lab device-link or abstract profiles, and tetrahedral interpolation otherwise.
/// </summary>
Auto,
/// <summary>
/// Uses trilinear interpolation for three input channels and multilinear interpolation for four input channels.
/// </summary>
Trilinear,
/// <summary>
/// Uses tetrahedral interpolation for three input channels and linearly blends tetrahedral results for four input channels.
/// </summary>
Tetrahedral
}

84
tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/ClutCalculatorTests.cs

@ -18,10 +18,92 @@ public class ClutCalculatorTests
[MemberData(nameof(IccConversionDataClut.ClutConversionTestData), MemberType = typeof(IccConversionDataClut))] [MemberData(nameof(IccConversionDataClut.ClutConversionTestData), MemberType = typeof(IccConversionDataClut))]
internal void ClutCalculator_WithClut_ReturnsResult(IccClut lut, Vector4 input, Vector4 expected) 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); Vector4 result = calculator.Calculate(input);
VectorAssert.Equal(expected, result, 4); 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);
}
} }

4
tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/LutABCalculatorTests.cs

@ -18,7 +18,7 @@ public class LutABCalculatorTests
[MemberData(nameof(IccConversionDataLutAB.LutAToBConversionTestData), MemberType = typeof(IccConversionDataLutAB))] [MemberData(nameof(IccConversionDataLutAB.LutAToBConversionTestData), MemberType = typeof(IccConversionDataLutAB))]
internal void LutABCalculator_WithLutAToB_ReturnsResult(IccLutAToBTagDataEntry lut, Vector4 input, Vector4 expected) 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); Vector4 result = calculator.Calculate(input);
@ -29,7 +29,7 @@ public class LutABCalculatorTests
[MemberData(nameof(IccConversionDataLutAB.LutBToAConversionTestData), MemberType = typeof(IccConversionDataLutAB))] [MemberData(nameof(IccConversionDataLutAB.LutBToAConversionTestData), MemberType = typeof(IccConversionDataLutAB))]
internal void LutABCalculator_WithLutBToA_ReturnsResult(IccLutBToATagDataEntry lut, Vector4 input, Vector4 expected) 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); Vector4 result = calculator.Calculate(input);

4
tests/ImageSharp.Tests/ColorProfiles/Icc/Calculators/LutEntryCalculatorTests.cs

@ -18,7 +18,7 @@ public class LutEntryCalculatorTests
[MemberData(nameof(IccConversionDataLutEntry.Lut8ConversionTestData), MemberType = typeof(IccConversionDataLutEntry))] [MemberData(nameof(IccConversionDataLutEntry.Lut8ConversionTestData), MemberType = typeof(IccConversionDataLutEntry))]
internal void LutEntryCalculator_WithLut8_ReturnsResult(IccLut8TagDataEntry lut, Vector4 input, Vector4 expected) 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); Vector4 result = calculator.Calculate(input);
@ -29,7 +29,7 @@ public class LutEntryCalculatorTests
[MemberData(nameof(IccConversionDataLutEntry.Lut16ConversionTestData), MemberType = typeof(IccConversionDataLutEntry))] [MemberData(nameof(IccConversionDataLutEntry.Lut16ConversionTestData), MemberType = typeof(IccConversionDataLutEntry))]
internal void LutEntryCalculator_WithLut16_ReturnsResult(IccLut16TagDataEntry lut, Vector4 input, Vector4 expected) 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); Vector4 result = calculator.Calculate(input);

6
tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs

@ -154,7 +154,8 @@ public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper)
ColorProfileConverter converter = new(new ColorConversionOptions ColorProfileConverter converter = new(new ColorConversionOptions
{ {
SourceIccProfile = TestIccProfiles.GetProfile(sourceProfile), SourceIccProfile = TestIccProfiles.GetProfile(sourceProfile),
TargetIccProfile = TestIccProfiles.GetProfile(targetProfile) TargetIccProfile = TestIccProfiles.GetProfile(targetProfile),
IccInterpolationMethod = IccInterpolationMethod.Trilinear
}); });
IccColorSpaceType sourceDataSpace = converter.Options.SourceIccProfile!.Header.DataColorSpace; IccColorSpaceType sourceDataSpace = converter.Options.SourceIccProfile!.Header.DataColorSpace;
@ -178,7 +179,8 @@ public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper)
ColorProfileConverter converter = new(new ColorConversionOptions ColorProfileConverter converter = new(new ColorConversionOptions
{ {
SourceIccProfile = TestIccProfiles.GetProfile(sourceProfile), SourceIccProfile = TestIccProfiles.GetProfile(sourceProfile),
TargetIccProfile = TestIccProfiles.GetProfile(targetProfile) TargetIccProfile = TestIccProfiles.GetProfile(targetProfile),
IccInterpolationMethod = IccInterpolationMethod.Trilinear
}); });
IccColorSpaceType sourceDataSpace = converter.Options.SourceIccProfile!.Header.DataColorSpace; IccColorSpaceType sourceDataSpace = converter.Options.SourceIccProfile!.Header.DataColorSpace;

Loading…
Cancel
Save