Browse Source

Extract conversion for v2 perceptual intent

pull/1567/head
Wacton 1 year ago
parent
commit
df3d230bfd
  1. 163
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
  2. 2
      tests/ImageSharp.Tests/ColorProfiles/Icc/ColorProfileConverterTests.Icc.cs
  3. 7
      tests/ImageSharp.Tests/ColorProfiles/Icc/TestIccProfiles.cs
  4. BIN
      tests/ImageSharp.Tests/TestDataIcc/Profiles/JapanColor2003WebCoated.icc

163
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

@ -28,7 +28,7 @@ internal static class ColorProfileConverterExtensionsIcc
throw new InvalidOperationException("Target ICC profile is missing.");
}
ColorProfileConverter pcsConverter = new(new ColorConversionOptions()
ColorProfileConverter pcsConverter = new(new ColorConversionOptions
{
MemoryAllocator = converter.Options.MemoryAllocator,
SourceWhitePoint = new CieXyz(converter.Options.SourceIccProfile.Header.PcsIlluminant),
@ -39,90 +39,28 @@ internal static class ColorProfileConverterExtensionsIcc
IccPcsToDataConverter targetConverter = new(converter.Options.TargetIccProfile);
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 funneled through XYZ in case PCS adjustments need to be made
CieXyz xyz;
Vector4 sourcePcs = sourceConverter.Calculate(source.ToScaledVector4());
switch (sourcePcsType)
{
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 = CieXyz.FromScaledVector4(sourcePcs);
break;
default:
throw new ArgumentOutOfRangeException($"Source PCS {sourcePcsType} not supported");
}
// 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;
Vector4 targetPcs;
// 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)
// TODO: handle PCS adjustment for absolute intent? would make this a lot more complicated
// TODO: alternatively throw unsupported error, since most profiles headers contain perceptual (i've encountered a couple of relative intent, but so far no saturation or absolute)
bool adjustSourcePcsForPerceptual = sourceIntent == IccRenderingIntent.Perceptual && sourceVersion.Major == 2;
bool adjustTargetPcsForPerceptual = targetIntent == IccRenderingIntent.Perceptual && targetVersion.Major == 2;
if (adjustSourcePcsForPerceptual ^ adjustTargetPcsForPerceptual)
{
// 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)
{
Vector3 iccXyz = xyz.ToScaledVector4().AsVector3();
Vector3 scale = Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3());
Vector3 offset = refBlack.ToScaledVector4().AsVector3();
Vector3 adjustedXyz = (iccXyz * scale) + offset;
xyz = CieXyz.FromScaledVector4(new Vector4(adjustedXyz, 1F));
}
if (adjustTargetPcsForPerceptual)
{
Vector3 iccXyz = xyz.ToScaledVector4().AsVector3();
Vector3 scale = Vector3.Divide(Vector3.One, Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3()));
Vector3 offset = -refBlack.ToScaledVector4().AsVector3() * scale;
Vector3 adjustedXyz = (iccXyz * scale) + offset;
xyz = CieXyz.FromScaledVector4(new Vector4(adjustedXyz, 1F));
}
targetPcs = GetTargetPcsWithPerceptualV2Adjustment(converter, sourcePcs, adjustSourcePcsForPerceptual, adjustTargetPcsForPerceptual, pcsConverter);
}
Vector4 targetPcs;
switch (targetPcsType)
else
{
case IccColorSpaceType.CieLab:
CieLab lab = pcsConverter.Convert<CieXyz, CieLab>(in xyz);
targetPcs = lab.ToScaledVector4();
if (adjustTargetPcsForPerceptual)
{
targetPcs = LabToLabV2(targetPcs);
}
else if (targetConverter.Is16BitLutEntry)
{
// TODO: find a way to test? needs a non-perceptual-intent v2 profile with 8-bit LUT...
targetPcs = LabToLabV2(targetPcs);
}
break;
case IccColorSpaceType.CieXyz:
targetPcs = xyz.ToScaledVector4();
break;
default:
throw new ArgumentOutOfRangeException($"Target PCS {targetPcsType} not supported");
// TODO: replace with function that bypasses PCS adjustment
targetPcs = GetTargetPcsWithPerceptualV2Adjustment(converter, sourcePcs, adjustSourcePcsForPerceptual, adjustTargetPcsForPerceptual, pcsConverter);
}
// Convert to the target space.
@ -243,6 +181,85 @@ internal static class ColorProfileConverterExtensionsIcc
TTo.FromScaledVector4(pcsNormalized, destination);
}
private static Vector4 GetTargetPcsWithPerceptualV2Adjustment(
ColorProfileConverter converter,
Vector4 sourcePcs,
bool adjustSource,
bool adjustTarget,
ColorProfileConverter pcsConverter)
{
IccDataToPcsConverter sourceConverter = new(converter.Options.SourceIccProfile!);
IccPcsToDataConverter targetConverter = new(converter.Options.TargetIccProfile!);
IccProfileHeader sourceHeader = converter.Options.SourceIccProfile!.Header;
IccProfileHeader targetHeader = converter.Options.TargetIccProfile!.Header;
IccColorSpaceType sourcePcsType = sourceHeader.ProfileConnectionSpace;
IccColorSpaceType targetPcsType = targetHeader.ProfileConnectionSpace;
// all conversions are funneled through XYZ in case PCS adjustments need to be made
CieXyz xyz;
switch (sourcePcsType)
{
// 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version
// so convert Lab to modern v4 encoding when returned from a 16-bit LUT
case IccColorSpaceType.CieLab:
sourcePcs = sourceConverter.Is16BitLutEntry ? LabV2ToLab(sourcePcs) : sourcePcs;
CieLab lab = CieLab.FromScaledVector4(sourcePcs);
xyz = pcsConverter.Convert<CieLab, CieXyz>(in lab);
break;
case IccColorSpaceType.CieXyz:
xyz = CieXyz.FromScaledVector4(sourcePcs);
break;
default:
throw new ArgumentOutOfRangeException($"Source PCS {sourcePcsType} not supported");
}
// 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);
// when converting from device to PCS with v2 perceptual intent
// the black point needs to be adjusted to v4 after converting the PCS values
if (adjustSource)
{
Vector3 iccXyz = xyz.ToScaledVector4().AsVector3();
Vector3 scale = Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3());
Vector3 offset = refBlack.ToScaledVector4().AsVector3();
Vector3 adjustedXyz = (iccXyz * scale) + offset;
xyz = CieXyz.FromScaledVector4(new Vector4(adjustedXyz, 1F));
}
// when converting from PCS to device with v2 perceptual intent
// the black point needs to be adjusted to v2 before converting the PCS values
if (adjustTarget)
{
Vector3 iccXyz = xyz.ToScaledVector4().AsVector3();
Vector3 scale = Vector3.Divide(Vector3.One, Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3()));
Vector3 offset = -refBlack.ToScaledVector4().AsVector3() * scale;
Vector3 adjustedXyz = (iccXyz * scale) + offset;
xyz = CieXyz.FromScaledVector4(new Vector4(adjustedXyz, 1F));
}
Vector4 targetPcs;
switch (targetPcsType)
{
// 16-bit Lab encodings changed from v2 to v4, but 16-bit LUTs always use the legacy encoding regardless of version
// so convert Lab back to legacy encoding before using in a 16-bit LUT
case IccColorSpaceType.CieLab:
CieLab lab = pcsConverter.Convert<CieXyz, CieLab>(in xyz);
targetPcs = lab.ToScaledVector4();
targetPcs = targetConverter.Is16BitLutEntry ? LabToLabV2(targetPcs) : targetPcs;
break;
case IccColorSpaceType.CieXyz:
targetPcs = xyz.ToScaledVector4();
break;
default:
throw new ArgumentOutOfRangeException($"Target PCS {targetPcsType} not supported");
}
return targetPcs;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector4 LabToLabV2(Vector4 input)
=> input * 65280F / 65535F;

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

@ -17,7 +17,6 @@ public class ColorProfileConverterTests
[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.JapanColor2011)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 16-bit vs 8-bit)
[InlineData(TestIccProfiles.JapanColor2011, TestIccProfiles.Fogra39)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 8-bit vs 16-bit)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.Cgats21)] // CMYK -> LAB -> CMYK (different LUT versions, v2 vs v4)
[InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV4)] // CMYK -> LAB -> RGB (different LUT versions, v2 vs v4)
@ -25,6 +24,7 @@ public class ColorProfileConverterTests
[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
// [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.JapanColor2003)] // CMYK -> LAB -> CMYK (different bit depth v2 LUTs, 16-bit vs 8-bit)
// [InlineData(TestIccProfiles.Fogra39, TestIccProfiles.StandardRgbV2)] // CMYK -> XYZ -> LAB -> RGB (different LUT tags, A2B vs TRC)
// [InlineData(TestIccProfiles.StandardRgbV2, TestIccProfiles.Fogra39)] // RGB -> XYZ -> LAB -> CMYK (different LUT tags, TRC vs A2B)
public void CanConvertCmykIccProfiles(string sourceProfile, string targetProfile)

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

@ -22,10 +22,15 @@ internal static class TestIccProfiles
public const string Swop2006 = "SWOP2006_Coated5v2.icc";
/// <summary>
/// v2 CMYK -> LAB, output, lut8
/// v2 CMYK -> LAB, output, lut8 (A2B tags)
/// </summary>
public const string JapanColor2011 = "JapanColor2011Coated.icc";
/// <summary>
/// v2 CMYK -> LAB, output, lut8 (B2A tags)
/// </summary>
public const string JapanColor2003 = "JapanColor2003WebCoated.icc";
/// <summary>
/// v4 CMYK -> LAB, output, lutAToB: B-CLUT-A
/// </summary>

BIN
tests/ImageSharp.Tests/TestDataIcc/Profiles/JapanColor2003WebCoated.icc

Binary file not shown.
Loading…
Cancel
Save