New feature: support for CTL scripts

Hi,
I’ve just added a new feature to ART, which I find pretty cool (but it might be just me). If someone is interested, it is described here:
https://bitbucket.org/agriggio/art/wiki/Luts#ctl-scripts

13 Likes

It’s not just you.

wow!

Hello Alberto, as an image tells mote than a thousands words, can you provide an example image conversion (before/after) that you consider cool? Curious!

Well, this is not about quality, it’s about flexibility and customization. So no pictures, sorry

Hey, that looks really great. I think this is a very useful feature for e.g. prototyping or bringing in one’s own processing.

Mind blowing! Thank you Alberto!

Both kinds of LUTs must be enabled at compile time, by setting respectively ENABLE_OCIO and ENABLE_CTL to True in CMake.

Hello Alberto, I don’t see a single file called CMake, so where do I set both values to True?

Hi Paul,

aren’t that CMake parameters? So when you configure the project, it’s like:

cmake .. -DENABLE_OCIO=True -DENABLE_CTL=True

HTH,
Flössie

Hello Flössie, yes of course it should be that, I wasn’t fully awake yet!

But when I run the cmake command, I see the errors below. I installed OpenColorIO this morning from Synaptic but that version is too old apparently. Am I supposed to find those missing things on Github?

Thanks for your help,
Paul.

CMAKE_BUILD_TYPE: Release
-- searching for library exiv2 in /usr/lib/x86_64-linux-gnu
--   result: /usr/lib/x86_64-linux-gnu/libexiv2.so
-- searching for library lensfun in /usr/lib/x86_64-linux-gnu
--   result: /usr/lib/x86_64-linux-gnu/liblensfun.so
-- using mimalloc library 2.0
-- using libraw library 0.21.1
-- Checking for module 'OpenColorIO>=2.0.0'
--   Requested 'OpenColorIO >= 2.0.0' but version of OpenColorIO is 1.1.1
-- OpenColorIO not found
-- Checking for module 'OpenEXR>=3'
--   No package 'OpenEXR' found
-- Checking for module 'IlmBase'
--   No package 'IlmBase' found
-- CTL interpreter not found

Sorry, the instructions were a bit terse indeed. To use the scripts you need to build and install the CTL interpreter from GitHub - ampas/CTL: The Color Transformation Language (CTL). I don’t think there are pre-built packages for this (but I might be wrong). Once installed, you need to tell ART where to find the CTL headers and libraries. Assuming you installed in /usr/local (the default), this should do:

cmake -DENABLE_CTL=1 -DCTL_INCLUDE_DIR=/usr/local/include/CTL /path/to/ART

HTH

Thanks, I’ll try that later today.

There’s an old package on Arch’s AUR repository, but it is too old and complains not finding ilmbase. In fact there was a change since version 3.0 of OpenEXR, and having just OpenEXR > 3.0 installed is enough for building the CTL interpreter from source, which is straightforward.
I confirm CTL scripts support works fine.

In the film color grading world, Davinci Resolve works now sith DCTL’s, I supposed it works on the same principle? Some cool DCTL’s have been made, some of them manipulating “color density” instead of “color saturation” allowing for a more “film saturation look”. I guess the same could be made in ART with appropriate CTL scripts?

Yes, that should be possible indeed. In fact, I’d expect that porting davinci scripts should be trivial, afaik dctl is effectively a variant of ctl with some davinci-specific functions for interfacing with the gui (I might be wrong though - if someone knows any better I’m all ears :slight_smile:
Do you have a link to the specific script you are talking about?

Indeed it doesn’t seem too complicated, even for the GUI interfacing. Many interesting dctl’s are actually encrypted to be sold as plugins, but I was able to find a free one here, and here’s the content of the color density script:

// Film Density OFX DCTL

DEFINE_UI_PARAMS(p_Den, Film Density, DCTLUI_SLIDER_FLOAT, 0, 0, 2, 0.001)
DEFINE_UI_PARAMS(p_WR, Red Weight, DCTLUI_SLIDER_FLOAT, 1, 0, 2, 0.001)
DEFINE_UI_PARAMS(p_WG, Green Weight, DCTLUI_SLIDER_FLOAT, 1, 0, 2, 0.001)
DEFINE_UI_PARAMS(p_WB, Blue Weight, DCTLUI_SLIDER_FLOAT, 1, 0, 2, 0.001)
DEFINE_UI_PARAMS(p_LimitS, Low Saturation Limiter, DCTLUI_SLIDER_FLOAT, 0, 0, 1, 0.001)
DEFINE_UI_PARAMS(p_LimitL, Low Luma Limiter, DCTLUI_SLIDER_FLOAT, 0, 0, 1, 0.001)
DEFINE_UI_PARAMS(p_Display, Display Alpha, DCTLUI_CHECK_BOX, 0)

#if (__RESOLVE_VER_MAJOR__ < 17)
__DEVICE__ float _floorf( float A) {
return (float)_floor(A);
}
#endif

__DEVICE__ float3 RGB_to_HSV( float3 RGB ) {
float3 HSV;
float min = _fminf(_fminf(RGB.x, RGB.y), RGB.z);
float max = _fmaxf(_fmaxf(RGB.x, RGB.y), RGB.z);
HSV.z = max;
float delta = max - min;
if (max != 0.0f) {
HSV.y = delta / max;
} else {
HSV.y = 0.0f;
HSV.x = 0.0f;
return HSV;
}
if (delta == 0.0f) {
HSV.x = 0.0f;
} else if (RGB.x == max) {
HSV.x = (RGB.y - RGB.z) / delta;
} else if (RGB.y == max) {
HSV.x = 2.0f + (RGB.z - RGB.x) / delta;
} else {
HSV.x = 4.0f + (RGB.x - RGB.y) / delta;
}
HSV.x *= 1.0f / 6.0f;
if (HSV.x < 0.0f)
HSV.x += 1.0f;
return HSV;
}

__DEVICE__ float3 HSV_to_RGB(float3 HSV) {
float3 RGB;
if (HSV.y == 0.0f) {
RGB.x = RGB.y = RGB.z = HSV.z;
} else {
HSV.x *= 6.0f;
float i = _floorf(HSV.x);
float f = HSV.x - i;
i = i >= 0.0f ? _fmod(i, 6.0f) : _fmod(i, 6.0f) + 6.0f;
float p = HSV.z * (1.0f - HSV.y);
float q = HSV.z * (1.0f - HSV.y * f);
float t = HSV.z * (1.0f - HSV.y * (1.0f - f));
RGB.x = i == 0.0f ? HSV.z : i == 1.0f ? q : i == 2.0f ? p : i == 3.0f ? p : i == 4.0f ? t : HSV.z;
RGB.y = i == 0.0f ? t : i == 1 ? HSV.z : i == 2.0f ? HSV.z : i == 3.0f ? q : i == 4.0f ? p : p;
RGB.z = i == 0.0f ? p : i == 1 ? p : i == 2.0f ? t : i == 3.0f ? HSV.z : i == 4.0f ? HSV.z : q;
}
return RGB;
}

__DEVICE__ float RGB_to_Sat( float3 RGB) {
float min = _fminf(_fminf(RGB.x, RGB.y), RGB.z);
float max = _fmaxf(_fmaxf(RGB.x, RGB.y), RGB.z);
float delta = max - min;
float Sat = max != 0.0f ? delta / max : 0.0f;
return Sat;
}

__DEVICE__ float3 Saturation(float3 RGB, float luma, float Sat) {
RGB.x = (1.0f - Sat) * luma + RGB.x * Sat;
RGB.y = (1.0f - Sat) * luma + RGB.y * Sat;
RGB.z = (1.0f - Sat) * luma + RGB.z * Sat;
return RGB;
}

__DEVICE__ float get_luma(float3 RGB, float Rw, float Gw, float Bw) {
float R, G, B;
R = Rw + 1.0f - (Gw / 2.0f) - (Bw / 2.0f);
G = Gw + 1.0f - (Rw / 2.0f) - (Bw / 2.0f);
B = Bw + 1.0f - (Rw / 2.0f) - (Gw / 2.0f);
float luma = (RGB.x * R + RGB.y * G + RGB.z * B) / 3.0f;
return luma;
}

__DEVICE__ float Limiter(float val, float limiter) {
float alpha = limiter > 1.0f ? val + (1.0f - limiter) * (1.0f - val) : limiter >= 0.0f ? (val >= limiter ? 1.0f : 
val / limiter) : limiter < -1.0f ? (1.0f - val) + (limiter + 1.0f) * val : val <= (1.0f + limiter) ? 1.0f : 
(1.0 - val) / (1.0f - (limiter + 1.0f));
alpha = _saturatef(alpha);
return alpha;
}

__DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p_R, float p_G, float p_B)
{
float3 rgbIn = make_float3(p_R, p_G, p_B);
if (p_Den == 0.0f && p_Display == 0)
return rgbIn;

float WR = 2.0f - p_WR;
float WG = 2.0f - p_WG;
float WB = 2.0f - p_WB;
float luma = get_luma(rgbIn, WR, WG, WB);
float SatA = 1.0f / (p_Den + 1.0f);
float3 rgbOut = Saturation(rgbIn, luma, SatA);

float alphaS, alphaL, alpha;
alphaS = alphaL = alpha = 1.0f;

if (p_LimitS > 0.0f) {
float sat = RGB_to_Sat(rgbIn);
alphaS = Limiter(sat, p_LimitS);
alpha = alphaS;
}
if (p_LimitL > 0.0f) {
alphaL = (rgbIn.x + rgbIn.y + rgbIn.z) / 3.0f;
alphaL = Limiter(alphaL, p_LimitL);
alpha *= alphaL;
}

rgbOut = RGB_to_HSV(rgbOut);
rgbOut.y *= 1.0f / SatA ;
rgbOut = HSV_to_RGB(rgbOut);

if (alpha < 1.0f)
rgbOut = rgbOut * alpha + (1.0f - alpha) * rgbIn;

if (p_Display)
return make_float3(alpha, alpha, alpha);

return rgbOut;
}

There are other free dctl’s that can be downloaded from the same website.

Ok. I’m not sure what this is supposed to do exactly, but here’s the translation for ART:

// Film Density OFX DCTL

struct float3 {
    float x;
    float y;
    float z;
};


float3 make_float3(float x, float y, float z)
{
    float3 res = { x, y, z };
    return res;
}


float fmin(float a, float b)
{
    if (a < b) {
        return a;
    } else {
        return b;
    }
}


float fmax(float a, float b)
{
    if (a > b) {
        return a;
    } else {
        return b;
    }
}


float3 RGB_to_HSV(float3 RGB)
{
    float3 HSV;
    float min = fmin(fmin(RGB.x, RGB.y), RGB.z);
    float max = fmax(fmax(RGB.x, RGB.y), RGB.z);
    HSV.z = max;
    float delta = max - min;
    if (max != 0.0) {
        HSV.y = delta / max;
    } else {
        HSV.y = 0.0;
        HSV.x = 0.0;
        return HSV;
    }
    if (delta == 0.0) {
        HSV.x = 0.0;
    } else if (RGB.x == max) {
        HSV.x = (RGB.y - RGB.z) / delta;
    } else if (RGB.y == max) {
        HSV.x = 2.0 + (RGB.z - RGB.x) / delta;
    } else {
        HSV.x = 4.0 + (RGB.x - RGB.y) / delta;
    }
    HSV.x = HSV.x * 1.0 / 6.0;
    if (HSV.x < 0.0) {
        HSV.x = HSV.x + 1.0;
    }
    return HSV;
}


float3 HSV_to_RGB(float3 HSV_in)
{
    float3 HSV = HSV_in;
    float3 RGB;
    if (HSV.y == 0.0) {
        RGB.x = HSV.z;
        RGB.y = HSV.z;
        RGB.z = HSV.z;
    } else {
        HSV.x = HSV.x * 6.0;
        float i = floor(HSV.x);
        float f = HSV.x - i;
        if (i >= 0) {
            i = fmod(i, 6.0);
        } else {
            i = fmod(i, 6.0) + 6.0;
        }
        float p = HSV.z * (1.0 - HSV.y);
        float q = HSV.z * (1.0 - HSV.y * f);
        float t = HSV.z * (1.0 - HSV.y * (1.0 - f));
        if (i == 0) {
            RGB.x = HSV.z;
        } else if (i == 1.0) {
            RGB.x = q;
        } else if (i == 2.0) {
            RGB.x = p;
        } else if (i == 3.0) {
            RGB.x = p;
        } else if (i == 4.0) {
            RGB.x = t;
        } else {
            RGB.x = HSV.z;
        }
        if (i == 0) {
            RGB.y = t;
        } else if (i == 1.0) {
            RGB.y = HSV.z;
        } else if (i == 2.0) {
            RGB.y = HSV.z;
        } else if (i == 3.0) {
            RGB.y = q;
        } else if (i == 4.0) {
            RGB.y = p;
        } else {
            RGB.y = p;
        }
        if (i == 0) {
            RGB.z = p;
        } else if (i == 1.0) {
            RGB.z = p;
        } else if (i == 2.0) {
            RGB.z = t;
        } else if (i == 3.0) {
            RGB.z = HSV.z;
        } else if (i == 4.0) {
            RGB.z = HSV.z;
        } else {
            RGB.z = q;
        }
    }
    return RGB;
}


float RGB_to_Sat(float3 RGB)
{
    float min = fmin(fmin(RGB.x, RGB.y), RGB.z);
    float max = fmax(fmax(RGB.x, RGB.y), RGB.z);
    float delta = max - min;
    float Sat = 0.0;
    if (max != 0.0) {
        Sat = delta / max;
    }
    return Sat;
}


float3 Saturation(float3 RGB_in, float luma, float Sat)
{
    float3 RGB = RGB_in;
    RGB.x = (1.0 - Sat) * luma + RGB.x * Sat;
    RGB.y = (1.0 - Sat) * luma + RGB.y * Sat;
    RGB.z = (1.0 - Sat) * luma + RGB.z * Sat;
    return RGB;
}


float get_luma(float3 RGB, float Rw, float Gw, float Bw)
{
    float R;
    float G;
    float B;
    R = Rw + 1.0 - (Gw / 2.0) - (Bw / 2.0);
    G = Gw + 1.0 - (Rw / 2.0) - (Bw / 2.0);
    B = Bw + 1.0 - (Rw / 2.0) - (Gw / 2.0);
    float luma = (RGB.x * R + RGB.y * G + RGB.z * B) / 3.0;
    return luma;
}


float Limiter(float val, float limiter)
{
    float alpha;
    if (limiter > 1.0) {
        alpha = val + (1.0 - limiter) * (1.0 - val);
    } else if (limiter >= 0.0) {
        if (val >= limiter) {
            alpha = 1.0;
        } else {
            alpha = val / limiter;
        }
    } else if (limiter < -1.0) {
        alpha =  (1.0 - val) + (limiter + 1.0) * val;
    } else if (val <= (1.0 + limiter)) {
        alpha = 1.0;
    } else { 
        alpha = (1.0 - val) / (1.0 - (limiter + 1.0));
    }
    if (alpha < 0) {
        alpha = 0;
    } else if (alpha > 1) {
        alpha = 1;
    }
    return alpha;
}


float3 transform(varying float p_R, varying float p_G, varying float p_B,
                 float p_Den, float p_WR, float p_WG, float p_WB,
                 float p_LimitS, float p_LimitL, bool p_Display)
{
    float3 rgbIn = make_float3(p_R, p_G, p_B);
    if (p_Den == 0.0 && p_Display == 0) {
        return rgbIn;
    }

    float WR = 2.0 - p_WR;
    float WG = 2.0 - p_WG;
    float WB = 2.0 - p_WB;
    float luma = get_luma(rgbIn, WR, WG, WB);
    float SatA = 1.0 / (p_Den + 1.0);
    float3 rgbOut = Saturation(rgbIn, luma, SatA);

    float alphaS = 1.0;
    float alphaL = 1.0;
    float alpha = 1.0;

    if (p_LimitS > 0.0) {
        float sat = RGB_to_Sat(rgbIn);
        alphaS = Limiter(sat, p_LimitS);
        alpha = alphaS;
    }
    if (p_LimitL > 0.0) {
        alphaL = (rgbIn.x + rgbIn.y + rgbIn.z) / 3.0;
        alphaL = Limiter(alphaL, p_LimitL);
        alpha = alpha * alphaL;
    }

    rgbOut = RGB_to_HSV(rgbOut);
    rgbOut.y = rgbOut.y * 1.0 / SatA ;
    rgbOut = HSV_to_RGB(rgbOut);

    if (alpha < 1.0) {
        rgbOut.x = rgbOut.x * alpha + (1.0 - alpha) * rgbIn.x;
        rgbOut.y = rgbOut.y * alpha + (1.0 - alpha) * rgbIn.y;
        rgbOut.z = rgbOut.z * alpha + (1.0 - alpha) * rgbIn.z;
    }

    if (p_Display) {
        rgbOut.x = alpha;
        rgbOut.y = alpha;
        rgbOut.z = alpha;
    }

    return rgbOut;
}

// @ART-colorspace: "rec2020"
// @ART-label: "Film Density"

// @ART-param: ["p_Den", "Film Density", 0, 2, 0, 0.001]
// @ART-param: ["p_WR", "Red Weight", 0, 2, 1, 0.001]
// @ART-param: ["p_WG", "Green Weight", 0, 2, 1, 0.001]
// @ART-param: ["p_WB", "Blue Weight", 0, 2, 1, 0.001]
// @ART-param: ["p_LimitS", "Low Saturation Limiter", 0, 1, 0, 0.001]
// @ART-param: ["p_LimitL", "Low Luma Limiter", 0, 1, 0, 0.001]
// @ART-param: ["p_Display", "Display Alpha"]

void ART_main(varying float r, varying float g, varying float b,
              output varying float rout,
              output varying float gout,
              output varying float bout,
              float p_Den, float p_WR, float p_WG, float p_WB,
              float p_LimitS, float p_LimitL, bool p_Display)
{
    float3 res = transform(r, g, b, p_Den, p_WR, p_WG, p_WB,
                           p_LimitS, p_LimitL, p_Display);
    rout = res.x;
    gout = res.y;
    bout = res.z;
}
1 Like

It works, thank you!
I think it’s supposed to “saturate” the colors but without increase the brightness, that’s why it’s called density instead of saturation.
Here’s a link to a short video showing off the use of commercial dctl’s to manipulate saturation by increasing color density (it compares classic RGB saturation to the dctl density): https://www.youtube.com/watch?v=F6z5q71xodg

3 Likes

Hi, I put some more example scripts in a separate repository. Initially I thought about bundling some of them with ART, but for now I think it’s better to keep them separate, so that they can evolve more easily.

2 Likes

Thanks Alberto, It works on Fedora 39 with the “CTL-devel” rpm package

1 Like

Hello @agriggio,
Is this feature planned to be included in future ART releases or it must be added by compilation of source code?
Thanks!

Hi,
Yes the interpreter will definitely be in the next release. The scripts will probably be kept separate, at least for now. Eventually if some of them turns out to be particularly useful it might get integrated, but we’ll see

1 Like