I like gophers.

An Algorithm to Generate Color Palettes

· Read in about 7 min · (1363 Words)
gamut k-means colors color palettes color spaces rgb cielab hcl golang

So you’re looking for a beautiful color palette for this website you’re working on? Maybe you recently equipped your house with RGB lighting or you’re about to paint your living room walls with some fresh colors? You just got this shiny new LED keyboard and want to make full use of its features? In my case it was a combination of all of these.

Whatever your situation might be, if you are just a tiny bit like me, you feel like you’re constantly tweaking your color schemes. You want a decent color palette, something a bit more charming and appealing than just plain red, green and blue. (Yes, yes, I hear you. I’ll have to change my blog’s sidebar color)

math.Rand

Being a programmer, I quickly wrote a few lines of code to generate random color palettes. Already aware that this approach might not yield the best results, I prepared for a couple of minutes of frantic “reload” key smashing. Surely I’d just need a bit of luck and patience and would be rewarded with a beautiful color theme that I would instantly fall in love with.

Boy was I wrong. Generating color palettes from random color values sucks: every now and then it would produce a lovely color, just to pair it with the ugliest, muddiest hues of brown or yellow. You would always end up with a selection of colors way too dark or way too bright, one that offered too little contrast or a palette where the colors looked too similar to another. There had to be a better way.

Color Spaces

Let’s start with the theory. There are a few commonly used color spaces to classify colors:

sRGB

RGB stands for Red Green Blue. This is how your computer screen works: it emits a colored light for each of the three color channels, which blend together as you perceive them. Each channel’s value ranges from 0 to 255. R:0, G:0, B:0 (or #000000 in hex) is black, R:255, G:255, B:255 (or #ffffff) is white.

CIE Lab

CIELab Color Space

The CIE Lab color space has a wider range than sRGB and includes all humanly perceivable colors. It is designed to be perceptually uniform. In other words, the distance between colors in this space is a perceptive distance: if two colors’ values are close to each other, they will also look similar to you. Two distant colors, on the other hand, will also be perceived as distinct colors. In CIE Lab there’s more room for saturated colors compared to darker or lighter ones. To your eyes, a very dark green is almost indistinguishable from a black, after all. This color space is also three-dimensional: L represents the lightness (between 0.0 and 1.0), whereas a & b (roughly between -1.0 and 1.0) are the color channels.

HCL

HCL Color Space

If RGB is how screens produce colors and CIE Lab is how we perceive colors, HCL is the color space that most closely resembles how we think about colors. Like the other two color spaces it’s three-dimensional, and uses the values H for Hue (between 0 and 360 degrees), C for Chroma and L for Luminance (both between 0.0 and 1.0).

If there’s just one thing you’re going to remember from this blog post, let it be that you should use the CIE Lab color space for computations and HCL when presenting color palettes to the user. You can eventually convert values from those color spaces to RGB, should you really need them in that format.

Partitioning a Color Space

Color Space Partitioning

As I want a set of unique, distinct colors, let’s first eliminate those that we perceive as too similar. The color space we want to operate in is three-dimensional, and the k-means clustering algorithm is a fantastic way to divide such low-dimensional data sets. It tries to partition the provided data points (in this case our color space) into k distinct areas. The palette is then made up from the clusters’ center points in these areas. The visualization to the right is a two-dimensional plotter output of the algorithm at work in the three-dimensional CIE Lab color space.

Let’s Write Some Code

With the Go implementation of the k-means algorithm, this problem can be solved in just a few lines of code. First, we want to prepare a data set with the color values in the CIE Lab space:

1
2
3
4
5
6
7
8
var d clusters.Observations
for l := 0.2; l <= 0.8; l += 0.05 {
    for a := -1.0; a < 1.0; a += 0.1 {
        for b := -1.0; b < 1.0; b += 0.1 {
            d = append(d, clusters.Coordinates{l, a, b})
        }
    }
}

As you can see, we can already tweak a couple of parameters and apply certain constraints to the colors we want to be generated. In this example we exclude colors that are either too dark (lightness <0.2) or too bright (lightness >0.8).

Let’s partition the color space we just created:

1
2
km := kmeans.New()
clusters, _ := km.Partition(d, 8)

The Partition function will return a slice of eight clusters. Each cluster has a Center point which represents a distinct color in the given color space. You can easily translate its coordinates to an RGB hex value:

1
2
col := colorful.Lab(c.Center[0], c.Center[1], c.Center[2])
col.Clamped().Hex()

Remember that the CIE Lab space has a wider color range than RGB, and therefore certain Lab values have no representation in the RGB space. Clamped converts those values to the closest matching color in the RGB spectrum.

Complete Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
    "github.com/muesli/kmeans"
    "github.com/muesli/clusters"
    colorful "github.com/lucasb-eyer/go-colorful"
)

func main() {
    // Create data points in the CIE L*a*b* color space
    // l for lightness, a & b for color channels
    var d clusters.Observations
    for l := 0.2; l <= 0.8; l += 0.05 {
        for a := -1.0; a <= 1.0; a += 0.1 {
            for b := -1.0; b <= 1.0; b += 0.1 {
                d = append(d, clusters.Coordinates{l, a, b})
            }
        }
    }

    // Partition the color space into 8 clusters
    km := kmeans.New()
    clusters, _ := km.Partition(d, 8)

    for _, c := range clusters {
        col := colorful.Lab(c.Center[0], c.Center[1], c.Center[2])
        fmt.Println("Color as Hex:", col.Clamped().Hex())
    }
}

A set of eight (not so) random colors generated by the code snippet above:

Define Your Own Color Space

We want a little more control over the colors we generate, though. Luckily it’s easy to control the data set we compute on and bend the color space to our needs. Let’s generate some milky Pastel colors:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func pastel(c colorful.Color) bool {
    _, s, v := col.Hsv()
    return 0.2 <= s && s <= 0.4 && 0.7 <= v && v <= 1.0
}

for l := 0.0; l <= 1.0; l += 0.05 {
    for a := -1.0; a <= 1.0; a += 0.1 {
        for b := -1.0; b <= 1.0; b += 0.1 {
            col := colorful.Lab(l, a, b)

            if col.IsValid() && pastel(col) {
                d = append(d, clusters.Coordinates{l, a, b})
            }
        }
    }
}

Yet another color space: HSV, which stands for Hue, Saturation and Value (brightness). You can look up the details on Wikipedia, but what’s important here is that Pastel colors in this space typically have high values for brightness, but low saturation.

The Pastels it generated:

Similarly, you can filter colors by their chroma and lightness to extract a set of “warm” colors:

1
2
3
4
func warm(col colorful.Color) bool {
	_, c, l := col.Hcl()
	return 0.1 <= c && c <= 0.4 && 0.2 <= l && l <= 0.5
}

Generates:

The gamut Package

I’m currently working on a library called gamut, where I’ll combine all the bits and pieces presented here into one convenient package that lets you generate and manage color palettes & themes in Go. You can already play with it, but it’s still work in progress. Stay tuned. More about gamut in the next blog post.

Comments