From 7d4a7420f1f1f05c7547ee45b29a1e7baa5842b4 Mon Sep 17 00:00:00 2001 From: Wacton Date: Sun, 15 Dec 2024 18:55:45 +0000 Subject: [PATCH] Add RGB to CMYK tests and fix TRC calculator --- .../ColorProfileConverterExtensionsIcc.cs | 2 +- .../Icc/Calculators/ColorTrcCalculator.cs | 23 +++-- .../Icc/ColorProfileConverterTests.Icc.cs | 93 ++++++++++++------- 3 files changed, 76 insertions(+), 42 deletions(-) diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs index 6ed7a35327..22c31e26b6 100644 --- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs +++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs @@ -263,7 +263,7 @@ internal static class ColorProfileConverterExtensionsIcc xyz = pcsConverter.Convert(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); diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/ColorTrcCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/ColorTrcCalculator.cs index 3629401032..c7028af5c6 100644 --- a/src/ImageSharp/ColorProfiles/Icc/Calculators/ColorTrcCalculator.cs +++ b/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); } } diff --git a/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs b/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs index 9ed51b5191..98651b9cd6 100644 --- a/tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs +++ b/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(new Cmyk(new Vector4(input))).ToScaledVector4(), - IccColorSpaceType.Cmyk when targetDataSpace == IccColorSpaceType.Rgb - => converter.Convert(new Cmyk(new Vector4(input))).ToScaledVector4(), - IccColorSpaceType.Rgb when targetDataSpace == IccColorSpaceType.Cmyk - => converter.Convert(new Rgb(new Vector3(input))).ToScaledVector4(), - IccColorSpaceType.Rgb when targetDataSpace == IccColorSpaceType.Rgb - => converter.Convert(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(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(new Cmyk(new Vector4(input))).ToScaledVector4(), + IccColorSpaceType.Cmyk when targetDataSpace == IccColorSpaceType.Rgb + => converter.Convert(new Cmyk(new Vector4(input))).ToScaledVector4(), + IccColorSpaceType.Rgb when targetDataSpace == IccColorSpaceType.Cmyk + => converter.Convert(new Rgb(new Vector3(input))).ToScaledVector4(), + IccColorSpaceType.Rgb when targetDataSpace == IccColorSpaceType.Rgb + => converter.Convert(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).