Browse Source

Add RGB to CMYK tests and fix TRC calculator

pull/1567/head
Wacton 2 years ago
parent
commit
7d4a7420f1
  1. 2
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
  2. 23
      src/ImageSharp/ColorProfiles/Icc/Calculators/ColorTrcCalculator.cs
  3. 93
      tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs

2
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

@ -263,7 +263,7 @@ internal static class ColorProfileConverterExtensionsIcc
xyz = pcsConverter.Convert<CieLab, CieXyz>(in lab); xyz = pcsConverter.Convert<CieLab, CieXyz>(in lab);
// DemoMaxICC clips negatives as part of IccUtil.cpp : icLabToXYZ > icICubeth // DemoMaxICC clips negatives as part of IccUtil.cpp : icLabToXYZ > icICubeth
xyz = new CieXyz(Vector3.Clamp(xyz.ToVector3(), Vector3.Zero, new Vector3(float.MaxValue, float.MaxValue, float.MaxValue))); xyz = new CieXyz(Vector3.Max(xyz.ToVector3(), Vector3.Zero));
break; break;
case IccColorSpaceType.CieXyz: case IccColorSpaceType.CieXyz:
xyz = CieXyz.FromScaledVector4(sourcePcs); xyz = CieXyz.FromScaledVector4(sourcePcs);

23
src/ImageSharp/ColorProfiles/Icc/Calculators/ColorTrcCalculator.cs

@ -39,16 +39,23 @@ internal class ColorTrcCalculator : IVector4Calculator
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 Calculate(Vector4 value) public Vector4 Calculate(Vector4 value)
{ {
// uses the descaled XYZ as DemoMaxICC IccCmm.cpp : CIccXformMatrixTRC::Apply()
Vector4 xyz = new(CieXyz.FromScaledVector4(value).ToVector3(), 1);
if (this.toPcs) if (this.toPcs)
{ {
value = this.curveCalculator.Calculate(xyz); // when data to PCS, output from calculator is descaled XYZ
return Vector4.Transform(value, this.matrix); // but expected return value is scaled XYZ
// see DemoMaxICC IccCmm.cpp : CIccXformMatrixTRC::Apply()
value = this.curveCalculator.Calculate(value);
CieXyz xyz = new(Vector4.Transform(value, this.matrix).AsVector3());
return xyz.ToScaledVector4();
}
else
{
// when PCS to data, input to calculator is scaled XYZ
// but need descaled XYZ for matrix multiplication
// see DemoMaxICC IccCmm.cpp : CIccXformMatrixTRC::Apply()
Vector4 xyz = new(CieXyz.FromScaledVector4(value).ToVector3(), 1);
value = Vector4.Transform(xyz, this.matrix);
return this.curveCalculator.Calculate(value);
} }
value = Vector4.Transform(xyz, this.matrix);
return this.curveCalculator.Calculate(value);
} }
} }

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

@ -6,11 +6,12 @@ using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using Wacton.Unicolour; using Wacton.Unicolour;
using Wacton.Unicolour.Icc; using Wacton.Unicolour.Icc;
using Xunit.Abstractions;
using Rgb = SixLabors.ImageSharp.ColorProfiles.Rgb; using Rgb = SixLabors.ImageSharp.ColorProfiles.Rgb;
namespace SixLabors.ImageSharp.Tests.ColorProfiles.Icc; namespace SixLabors.ImageSharp.Tests.ColorProfiles.Icc;
public class ColorProfileConverterTests public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper)
{ {
[Theory] [Theory]
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Fogra39)] // CMYK -> LAB -> CMYK (commonly used v2 profiles) [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Fogra39)] // CMYK -> LAB -> CMYK (commonly used v2 profiles)
@ -23,36 +24,16 @@ public class ColorProfileConverterTests
[InlineData(TestIccProfiles.StandardRgbV4, TestIccProfiles.Fogra39)] // RGB -> LAB -> CMYK (different LUT versions, v4 vs v2) [InlineData(TestIccProfiles.StandardRgbV4, TestIccProfiles.Fogra39)] // RGB -> LAB -> CMYK (different LUT versions, v4 vs v2)
[InlineData(TestIccProfiles.StandardRgbV4, TestIccProfiles.RommRgb)] // RGB -> LAB -> XYZ -> RGB (different LUT elements, B-Matrix-M-CLUT-A vs B-Matrix-M) [InlineData(TestIccProfiles.StandardRgbV4, TestIccProfiles.RommRgb)] // RGB -> LAB -> XYZ -> RGB (different LUT elements, B-Matrix-M-CLUT-A vs B-Matrix-M)
[InlineData(TestIccProfiles.RommRgb, TestIccProfiles.StandardRgbV4)] // RGB -> XYZ -> LAB -> RGB (different LUT elements, B-Matrix-M vs B-Matrix-M-CLUT-A) [InlineData(TestIccProfiles.RommRgb, TestIccProfiles.StandardRgbV4)] // RGB -> XYZ -> LAB -> RGB (different LUT elements, B-Matrix-M vs B-Matrix-M-CLUT-A)
// TODO: enable once supported by Unicolour - in the meantime, manually test known values // TODO: add once supported by Unicolour in v4.8
// [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.JapanColor2003)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 16-bit vs 8-bit) // [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.JapanColor2003)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 16-bit vs 8-bit)
// [InlineData(TestIccProfiles.StandardRgbV2, TestIccProfiles.Fogra39)] // RGB -> XYZ -> LAB -> CMYK (different LUT tags, TRC vs A2B)
public void CanConvertCmykIccProfiles(string sourceProfile, string targetProfile) public void CanConvertCmykIccProfiles(string sourceProfile, string targetProfile)
{ {
float[] input = [GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue()]; float[] input = [GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue()];
double[] expectedTargetValues = GetExpectedTargetValues(sourceProfile, targetProfile, input); double[] expectedTargetValues = GetExpectedTargetValues(sourceProfile, targetProfile, input);
Vector4 actualTargetValues = GetActualTargetValues(input, sourceProfile, targetProfile);
ColorProfileConverter converter = new(new ColorConversionOptions testOutputHelper.WriteLine($"Input {string.Join(", ", input)} · Expected output {string.Join(", ", expectedTargetValues)}");
{ const double tolerance = 0.00001;
SourceIccProfile = TestIccProfiles.GetProfile(sourceProfile),
TargetIccProfile = TestIccProfiles.GetProfile(targetProfile)
});
IccColorSpaceType sourceDataSpace = converter.Options.SourceIccProfile!.Header.DataColorSpace;
IccColorSpaceType targetDataSpace = converter.Options.TargetIccProfile!.Header.DataColorSpace;
Vector4 actualTargetValues = sourceDataSpace switch
{
IccColorSpaceType.Cmyk when targetDataSpace == IccColorSpaceType.Cmyk
=> converter.Convert<Cmyk, Cmyk>(new Cmyk(new Vector4(input))).ToScaledVector4(),
IccColorSpaceType.Cmyk when targetDataSpace == IccColorSpaceType.Rgb
=> converter.Convert<Cmyk, Rgb>(new Cmyk(new Vector4(input))).ToScaledVector4(),
IccColorSpaceType.Rgb when targetDataSpace == IccColorSpaceType.Cmyk
=> converter.Convert<Rgb, Cmyk>(new Rgb(new Vector3(input))).ToScaledVector4(),
IccColorSpaceType.Rgb when targetDataSpace == IccColorSpaceType.Rgb
=> converter.Convert<Rgb, Rgb>(new Rgb(new Vector3(input))).ToScaledVector4(),
_ => throw new NotSupportedException("Unexpected ICC profile data color spaces")
};
const double tolerance = 0.000005;
for (int i = 0; i < expectedTargetValues.Length; i++) for (int i = 0; i < expectedTargetValues.Length; i++)
{ {
Assert.Equal(expectedTargetValues[i], actualTargetValues[i], tolerance); Assert.Equal(expectedTargetValues[i], actualTargetValues[i], tolerance);
@ -68,24 +49,46 @@ public class ColorProfileConverterTests
[InlineData(0, 0, 1, 0, 1, 0.937102795, 0)] [InlineData(0, 0, 1, 0, 1, 0.937102795, 0)]
[InlineData(0, 0, 0, 1, 0.104899481, 0.103322059, 0.0991369858)] [InlineData(0, 0, 0, 1, 0.104899481, 0.103322059, 0.0991369858)]
[InlineData(1, 1, 1, 1, 0, 0, 1.95495249e-05)] [InlineData(1, 1, 1, 1, 0, 0, 1.95495249e-05)]
[InlineData(0.8, 0.6, 0.4, 0.2, 0.26316157, 0.348658293, 0.43705827)]
[InlineData(0.2, 0.4, 0.6, 0.8, 0.283472508, 0.222469613, 0.148681521)]
public void CanConvertCmykIccProfilesToRgbUsingMatrixTrc(float c, float m, float y, float k, float expectedR, float expectedG, float expectedB) public void CanConvertCmykIccProfilesToRgbUsingMatrixTrc(float c, float m, float y, float k, float expectedR, float expectedG, float expectedB)
{ {
float[] input = [c, m, y, k]; float[] input = [c, m, y, k];
float[] expectedTargetValues = [expectedR, expectedG, expectedB];
Vector4 actualTargetValues = GetActualTargetValues(input, TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV2);
ColorProfileConverter converter = new(new ColorConversionOptions // TODO: investigate lower tolerance than CanConvertCmykIccProfiles()
// currently assuming it's a rounding error in the process of gathering test data manually
const double tolerance = 0.0005;
for (int i = 0; i < expectedTargetValues.Length; i++)
{ {
SourceIccProfile = TestIccProfiles.GetProfile(TestIccProfiles.Fogra39), Assert.Equal(expectedTargetValues[i], actualTargetValues[i], tolerance);
TargetIccProfile = TestIccProfiles.GetProfile(TestIccProfiles.StandardRgbV2) }
}); }
Rgb actualRgb = converter.Convert<Cmyk, Rgb>(new Cmyk(new Vector4(input))); // TODO: replace with random Unicolour comparison once supported
// RGB -> XYZ -> LAB -> CMYK (different LUT tags, TRC vs A2B)
[Theory]
[InlineData(0, 0, 0, 0.7701597, 0.6655727, 0.5460027, 0.9999934)]
[InlineData(1, 0, 0, 0.00024405644, 0.9664673, 0.96581775, 0)]
[InlineData(0, 1, 0, 0.7057834, 5.4162505E-05, 0.99998796, 0)]
[InlineData(0, 0, 1, 0.993157, 0.79850656, 0.00074962573, 0.0003229109)]
[InlineData(1, 1, 1, 2.5115267E-05, 8.9339E-05, 0.00010595919, 0)]
[InlineData(0.75, 0.5, 0.25, 0.041562695, 0.45613098, 0.7557201, 0.23913471)]
[InlineData(0.25, 0.5, 0.75, 0.7424422, 0.40337864, 0.005461347, 0.05777717)]
public void CanConvertRgbIccProfilesToCmykUsingMatrixTrc(float r, float g, float b, float expectedC, float expectedM, float expectedY, float expectedK)
{
float[] input = [r, g, b];
float[] expectedTargetValues = [expectedC, expectedM, expectedY, expectedK];
Vector4 actualTargetValues = GetActualTargetValues(input, TestIccProfiles.StandardRgbV2, TestIccProfiles.Fogra39);
// TODO: investigate lower tolerance than CanConvertCmykIccProfiles() // TODO: investigate lower tolerance than CanConvertCmykIccProfiles()
// currently assuming it's a rounding error in the process of gathering test data manually // currently assuming it's a rounding error in the process of gathering test data manually
const double tolerance = 0.0005; const double tolerance = 0.0005;
Assert.Equal(expectedR, actualRgb.R, tolerance); for (int i = 0; i < expectedTargetValues.Length; i++)
Assert.Equal(expectedG, actualRgb.G, tolerance); {
Assert.Equal(expectedB, actualRgb.B, tolerance); Assert.Equal(expectedTargetValues[i], actualTargetValues[i], tolerance);
}
} }
private static double[] GetExpectedTargetValues(string sourceProfile, string targetProfile, float[] input) private static double[] GetExpectedTargetValues(string sourceProfile, string targetProfile, float[] input)
@ -105,6 +108,30 @@ public class ColorProfileConverterTests
return target.Icc.Values; return target.Icc.Values;
} }
private static Vector4 GetActualTargetValues(float[] input, 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;
return sourceDataSpace switch
{
IccColorSpaceType.Cmyk when targetDataSpace == IccColorSpaceType.Cmyk
=> converter.Convert<Cmyk, Cmyk>(new Cmyk(new Vector4(input))).ToScaledVector4(),
IccColorSpaceType.Cmyk when targetDataSpace == IccColorSpaceType.Rgb
=> converter.Convert<Cmyk, Rgb>(new Cmyk(new Vector4(input))).ToScaledVector4(),
IccColorSpaceType.Rgb when targetDataSpace == IccColorSpaceType.Cmyk
=> converter.Convert<Rgb, Cmyk>(new Rgb(new Vector3(input))).ToScaledVector4(),
IccColorSpaceType.Rgb when targetDataSpace == IccColorSpaceType.Rgb
=> converter.Convert<Rgb, Rgb>(new Rgb(new Vector3(input))).ToScaledVector4(),
_ => throw new NotSupportedException($"Unsupported ICC profile data color space conversion: {sourceDataSpace} -> {targetDataSpace}")
};
}
private static float GetNormalizedRandomValue() private static float GetNormalizedRandomValue()
{ {
// Generate a random value between 0 (inclusive) and 1 (exclusive). // Generate a random value between 0 (inclusive) and 1 (exclusive).

Loading…
Cancel
Save