Radiohead colour palettes

Creating unsupervised(ish) colour palettes from Radiohead album covers

A little while ago, I had the idea of creating colour palettes in R (for plotting/data visualisation) that were based on colours present in the album covers of my favourite band, Radiohead. The idea came to me after seeing the wonderful Wes Anderson palettes project on github (with “Zissou1” being a particular favourite of mine!).

After thinking of the idea, it took me all of two seconds to come up with a suitable name for the project…inRainbows.

And about 2 seconds after coming up with the name for the project, I started to think about ways of trying to automatically (in an unsupervised manner) generate the colour palettes from the album covers rather than manually selecting them, and also if this could be done in a general way so that a colour palette could be created from any given image.

My first (and only) real attempt at producing palettes in an unsupervised manner was to use kmeans clustering. I am no expert at using kmeans and have only a rudimental understanding of how it works, but I managed to hobble together the code here which works well enough for my simple purposes here.

The project is shared on github.

As per usual, begin with the adding the tidyverse functions to the search path

library(tidyverse)

Creating palettes

I created a function that basically wrapped around a little script in order to allow me run the same code over all album covers. I have tried to give the code verbose commenting, but essentially this function…

  • Reads in the album cover as an image (an array of the dimensions height x width x channels) and then wrangles the data into a dataframe that splits out the RGB channels per xy coordinate and converts them into a hex string.
  • Sets a seed and runs kmeans on the RGB channels - computing the hex string colour kmeans center for each xy coordinate pair of the original image
  • Counts the occurences of each kmeans centre to create the palette (sorted from most common to least common) and returns a list of the palette colour hex strings and the image dataframe (with kmeans center colours appended)
create_palette <- function(img_path, ncol=5, my.seed=3, ...){

  # Read jpeg image to an array
  i <- jpeg::readJPEG(img_path)

  # Extract dimensions of image
  dim_x <- dim(i)[1]
  dim_y <- dim(i)[2]

  # Wrangle dataframe from image to a tidy format
  df <-
    reshape2::melt(i) %>%
    spread(Var3, value) %>%
    rename(x=Var2, y=Var1, red=`1`, green=`2`, blue=`3`) %>%
    mutate(y = -y + dim_y,
           original_colour = pmap_chr(list(red, green , blue), rgb))

  # Set seed and run kmeans
  set.seed(my.seed)
  kMeans <- kmeans(df[c("red", "green", "blue")], ncol, ...)

  # Compute the hex code colour for each element of the image
  palette_colours <- rgb(kMeans$centers[kMeans$cluster, ])

  # Define the palette colours and order them from most to least frequent
  palette <- table(palette_colours) %>% sort(decreasing = T)

  # Return a list of the palette colours and the image dataframe
  list(palette = palette, palette_data = df %>% mutate(palette_colour = palette_colours))
}

Visualising palettes

Once the palettes have been created, I want a quick way to see what colours have been picked and how representative of the original album cover they are. The following code plots the original album cover through ggplot2::geom_tile() and it also plots the album cover made with the colour palette picked out by kmeans. As it is slow to render these ggplot2::geom_tile() images, an optional frac can be supplied to plot only a random fraction of the image (which speeds things up considerably). The function also plots the full colour palette and a reduced colour palette (if palette_elements are supplied).

visualise_palette <- function(palette, palette_elements=NULL, frac = 1){

  images <-
    palette$palette_data %>%
    rename(Original = original_colour,
           `Palette (full) image` = palette_colour) %>%
    pivot_longer(cols = c(Original, `Palette (full) image`)) %>%
    sample_frac(frac) %>%
    ggplot(aes(x, y, fill=value))+
    geom_tile()+
    facet_wrap(~name)+
    coord_equal()+
    scale_fill_identity()+
    theme_void()

  colours <- tibble(colours = names(palette$palette), n=as.integer(palette$palette))

  palette_bars <-
    colours %>%
    ggplot(aes(reorder(colours, n), fill=colours))+
    geom_bar(col=1, position = "fill")+
    scale_fill_identity()+
    theme_minimal()+
    labs(x="", y="")+
    scale_y_continuous(labels=NULL)+
    coord_flip()+
    labs(title="Full colour palette")

  if(!is.null(palette_elements)){
    colours <- colours[palette_elements,]

    palette_bars_reduced <-
      colours %>%
      ggplot(aes(reorder(colours, n), fill=colours))+
      geom_bar(col=1, position = "fill")+
      scale_fill_identity()+
      theme_minimal()+
      labs(x="", y="")+
      scale_y_continuous(labels=NULL)+
      coord_flip()+
      labs(title="Reduced colour palette")

    return(gridExtra::grid.arrange(images, palette_bars, palette_bars_reduced,
                                   layout_matrix = matrix(c(1,1,2,3), ncol = 2, byrow=TRUE)))
    }
  
  gridExtra::grid.arrange(images, palette_bars, nrow=2, ncol=1)
  }

Examples

create_palette("data-raw/album_covers/the_bends.jpg", ncol = 5, my.seed = 4) %>% visualise_palette()

create_palette("data-raw/album_covers/king_of_limbs.jpg", ncol = 10, my.seed = 4) %>% visualise_palette()

Manual ‘pruning’

You’ll notice in the king of limbs visualisation and colour palette above that some of the colours chosen are very similar. So I decided to manually reduce the colour palettes by removing the colours that were similar to other colours. I also ran the code above on all palettes specifying that 20 colours be returned from the kmeans clustering. That way, accent or highlight colours that are not particularly prevalent in the album cover (but important to have in the palette (to my eye)) get picked up - meaning I can then manually choose to keep them.

I will also mention that I chose to remove very light or white colours from the palettes. This is because I think they are fairly annoying for most plotting/visualisation use cases (unless plotting on a dark background). Doing this clearly means I am favouring ‘useability’ over strict artistic colour palette creation. Some future compromise could be to create a palette of full colours and a ‘reduced’ palette that is favourable for plotting?

The example below shows the 20 colour king of limbs palette with the reduced (final) palette next to it.

And that’s about it - I create a list containing all of the album colour palettes and then create some functions for gaining access to the palettes. All code can be found on github. All code to produce the palettes and the manual pruning is held in data-raw folder.

library(inRainbows)

See all palettes and their names with

inrainbows_summary()

Return n colours from a single palette

inrainbows(album = "ok_computer", n = 4)
## [1] "#E5EBF3" "#9DB5CC" "#6993CC" "#24232E"

And ggplot colour and fill scales

ggplot(morley, aes(Expt, Speed, fill = factor(Expt))) +
  geom_boxplot() +
  scale_fill_inrainbows("kid_a", guide=FALSE)

Avatar
Chris Holmes
Senior Data Scientist

PhD physicist making his way in the world of data science!

comments powered by Disqus