Browse Source

Add RGB to CMYK tests and fix TRC calculator

pull/1567/head
Wacton 1 year 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);
// 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;
case IccColorSpaceType.CieXyz:
xyz = CieXyz.FromScaledVector4(sourcePcs);

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

@ -39,16 +39,23 @@ internal class ColorTrcCalculator : IVector4Calculator
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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)
{
value = this.curveCalculator.Calculate(xyz);
return Vector4.Transform(value, this.matrix);
// when data to PCS, output from calculator is descaled XYZ
// 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 Wacton.Unicolour;
using Wacton.Unicolour.Icc;
using Xunit.Abstractions;
using Rgb = SixLabors.ImageSharp.ColorProfiles.Rgb;
namespace SixLabors.ImageSharp.Tests.ColorProfiles.Icc;
public class ColorProfileConverterTests
public class ColorProfileConverterTests(ITestOutputHelper testOutputHelper)
{
[Theory]
[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.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)
// 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.StandardRgbV2, TestIccProfiles.Fogra39)] // RGB -> XYZ -> LAB -> CMYK (different LUT tags, TRC vs A2B)
public void CanConvertCmykIccProfiles(string sourceProfile, string targetProfile)
{
float[] input = [GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue()];
double[] expectedTargetValues = GetExpectedTargetValues(sourceProfile, targetProfile, input);
Vector4 actualTargetValues = GetActualTargetValues(input, sourceProfile, 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;
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;
testOutputHelper.WriteLine($"Input {string.Join(", ", input)} · Expected output {string.Join(", ", expectedTargetValues)}");
const double tolerance = 0.00001;
for (int i = 0; i < expectedTargetValues.Length; i++)
{
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, 0, 1, 0.104899481, 0.103322059, 0.0991369858)]
[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)
{
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),
TargetIccProfile = TestIccProfiles.GetProfile(TestIccProfiles.StandardRgbV2)
});
Assert.Equal(expectedTargetValues[i], actualTargetValues[i], tolerance);
}
}
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()
// currently assuming it's a rounding error in the process of gathering test data manually
const double tolerance = 0.0005;
Assert.Equal(expectedR, actualRgb.R, tolerance);
Assert.Equal(expectedG, actualRgb.G, tolerance);
Assert.Equal(expectedB, actualRgb.B, tolerance);
for (int i = 0; i < expectedTargetValues.Length; i++)
{
Assert.Equal(expectedTargetValues[i], actualTargetValues[i], tolerance);
}
}
private static double[] GetExpectedTargetValues(string sourceProfile, string targetProfile, float[] input)
@ -105,6 +108,30 @@ public class ColorProfileConverterTests
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()
{
// Generate a random value between 0 (inclusive) and 1 (exclusive).

Loading…
Cancel
Save