Joe Maller: FXScript Reference: Building Joe's Levels

How a levels filter works, also how to build one for Final Cut Pro using FXScript and FXBuilder.

Visit the New FXScript Reference and Joe's Filters sites. These pages will be phased out soon and may already be out of date.

Joe's Filters
FXScript Reference

This page describes the FXScript concepts behind Joe's Levels. If you're looking for the page about how to use the filter, click here.

Overview

Joe's Levels was the filter that got it all started. I could not make sense of the Levels filter in Final Cut Pro, even though Levels is my favorite adjustment in Photoshop. I did spend some time trying to figure out what the filter was doing, and realized that it wasn't very efficient, needing to be applied several times to create an effect that should be possible in one adjustment.

At first, I thought I might be able to simply change the inputs and somehow end up with a working Levels filter. I had modified the default setting on a few other Final Cut Pro filters, but I'd never dealt with the actual code section before and hoped I wouldn't have to. Of course, changing inputs didn't work. I blindly changed a few numbers here and there but only succeeded in breaking the filter completely. After a few hours things only got worse. Out of frustration I shelved the idea somewhere in the back of my mind and forgot about it.

A few months later, and I swear this happened, I was falling asleep when the solution came to me. I don't remember exactly what it was, but in three hours I had a complete working Levels filter. Better yet, I even understood why it worked. The doorway to FXScript had been kicked open.

Credit where due

Several portions of this filter originated with the Levels filter included with Final Cut Pro. Eventually I rewrote it completely and created my own Levels operation from scratch for Building Joe's Threshold, but many of the variable names and routines from the original filter have survived into this one. It's easier to credit sources than to try and hide them.

CLUT is not a dirty word

The acronym CLUT stands for Color Look Up Table. Most of my past experiences with CLUTs have referred to the palette used by a GIF file or Director movie. Naming the main adjustment array clut[256] is a leftover from the original Final Cut Pro adjustment filter, many of the included FXScript use variable names based on variations of the LUT acronym. This and all 8-bit Levels adjustments work by assuming an image has 256 values of gray. The LevelMap function at the end of Joe's Levels will use the values in clut[256] to modify the values in the final image. Arrays are discussed on the FXScript Variables page. All FXScript arrays are filled with zeros by default until some other value is stored.

Another look up table array is LinearRamp. This is a pre-defined 256 value array which passes unchanged images through the levelmap function. Dissecting LinearRamp shows that it is composed of 256 incremental decimal values between zero and one, as discussed on the FXScripts Constants page and illustrated by the Joe's LinearRamp Tester exploration filter. (The Final Cut Pro manual wrongly states that it contains values between 0 and 255.) Remember that computers start counting at zero, so the first element of the array is labeled 0 and the last element is labelled 255. Understanding the values in LinearRamp helped me to understand how the LevelMap function worked.

A Levels adjustment works by creating a Clut that is not a perfect Ramp from zero to one. Below are two simplified sets of numbers representing LinearRamp and a Levels-adjusted ramp showing the result of changing the Input Black setting (in this case to 3). The total value scale is then stretched through the remaining spaces.

LinearRamp:

0.10 0.20 0.30 0.40 0.50 0.60 0.70 0.80 0.90 1.00

Adjusted clut[10]:

0.00 0.00 0.00 0.14 0.28 0.42 0.57 0.71 0.85 1.00

 

The adjusted decimal values are determined by mulitplying the current position in the scaled range of values by the total possible number of values. Since the first three places are ramped to zero, there are seven values remaining. The first value is 1/7 * 10 = 1.42, this works the same as it did in LinearRamp: 1/10 * 10 = 1.

That is the whole concept of a levels adjustment. Almost everything in this filter is used to build the adjustment clut, once those 256 levels are defined, the rest is easy.

Looping

This filter uses several For/Next loops to generate incremental variables. Below is an example loop:

For x = 1 to 5
   array[x - 1] = x;
next;

The above loop will repeat 5 times, and each loop would increase the x variable by one. After one loop, array would contain {1, 0, 0, 0, 0}, after the loop had finished, array would contain {1, 2, 3, 4, 5}. The filter uses several of these loops loops to build the adjustment clut. Since the first index of an array is labeled 0, I used x-1 to define the array position.

Input White and Black

First the script sets all the values above the input White setting to 1. A for/next loop steps through all the values from Input White up to 255 and sets the corresponding array index to 1.

for i = inputWhite to 255
   clut[i] = 1
next;

At this step, essentially the image has been thresholded to the Input White value. All other elements of the clut[256] array contain zero, so mapping the image with the current clut would result in a purely black and white image.

The next step is to determine how many spaces in the array will be used to represent the curve from black to white. This is accomplished using the following if/else statement

if inputWhite > inputBlack
   j = inputWhite - inputBlack
else
   j = 1
end if;

It's not possible for inputWhite to be less than inputBlack and the statement catches that impossibility and replaces it with 1. Otherwise the distance between inputWhite and inputBlack is placed into the variable j.

Gamma

This is the part that initially confused me the most. Without gamma, image values are ramped straight from white to black. Gamma bends that line towards black or white, making it possible to adjust the middle values in an image without affecting the extremes. Here are three diagrams showing how gamma will affect the values in the adjustment clut:

Gamma = 0.5
Gamma = 0.5
Gamma = 1.0
Gamma = 1.0
Gamma = 1.5
Gamma = 1.5

If you wanted to generate graphs like that, the formula is: x = y(gamma) The graph can also be visualized with Photoshop's Curves adjustment, which plainly shows input values and output values.

The next step is probably the most important because this is where the adjustment clut will be filled with the adjusted range of values.

for i = inputBlack to inputWhite
   clut[i] = power((i-inputBlack)/j, gamma);
next;

The power() function is just FXScript's syntax for an exponential value. The code to square 3 (32) with FXScript would look like power(3, 2), the first item is the base number, the second is the exponent.

The for/next loop counts from inputBlack to inputWhite and assigns a value into each corresponding index of the adjustment clut. Since the value needs to be somewhere between zero and one, the pre-gamma value subtracts inputBlack to place between 0 and the total number of values, j. That number is then divided by j, resulting in the same simple fraction used in the first explanation of an adjusted clut earlier on this page.

That fractional value is then used as the base number of the power() function which applies the gamma exponent. Without gamma, or if gamma equals one, the script would build a straight line of between zero and one.

Output White and Black

Output White and black define the darkest and lightest possible values in the image. Output settings use compound operators to affect every index in the adjustment clut in one operation. (Compound Operators are discussed at the bottom of the FXScript Variables page.)

First, the script multiplies every item in the adjustment clut by the difference between outputWhite and outputBlack:

clut *= outputWhite-outputBlack;

This is the first time an operation has increased values in the adjustment clut above one. The maximum possible number is 255, so now the adjustment clut is populated with values between 0 and 255. Before this section ends, the values will be brought back down between zero and one.

Next the script adds the value of outputBlack to every element of the adjustment clut:

clut += outputBlack;

This step increases the bottommost value to equal outputBlack and because the maximum value was set by the difference between outputBlack and outputWhite, the maximum value is now equal to outputWhite.

Finally the script divides the entire adjustment clut by 255 (the maximum possible value. This turns everything in the adjustment clut into a fractional value of 255, all of which are less than one. Math can be really cool.

clut /= 255;

RGB or YUV

One side effect of RGB levels is that colors can appear more saturated. This is a natural side effect of compressing channel information. Applying levels only to the Y of YUV makes it easy to affect only the luminance of the original image while preserving the separate color information. More information about YUV is available on the RGB and YUV Color page.

This section of the filter uses the same kind of paranoid PixelFormat checks as appear in Building Joe's Color Glow. I'm going to skip the color conversion statements here and focus on the actual levelMap() function.

The end of this filter wrapped in a big if/else statement. The first section applies the adjustment clut as RGB, the second applies it to the Y of YUV.

if YUVcheck == 0;

There is a series of color conversion statements here which move the source image into a new image buffer called xbuffer. Then script applies the conversion to xbuffer, placing the result into dest.

levelMap(xbuffer, Dest, LinearRamp, clut, clut, clut);

The levelMap() function converts each channel separately as defined by the last four options. The order is FXScript's standard channel/color reference order, alpha, red, green and blue. The above example uses LinearRamp on the alpha channel, remember that applying LinearRamp will pass the channel through unchanged. The adjustment clut is then assigned to each of the three color channels.

If the user has selected Y only (luminance), the following LevelMap() command is run, after another set of pixelFormat checks and conversions.

levelMap(xbuffer, Dest, LinearRamp, clut, LinearRamp, LinearRamp);

The filter ends with one more format conversion, to make sure dest contains YUV information.

Conclusion

The complete FXScript source code for Joe's Levels is included with the paid version of Joe's Filters.

Buy Joe's Filters Download the Free Trial
 
page last modified: October 23, 2017
Copyright © 1996-2003 Joe Maller