RGB/HSL Conversions in Python

A while ago I wrote a post called Colour Converter in C about converting RGB (red/green/blue) values to HSL (hue/saturation/lightness) and back again. As part of a project which I am planning I need to be able to do the same in Python and was intending to re-write the C in Python, or probably Cython. However, I then found a module called colorsys in an obscure corner of the Python Standard Library. This module includes functions to do the conversions I will need, and in this post I will demonstrate using them to create an array of HSL values from a Pillow image, and then convert these back to RGB to create a new identical image.

On its own this isn't a useful thing to do but I find HSL to be more natural and intuitive than RGB, for example if you need to change an image's saturation or lightness you can just add or subtract from the saturation or lightness respectively.

This project consists of one short file called rgbhsl.py which you can download as a zip, or clone/download the Github repository if you prefer.

Source Code Links

ZIP File
GitHub

This is the source code listing for the entire file.

rgbhsl.py

import colorsys
from PIL import Image
import numpy


def main():

    print("-------------------------")
    print("| codedrome.com         |")
    print("| RGB to HSL Conversion |")
    print("-------------------------\n")

    try:

        image = Image.open("canterbury.jpg")

        hls_array = create_hls_array(image)

        new_image = image_from_hls_array(hls_array)

        new_image.save("canterbury_from_hsl.jpg", quality=95)

    except IOError as e:

        print(e)


def create_hls_array(image):

    """
    Creates a numpy array holding the hue, lightness
    and saturation values for the Pillow image.
    """

    pixels = image.load()

    hls_array = numpy.empty(shape=(image.height, image.width, 3), dtype=float)

    for row in range(0, image.height):

        for column in range(0, image.width):

            rgb = pixels[column, row]

            hls = colorsys.rgb_to_hls(rgb[0]/255, rgb[1]/255, rgb[2]/255)

            hls_array[row, column, 0] = hls[0]
            hls_array[row, column, 1] = hls[1]
            hls_array[row, column, 2] = hls[2]

    return hls_array


def image_from_hls_array(hls_array):

    """
    Creates a Pillow image from the HSL array
    generated by the create_hls_array function.
    """

    new_image = Image.new("RGB", (hls_array.shape[1], hls_array.shape[0]))

    for row in range(0, new_image.height):

        for column in range(0, new_image.width):

            rgb = colorsys.hls_to_rgb(hls_array[row, column, 0],
                                      hls_array[row, column, 1],
                                      hls_array[row, column, 2])

            rgb = (int(rgb[0]*255), int(rgb[1]*255), int(rgb[2]*255))

            new_image.putpixel((column, row), rgb)

    return new_image


main()

imports

The colorsys module is part of the Python Standard Library, but this project also uses a numpy array and the Pillow imaging library which you will need to install if you don't already have them.

main

This is just a short function to open a Pillow image, and then call functions to create our HSL array, create a new image from the array and then save it.

create_hls_array

Firstly we need to get an array of pixels using the Pillow image's load method, and also an empty three-dimensional numpy array. The first two dimensions are the rows and columns in the image, the third having a fixed size of 3 for the HSL values. The array has a float type because saturation and lightness values can contain decimal fractions.

We then iterate the rows and columns of pixels in nested loops, getting an RGB tuple for each pixel before calling colorsys.rgb_to_hls. There are two very important points to note about colorsys.rgb_to_hls:

  • The function uses hue, lightness and saturation instead of the more usual hue, saturation and lightness

  • The arguments are fractions between 0 and 1 rather than integers between 0 and 255 so we need to divide each by 255

We then set the three third-dimension values to the hue, lightness and saturation values respectively. After the loop exits we just return the array.

image_from_hls_array

Firstly we create a new Pillow image the same size as the numpy array, after which we effectively reverse the process carried out by create_hls_array.

Within nested loops for the rows and columns we call colorsys.hls_to_rgb (again note the HLS rather than HSL) to get a tuple of RGB values. Remember these are decimals from 0 to 1 so then need to be multiplied by 255 and cast to ints. Finally we set the relevant pixel to the RGB value. After the loops we just need to return the new image.

I tried out the program with the following image which is called canterbury.jpg, and hard-coded the filename. You will probably want to use you own image so just change the open and save filenames in main before running the program.

Run

python3.7 rgbhsl.py

The program is inevitably slow as it has to read and write each pixel, but when it has run you will find a new image file identical to the original.

As I mentioned at the top this is just a small but vital part of a much larger project which I will begin to release into the wild in the coming months.