New feature: support for CTL scripts

Thanks a lot. Reading your code is a good starting point to understand how to apply transfer functions to each pixel channel value. I will study deeply. Thanks.

As I wrote different ops, I tried to keep it readable, mainly for me. I also put various verbiage in comments at the top of most ops, describing a bit where they came from and how they worked. There are quite few reference cites scattered about, so you might spend some time skimming down gimage.cpp…

Note though that CTL scripts operate on RGB vectors, not on individual channels. So you can do more than per-channel functions. I’ll try to find some time to describe one possible split toning implementation in the next days

1 Like

Thanks a lot @agriggio. I will keep on eye on it!

Oh, good distinction. To put it in perspective, most of the rawproc ops default to “vector operations”, the same number applied to all three channels of a pixel. Some tools have a “single-channel” option, where an op can be applied to, say, only the blue channel of all pixels…

This is not what I meant though. I rather meant that CTL scripts implement functions of the form \mathbf{y} = f(\mathbf{x}) where \mathbf{x} and \mathbf{y} are vectors (r~g~b)^{\textsf{T}} in some RGB space.

Anyway, this is not super important. Back to the question:

Here is one possible way of doing this. I’ll try to break down the script in its various parts, and then show the final version.

Let’s start with the declaration of ART_main, which is our entry point:

// @ART-param: ["hhue", "Hue", 0, 360, 0, 0.1, "Highlights"]
// @ART-param: ["hsat", "Strength", 0, 1, 0, 0.01, "Highlights"]
// @ART-param: ["shue", "Hue", 0, 360, 0, 0.1, "Shadows"]
// @ART-param: ["ssat", "Strength", 0, 1, 0, 0.01, "Shadows"]
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 hhue, float hsat, float shue, float ssat)

Here we define our main function to take 4 parameters: a (Hue, Strength) pair for coloring the highlights, and another one for coloring the shadows. We use the // @ART-param: lines to define the sliders for the GUI.

In order to achieve split toning, we want to operate on the chrominance of the pixels. There are many ways of extracting the chrominance component, here we are using a very simple method that just subtracts the R and B channels from the luma:

    float luma = rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114;
    float chroma[2] = { luma - rgb[2], rgb[0] - luma };

Next, we are going to alter the chroma components by adding our desired tint, accoring to the user-selected parameters (Hue and Strength of the tinting for the highlights and the shadows). First, we convert the user parameters to internal values, by converting degrees to radians and by scaling the strength factor for the shadows to get a more reasonable range for the GUI slider:

    const float hue[2] = { hhue * M_PI / 180.0, shue * M_PI / 180.0 };
    const float sat[2] = { hsat, ssat / 5 };

Then, in order to achive our split toning, we simply add a tint to the chroma components, as follows:

    float tint[2] = {
        luma * sat[0] * sin(hue[0]) + sat[1] * sin(hue[1]),
        luma * sat[0] * cos(hue[0]) + sat[1] * cos(hue[1])
    };

here are the ideas that we are using:

  • the two hue/sat input pairs that are essentially the polar representation of the tint to add to the chroma components. We convert from polar to cartesian in the usual way: (x, y) = (sat \cdot sin(hue), sat \cdot cos(hue))
  • our tint modifiers are defined as a weighted sum between the highlights and the shadows tints selected by the user. Specifically, we add the shadows tint with the highlights tint multiplied by the pixel luma value: in this way, for brighter pixels (high luma value), the highlights component dominates, whereas for darker ones (low luma value), the shadows tint will take over; for midtones, we will have a blend of the two.

Finally, we convert back to rgb:

rout = luma + (chroma[1] + tint[1]);
gout = g;
bout = luma - (chroma[0] + tint[0]);

This is essentially it. There’s just one final point: instead of working on linear data, we apply a gamma compression before computing the tint correction, and revert it at the end, as this gives a more pleasing result (to my eyes at least):

    float rgb[3] = {
        pow(clip0(r), igamma), pow(clip0(g), igamma), pow(clip0(b), igamma)
    };
    rout = pow(clip0(chroma[1] + tint[1] + luma), gamma);
    gout = g;
    bout = pow(clip0(luma - (chroma[0] + tint[0])), gamma);

here, clip0 is an auxiliary function that clips negative values to zero to avoid trouble.

Here’s the full script:

// @ART-colorspace: "rec2020"
// @ART-label: "Simple split toning"

float clip0(float x) { if (x < 0) return 0; else return x; }

// @ART-param: ["hhue", "Hue", 0, 360, 0, 0.1, "Highlights"]
// @ART-param: ["hsat", "Strength", 0, 1, 0, 0.01, "Highlights"]
// @ART-param: ["shue", "Hue", 0, 360, 0, 0.1, "Shadows"]
// @ART-param: ["ssat", "Strength", 0, 1, 0, 0.01, "Shadows"]
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 hhue, float hsat, float shue, float ssat)
{
    const float hue[2] = { hhue * M_PI / 180.0, shue * M_PI / 180.0 };
    const float sat[2] = { hsat, ssat / 5 };

    const float gamma = 2.2;
    const float igamma = 1 / gamma;
    
    float rgb[3] = {
        pow(clip0(r), igamma), pow(clip0(g), igamma), pow(clip0(b), igamma)
    };
    
    float luma = rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114;
    float chroma[2] = { luma - rgb[2], rgb[0] - luma };
    
    float tint[2] = {
        luma * sat[0] * sin(hue[0]) + sat[1] * sin(hue[1]),
        luma * sat[0] * cos(hue[0]) + sat[1] * cos(hue[1])
    };

    rout = pow(clip0(chroma[1] + tint[1] + luma), gamma);
    gout = g;
    bout = pow(clip0(luma - (chroma[0] + tint[0])), gamma);
}
3 Likes

Dear @agriggio, thanks a lot for the example provided and the detailed explanations of the procedure. It helps me to understand… how to change colors varying chroma representation of rgb values, it is simple and really elegant. I suppose that there could be other approaches using other representations but not necessary as simple as the one you provided. To test if I understand correctly, please take this example: if I want to simulate a split toning of a b&w image, from the original color, I have to discard chroma values, and add directly the tint (same procedure), isn’t it?
Also you put the accent in what I am looking for… the way to code (and identify the most adequate color representation to perform any transformation such this one, so if you can suggest any tutorial or textbook it will be really useful.
Thank you very much for your support and your exceptional software.

On your homepage is mentioned “simpler and (hopefully) easier to use interface, while still maintaining the power and quality”. Does n’t it make more sense to spend your efforts to subjects in that direction?

It’s all explained in the FAQ

Alberto, i love what you are doing. I have used Rawtherapee, DXO, ON1. An approach to make it as simple as possible is the best. No scripts, a GUI should do the job. Kind regards

Well, but the scripts come with a gui. In fact, they are a way (IMHO) to make the program simpler, as they allow users to extend/customise art to their needs without any change to the core.
If you don’t like the concept, just don’t install any script and you will not even notice the feature exists.

Best

2 Likes

Yes, that’s one way. In general, to convert an image to b&w you simply need to make all the channels equal, i.e. set the r, g and b component of each pixel to the same value. Setting them to the luma value is one of the possible ways. Once you have the b&w, just add the tints for the split toning.

HTH

Even installed they are completely out of the way and don’t intrude on the base UI of Art so in that regard they have the potential as you say to extend and customize ART as a user sees fit without really changing the look or feel of the main Art program. I suspect many people won’t even discover them unless they follow your efforts and check-in on your work and the updates to the wiki site. I think it is a really powerful addition…

4 Likes

Hi @agriggio, thanks a lot for your further explanation.
I recently downloaded version 1.23 and I see that there is additional support for 1D LUTs (curves) with CTL scripts… I also noted a description of “arrays of float” in Luts wiki web page, but no example to clarify the use, parameters an the different options in GUI is available. Could you please give a sample of CTL script to better understand the usage of this new functionality? Thanks a lot for this fantastic software!!

Hi, there are a few examples in the ctlscripts repository (tone curve and some graphic equalisers): Bitbucket

HTH

2 Likes

Thanks a lot for the quick answer!

Hi, Alberto
You added Graphic UI support to CTL script in Ver. 1.23 like below.

ART_Graphic01
ART_Graphic02

Would you please explain the meaning of parameters to make these graphic UI?

Hi,
There’s a bit of documentation (admittedly a bit concise) here: agriggio / ART / wiki / Luts — Bitbucket

If you have more specific questions, feel free to ask

in hueeqg.ctl, sateqg.ctl or lumeqg.ctl

// @ART-param: [“hcurve”, “H”, 2, [“ControlPoints”,
// @ART-param: [“scurve”, “S”, 2, [“ControlPoints”,
// @ART-param: [“lcurve”, “L”, 2, [“ControlPoints”,

// @ART-param: [“hcurve”, “H”, 1, [“ControlPoints”,
// @ART-param: [“scurve”, “S”, 1, [“ControlPoints”,
// @ART-param: [“lcurve”, “L”, 1, [“ControlPoints”,

between Channel name (H, S, or L) and “[“ControlPoints”,”, there is a number (1 or 2). What is the meaning of this number? And the other number (like 0 or 3) can be usable?

And in the last of each line…

]], 0, “Channel”]
]], 0, “Channel”]
]], 0, “Channel”]

There is number 0 before “Channel”. What is the meaning?

Hi, quoting from the docs:

The 3rd parameter indicates the curve type: 0 for diagonal, 1 for flat, and 2 for periodic flat

And

The 5th and 6th parameters can be used to define the gradients appearing at the bottom and left of the curves in the GUI

Where 0 means no gradient

HTH

1 Like