Browse Source

Adjust PCS values for v2 profiles using perceptual intent

pull/1567/head
Wacton 2 years ago
parent
commit
0327dca733
  1. 124
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
  2. 4
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs
  3. 2
      src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs
  4. 78
      tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs
  5. 12
      tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs
  6. 3
      tests/ImageSharp.Tests/TestDataIcc/Profiles/CGATS21_CRPC7.icc
  7. 3
      tests/ImageSharp.Tests/TestDataIcc/Profiles/ISO22028-2_ROMM-RGB.icc
  8. 3
      tests/ImageSharp.Tests/TestDataIcc/Profiles/JapanColor2011Coated.icc
  9. 3
      tests/ImageSharp.Tests/TestDataIcc/Profiles/sRGB2014.icc
  10. 3
      tests/ImageSharp.Tests/TestDataIcc/Profiles/sRGB_v4_ICC_preference.icc

124
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

@ -5,6 +5,7 @@ using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
@ -33,64 +34,103 @@ internal static class ColorProfileConverterExtensionsIcc
MemoryAllocator = converter.Options.MemoryAllocator,
// TODO: Double check this but I think these are normalized values.
SourceWhitePoint = CieXyz.FromScaledVector4(new(converter.Options.SourceIccProfile.Header.PcsIlluminant, 1F)),
TargetWhitePoint = CieXyz.FromScaledVector4(new(converter.Options.TargetIccProfile.Header.PcsIlluminant, 1F)),
SourceWhitePoint = new CieXyz(converter.Options.SourceIccProfile.Header.PcsIlluminant),
TargetWhitePoint = new CieXyz(converter.Options.TargetIccProfile.Header.PcsIlluminant),
});
IccDataToPcsConverter sourceConverter = new(converter.Options.SourceIccProfile);
IccPcsToDataConverter targetConverter = new(converter.Options.TargetIccProfile);
IccColorSpaceType sourcePcsType = converter.Options.SourceIccProfile.Header.ProfileConnectionSpace;
IccColorSpaceType targetPcsType = converter.Options.TargetIccProfile.Header.ProfileConnectionSpace;
IccVersion sourceVersion = converter.Options.SourceIccProfile.Header.Version;
IccVersion targetVersion = converter.Options.TargetIccProfile.Header.Version;
Vector4 pcs = sourceConverter.Calculate(source.ToScaledVector4());
// Profile connecting spaces can only be Lab, XYZ.
if (sourcePcsType is IccColorSpaceType.CieLab && targetPcsType is IccColorSpaceType.CieXyz)
{
// Convert from Lab to XYZ.
CieLab lab = CieLab.FromScaledVector4(pcs);
CieXyz xyz = pcsConverter.Convert<CieLab, CieXyz>(in lab);
pcs = xyz.ToScaledVector4();
}
else if (sourcePcsType is IccColorSpaceType.CieXyz && targetPcsType is IccColorSpaceType.CieLab)
IccProfileHeader sourceHeader = converter.Options.SourceIccProfile.Header;
IccProfileHeader targetHeader = converter.Options.TargetIccProfile.Header;
IccColorSpaceType sourcePcsType = sourceHeader.ProfileConnectionSpace;
IccColorSpaceType targetPcsType = targetHeader.ProfileConnectionSpace;
IccRenderingIntent sourceIntent = sourceHeader.RenderingIntent;
IccRenderingIntent targetIntent = targetHeader.RenderingIntent;
IccVersion sourceVersion = sourceHeader.Version;
IccVersion targetVersion = targetHeader.Version;
// all conversions are funnelled through XYZ in case PCS adjustments need to be made
CieXyz xyz;
Vector4 sourcePcs = sourceConverter.Calculate(source.ToScaledVector4());
switch (sourcePcsType)
{
// Convert from XYZ to Lab.
CieXyz xyz = CieXyz.FromScaledVector4(pcs);
CieLab lab = pcsConverter.Convert<CieXyz, CieLab>(in xyz);
pcs = lab.ToScaledVector4();
case IccColorSpaceType.CieLab:
if (sourceConverter.Is16BitLutEntry)
{
sourcePcs = LabV2ToLab(sourcePcs);
}
CieLab lab = CieLab.FromScaledVector4(sourcePcs);
xyz = pcsConverter.Convert<CieLab, CieXyz>(in lab);
break;
case IccColorSpaceType.CieXyz:
xyz = new CieXyz(sourcePcs[0], sourcePcs[1], sourcePcs[2]);
xyz = pcsConverter.Convert<CieXyz, CieXyz>(in xyz);
break;
default:
throw new ArgumentOutOfRangeException($"Source PCS {sourcePcsType} not supported");
}
else if (sourcePcsType is IccColorSpaceType.CieXyz && targetPcsType is IccColorSpaceType.CieXyz)
{
// Convert from XYZ to XYZ.
CieXyz xyz = CieXyz.FromScaledVector4(pcs);
CieXyz targetXyz = pcsConverter.Convert<CieXyz, CieXyz>(in xyz);
pcs = targetXyz.ToScaledVector4();
}
else if (sourcePcsType is IccColorSpaceType.CieLab && targetPcsType is IccColorSpaceType.CieLab)
// TODO: handle PCS adjustment for absolute intent?
// TODO: or throw unsupported error, since most profiles headers contain perceptual (i've encountered a couple of relative, but so far no saturation or absolute)
bool adjustSourcePcsForPerceptual = sourceIntent == IccRenderingIntent.Perceptual && sourceVersion.Major == 2;
bool adjustTargetPcsForPerceptual = targetIntent == IccRenderingIntent.Perceptual && targetVersion.Major == 2;
// if both profiles need PCS adjustment, they both share the same unadjusted PCS space
// effectively cancelling out the need to make the adjustment
bool adjustPcsForPerceptual = adjustSourcePcsForPerceptual ^ adjustTargetPcsForPerceptual;
if (adjustPcsForPerceptual)
{
// Convert from Lab to Lab.
if (sourceVersion.Major == 4 && targetVersion.Major == 2)
// as per DemoIccMAX icPerceptual values in IccCmm.h
CieXyz refBlack = new(0.00336F, 0.0034731F, 0.00287F);
CieXyz refWhite = new(0.9642F, 1.0000F, 0.8249F);
if (adjustSourcePcsForPerceptual)
{
// Convert from Lab v4 to Lab v2.
pcs = LabToLabV2(pcs);
Vector3 iccXyz = xyz.ToScaledVector4().AsVector128().AsVector3();
Vector3 scale = Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3());
Vector3 offset = refBlack.ToScaledVector4().AsVector128().AsVector3();
Vector3 adjustedXyz = (iccXyz * scale) + offset;
xyz = CieXyz.FromScaledVector4(new Vector4(adjustedXyz, 1F));
}
else if (sourceVersion.Major == 2 && targetVersion.Major == 4)
if (adjustTargetPcsForPerceptual)
{
// Convert from Lab v2 to Lab v4.
pcs = LabV2ToLab(pcs);
Vector3 iccXyz = xyz.ToScaledVector4().AsVector128().AsVector3();
Vector3 scale = Vector3.Divide(Vector3.One, Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3()));
Vector3 offset = -refBlack.ToScaledVector4().AsVector128().AsVector3() * scale;
Vector3 adjustedXyz = (iccXyz * scale) + offset;
xyz = CieXyz.FromScaledVector4(new Vector4(adjustedXyz, 1F));
}
}
CieLab lab = CieLab.FromScaledVector4(pcs);
CieLab targetLab = pcsConverter.Convert<CieLab, CieLab>(in lab);
pcs = targetLab.ToScaledVector4();
Vector4 targetPcs;
switch (targetPcsType)
{
case IccColorSpaceType.CieLab:
CieLab lab = pcsConverter.Convert<CieXyz, CieLab>(in xyz);
targetPcs = lab.ToScaledVector4();
if (adjustTargetPcsForPerceptual)
{
targetPcs = LabToLabV2(targetPcs);
}
break;
case IccColorSpaceType.CieXyz:
CieXyz targetXyz = pcsConverter.Convert<CieXyz, CieXyz>(in xyz);
targetPcs = targetXyz.ToScaledVector4();
break;
default:
throw new ArgumentOutOfRangeException($"Target PCS {targetPcsType} not supported");
}
// Convert to the target space.
return TTo.FromScaledVector4(targetConverter.Calculate(pcs));
Vector4 targetValue = targetConverter.Calculate(targetPcs);
return TTo.FromScaledVector4(targetValue);
}
// 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>
@ -203,6 +243,8 @@ internal static class ColorProfileConverterExtensionsIcc
TTo.FromScaledVector4(pcsNormalized, destination);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector4 LabToLabV2(Vector4 input)
=> input * 65280F / 65535F;

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

@ -20,14 +20,18 @@ internal class LutEntryCalculator : IVector4Calculator
{
Guard.NotNull(lut, nameof(lut));
this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix);
this.Is16Bit = false;
}
public LutEntryCalculator(IccLut16TagDataEntry lut)
{
Guard.NotNull(lut, nameof(lut));
this.Init(lut.InputValues, lut.OutputValues, lut.ClutValues, lut.Matrix);
this.Is16Bit = true;
}
internal bool Is16Bit { get; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 Calculate(Vector4 value)
{

2
src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs

@ -13,6 +13,8 @@ internal abstract partial class IccConverterBase
{
private IVector4Calculator calculator;
public bool Is16BitLutEntry => this.calculator is LutEntryCalculator { Is16Bit: true };
/// <summary>
/// Checks the profile for available conversion methods and gathers all the information's necessary for it.
/// </summary>

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

@ -1,52 +1,74 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.ColorProfiles;
using Wacton.Unicolour;
using Wacton.Unicolour.Icc;
using Rgb = SixLabors.ImageSharp.ColorProfiles.Rgb;
namespace SixLabors.ImageSharp.Tests.ColorProfiles.Icc;
public class ColorProfileConverterTests
{
[Theory]
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Fogra39)]
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Swop2006)]
[InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Fogra39)]
[InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Swop2006)]
public void CanConvertCmykIccProfiles(string sourceProfileName, string targetProfileName)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Fogra39, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (commonly used v2 profiles)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Swop2006, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (commonly used v2 profiles)
[InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Fogra39, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (commonly used v2 profiles)
[InlineData(TestIccProfiles.Swop2006, TestIccProfiles.Swop2006, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (commonly used v2 profiles)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.JapanColor2011, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 16-bit vs 8-bit)
[InlineData(TestIccProfiles.JapanColor2011, TestIccProfiles.Fogra39, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 8-bit vs 16-bit)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Cgats21, IccConversion.CmykToCmyk)] // CMYK -> LAB -> CMYK (different LUT versions, v2 vs v4)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV4, IccConversion.CmykToRgb)] // CMYK -> LAB -> RGB (different LUT versions, v2 vs v4)
[InlineData(TestIccProfiles.StandardRgbV4, TestIccProfiles.Fogra39, IccConversion.RgbToCmyk)] // RGB -> LAB -> CMYK (different LUT versions, v4 vs v2)
[InlineData(TestIccProfiles.StandardRgbV4, TestIccProfiles.RommRgb, IccConversion.RgbToRgb)] // RGB -> LAB -> XYZ -> RGB (different LUT elements, B-Matrix-M-CLUT-A vs B-Matrix-M)
// TODO: enable once supported by Unicolour - in the meantime, manually test known values
// [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV2, IccConversion.CmykToRgb)] // CMYK -> XYZ -> LAB -> RGB (different LUT tags, A2B vs TRC)
// [InlineData(TestIccProfiles.StandardRgbV2, TestIccProfiles.Fogra39, IccConversion.RgbToCmyk)] // RGB -> XYZ -> LAB -> CMYK (different LUT tags, TRC vs A2B)
public void CanConvertCmykIccProfiles(string sourceProfile, string targetProfile, IccConversion iccConversion)
{
Cmyk input = new(GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue());
// TODO: delete after testing
float[] input = [0.734798908f, 0.887050927f, 0.476583719f, 0.547810674f];
// float[] input = [GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue(), GetNormalizedRandomValue()];
double[] expectedTargetValues = GetExpectedTargetValues(sourceProfile, targetProfile, input);
ColorProfileConverter converter = new(new ColorConversionOptions
{
SourceIccProfile = TestIccProfiles.GetProfile(sourceProfileName),
TargetIccProfile = TestIccProfiles.GetProfile(targetProfileName),
SourceIccProfile = TestIccProfiles.GetProfile(sourceProfile),
TargetIccProfile = TestIccProfiles.GetProfile(targetProfile)
});
Cmyk expectedTargetValues = GetExpectedTargetCmyk(sourceProfileName, targetProfileName, input);
Cmyk actualTargetValues = converter.Convert<Cmyk, Cmyk>(input);
Vector4 actualTargetValues = iccConversion switch
{
IccConversion.CmykToCmyk => converter.Convert<Cmyk, Cmyk>(new Cmyk(new Vector4(input))).ToScaledVector4(),
IccConversion.CmykToRgb => converter.Convert<Cmyk, Rgb>(new Cmyk(new Vector4(input))).ToScaledVector4(),
IccConversion.RgbToCmyk => converter.Convert<Rgb, Cmyk>(new Rgb(new Vector3(input))).ToScaledVector4(),
IccConversion.RgbToRgb => converter.Convert<Rgb, Rgb>(new Rgb(new Vector3(input))).ToScaledVector4(),
_ => throw new ArgumentOutOfRangeException(nameof(iccConversion), iccConversion, null)
};
const double tolerance = 0.0000005;
Assert.Equal(expectedTargetValues.C, actualTargetValues.C, tolerance);
Assert.Equal(expectedTargetValues.M, actualTargetValues.M, tolerance);
Assert.Equal(expectedTargetValues.Y, actualTargetValues.Y, tolerance);
Assert.Equal(expectedTargetValues.K, actualTargetValues.K, tolerance);
const double tolerance = 0.000005;
for (int i = 0; i < expectedTargetValues.Length; i++)
{
Assert.Equal(expectedTargetValues[i], actualTargetValues[i], tolerance);
}
}
private static Cmyk GetExpectedTargetCmyk(string sourceProfileName, string targetProfileName, Cmyk sourceCmyk)
private static double[] GetExpectedTargetValues(string sourceProfile, string targetProfile, float[] input)
{
Wacton.Unicolour.Configuration sourceConfig = TestIccProfiles.GetUnicolourConfiguration(sourceProfileName);
Wacton.Unicolour.Configuration targetConfig = TestIccProfiles.GetUnicolourConfiguration(targetProfileName);
Wacton.Unicolour.Configuration sourceConfig = TestIccProfiles.GetUnicolourConfiguration(sourceProfile);
Wacton.Unicolour.Configuration targetConfig = TestIccProfiles.GetUnicolourConfiguration(targetProfile);
if (sourceConfig.Icc.Error != null || targetConfig.Icc.Error != null)
{
Assert.Fail("Unicolour does not support the ICC profile - test values manually in the meantime");
}
Channels channels = new(sourceCmyk.C, sourceCmyk.M, sourceCmyk.Y, sourceCmyk.K);
Channels channels = new(input.Select(value => (double)value).ToArray());
Unicolour source = new(sourceConfig, channels);
ColourSpace pcs = sourceConfig.Icc.Profile!.Header.Pcs == "Lab " ? ColourSpace.Lab : ColourSpace.Xyz;
ColourTriplet pcsTriplet = pcs == ColourSpace.Lab ? source.Lab.Triplet : source.Xyz.Triplet;
Unicolour target = new(targetConfig, pcs, pcsTriplet.Tuple);
double[] targetCmyk = target.Icc.Values;
return new Cmyk((float)targetCmyk[0], (float)targetCmyk[1], (float)targetCmyk[2], (float)targetCmyk[3]);
Unicolour target = source.ConvertToConfiguration(targetConfig);
return target.Icc.Values;
}
private static float GetNormalizedRandomValue()
@ -60,4 +82,12 @@ public class ColorProfileConverterTests
// 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);
}
public enum IccConversion
{
CmykToCmyk,
CmykToRgb,
RgbToCmyk,
RgbToRgb
}
}

12
tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs

@ -12,9 +12,13 @@ internal static class TestIccProfiles
private static readonly ConcurrentDictionary<string, IccProfile> ProfileCache = new();
private static readonly ConcurrentDictionary<string, Wacton.Unicolour.Configuration> UnicolourConfigurationCache = new();
public const string Fogra39 = "Coated_Fogra39L_VIGC_300.icc";
public const string Swop2006 = "SWOP2006_Coated5v2.icc";
public const string Fogra39 = "Coated_Fogra39L_VIGC_300.icc"; // v2 CMYK -> LAB, output, lut16
public const string Swop2006 = "SWOP2006_Coated5v2.icc"; // v2 CMYK -> LAB, output, lut16
public const string JapanColor2011 = "JapanColor2011Coated.icc"; // v2 CMYK -> LAB, output, lut8
public const string Cgats21 = "CGATS21_CRPC7.icc"; // v4 CMYK -> LAB, output, lutAToB: B-CLUT-A
public const string RommRgb = "ISO22028-2_ROMM-RGB.icc"; // v4 RGB -> XYZ, colorspace, lutAToB: B-Matrix-M [only intent 0]
public const string StandardRgbV4 = "sRGB_v4_ICC_preference.icc"; // v4 RGB -> LAB, colorspace, lutAToB: B-Matrix-M-CLUT-A [only intent 0 & 1]
public const string StandardRgbV2 = "sRGB2014.icc"; // v2 RGB -> XYZ, display, TRCs
public static IccProfile GetProfile(string file)
=> ProfileCache.GetOrAdd(file, f => new IccProfile(File.ReadAllBytes(GetFullPath(f))));
@ -22,7 +26,7 @@ internal static class TestIccProfiles
public static Wacton.Unicolour.Configuration GetUnicolourConfiguration(string file)
=> UnicolourConfigurationCache.GetOrAdd(
file,
f => new Wacton.Unicolour.Configuration(iccConfiguration: new(GetFullPath(f), Intent.Unspecified)));
f => new Wacton.Unicolour.Configuration(iccConfiguration: new(GetFullPath(f), Intent.Unspecified, file)));
private static string GetFullPath(string file)
=> Path.GetFullPath(Path.Combine(".", "TestDataIcc", "Profiles", file));

3
tests/ImageSharp.Tests/TestDataIcc/Profiles/CGATS21_CRPC7.icc

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2b186e6dadc38883138ccd30203b2870fb71db6451d3a5d24f9590cfabd26689
size 3462496

3
tests/ImageSharp.Tests/TestDataIcc/Profiles/ISO22028-2_ROMM-RGB.icc

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:96b2f2987f83e2a545e607799fbfdff43ef8158fb9b215b187c574db8f145aaf
size 864

3
tests/ImageSharp.Tests/TestDataIcc/Profiles/JapanColor2011Coated.icc

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:73202ea802e57a63a0be05ed993fde8e4d51cc436120e6c47815ce785ff42916
size 1979304

3
tests/ImageSharp.Tests/TestDataIcc/Profiles/sRGB2014.icc

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:384b832de3412066743b52a75ee906b6fb9fb8d9e09e936fc2c43223815c6e0a
size 3024

3
tests/ImageSharp.Tests/TestDataIcc/Profiles/sRGB_v4_ICC_preference.icc

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:83174717332326ddc198d9df188a4daec27b8979ba152cebbfc470c793d0bb11
size 60960
Loading…
Cancel
Save