Browse Source

Fix bulk PCS to V2 black point and add tests

pull/1567/head
Wacton 9 months ago
parent
commit
0bb67a3dfa
  1. 21
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
  2. 145
      tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs
  3. 2
      tests/ImageSharp.Tests/ImageSharp.Tests.csproj

21
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

@ -21,7 +21,7 @@ internal static class ColorProfileConverterExtensionsIcc
0.9965153F, 0.9965269F, 0.9965208F, 1F,
0.9965153F, 0.9965269F, 0.9965208F, 1F];
private static readonly float[] PcsV2FromBlackPointAdd =
private static readonly float[] PcsV2FromBlackPointOffset =
[0.00336F, 0.0034731F, 0.00287F, 0F,
0.00336F, 0.0034731F, 0.00287F, 0F,
0.00336F, 0.0034731F, 0.00287F, 0F,
@ -33,7 +33,7 @@ internal static class ColorProfileConverterExtensionsIcc
1.0034969F, 1.0034852F, 1.0034913F, 1F,
1.0034969F, 1.0034852F, 1.0034913F, 1F];
private static readonly float[] PcsV2ToBlackPointAdd =
private static readonly float[] PcsV2ToBlackPointOffset =
[0.0033717495F, 0.0034852044F, 0.0028800198F, 0F,
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F,
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F,
@ -81,7 +81,6 @@ internal static class ColorProfileConverterExtensionsIcc
return TTo.FromScaledVector4(targetParams.Converter.Calculate(targetPcs));
}
// TODO: update to match workflow of the function above
internal static void ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom>
where TTo : struct, IColorProfile<TTo>
@ -528,7 +527,7 @@ internal static class ColorProfileConverterExtensionsIcc
{
// TODO: Check our constants. They may require scaling.
Vector<float> vScale = new(PcsV2FromBlackPointScale.AsSpan()[..Vector<float>.Count]);
Vector<float> vAdd = new(PcsV2FromBlackPointAdd.AsSpan()[..Vector<float>.Count]);
Vector<float> vOffset = new(PcsV2FromBlackPointOffset.AsSpan()[..Vector<float>.Count]);
// SIMD loop
int i = 0;
@ -538,9 +537,9 @@ internal static class ColorProfileConverterExtensionsIcc
// Load the vector from source span
Vector<float> v = Unsafe.ReadUnaligned<Vector<float>>(ref Unsafe.As<Vector4, byte>(ref source[i]));
// Scale and add the vector
// Scale and offset the vector
v *= vScale;
v += vAdd;
v += vOffset;
// Write the vector to the destination span
Unsafe.WriteUnaligned(ref Unsafe.As<Vector4, byte>(ref destination[i]), v);
@ -576,7 +575,7 @@ internal static class ColorProfileConverterExtensionsIcc
{
// TODO: Check our constants. They may require scaling.
Vector<float> vScale = new(PcsV2ToBlackPointScale.AsSpan()[..Vector<float>.Count]);
Vector<float> vAdd = new(PcsV2ToBlackPointAdd.AsSpan()[..Vector<float>.Count]);
Vector<float> vOffset = new(PcsV2ToBlackPointOffset.AsSpan()[..Vector<float>.Count]);
// SIMD loop
int i = 0;
@ -586,9 +585,9 @@ internal static class ColorProfileConverterExtensionsIcc
// Load the vector from source span
Vector<float> v = Unsafe.ReadUnaligned<Vector<float>>(ref Unsafe.As<Vector4, byte>(ref source[i]));
// Scale and add the vector
// Scale and offset the vector
v *= vScale;
v += vAdd;
v -= vOffset;
// Write the vector to the destination span
Unsafe.WriteUnaligned(ref Unsafe.As<Vector4, byte>(ref destination[i]), v);
@ -599,7 +598,7 @@ internal static class ColorProfileConverterExtensionsIcc
{
Vector4 s = source[i];
s *= new Vector4(1.0034969F, 1.0034852F, 1.0034913F, 1F);
s += new Vector4(0.0033717495F, 0.0034852044F, 0.0028800198F, 0F);
s -= new Vector4(0.0033717495F, 0.0034852044F, 0.0028800198F, 0F);
destination[i] = s;
}
}
@ -610,7 +609,7 @@ internal static class ColorProfileConverterExtensionsIcc
{
Vector4 s = source[i];
s *= new Vector4(1.0034969F, 1.0034852F, 1.0034913F, 1F);
s += new Vector4(0.0033717495F, 0.0034852044F, 0.0028800198F, 0F);
s -= new Vector4(0.0033717495F, 0.0034852044F, 0.0028800198F, 0F);
destination[i] = s;
}
}

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

@ -13,6 +13,21 @@ namespace SixLabors.ImageSharp.Tests.ColorProfiles.Icc;
public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper)
{
// for 3-channel spaces, 4th item is ignored
private static readonly List<float[]> Inputs =
[
[0, 0, 0, 0],
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1],
[1, 1, 1, 1],
[0.5f, 0.5f, 0.5f, 0.5f],
[0.199678659f, 0.67982769f, 0.805381715f, 0.982666492f], // requires clipping before source is PCS adjusted for Fogra39 -> sRGBv2
[0.776568174f, 0.961630166f, 0.31032759f, 0.895294666f], // requires clipping after target is PCS adjusted for Fogra39 -> sRGBv2
[GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue()]
];
[Theory]
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Fogra39)] // CMYK -> LAB -> CMYK (commonly used v2 profiles)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Swop2006)] // CMYK -> LAB -> CMYK (commonly used v2 profiles)
@ -29,30 +44,41 @@ public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper)
[InlineData(TestIccProfiles.StandardRgbV2, TestIccProfiles.Fogra39)] // RGB -> XYZ -> LAB -> CMYK (different LUT tags, TRC vs A2B)
public void CanConvertIccProfiles(string sourceProfile, string targetProfile, double tolerance = 0.00005)
{
// for 3-channel spaces, 4th item is ignored
List<float[]> inputs =
[
[0, 0, 0, 0],
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1],
[1, 1, 1, 1],
[0.5f, 0.5f, 0.5f, 0.5f],
[0.199678659f, 0.67982769f, 0.805381715f, 0.982666492f], // requires clipping before source is PCS adjusted for Fogra39 -> sRGBv2
[0.776568174f, 0.961630166f, 0.31032759f, 0.895294666f], // requires clipping after target is PCS adjusted for Fogra39 -> sRGBv2
[GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue()]
];
foreach (float[] input in inputs)
{
double[] expectedTargetValues = GetExpectedTargetValues(sourceProfile, targetProfile, input, testOutputHelper);
testOutputHelper.WriteLine($"Input {string.Join(", ", input)} · Expected output {string.Join(", ", expectedTargetValues)}");
List<Vector4> actual = Inputs.Select(input => GetActualTargetValues(input, sourceProfile, targetProfile)).ToList();
AssertConversion(sourceProfile, targetProfile, actual, tolerance, testOutputHelper);
}
[Theory]
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Fogra39)] // CMYK -> LAB -> CMYK (commonly used v2 profiles)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Swop2006)] // CMYK -> LAB -> CMYK (commonly used v2 profiles)
[InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Fogra39)] // CMYK -> LAB -> CMYK (commonly used v2 profiles)
[InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Swop2006)] // CMYK -> LAB -> CMYK (commonly used v2 profiles)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.JapanColor2003)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 8-bit vs 16-bit)
[InlineData(TestIccProfiles.JapanColor2011, TestIccProfiles.Fogra39)] // CMYK -> LAB -> CMYK (different LUT versions, v2 vs v4)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Cgats21)] // CMYK -> LAB -> RGB (different LUT versions, v2 vs v4)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV4)] // RGB -> LAB -> CMYK (different LUT versions, v4 vs v2)
[InlineData(TestIccProfiles.StandardRgbV4, TestIccProfiles.Fogra39)] // RGB -> LAB -> XYZ -> RGB (different LUT elements, B-Matrix-M-CLUT-A vs B-Matrix-M)
[InlineData(TestIccProfiles.StandardRgbV4, TestIccProfiles.RommRgb)] // RGB -> XYZ -> LAB -> RGB (different LUT elements, B-Matrix-M vs B-Matrix-M-CLUT-A)
[InlineData(TestIccProfiles.RommRgb, TestIccProfiles.StandardRgbV4)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 16-bit vs 8-bit)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV2, 0.0005)] // CMYK -> LAB -> XYZ -> RGB (different LUT tags, A2B vs TRC) --- tolerance slightly higher due to difference in inverse curve implementation
[InlineData(TestIccProfiles.StandardRgbV2, TestIccProfiles.Fogra39)] // RGB -> XYZ -> LAB -> CMYK (different LUT tags, TRC vs A2B)
public void CanBulkConvertIccProfiles(string sourceProfile, string targetProfile, double tolerance = 0.00005)
{
List<Vector4> actual = GetBulkActualTargetValues(Inputs, sourceProfile, targetProfile);
AssertConversion(sourceProfile, targetProfile, actual, tolerance, testOutputHelper);
}
Vector4 actualTargetValues = GetActualTargetValues(input, sourceProfile, targetProfile);
for (int i = 0; i < expectedTargetValues.Length; i++)
private static void AssertConversion(string sourceProfile, string targetProfile, List<Vector4> actual, double tolerance, ITestOutputHelper testOutputHelper)
{
List<double[]> expected = Inputs.Select(input => GetExpectedTargetValues(sourceProfile, targetProfile, input, testOutputHelper)).ToList();
Assert.Equal(expected.Count, actual.Count);
for (int i = 0; i < expected.Count; i++)
{
Log(testOutputHelper, Inputs[i], expected[i], actual[i]);
for (int j = 0; j < expected[i].Length; j++)
{
Assert.Equal(expectedTargetValues[i], actualTargetValues[i], tolerance);
Assert.Equal(expected[i][j], actual[i][j], tolerance);
}
}
}
@ -147,6 +173,74 @@ public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper)
};
}
private static List<Vector4> GetBulkActualTargetValues(List<float[]> inputs, string sourceProfile, string targetProfile)
{
ColorProfileConverter converter = new(new ColorConversionOptions
{
SourceIccProfile = TestIccProfiles.GetProfile(sourceProfile),
TargetIccProfile = TestIccProfiles.GetProfile(targetProfile)
});
IccColorSpaceType sourceDataSpace = converter.Options.SourceIccProfile!.Header.DataColorSpace;
IccColorSpaceType targetDataSpace = converter.Options.TargetIccProfile!.Header.DataColorSpace;
switch (sourceDataSpace)
{
case IccColorSpaceType.Cmyk:
{
Span<Cmyk> inputSpan = inputs.Select(x => new Cmyk(new Vector4(x))).ToArray();
switch (targetDataSpace)
{
case IccColorSpaceType.Cmyk:
{
Span<Cmyk> outputSpan = stackalloc Cmyk[inputs.Count];
converter.Convert<Cmyk, Cmyk>(inputSpan, outputSpan);
return outputSpan.ToArray().Select(x => x.ToScaledVector4()).ToList();
}
case IccColorSpaceType.Rgb:
{
Span<Rgb> outputSpan = stackalloc Rgb[inputs.Count];
converter.Convert<Cmyk, Rgb>(inputSpan, outputSpan);
return outputSpan.ToArray().Select(x => x.ToScaledVector4()).ToList();
}
default:
throw new NotSupportedException($"Unsupported ICC profile data color space conversion: {sourceDataSpace} -> {targetDataSpace}");
}
}
case IccColorSpaceType.Rgb:
{
Span<Rgb> inputSpan = inputs.Select(x => new Rgb(new Vector3(x))).ToArray();
switch (targetDataSpace)
{
case IccColorSpaceType.Cmyk:
{
Span<Cmyk> outputSpan = stackalloc Cmyk[inputs.Count];
converter.Convert<Rgb, Cmyk>(inputSpan, outputSpan);
return outputSpan.ToArray().Select(x => x.ToScaledVector4()).ToList();
}
case IccColorSpaceType.Rgb:
{
Span<Rgb> outputSpan = stackalloc Rgb[inputs.Count];
converter.Convert<Rgb, Rgb>(inputSpan, outputSpan);
return outputSpan.ToArray().Select(x => x.ToScaledVector4()).ToList();
}
default:
throw new NotSupportedException($"Unsupported ICC profile data color space conversion: {sourceDataSpace} -> {targetDataSpace}");
}
}
default:
throw new NotSupportedException($"Unsupported ICC profile data color space conversion: {sourceDataSpace} -> {targetDataSpace}");
}
}
private static float GetNormalizedRandomValue()
{
// Generate a random value between 0 (inclusive) and 1 (exclusive).
@ -158,4 +252,11 @@ public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper)
// Clamp the result between 0 and 1 to ensure it does not exceed the bounds.
return value == 0 ? 0F : Math.Clamp((float)value + 0.0000001F, 0, 1);
}
private static void Log(ITestOutputHelper testOutputHelper, float[] input, double[] expected, Vector4 actual)
{
string inputText = string.Join(", ", input);
string expectedText = string.Join(", ", expected.Select(x => $"{x:f8}"));
testOutputHelper.WriteLine($"Input {inputText} · Expected output {expectedText} · Actual output {actual}");
}
}

2
tests/ImageSharp.Tests/ImageSharp.Tests.csproj

@ -50,7 +50,7 @@
<PackageReference Include="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Condition="'$(IsOSX)'=='true'" />
<PackageReference Include="SharpZipLib" />
<PackageReference Include="System.Drawing.Common" />
<PackageReference Include="Wacton.Unicolour" Version="5.0.0" />
<PackageReference Include="Wacton.Unicolour" Version="6.0.0" />
</ItemGroup>
<ItemGroup>

Loading…
Cancel
Save