New feature: support for CTL scripts

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

Thanks a lot! Hope new release will be issued soon.

@agriggio Hi Alberto, I don’t know if it’s of interest for you, but I found this source of utility DCTL’s, maybe you’ll find something cool: GitHub - thatcherfreeman/utility-dctls: My creative and util DCTLs.

By the way, did you translate de Film Density dctl by hand, or do you have some translation script? I tried to translate a dctl by hand, using your Film Density ctl as an example, but I failed.

1 Like

Hi,
Thanks for the pointer, I’ll check it out.

Yes. But I don’t like it that much. I think the tetrahedral warping ones (especially the hsl which is easier to use) are much better.

As for the translation, if something goes wrong you should get error messages on the terminal that are hopefully helpful…

2 Likes

Hi,

Now you should be able to use ART-cli to do some error checking, like this:

$ ART-cli --check-lut /path/to/script.ctl

If you don’t get any output, the script should load fine. Otherwise, you will see a list of errors.

HTH

1 Like