Image Analysis: Colour Palette Generator

Tools used: Python 3, Matplotlib, colourgram and webcolors


Abstract

This colour palette generator analyses multiple images and calculates the most common colours within each image.

With the comination of a few Python modules, we are able to use hexadecimal and RGB values and convert them to their nearest colour ID.

Finally, we use Matplotlib to plot our colours and output an image file for out colour palette.

script example


Requirements

There are a few Python 3 packges requried for this tool:

  • pip install colorgram.py - colourgram is a simple module which allows us to get the RGB and HSL values from an image

  • pip install webcolors - this module provides HTML and CSS colour definitions, allowing us to convert RBG and other colour formats their closest colour names

  • pip install matplotlib - we are using matplotlib to view and save our colour palettes

Extracting Colours

The first step is to extract the most dominant colours from each image. We do this using colorgram. colorgram has a simple .extract function which gives us the following datapoints for each image:

  • Most dominant Red, Green and Blue values (RGB) within the image
  • Hue, Saturation and Light (HSL) values for each RGB value
  • Proportion (%) of the image which each RGB value takes up

The following function takes a folder from your directory, loads each image file and extracts the RGB and proportion values. These are all saved into a list of lists:

import os
import math
import colorgram
import webcolors
import matplotlib.pyplot as plt
from PIL import Image

TOP_COLOUR_SKIP = 0
MAIN_FOLDER = 'images/'
IMAGE_FOLDER = os.listdir(MAIN_FOLDER)
COLOURS_WANTED = 30
proportions = []
def extract_colour_values(folder):
    TEMP_FOLDER_IMAGES = os.listdir(MAIN_FOLDER + folder)
    FOLDER_IMAGES = [file_name for file_name in TEMP_FOLDER_IMAGES]
    FILE_COUNT = len(FOLDER_IMAGES)

    print(f"Number of images processing: {FILE_COUNT}")
    colour_extracts = []
    closet_colour_name = []

    for filename in FOLDER_IMAGES[:FILE_COUNT]:
        print(filename)
        # Extract X colours from the image
        img = Image.open(MAIN_FOLDER + folder + "/" + filename)

        # resize the image to speed up processing
        img = img.resize((160,300),Image.ANTIALIAS)
        
        colours = colorgram.extract(img, 999)
        for color in colours:
            colour_extracts.append((color.rgb.r, color.rgb.g, color.rgb.b, color.proportion, filename))

    return colour_extracts

cologram.extract takes in two parameters: 1) path to the image and 2) max number of colours to extract from the image. Don't get this max number of colours confused with the 'COLOURS_WANTED' constant at the start of the script. Here, we are simply setting a max for the specific image, whereas COLOURS_WANTED refers to the number of colours we want for our output palette.

The above function uses an image folder, which contains multiple other folders. Each folder has it's own set of images (one palette per folder). For this example, we only have 1 set of images

With this, we can start to write our main function. Save the colour extract data to a list and sort this list by the RGB proportion index (3ith index). We want to do this so we have the most dominant colours, from all images combined, at the start of the list

def print_palette(folder):
    print(f"Folder Name: {folder}")
    colour_extracts = extract_colour_values(folder)
    colour_extracts.sort(key=lambda x: x[3], reverse=True)

Next, we start to build our matplotlib figure. We set the figsize, title and turn off the axis.

We also set the number of columns and rows we want our palette to use. Here, we use the number of colours we want and split it into rows of 10

  fig = plt.figure(figsize=(24, 14))
    plt.title(folder.title(), fontsize=25)
    plt.axis('off')

    palette_split = COLOURS_WANTED / 10
    if palette_split < 1:
        columns = COLOURS_WANTED
        rows = math.ceil(palette_split)
    else:
        columns = 10
        rows = math.ceil(palette_split)

Now we loop through each RGB from our colour_extracts list, and create an img. For each RGB value, we divide by 255 to convert it into a proportional float.

  for i in range(COLOURS_WANTED):
        print(f"""Rank {i} Colour: R:{colour_extracts[i][0]} G:{colour_extracts[i][1]} B:{colour_extracts[i][2]}, Proportion:{colour_extracts[i][3]}""")
        img = [[(colour_extracts[i][0]/255, colour_extracts[i][1]/255, colour_extracts[i][2]/255)]]

Finally, we plot the img onto our figure, number the colour on our palette and save the plotted image (our colour palette) to a .jpg file.

        fig.add_subplot(rows, columns, i + 1)
        plt.axis('off')
        title_name = str(i+1)
        plt.title(title_name, fontsize=30)
        plt.imshow(img)
        plt.subplots_adjust(wspace=0, left=0.02, right=0.98)
        # plt.show()
        plt.savefig('exports/' + folder + '.jpg')

Output Palette

script example

The output here works well for the most part, however there are improvements which can be made:

  • The same RGB colour could be displayed multiple times - here, the proportions should be summed
  • Slightly different shades of the same colour will be counted separately which can skew certain images.
  • Larger image files will take longer to process - a compression function is required to optimise this step.

The main improvement which we can make is assign 'names' to our RGB values which will allow us to combined multiple shades of the same colour into one.


RGB Conversions to Colour Names

The first step to convert RGB to colour names is to edit our colour extraction function.

Here, while looping through each colour extracted, we call a new function, using the webcolors module. Credit to this thread for the closest colour algorithm here

def get_colour_name(requested_colour):
    try:
        rgb_to_colour_name = actual_name = webcolors.rgb_to_name(requested_colour)
    except ValueError:
        rgb_to_colour_name = closest_colour(requested_colour)
        actual_name = None
    
    return rgb_to_colour_name

the above function takes in the RGB values (requested_colour) and calls the webcolors.rgb_to_name function to find a matching colour, with the help of it's HTML and CSS colour index

The problem here is if webcolors doesn't find a match, an error will be thrown out. Here, we need a function to find the closest colour to our RGB values

We can match each RGB value to its closest Euclidian distance in the RGB space.

def closest_colour(requested_colour):
    min_colours = {}
    for key, name in webcolors.css3_hex_to_names.items():
        r_c, g_c, b_c = webcolors.hex_to_rgb(key)
        rd = (r_c - requested_colour[0]) ** 2
        gd = (g_c - requested_colour[1]) ** 2
        bd = (b_c - requested_colour[2]) ** 2
        min_colours[(rd + gd + bd)] = name

    return min_colours[min(min_colours.keys())]

Now, instead of having a list of RGB and proportion values, we instead have a list of the most common colour names.

Here, we can use the Counter functions to count and get the most common colours in our list. We can also save the colour proportions (if we want to use them for another use).

Our colour palette will still need to use RGB values, so we simply convert our colour names back to RGB values using webcolors.name_to_rgb

def print_palette(folder):
    top_colours = Counter(extract_colour_names(folder))
    top_colours = colour_names.most_common(9999)
    colour_rgb_values = []
    colour_names = []
    colour_proportions = []

    for colour in top_colours:
        RGB = webcolors.name_to_rgb(colour[0])
        colour_rgb_values.append(RGB)
        colour_names.append(colour[0])
        colour_proportions.append((folder, colour[0], colour[1]))

Now, everthing should be good to go!

Main Function

def print_palette(folder):
    top_colours = Counter(extract_colour_names(folder))
    top_colours = colour_names.most_common(9999)
    colour_rgb_values = []
    colour_names = []
    colour_proportions = []

    for colour in top_colours:
        RGB = webcolors.name_to_rgb(colour[0])
        colour_rgb_values.append(RGB)
        colour_names.append(colour[0])
        colour_proportions.append((folder, colour[0], colour[1]))

    fig=plt.figure(figsize=(24, 14))
    plt.title(folder.title(), fontsize=25)
    plt.axis('off')
    COLUMNS = 5
    ROWS = 2
    row_count = 0
    for i in range(1, COLUMNS*ROWS +1):
        img = [[(colour_rgb_values[row_count][0]/255, colour_rgb_values[row_count][1]/255, colour_rgb_values[row_count][2]/255)]]
        fig.add_subplot(ROWS, COLUMNS, i)
        plt.axis('off')
        plt.title(colour_names[row_count].capitalize(), x=0.5, y=0.5, fontsize=30)
        plt.imshow(img)
        plt.subplots_adjust(wspace=0, left=0.02, right=0.98)
        row_count += 1

    for i in colour_proportions:
        print(i)
    # plt.show()
    plt.savefig('exports/' + folder + '.png')
    return colour_proportions


Final Output Palette - RBG to HTML/ CSS Colour Names

script example