Color Theory/Algorithms

"color operations should be done ...to either model human perception or the physical behavior of light" Björn Ottosson : How software gets color wrong

color conversion

 * sRGB to linear conversion
 * AMPAS Academy Color Encoding System Developer Resources

linearisation
Steps
 * read value ( from sRGB)
 * linearize and widening your intensity (convert 8-bit integer to 16 bit integer of float)
 * process image
 * delinearize and convert to 8-bit
 * save as sRGB

When you're reading an sRGB image, and you want linear intensities, apply this formula to each intensity:

float s = read_channel; float linear; if (s <= 0.04045) linear = s / 12.92; else linear = pow((s + 0.055) / 1.055, 2.4);

Going the other way, when you want to write an image as sRGB, apply this formula to each linear intensity:

float linear = do_processing; float s; if (linear <= 0.0031308) s = linear * 12.92; else s = 1.055 * pow(linear, 1.0/2.4) - 0.055;

Stages:
 * applying the inverse of the sRGB nonlinear transform function f_inv
 * doing computations
 * then switching back by f

For colors in sRGB in range 0.0 to 1.0 this can be done by applying these functions component wise (provided in C-like pseudocode):

float f_inv(float x) { if (x >= 0.04045) return ((x + 0.055)/(1 + 0.055))^2.4 else return x / 12.92 }

float f(float x) { if (x >= 0.0031308) return (1.055) * x^(1.0/2.4) - 0.055 else return 12.92 * x }

Convertion between HSL and sRGB
Converting an HSL color to sRGB in JavaScript.

Converting sRGB Colors to HSL

Converting from linear sRGB to Oklab
A color in Oklab is represented with three coordinates Lab:
 * L – perceived lightness ( a unitless number in the range [0,1] )
 * a – how green/red the color is
 * b – how blue/yellow the color is

It's corresponding polar form is called Oklch.

The standard coordinate can also be transformed into polar form ( Lch), with the coordinates:
 * L = lightness
 * c = chroma
 * h = hue


 * $$C = \sqrt{a^2 + b^2}$$


 * $$h^{\circ}=\text{atan2}(b,a)$$

In reverse direction:


 * $$a=C\cos(h^{\circ})$$


 * $$b=C\sin(h^{\circ})$$

Converting from linear sRGB to Oklab in c++

Image conversion

 * color convert from colorjs library

C code

Gray conversion
Python code

matrix multiplication
matrix multiplication. (This is more readable than inlining all the multiplies and adds). The matrices are in column-major order.

Mix colors
Methods
 * simple interpolation of the sRGB values (gamma-corrected values). Lerp
 * interpolation on linear values makes the red-green gradient better, but at the expense of the back-white gradient.
 * Hue Interpolation ( in HSV, HSB) . We could convert RGB to HSV and lerp each of the H, S, and V components. This is also pretty bad.
 * separating the light intensities from the color
 * Luminosity Interpolation: The best way is to lerp in perceptually linear colourspace ( LAB, OKLab, HCL...). The default color space for mixing (and gradients) in CSS is oklab

Goal: The intensity of the gradient must be constant

Code:
 * d3js: interpolate color in js
 * R colorRamp function

simple interpolation
Mix 2 RGB colors ( naive and wrong form)

//This is the wrong algorithm. Don't do this Color ColorMixWrong(Color c1, Color c2, Single mix) {  //Mix [0..1] // 0   --> all c1   //  0.5 --> equal mix of c1 and c2   //  1   --> all c2   Color result;

result.r = c1.r*(1-mix) + c2.r*(mix); result.g = c1.g*(1-mix) + c2.g*(mix); result.b = c1.b*(1-mix) + c2.b*(mix);

return result; }

Simple interpolation of the sRGB values, particularly the red-green gradient is too dark in the middle.

Intuitively, lerping ( linear interpolations) draws a straight line between two points in the colourspace being used. However, a straight line in the colourspace usually does not result in a perceptually linear interpolation.

interpolation on linear values
RGB colors on computers are in the sRGB color space. And those numerical values have a gamma of approx 2.4 applied.

Without applying the inverse gamma, the mixed colors are darker than they're supposed to be.

In order to mix the colors correctly you must first undo this gamma adjustment. Correct steps are:
 * undo the gamma adjustment ( Invert sRGB gamma compression, InverseSrgbCompanding)
 * apply your r,g,b mixing algorithm above
 * reapply the gamma ( Reapply sRGB gamma compression, SrgbCompanding)

//This is the wrong algorithm. Don't do this Color ColorMix(Color c1, Color c2, Single mix) {  //Mix [0..1] // 0   --> all c1   //  0.5 --> equal mix of c1 and c2   //  1   --> all c2

//Invert sRGB gamma compression c1 = InverseSrgbCompanding(c1); c2 = InverseSrgbCompanding(c2);

result.r = c1.r*(1-mix) + c2.r*(mix); result.g = c1.g*(1-mix) + c2.g*(mix); result.b = c1.b*(1-mix) + c2.b*(mix);

//Reapply sRGB gamma compression result = SrgbCompanding(result);

return result; }

The gamma adjustment of sRGB isn't quite just 2.4. They actually have a linear section near black - so it's a piecewise function.

Helper functions: Color InverseSrgbCompanding(Color c) { //Convert color from 0..255 to 0..1 Single r = c.r / 255; Single g = c.g / 255; Single b = c.b / 255;

//Inverse Red, Green, and Blue if (r > 0.04045) r = Power((r+0.055)/1.055, 2.4) else r = r / 12.92; if (g > 0.04045) g = Power((g+0.055)/1.055, 2.4) else g = g / 12.92; if (b > 0.04045) b = Power((b+0.055)/1.055, 2.4) else b = b / 12.92;

//return new color. Convert 0..1 back into 0..255 Color result; result.r = r*255; result.g = g*255; result.b = b*255;

return result; }

And you re-apply the companding as:

Color SrgbCompanding(Color c) { //Convert color from 0..255 to 0..1 Single r = c.r / 255; Single g = c.g / 255; Single b = c.b / 255;

//Apply companding to Red, Green, and Blue if (r > 0.0031308) r = 1.055*Power(r, 1/2.4)-0.055 else r = r * 12.92; if (g > 0.0031308) g = 1.055*Power(g, 1/2.4)-0.055 else g = g * 12.92; if (b > 0.0031308) b = 1.055*Power(b, 1/2.4)-0.055 else b = b * 12.92;

//return new color. Convert 0..1 back into 0..255 Color result; result.r = r*255; result.g = g*255; result.b = b*255;

return result; }

The color blending in linear RGB space is good when colors are equal RGB total value; but the linear blending scale does not seem linear - especially for the black-white case. Using interpolation on linear values rather than gamma-corrected values makes the red-green gradient better, but at the expense of the back-white gradient.

separating the light intensities from the color
Steps
 * calculate L (lightness) for an RGB color:
 * calculate only the Y (luminance) of CIE XYZ and use that to get L
 * That gives L as 0-1 for any RGB
 * Then to lerp RGB:
 * first interpolate linear RGB,
 * fix lightness by lerping the start/end L
 * scale the RGB by targetL / resultL

LAB
How to generate a smooth color gradient between two colors.

The intensity of the gradient must be constant in a perceptual color space or it will look unnaturally dark or light at points in the gradient. You can see this easily in a gradient based on simple interpolation of the sRGB values, particularly the red-green gradient is too dark in the middle. Using interpolation on linear values rather than gamma-corrected values makes the red-green gradient better, but at the expense of the back-white gradient. By separating the light intensities from the color you can get the best of both worlds.

Often when a perceptual color space is required, the Lab color space will be proposed. I think sometimes it goes too far, because it tries to accommodate the perception that blue is darker than an equivalent intensity of other colors such as yellow. This is true, but we are used to seeing this effect in our natural environment and in a gradient you end up with an overcompensation.

A power-law function of 0.43 was experimentally determined by researchers to be the best fit for relating gray light intensity to perceived brightness.

Algorithm in pseudocode:

Algorithm MarkMix Input: color1: Color, (rgb)  The first color to mix color2: Color, (rgb)  The second color to mix mix:   Number, (0..1) The mix ratio. 0 ==> pure Color1, 1 ==> pure Color2 Output: color: Color, (rgb)   The mixed color //Convert each color component from 0..255 to 0..1 r1, g1, b1 ← Normalize(color1) r2, g2, b2 ← Normalize(color1)

//Apply inverse sRGB companding to convert each channel into linear light r1, g1, b1 ← sRGBInverseCompanding(r1, g1, b1) r2, g2, b2 ← sRGBInverseCompanding(r2, g2, b2)

//Linearly interpolate r, g, b values using mix (0..1) r ← LinearInterpolation(r1, r2, mix) g ← LinearInterpolation(g1, g2, mix) b ← LinearInterpolation(b1, b2, mix)

//Compute a measure of brightness of the two colors using empirically determined gamma gamma ← 0.43 brightness1 ← Pow(r1+g1+b1, gamma) brightness2 ← Pow(r2+g2+b2, gamma)

//Interpolate a new brightness value, and convert back to linear light brightness ← LinearInterpolation(brightness1, brightness2, mix) intensity ← Pow(brightness, 1/gamma)

//Apply adjustment factor to each rgb value based if ((r+g+b) != 0) then factor ← (intensity / (r+g+b)) r ← r * factor g ← g * factor b ← b * factor end if

//Apply sRGB companding to convert from linear to perceptual light r, g, b ← sRGBCompanding(r, g, b)

//Convert color components from 0..1 to 0..255 Result ← MakeColor(r, g, b) End Algorithm MarkMix

Python code by Mark Ransom

The default color space for mixing (and gradients) in CSS is oklab

perceptually uniform gradient

 * color-gradient-algorithm by Roman Frołow
 * perceptually-smooth-multi-color-linear-gradients by Matt DesLauriers
 * color-gradient-algorithm-in-lab-color-space
 * pereptual-color by Olivier Vicario

How to Programmatically Lighten a Color ?

 * SO Q&A

How to convert hsl color to name ?

 * Lea Verou: human-colours: A set of functions you can use to translate the HSL colour notation to human readable text.
 * Trying to map the correct human colour names to hsl-hue values by VASILIS VAN GEMERT
 * Trying to map the correct human saturation names to hsl-saturation values.

Examples:
 * hsl(120,100%,50%) = A highly saturated green
 * hsl(120,65%,25%) = A rather saturated, dark green.

spline gradient

 * spline gradient in js by Matt DesLauriers