-
-
Notifications
You must be signed in to change notification settings - Fork 851
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Color conversion with ICC profiles #1567
base: main
Are you sure you want to change the base?
Changes from 1 commit
16179b3
eeb40f5
8b96d37
aa2f1e9
3debb26
0812085
1094dc6
759f053
4cde4ef
a973713
33339d0
cdb495c
5f7acc1
7e2bc20
dc166a7
dcc8147
73c8d8f
d229fed
0a08da0
daf366b
54856ff
eee14c6
f60d4b8
8de137e
fb8003c
9f0f9cb
ece11eb
bd7257b
9a21485
ba76964
98d1758
66554cb
e90f165
8c580a7
a81dac9
902ed99
e3aa452
0dd68fe
6e3dc81
b7833a4
c3984aa
0fff06d
f1c05ee
3be31c3
6c2ee90
20e9b7f
d6fbc01
67ed4ce
b036cc3
52f88c8
ed47678
10bea86
5b131ad
ed8091b
6225db3
a65c599
60f3d9d
c940b86
a567613
3e06687
c1ebbfe
d89d8c5
63c89ca
3389d7a
7b0ff3b
29ed2b4
5f975e5
79f5dfa
b88b2a9
bca4cad
6e2c29c
5b374da
441f07e
6654218
21fec4e
fd2e8a9
19ff69d
3cd7d67
0327dca
9de3935
8e92f20
312b55e
44aae40
df3d230
d20fddb
d60ac76
cfa2760
369bf5f
7d4a742
f4e9509
9ceed23
f21c0c2
f147aad
7492109
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
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 @@ | |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reading below we only need this if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll take a shot at avoiding the overhead when unnecessary. I expect it will result in functions that look very similar like |
||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is a 16bit LUT calculator treated differently and why is that not version specific? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LAB encodings only changed for 16-bit representations:
But for the LUTs, the 16-bit type continues to use the legacy encoding:
|
||
{ | ||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like we're mixing up normalized and standard values here and it's a little confusing. // We use the original ref values here...
Vector3 scale = Vector3.One - Vector3.Divide(refBlack.ToVector3(), refWhite.ToVector3());
// But scale them here?
Vector3 offset = refBlack.ToScaledVector4().AsVector128().AsVector3(); I would extract the methods out with an explanation of the theory behind them also. For example, I don't understand why the math for source and targeted PCS adjustments is different. We're going to need to vectorize these also. (Which may mean providing your reference colors as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, happy to refactor to methods with explanations. I think I need to do some reading on best practices regarding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tried to make this clearer and have also precalculated the scale and offset vectors. Also realised converting XYZ values to XYZ-scaled values (the other type of scaling 😃) was unnecessary - a small saving I can take back over to Unicolour. |
||
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> | ||
|
@@ -202,7 +242,9 @@ | |
targetConverter.Calculate(pcsNormalized, pcsNormalized); | ||
TTo.FromScaledVector4(pcsNormalized, destination); | ||
} | ||
|
||
Check failure on line 245 in src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs GitHub Actions / Build (false, ubuntu-latest, net9.0, 9.0.x, true, -x64, false)
Check failure on line 245 in src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs GitHub Actions / Build (false, ubuntu-latest, net9.0, 9.0.x, true, -x64, false)
Check failure on line 245 in src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs GitHub Actions / Build (false, ubuntu-latest, net8.0, 8.0.x, -x64, false)
Check failure on line 245 in src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs GitHub Actions / Build (false, ubuntu-latest, net8.0, 8.0.x, -x64, false)
|
||
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
private static Vector4 LabToLabV2(Vector4 input) | ||
=> input * 65280F / 65535F; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are the illuminant values not given using the ICC scaling? I would have assumed they were given we need to pass them as such.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has sent me down a rabbit hole and I don't feel any closer to understanding why, but the illuminant values are stored in the header in the range [0, 1], no scaling needed.
I can't find solid information why the scaling is even needed for XYZ LUTs other than "that's what the DemoIccMAX code does". The closest thing I can find in the v4 spec itself is this footnote in Annex F.3 page 102:
(The spec is so cumbersome, the information I'm looking for could easily be buried elsewhere...)
At this point I'm assuming either
🤕
One other note, as far as I understand the PCS illuminant must be D50 (in case that enables any further optimisation)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about it a bit more, it's going to be related to LUTs storing
uInt16
[0, 65535] but XYZ values being encoded ass15Fixed16
[-32768, ~32768], and needing to account for that.