Closed sommerluk closed 1 year ago
Here is the definition of Oklab: https://bottosson.github.io/posts/oklab/
That seems a nice addition, and would be as easy as adding a color space profile bult-in. What I need is the set equations to convert CIE Lab* to this space and vice-versa. Also it would need some sort of chromatic adaptation to convert from D65 to D50, which is where ICC colorimetry is based.
That’s great to hear!
Currently, I’m doing this conversion in my own code. Probably this is not directly useful for you, because it’s hard-code and depends on C++ and Qt. But those are the steps:
This is done with LittleCMS as usual. Let’s call the result V₁.
Let’s call the result V₂. The chromatic adaption is done with the Bradford transformation, as proposed here. Formula: V₂ = M⁻¹ × V₁.
The transformation is done as described here in the Oklab definition.
[Note that l
, m
and s
might be negative. Therefore, in my own code, I do not use std::pow(num, 1.0 / 3)
which would not work for a negative num
. Instead, I use std::cbrt(num)
which is defined also for a negative num
.]
This can be done via void cmsLab2LCh(cmsCIELCh*LCh, const cmsCIELab* Lab)
This can be done via void cmsLCh2Lab(cmsCIELab* Lab, const cmsCIELCh* LCh)
The transformation is done as described here in the Oklab definition. Let’s call the result V₂.
Let’s call the result V₁. The chromatic adaption is done with the Bradford transformation, as proposed here. Formula: V₁ = M × V₂.
This is done with LittleCMS as usual.
@sommerluk In f4e9f9122a001b71219d9b2d2a6ec5b8c784d785 you can see a preliminary implementation of OkLab space. This built-in profile cannot be saved as an ICC file, so its use is programmatically only.
It is a colorspace profile, so It can deal with OkLab-> whatever and whatever->OkLab. Note that it uses D50 as PCS white point.
The stages are:
[Conversion D60 to D65] ->[Conversion to LMS cone space] -> [Non-linearity] -> [Final matrix to OkLab]
The input direction pipeline is like this one but reversed.
I still have to figure out how to deal with white point in unadapted absolute colorimetric, probably a D65 should be used as media white. It should do its way to 2.16, which is still months ahead.
Thanks again for the idea.
That's great! I've downloaded it and played around, and it works fine.
Maybe it makes sense to add
#define TYPE_OKLAB_DBL (FLOAT_SH(1)|COLORSPACE_SH(PT_MCH3)|CHANNELS_SH(3)|BYTES_SH(0))
to the public header?
Thanks for implementing this as fast! That's a super-conveniant feature!
Full support is now added by 6cabbcec8c229bf09dfa13b2b68eb0ab9f1030c8
That's great. Thanks a lot!
int main()
{
cmsSetLogErrorHandlerTHR(nullptr, log);
cmsContext ctx = cmsCreateContext(nullptr, nullptr);
cmsCIExyY D65xyY;
cmsWhitePointFromTemp( &D65xyY, 6504);
cmsHPROFILE oklabProfile = cmsCreateLab4Profile(&D65xyY);
setupMetadata(ctx, oklabProfile);
// Strict transformation between LAB and XYZ
cmsSetDeviceClass(oklabProfile, cmsSigColorSpaceClass);
cmsSetColorSpace(oklabProfile, cmsSigMCH3Data);
cmsSetPCS(oklabProfile, cmsSigXYZData);
cmsSetHeaderRenderingIntent(oklabProfile, INTENT_RELATIVE_COLORIMETRIC);
const double M_D65_D50[] =
{
1.047886, 0.022919, -0.050216,
0.029582, 0.990484, -0.017079,
-0.009252, 0.015073, 0.751678
};
const double M_D50_D65[] =
{
0.955512609517083, -0.023073214184645, 0.063308961782107,
-0.028324949364887, 1.009942432477107, 0.021054814890112,
0.012328875695483, -0.020535835374141, 1.330713916450354
};
cmsStage* D65toD50 = cmsStageAllocMatrix(ctx, 3, 3, M_D65_D50, NULL);
cmsStage* D50toD65 = cmsStageAllocMatrix(ctx, 3, 3, M_D50_D65, NULL);
const double M_D65_LMS[] =
{
0.819022437996703, 0.3619062600528904, -0.1288737815209879,
0.03298365393238847, 0.9292868615863434, 0.03614466635064236,
0.04817718935962421, 0.2642395317527308, 0.6335478284694309
};
const double M_LMS_D65[] =
{
1.226879875845924, -0.5578149944602171, 0.2813910456659647,
-0.04057574521480083, 1.112286803280317, -0.07171105806551635,
-0.07637293667466008, -0.42149333240224324, 1.5869240198367818
};
cmsStage* D65toLMS = cmsStageAllocMatrix(ctx, 3, 3, M_D65_LMS, NULL);
cmsStage* LMStoD65 = cmsStageAllocMatrix(ctx, 3, 3, M_LMS_D65, NULL);
const double RootParameters[] = {1.0 / 3.0, 1, 0, 0};
const double CubeParameters[] = {3.0, 1, 0, 0};
cmsToneCurve* Root = cmsBuildParametricToneCurve(ctx, 6, RootParameters);
cmsToneCurve* Cube = cmsBuildParametricToneCurve(ctx, 6, CubeParameters);
cmsToneCurve* Roots[3] = { Root, Root, Root };
cmsToneCurve* Cubes[3] = { Cube, Cube, Cube };
cmsStage* NonLinearityFw = cmsStageAllocToneCurves(ctx, 3, Roots);
cmsStage* NonLinearityRv = cmsStageAllocToneCurves(ctx, 3, Cubes);
const double M_LMSprime_OkLab[] =
{
0.21045426830931396, 0.7936177747023053, -0.0040720430116192585,
1.9779985324311686, -2.42859224204858, 0.450593709617411,
0.025904042465547734, 0.7827717124575297, -0.8086757549230774
};
const double M_OkLab_LMSprime[] =
{
1.0, 0.3963377773761749, 0.21580375730991364,
1.0, -0.10556134581565857, -0.0638541728258133,
1.0, -0.08948417752981186, -1.2914855480194092
};
cmsStage* LMSprime_OkLab = cmsStageAllocMatrix(ctx, 3, 3, M_LMSprime_OkLab, NULL);
cmsStage* OkLab_LMSprime = cmsStageAllocMatrix(ctx, 3, 3, M_OkLab_LMSprime, NULL);
//LAB -> XYZD50
cmsPipeline* AToB = cmsPipelineAlloc(ctx, 3, 3);
cmsPipelineInsertStage(AToB, cmsAT_END, cmsStageDup(OkLab_LMSprime)); // Matrix = LAB -> LMS
cmsPipelineInsertStage(AToB, cmsAT_END, cmsStageDup(NonLinearityRv));
cmsPipelineInsertStage(AToB, cmsAT_END, cmsStageDup(LMStoD65)); // Matrix = LMS -> XYZ
cmsPipelineInsertStage(AToB, cmsAT_END, cmsStageDup(D65toD50)); // Matrix = D65 -> D50
cmsWriteTag(oklabProfile, cmsSigDToB0Tag, AToB);
//XYZD50 -> LAB
cmsPipeline* BToA = cmsPipelineAlloc(ctx, 3, 3);
cmsPipelineInsertStage(BToA, cmsAT_END, cmsStageDup(D50toD65)); // Matrix = D50 -> D65
cmsPipelineInsertStage(BToA, cmsAT_END, cmsStageDup(D65toLMS)); // Matrix = XYZ -> LMS
cmsPipelineInsertStage(BToA, cmsAT_END, cmsStageDup(NonLinearityFw));
cmsPipelineInsertStage(BToA, cmsAT_END, cmsStageDup(LMSprime_OkLab)); // Matrix = LMS -> LAB
cmsWriteTag(oklabProfile, cmsSigBToD0Tag, BToA);
cmsPipelineFree(AToB);
cmsPipelineFree(BToA);
cmsFreeToneCurve(Root);
cmsFreeToneCurve(Cube);
if (!cmsMD5computeID(oklabProfile)) {
std::cerr << "Failed MD5 computation" << std::endl;
return -1;
}
const std::string profileName{"oklab.icc"};
if (!cmsSaveProfileToFile(oklabProfile, profileName.c_str())) {
std::cerr << "CANNOT WRITE PROFILE" << std::endl;
return -2;
}
}
Hi, @mm2, I have created an ICC profile for oklab in this situation, but I found that when a
or b
is negative, when using cmsCreateTransform
to convert oklab
to an integer colour space, such as rgb8
or rgb16
, the result of the conversion seems to be incorrect (a
and b
seem to be clipped to 0
before the conversion) . But when converting to floating point such as rgbF16
or rgbF32
the result is correct, could the error be caused by Optimisation
? Thanks a lot!
You are including only BtoD /DToB tags which according the spec are to be used as a complement specialized on floats. You should use AtoB/BToA tags and if you want more precision on floats then also add DToB/BToD . If you could use unstable, 2.16 has an OkLab implementation on cmsCreate_OkLabProfile(). You can look at the code too.
Okay, thank you so much!
You are including only BtoD /DToB tags which according the spec are to be used as a complement specialized on floats. You should use AtoB/BToA tags and if you want more precision on floats then also add DToB/BToD . If you could use unstable, 2.16 has an OkLab implementation on cmsCreate_OkLabProfile(). You can look at the code too.
You are including only BtoD /DToB tags which according the spec are to be used as a complement specialized on floats. You should use AtoB/BToA tags and if you want more precision on floats then also add DToB/BToD . If you could use unstable, 2.16 has an OkLab implementation on cmsCreate_OkLabProfile(). You can look at the code too.
Well, I tried to create the oklab icc profile in krita using cmsCreate_OkLabProfile()
, but since krita uses cmsSaveProfileToMem
, it doesn't seem to be able to do it successfully, which is really sad.
This particular profile cannot be saved. The ICC file format does not support the combination of stages in the pipeline. It only works as a built-in.
This particular profile cannot be saved. The ICC file format does not support the combination of stages in the pipeline. It only works as a built-in.
Okay, I got it. Thank you.
Hi, @mm2, I created the oklab profile using cmsCreate_OkLabProfile()
but it doesn't seem to convert to rgb16 correctly, here's the code I'm testing with:
#define TYPE_LABA_F32 (FLOAT_SH(1)|COLORSPACE_SH(PT_MCH3)|EXTRA_SH(1)|CHANNELS_SH(3)|BYTES_SH(4))
cmsUInt16Number rgb[3];
cmsFloat32Number lab[3];
cmsHPROFILE labProfile = cmsCreate_OkLabProfile(NULL);
cmsHPROFILE rgbProfile = cmsCreate_sRGBProfile();
cmsHTRANSFORM hBack = cmsCreateTransform(labProfile, TYPE_LABA_F32, rgbProfile, TYPE_RGB_16, INTENT_RELATIVE_COLORIMETRIC, 0);
cmsHTRANSFORM hForth = cmsCreateTransform(rgbProfile, TYPE_RGB_16, labProfile, TYPE_LABA_F32, INTENT_RELATIVE_COLORIMETRIC, 0);
cmsCloseProfile(labProfile);
cmsCloseProfile(rgbProfile);
rgb[0] = 0;
rgb[1] = 0;
rgb[2] = 65535;
cmsDoTransform(hForth, rgb, &lab, 1);
cmsDoTransform(hBack, lab, &rgb, 1);
cmsDeleteTransform(hBack);
cmsDeleteTransform(hForth);
std::cout<<rgb[0]<<' '<<rgb[1]<<' '<<rgb[2]<< std::endl;
//Target results: 0 0 65535
//Actual results: 22025 22020 22012
Thanks for reporting. This seems not related to OkLab but on transforms going float->integer. If you use floats on rgb the roundtrip works well, so it is not the profile. I will file a bug regarding this issue, not limited to OkLab.
See below the code that works fine
`
cmsFloat32Number rgb[3];
cmsFloat32Number lab[4];
cmsHPROFILE labProfile = cmsCreate_OkLabProfile(NULL);
cmsHPROFILE rgbProfile = cmsCreate_sRGBProfile();
cmsHTRANSFORM hBack = cmsCreateTransform(labProfile, TYPE_LABA_F32, rgbProfile, TYPE_RGB_FLT, INTENT_RELATIVE_COLORIMETRIC, 0);
cmsHTRANSFORM hForth = cmsCreateTransform(rgbProfile, TYPE_RGB_FLT, labProfile, TYPE_LABA_F32, INTENT_RELATIVE_COLORIMETRIC, 0);
cmsCloseProfile(labProfile);
cmsCloseProfile(rgbProfile);
rgb[0] = 0;
rgb[1] = 0;
rgb[2] = 1.0f;
cmsDoTransform(hForth, rgb, &lab, 1);
cmsDoTransform(hBack, lab, &rgb, 1);
cmsDeleteTransform(hBack);
cmsDeleteTransform(hForth);
`
Okay, that's great. Thanks!
It is now fixed by 4c0c66e7109a903c470159ecd3d753e8a4d56c79, but I have to check it more carefully.
Oklab is an alternative to Cielab. It works with the same logic, but claims to be more perceptually uniform than Cielab. It uses a D65 whitepoint. There are conversions defined from and to XYZ-D65 (a matrix + a cubic root respective power of three + another matrix).
Since its inclusion in the CSS Color Module Level 4 [W3C Candidate Recommendation Draft] it’s gaining wider support.
Would it be possible to support Oklab (and its derived cylindrical variant, Oklch) in LittleCMS out-of-the-box? So that pipelines can directly convert between Oklab/Oklch and an arbitrary ICC profile?