Post

ENG | Why RGB gradients look wrong and boring and how CIE LCH/OkLCH fix them

A visual exploration of color spaces for gradient creation, comparing RGB, CIE LCH, and OkLCH approaches with gamma correction.

ENG | Why RGB gradients look wrong and boring and how CIE LCH/OkLCH fix them

While creating slime mold simulation, I tried to add some nice color gradients. I’m aware that RGB is one of many ways how to describe colors and there are ones which claim to be vastly superior. So this became project within project.

Background

There are basically these methods how to describe color

  • Mixing ratio of red, green, blue light
  • Angle on color wheel (hue), saturation/chroma, brightness/value/intensity/luminance
  • Luminance and two color components (such as blue- vs yellow+, purple- vs green+)

Main details are

  • Are red, green, blue, intensity perceptually linear or exponential (for RGB they are not and gamma correction is applied - not many people know this and naively blurring image makes it darker, using RGB in 3d rendering creates too dark shadows,… no, #808080 is not 50% gray.)
  • Does angle on color wheel keep saturation and brightness constant? In HSV, blue and yellow are obviously not perceived as equally bright.
  • Are same anglular distances perceived as the same difference in color (difference between orange and yellow-green is huge, difference between blue with red and green tint is not)

Pro-Tip: Whenever when you are dealing with image processing or rendering, at least consider doing gamma correction before and after image operations like resampling, blurring. I know this for roughly 15 years and it was a surprise, considering I’ve dealt with all of this professionally. Some tools like GIMP, ImageMagick, Paint.NET handle it correctly. FastStone Image Viewer does not. Effect is not always obvious, correction using two pow function is not cheap, but here are two examples.

Image blur with and without gamma correction.

Rendering with and without gamma correction. Well, it’s ancient Phong model and in this case both results are debatable.

Well known formats are

  • RGB/sRGB - what monitor, color picker and most file formats use
  • HSL, HSV - Quite intuitive colorpickers with wheel and triangle.

Lesser known are

  • Linear RGB - linear intensity
  • YUV, YCbCr - image and video compression - intensity + two color components)
  • XYZ - it’s often used as intermediate format for conversions, for example to CieLAB
  • CIE LAB - tries to achieve uniform intensity (luminance), A,B are color components. Distance from center is saturation (chroma), angle from center to point at a, b is hue.
  • CIE LCH - as above, but using polar coordinates H=atan(b,a), C=sqrt(a^2 + b^2), a=C*cos(H), b=C*sin(H)

I was aware that these exists.

And I knew that using RGB for color interpolation is not the best idea cause it gives weird gradients such as red -> dark brown -> bright green or blue -> gray -> yellow.

I also knewn that many colormaps in scientific visualization tools such as matplotlib, seaborn use colormaps defined by Bézier curves in likely CIE LAB color space such as Viridis, Inferno and Magma, which are something I wanted to achieve.

Image from Matplotlib documentation

Obviously CieLCH color space can help - I can interpolate luminance and chroma in a linear manner and interpolate hue using shorter arc, creating piece of helix in color space.

So I asked some LLM (I think it was ChatGPT) if it can write RGB to LAB conversion in C++ and checked it against OpenCV documentation. Doing conversion to LCH was easy, it’s conversion between cartesian and polar coordinates.

The result was almost better than I expected. I was able to get good looking gradients, similar to colormaps from matplotlib using just three control points. The only problem was to give them some crazy names 😀

OkLAB/OkLCH discovery

I found this somewhat by accident, but the web page included ready to use implementation with MIT license, conversion was easier than CIELAB which uses XYZ colorspace first, so I implemented it. Conversion looks like a small neural network. Do gamma correction -> Multiply by 3x3 matrix -> cube -> multiply by 3x3 matrix -> result.

In my opinion, LAB/LCH color spaces has one disadvantage. For OkLCH dark, useable colors have luminosity roughly at 0.55, pastel colors 0.85. 0.6-0.75 is reasoable. Ok, I can get used to it. But Chroma is in range 0-0.3 and mostly below 0.2 with clipping at random values. This is not much intuitive.

NOTE: when one end is desatured, hue is undefined and LAB is used as a fallback

So, is it better?

I would say OkLAB is certainly better than CieLAB. It avoids purple tint in blue gradients (blue -> white, blue -> orange, blue -> green).

For LCH I’m not sure. Red-green gradient seem to contain too much reds in OkLCH, while CieLCH has the same issue in red-blue. In purple-gold gradient, CieLCH has nice, saturated pink. I’m not sure if it should be there, but it looks better.

In the end, many colormaps are not based on color science, but to look somewhat uniform and good.

Here are ones used in slime mold simulation as of 2025-08-15:

Color gradients defined by endpoints and midpoint

Source code with functions for creating gradients, color map presets and test application are here:

Side note: Viridis color map.

Personally I don’t like this colormap much. It was everywhere around 2020 and feels overused. I also doubt it adds more information than plain grayscale, which often gives higher contrast.

Nonetheless, I wanted to know, how it was created. It turns out it was designed with viscm, a tool that lets you draw Bézier curves through the potato-shaped CAM02-UCS color space (similar to CIELAB). The catch is you only see 2D cross-section at the selected endpoint. When you modify endpoints and squash curve in Z direction, you can’t see what was wrong before. The result feels like a mini-game where the rules are stacked against you. The ultimate goal is to stay inside the potato (so colors are not clipped), but as close to it’s surface as possible to have saturated colors.

To get uniform brightness with high contrast, the bright endpoint must be close to yellow, teal, or maybe pink. This limits the options.

Does “perceptually uniform” really mean good for anything else than academic papers about color science? Is there any advantage in not going outside of gamut while staying as close to borders as possible? For “scientific” colormaps like Viridis, personal taste and context IMHO matter more than mathematical theory.

But I assume it should be possible to create a better tool. Why not use Catmull-Rom splines that go through control points, and allow users to see more slices through the potato at control points or regular intervals?

Support

Sadly as of August 2025, OkLAB is not supported in OpenCV. Few programs (GIMP) support CieLAB gradients 😥. Some support CieLAB color pickers (GIMP, Affinity Designer) and many programs do not support even image operations in linear RGB 😡. I wonder about adoption when CieLAB exists for 50 years. But! It’s supported by CSS styles!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>OkLab Gradient</title>
</head>
<body style="font-family: 'JetBrains Mono', monospace; background-color: black; color: oklch(0.8 none none) ; ">
   <p>Linear in CIELAB and OkLAB</p>
   <div style="width: 640px; height: 40px; background: linear-gradient(to right in lab, blue, white);"></div> 
   <div style="width: 640px; height: 40px; background: linear-gradient(to right in oklab, blue, white);"></div> 
   <div style="width: 640px; height: 40px; background: linear-gradient(to right in lab, #0077b3, #e2de00);"></div> 
   <div style="width: 640px; height: 40px; background: linear-gradient(to right in oklab, #0077b3, #e2de00);"></div>

   <p>Spiral in HSV, CIELCH, OkLCH</p> 
   <div style="width: 640px; height: 40px; background: linear-gradient(to right in hsl, #0077b3, #e2de00);"></div>
   <div style="width: 640px; height: 40px; background: linear-gradient(to right in lch, #0077b3, #e2de00);"></div>
   <div style="width: 640px; height: 40px; background: linear-gradient(to right in oklch, #0077b3, #e2de00);"></div>

   <p>Longer hue spiral in HSV, CIELCH, OkLCH<br/>
   <div style="width: 640px; height: 40px; background: linear-gradient(to right in hsl longer hue, #0077b3,#e2de00);"></div>
   <div style="width: 640px; height: 40px; background: linear-gradient(to right in lch longer hue, #0077b3,#e2de00);"></div>
   <div style="width: 640px; height: 40px; background: linear-gradient(to right in oklch longer hue, #0077b9, #e2de00);"></div>
</body>
</html>

Conclusion

Exploring gradients in different color spaces was both fun and revealing.

RGB interpolation is boring, HSV interpolation is weird (I tried it using online tools) while CIE LCH and OkLCH give nice and balanced results.

For me, OkLab/OkLCH feels like the most practical option - simple to implement, well-documented, no purple tints. Yes, it’s debatable if CieLCH look better or not, but fallback option for transition to black, white, or gray is OkLAB which is better. My only issue is that for me it’s not intuitive, because I’m not used to it.

Addendum 2025-08-18

In just three days I found it useful for generating quantitative color map without any dominant color, using golden ratio on circle (180.0*(3.0-sqrt(5.0)) = 137.50776405003785) to split hue. Now, whatever number of colors I need, they are roughly equally distributed. It’s good that it can be relatively fast prototyped using a Python script and a simple HTML page.

Quantitative palettes in OkLCH and OkLrCH spaces

Palettes were initially created using online colorpicker and manual step, but Python with coloraide module works as well:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from coloraide import Color
from coloraide.spaces.oklrch import OkLrCh
from coloraide.spaces.oklrab import Oklrab
import numpy as np

Color.register(OkLrCh())
Color.register(Oklrab())
#Color("oklrch", [0.66, 0.12, 0]).convert("srgb").to_string(hex=True)

golden_angle_deg = 180 * (3 - np.sqrt(5))
a = 0
for i in range(0, 15):
   print(f'{i:02d}:{a:20}   {Color("oklrch", [0.66, 0.12, a]).convert("srgb").to_string()}')
   a = (a + golden_angle_deg) % 360.0

Using OkLCH and OkLAB color spaces is easier, they work without registration.

OkLab/Lch colorspace

HSLuv colorspace

  • HSLuv color picker. Interesting color picker which handles saturation as a percentage of maximum chroma. CIE Luv seems as yet another alternative to CieLAB, OkLAB. This one exists inside InkScape.

Interesting palettes

  • ColorBrewer Hand-crafted palettes
  • Nord Theme one color scheme I liked to use in Visual Studio and for some plots (fixed colors)
  • Solarized Theme color scheme that tries to work equally well for light and dark thema in text editors. Personally I like light version and dislike dark one.

Other tools

  • CIELab.io tool for creating and comparing multicolor palettes

Inspiration

This post is licensed under CC BY 4.0 by the author.