MATH OF COLORS

Color Grading, Color Science, DaVinci Resolve, Gamut -

MATH OF COLORS

(or: A journey into the wonderful world of colors)

LUTs

LUTs are great when they're done right. Ultimately, everything in the digital world is based on math, including color grading. All the better if you know and understand a few basic simple relationships.

Let's take a look at some very simple models and calculations using the example of transforming from RGB to HEX and from RGB to HSL.

HEX

On the web, HEX is a very well-known and used format for defining colors. The name is derived from the term hexadecimal. HEX is used by us humans to shorten the binary format to a form that is easier to understand. Each of the HEX numbers uses numbers and letters to represent values between 0-16. Numbers are used for the range 0-9 and letters A, B, C, D, E, F are used for the range 10-16. So far nothing new.

A HEX code is nothing more than a HEX triplet representing three separate values. These in turn are nothing more than the levels of the component colors and are called bytes. So they form a six-digit hexadecimal number, you know this for example from HTML or CSS, but also from Photoshop or DaVinci Resolve.

The first pair of values refers to red, the second to green and the third to blue. A byte represents a number in the range 0 to 255 in decimal notation. In HEX, however, the notation ranges in scale from the lowest (00) to the highest (FF) intensity of each color. HEX codes begin with a hashtag character and are followed by the named format:

#8d004b // 8d = red, 00 = green, 4b = blue

So it is just a coding of the 3 color values ​​for red, green and blue in the RGB color model. The RGB color model is additive, ie 100% red + 100% green + 100% blue result in white. All colors in the RGB color model are therefore mixed by addition. The RGB color model underlies all displays and is one of the technical bases for the representation of colors on the displays.

CONVERSION FROM RGB TO HEX

First, let's simply convert an RGB code into a HEX code. This is done by converting the decimal numbers of the given RGB values into hexadecimal numbers. We achieve this by dividing our values by 16:

R/16 = X1 + Y1
G/16 = X2 + Y2
B / 16 = X3 + Y3

In this formula, X is called the quotient and Y is the "remainder". These two numbers are used to represent the HEX value pair for each specific color of our color model, i.e. red, green and blue. With this, a HEX code can be calculated from these values ​​as #X1Y1X2Y2X3Y3. The X1Y1 values ​​for red, X2Y2 for green and X3Y3 for blue are determined quite quickly. Let's look at this with the following RGB values:

R = 201 / 16 = 12 + 0.5625 // 0.5625 is the remainder
G = 25 / 16 = 1 + 0.5625
B = 75 / 16 = 4 + 0.6875

Now we multiply the remainder by 16, so

0.5625 * 16 = 9 // Red
0.5625 * 16 = 9 // Green
0.6875 * 16 = 11 // Blue

In order to determine the hex format of the individual digits, we now have to refer to this table, which is also used for converting HEX color to RGB:

 

It's easy to read that 12 is a C and 9 is a 9 in HEX as well. Therefore, our red is expressed in hexadecimal as #C9. Accordingly, our green becomes #19 and our blue becomes #4b. Fine! The RGB values ​​201,25,75 (red, green, blue) result in the HEX code #c9194b. It wasn't that difficult.

HEX TO RGB

Of course, the conversion can also take place in the opposite direction. All we need to do is multiply the quotients by 16 and add the corresponding remainders:

R = X1 x 16 + Y1
G = X2 x 16 + Y2
B = X3 x 16 + Y3

We already know the quotients (X1, X2, X3) and the remainders (Y1, Y2, Y3). To convert our HEX code back, all we have to do is convert the HEX code values ​​for each element to decimal values. To do this, we simply use our table again. So let's take the decimal value of C from the table (12) and multiply it by 16:

R = 12 x 16 + 0.5625 = 192

Of course, 192 is not equal to 201, but this was our initial value for red. But if we now look into our table and add the second digit of the HEX code #c9 (9 is the second digit, the 9 in Hex corresponds to the 9 in RGB), we get the value for red:

192 + 9 = 201

That's it. We now repeat this step for green and blue and at the end we get our three RGB values ​​201,25,75 (red, green, blue).

So far it has been very simple mathematics, for which we have used what is known as a look-up table, or LUT for short. Because nothing else is this table. And this in turn brings us to the fact that LUTs fundamentally have nothing to do with color grading in the first place, but are primarily a kind of "lookup table". We find these very often in mathematics, for example in encryption processes (AES) and many other applications.

Look Up Tables help us to "translate" values ​​from one encoding into another encoding. In simplified terms, these tables can also be thought of as "matching tables". This is not expressed correctly mathematically, but I am more concerned with the concept and the principle to clear up the very common half-knowledge about LUTs in color grading.

However, we often do not need such look-up tables at all, but can actually convert values ​​purely mathematically using formulas, such as when converting/transforming RGB values ​​into HSL values. Let's take a closer look.

HSL

First of all, HSL is nothing more than a common cylindrical coordinate representation of points in an RGB color model. HSL stands for Hue, Saturation and Luminosity. Hue refers to the color family of the specific color we are looking at. Hue indicates the actually dominant color on the RGB color wheel. The luminosity indicates how much white or black is added to the color, i.e. whether it becomes darker or lighter. Saturation expresses how much gray is in the same color, less gray means more saturation, more gray means less saturation.

First, let's look at the Luminosity.

LUMINOSITY (L)

The luminosity (also called brightness or luminance) stands for the intensity of the energy output of a visible light source. It essentially indicates how light a color is. The scale is as follows:

L = [0, 1] // the values ​​are between 0 and 1



Let's first determine the maximum and minimum values ​​of the RGB values, which we can determine for each image and which are also mandatory for certain calculations or effects.

Max(RGB) stands for the highest and Min(RGB) for the lowest value in red, green and blue. In order to be able to carry out our further calculations, we have to convert the RGB values ​​into the range 0-1, because we have to take the "detour" via luminosity and take this into account in the calculation. By dividing by 255 we easily get the values:

R = 201 / 255 = 0.788
G = 25 / 255 = 0.098
B = 75 / 255 = 0.294

The luminosity of the respective RGB values ​​(201, 25, 75) can now be calculated from the Max(RGB) and Min(RGB) values ​​as follows:

L = (1 / 2) x (Max(RGB) + Min(RGB))

Looking at our RGB values, we find that R is the largest at 201 and G is the smallest at 25. So we already know the values ​​Max(RGB) = R = 0.788 and Min(RGB) = G = 0.098, because of course the values ​​between 0 and 1 in size ratio remained the same after the conversion. Green still has the smallest and red still has the largest value. So we simply calculate the luminosity as well

L = (1 / 2) x (0.788 + 0.098) = 0.443 (ie around 44%)

Now we come to Hue.

HUE (H)

Most visible light sources contain energy in a band of wavelengths. Hue corresponds to the wavelength within the visible light spectrum at which the energy output of a light source is greatest. Hue is specified by its position (in degrees) on the RGB color wheel and is therefore described in a scale of values ​​as follows:

H = [0°, 360°]

The formula for the hue depends on which value represents the Max(RGB) and Min(RGB) values, i.e. how the RGB values ​​relate to one another. We can use it to write the following "formulas" in pseudo code:

(A) If R ≥ G ≥ B | H = 60° x [(GB)/(RB)]
(B) If G > R ≥ B | H = 60° x [2 - (RB)/(GB)]
(C) If G ≥ B > R | H = 60° x [2 + (BR)/(GR)]
(D) If B > G > R | H = 60° x [4 - (GR)/(BR)]
(E) If B > R ≥ G | H = 60° x [4 + (RG)/(BG)]
(F) If R ≥ B > G | H = 60° x [6 - (BG)/(RG)]

These "conversions" or "formulas" often form the base for effects and calculations of saturation or color space manipulations. However, the basic functions in pseudo code mentioned here can be further refined, modified and also combined. Basically, when using these simple "formulas" it should be noted that with a trinlinear calculation and a low bit depth, artefacts and bendings are quickly introduced into the image, so be particularly careful here! (The formulas mentioned are therefore very simple and should be further refined for a real application)

So the hue of our RGB color from the example above (201, 25, 75) can be calculated using the appropriate formula from the pool of "formulas" above:

R = 201 / 255 = 0.788
G = 25 / 255 = 0.098
B = 75 / 255 = 0.294

It follows:

R > B > G seems to apply to our example, so our match from the pool above is the formula F. So we write:

H = 60° x [6 - (0.294 - 0.098) / (0.788 - 0.098)]

H = 60° x [6 - (0.196) / (0.69)] = 342.96°

Voila! Rounded, this now gives a value of 343°. If you want to check this quickly and easily, you can easily use Photoshop, for example. Just enter the hex value and then check the value in field H.

Now we come to saturation.

SATURATION (S)

Saturation is nothing more than an expression of the relative bandwidth of the visible output of a light source.  Ok, a little simpler again: the higher the saturation, the purer the colors appear. On the other hand, if the saturation decreases, the colors appear more "washed out". The value scale of saturation is between 0 and 1, so we write:

S = [0, 1]

For the calculation of the saturation we now need the values ​​Min(RGB), Max(RGB) and Luminosity. So we write in pseudo code:

If L < 1 | S = (Max(RGB) — Min(RGB)) / (1 — |2L - 1|)
If L = 1 | S = 0

Fortunately, since we have already calculated the luminosity, we can use L = 0.443.

Now L with a value of 0.443 < 1. So we have to use the upper formula of the two pseudo code "formulas". Also, we know that Max(RGB) = 0.788 (red) and Min(RGB) = 0.098 (green). And with that, we have all the information we need to calculate the saturation for our color. And as you can already see in the above image of the Photoshop Color Picker dialog, it is a beautiful magenta hue. 😄

Now we can calculate:

S = (0.788 - 0.098) / (1 - |2 x 0.443 - 1|)

S = 0.69 / (1 — |0.886 - 1|) = 0.778 (about 78%)

So we have determined a saturation of around 78% for our color, great! Based on our calculation, we can now go further and define our own rules for colors. And that's exactly what we do when we work with color spaces and light in our own tools, for example to determine a certain spectrum for a research work or to develop our own rules for film print emulations. Of course, the calculations then go much further and quickly become very complex, but the basis lies in such quite simple fundamentals.

AND AGAIN LUTs

Finally, you can also write the results into a LUT, but those are just the results and not real calculations. So if you don't know how these were done (be it by means of nodes or actual mathematical calculations), you don't know how well or badly these LUTs work or where their limits are. And that brings us back to the realization that, in the end, LUTs are just what they are: dumb matching tables, nothing more, nothing less. If you use them "wrong" you will get unclean results and in the worst case you will destroy your image.

So be careful when using LUTs and questioning how they came about. Filmentor creates its LUTs on the base of proofed processes and mathematical models which are also primarily used in Filmentor's DCTLs.

I hope I was able to introduce you to the wonderful world of mathematics for colors with the derivation of the conversion from RGB to HEX and finally to HSL.

As always, stay colorful and do better!

Tim


6 comments

  • Jaime

    Very good explanation! Thanks for it!

  • Butch

    Well done, and thanks for your clear explanation!

    Now I am quite curious if it is possible to calculate an RGB value for a new color, given the next two specifications 1 and 2 (plus requirements A and B):

    1) An arbitrary color
    2) The desired Hue of the new color

    A) Luminosity as being required to be equal
    B) Saturation as being required to be equal

    The new color should only differ by it’s hue. It should have similar (if not exactly equal) luminosity and saturation.

    An example of usage is the creation of multiple buttons having different colors in a software application which should match perfectly.

    Probably as well with elements having the same hue and saturation, but with a different lumination. Thus it might be nice to develop an option for such calculation as well, having a lumination parameter as input .

    Anybody will give this a try?

  • Javier

    If anyone is intrested this is what rgb to hex ended up looking for me in python code. Seems to work fine.

    def rgb_to_hsl(rgb:tuple)→tuple:
    rgb = tuple(v/255 for v in rgb)
    l = rgb_get_lum(rgb)
    h = rgb_get_hue(rgb)
    s = rgb_get_sat(rgb, l)
    return (int(round(h,0)), int(round(s*100, 0)), int(round(l*100, 0)))

    def rgb_get_lum(rgbp:tuple)→float:
    return 0.5 * ( np.min(rgbp) + np.max(rgbp) )

    def rgb_get_hue(rgbp:tuple)→float:
    r, g, b = rgbp
    if r == b == b:
    return 0
    if r >= g >= b:
    return 60 * ( (g-b) /(r-b) )
    if g > r > b:
    return 60 * (2 – (r-b)/(g-b))
    if g >= b > r:
    return 60 * (2 + (b-r)/(g-r))
    if b > g > r:
    return 60 * (4 – (g-r)/(b-r))
    if b > r >= g:
    return 60 * (4 + (r-g)/(b-g))
    return 60 * (6 – (b-g)/(r-g))

    def rgb_get_sat(rgbp:tuple, l:float)→float:
    if l == 1 : return 0
    return ( np.max(rgbp) – np.min(rgbp) ) / (1 – abs(2*l – 1) )

    def hex_to_hsl(hex_val:str, suffix:str = ’’):
    rgb = hex_to_rgb(hex_val)
    hsl = rgb_to_hsl(rgb)
    return hsl

  • Javier

    Nice! I transformed it into pyhon code and it works almost perfectly. There is only one issue. On the hue calculation “pseudocode” you don’t account the possibility of having a division by 0 on the first case.

    I understand mathematicly the division by 0 indicates any degrees would actually be possible, but if someone is trying to implement this adding a previous statement would resolve this issue.

    (0) If R = G = B | H = 0°
    (A) If R ≥ G ≥ B | H = 60° x [(GB)/(RB)]
    (B) If G > R ≥ B | H = 60° x [2 – (RB)/(GB)]
    © If G ≥ B > R | H = 60° x [2 + (BR)/(GR)]
    (D) If B > G > R | H = 60° x [4 – (GR)/(BR)]
    (E) If B > R ≥ G | H = 60° x [4 + (RG)/(BG)]
    (F) If R ≥ B > G | H = 60° x [6 – (BG)/(RG)]

  • Helmut

    Sehr schön , das muss doch mal erklärt werden, wer machts denn sonst außer Tim

Leave a comment