Skip to content
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

Support for Oklab/Oklch? #380

Closed
sommerluk opened this issue May 8, 2023 · 18 comments
Closed

Support for Oklab/Oklch? #380

sommerluk opened this issue May 8, 2023 · 18 comments

Comments

@sommerluk
Copy link
Contributor

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?

@sommerluk
Copy link
Contributor Author

Here is the definition of Oklab: https://bottosson.github.io/posts/oklab/

@mm2
Copy link
Owner

mm2 commented May 10, 2023

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.

@sommerluk
Copy link
Contributor Author

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:

Conversion from an arbitrary ICC profile to Oklch

Arbitrary ICC profile → XYZ-D50

This is done with LittleCMS as usual. Let’s call the result V₁.

XYZ-D50 → XYZ-D65

Let’s call the result V₂. The chromatic adaption is done with the Bradford transformation, as proposed here. Formula: V₂ = M⁻¹ × V₁.
grafik

XYZ-D65 → Oklab

The transformation is done as described here in the Oklab definition.
grafik
grafik
grafik
grafik
grafik
[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.]
grafik
grafik
grafik
grafik
grafik

Oklab → Oklch

This can be done via void cmsLab2LCh(cmsCIELCh*LCh, const cmsCIELab* Lab)

Conversion from Oklch to an arbitrary ICC profile

Oklch → Oklab

This can be done via void cmsLCh2Lab(cmsCIELab* Lab, const cmsCIELCh* LCh)

Oklab → XYZ-D65

The transformation is done as described here in the Oklab definition. Let’s call the result V₂.
grafik

XYZ-D65 → XYZ-D50

Let’s call the result V₁. The chromatic adaption is done with the Bradford transformation, as proposed here. Formula: V₁ = M × V₂.

XYZ-D50 → Arbitrary ICC profile

This is done with LittleCMS as usual.

@mm2
Copy link
Owner

mm2 commented May 15, 2023

@sommerluk
In f4e9f91 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.

@sommerluk
Copy link
Contributor Author

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!

@mm2
Copy link
Owner

mm2 commented Jul 31, 2023

Full support is now added by 6cabbce

@mm2 mm2 closed this as completed Jul 31, 2023
@sommerluk
Copy link
Contributor Author

That's great. Thanks a lot!

@killyorz
Copy link

killyorz commented Nov 13, 2023

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!

@mm2
Copy link
Owner

mm2 commented Nov 13, 2023

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.

@killyorz
Copy link

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.

@killyorz
Copy link

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.

@mm2
Copy link
Owner

mm2 commented Nov 14, 2023

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.

@killyorz
Copy link

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.

@killyorz
Copy link

killyorz commented Nov 15, 2023

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

@mm2
Copy link
Owner

mm2 commented Nov 15, 2023

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

`
#define TYPE_LABA_F32 (FLOAT_SH(1)|COLORSPACE_SH(PT_MCH3)|EXTRA_SH(1)|CHANNELS_SH(3)|BYTES_SH(4))

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);

`

@killyorz
Copy link

Okay, that's great. Thanks!

@mm2
Copy link
Owner

mm2 commented Nov 15, 2023

It is now fixed by 4c0c66e, but I have to check it more carefully.

@killyorz
Copy link

killyorz commented Nov 15, 2023

It is now fixed by 4c0c66e, but I have to check it more carefully.

Snipaste_2023-11-15_23-00-23

Now it works fine in krita! Really thank you for your help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants