Codementor Events

Intermediate Image Filters

Published Aug 18, 2018Last updated Feb 14, 2019
Intermediate Image Filters

Why Image Filters?

Many popular applications like Photoshop allows any user to add filters to create eye-catching images quickly. Unfortunately, not everyone has a wallet to pay for the subscription prices that Adobe is charging.
How about if we create our image filters and implement some traditional filters for cheap, which in this tutorial we will cover by using Python and Pillow.

Quick Notes

  1. Pillow is a fork of PIL (Python Imaging Library)
  2. Pillow and PIL cannot co-exist in the same environment. Before installing Pillow, uninstall PIL.
  3. libjpeg-dev is required to be able to process jpeg's with Pillow.
  4. Pillow >= 2.0.0 supports Python versions 2.6, 2.7, 3.2, 3.3, and 3.4
  5. Pillow >= 1.0 no longer supports import Image. Use from PIL import Image instead.

Installing Required Library

Before we start, we will need Python 3 and Pillow. If you are using Linux, Pillow will probably be there already, since major distributions including Fedora, Debian/Ubuntu, and ArchLinux include Pillow in packages that previously contained PIL.

The easiest way to install it is to use pip:

pip install Pillow

The installation process should look similar to the following.

Pillow installation on Windows 10

If any trouble happens, I recommend reading the documentation from the Pillow Manual.

Basic Methods

We will need some basic methods to manipulate pixels, reading/writing images, and creating empty images.

# Imported PIL Library
from PIL import Image, ImageDraw


# Open an Image
def open_image(path):
    newImage = Image.open(path)
    return newImage


# Save Image
def save_image(image, path):
    image.save(path, 'png')


# Create a new image with the given size
def create_image(i, j):
    image = Image.new("RGB", (i, j), "white")
    return image


# Get the pixel from the given image
def get_pixel(image, i, j):
    # Inside image bounds?
    width, height = image.size
    if i > width or j > height:
        return None

    # Get Pixel
    pixel = image.getpixel((i, j))
    return pixel

Sepia Toning Filter

Sepia toning is a traditional photographic print toning added to a black and white photograph within a darkroom to change the temperature of the image.

There are three types of sepia toning,

  1. Sodium sulfide toners.
  2. Thiourea toners.
  3. Polysulfide or 'direct' toners.
  4. Soda and Sodium sulfide toners.

As a disclaimer, I worked in a printing business and learned to do traditional sepia tone, as you may be wondering about the soda toners these was a unique mixture in which some Coca-Cola was added to the Sodium mixture to decrease the artificial yellowish color.

To apply this filter, we will get each pixel of the image, exaggerate the Red, Yellow, and Brown tones of the image by using the values in get_sepia_pixel.
This filter is based on the Java Sepia Filter from Oracle.

# Sepia is a filter based on exagerating red, yellow and brown tones
# This implementation exagerates mainly yellow with a little brown
def get_sepia_pixel(red, green, blue, alpha):
    # This is a really popular implementation
    tRed = get_max((0.759 * red) + (0.398 * green) + (0.194 * blue))
    tGreen = get_max((0.676 * red) + (0.354 * green) + (0.173 * blue))
    tBlue = get_max((0.524 * red) + (0.277 * green) + (0.136 * blue))

    # Return sepia color
    return tRed, tGreen, tBlue, alpha


# Convert an image to sepia
def convert_sepia(image):
    # Get size
    width, height = image.size

    # Create new Image and a Pixel Map
    new = create_image(width, height)
    pixels = new.load()

    # Convert each pixel to sepia
    for i in range(0, width, 1):
        for j in range(0, height, 1):
            p = get_pixel(image, i, j)
            pixels[i, j] = get_sepia_pixel(p[0], p[1], p[2], 255)

    # Return new image
    return new

By applying the filter with the above code, we get the following result.

Sepia Filter

Pointilize Filter

Pointillism is a technique of painting in which small, distinct dots of color are applied in patterns to form an image.
Vincent van Gogh used it to do a self-portrait and Georges Seurat to do the famous "A Sunday Afternoon on the Island of La Grande Jatte"; if you have been following me you might have noticed that I use it to create the title images for some tutorials as the Semantic Web 101 tutorial.

To apply this filter we must get the color average of the area in which we will draw a circle, and we additionally add some error in the position we draw the circle to create a nice effect.

# Return the color average
def color_average(image, i0, j0, i1, j1):
    # Colors
    red, green, blue, alpha = 0, 0, 0, 255

    # Get size
    width, height = image.size

    # Check size restrictions for width
    i_start, i_end = i0, i1
    if i0 < 0:
        i_start = 0
    if i1 > width:
        i_end = width

    # Check size restrictions for height
    j_start, j_end = j0, j1
    if j0 < 0:
        j_start = 0
    if j1 > height:
        j_end = height

    # This is a lazy approach, we discard half the pixels we are comparing
    # This will not affect the end result, but increase speed
    count = 0
    for i in range(i_start, i_end - 2, 2):
        for j in range(j_start, j_end - 2, 2):
            count+=1
            p = get_pixel(image, i, j)
            red, green, blue = p[0] + red, p[1] + green, p[2] + blue

    # Set color average
    red /= count
    green /= count
    blue /= count

    # Return color average
    return int(red), int(green), int(blue), alpha


# Create a Pointilize version of the image
def convert_pointilize(image):
    # Get size
    width, height = image.size

    # Radius
    radius = 6

    # Intentional error on the positionning of dots to create a wave-like effect
    count = 0
    errors = [1, 0, 1, 1, 2, 3, 3, 1, 2, 1]

    # Create new Image
    new = create_image(width, height)

    # The ImageDraw module provide simple 2D graphics for Image objects
    draw = ImageDraw.Draw(new)

    # Draw circles
    for i in range(0, width, radius+3):
        for j in range(0, height, radius+3):
            # Get the color average
            color = color_average(image, i-radius, j-radius, i+radius, j+radius)
            
            # Set error in positions for I and J
            eI = errors[count % len(errors)]
            count += 1
            eJ = errors[count % len(errors)]

            # Create circle
            draw.ellipse((i-radius+eI, j-radius+eJ, i+radius+eI, j+radius+eJ), fill=(color))

    # Return new image
    return new

By applying the filter with the above code, we get the following result.

Seattle Pointillism

Complete Source

The complete code to process images takes a PNG file in RGB color mode (with no transparency), saving the output as different images.

Due to limitations with JPEG support on various operating systems, I choose the PNG format.

'''
    This Example opens an Image and transform the image using Pointilize.
    We also use a sepia filter as an optional step.
    You need PILLOW (Python Imaging Library fork) and Python 3.5
    -Isai B. Cicourel
'''

# Imported PIL Library
from PIL import Image, ImageDraw


# Open an Image
def open_image(path):
    newImage = Image.open(path)
    return newImage


# Save Image
def save_image(image, path):
    image.save(path, 'png')


# Create a new image with the given size
def create_image(i, j):
    image = Image.new("RGB", (i, j), "white")
    return image


# Get the pixel from the given image
def get_pixel(image, i, j):
    # Inside image bounds?
    width, height = image.size
    if i > width or j > height:
        return None

    # Get Pixel
    pixel = image.getpixel((i, j))
    return pixel


# Limit maximum value to 255
def get_max(value):
    if value > 255:
        return 255

    return int(value)


# Sepia is a filter based on exagerating red, yellow and brown tones
# This implementation exagerates mainly yellow with a little brown
def get_sepia_pixel(red, green, blue, alpha):
    # Filter type
    value = 0

    # This is a really popular implementation
    tRed = get_max((0.759 * red) + (0.398 * green) + (0.194 * blue))
    tGreen = get_max((0.676 * red) + (0.354 * green) + (0.173 * blue))
    tBlue = get_max((0.524 * red) + (0.277 * green) + (0.136 * blue))

    if value == 1:
        tRed = get_max((0.759 * red) + (0.398 * green) + (0.194 * blue))
        tGreen = get_max((0.524 * red) + (0.277 * green) + (0.136 * blue))
        tBlue = get_max((0.676 * red) + (0.354 * green) + (0.173 * blue))
    if value == 2:
        tRed = get_max((0.676 * red) + (0.354 * green) + (0.173 * blue))
        tGreen = get_max((0.524 * red) + (0.277 * green) + (0.136 * blue))
        tBlue = get_max((0.524 * red) + (0.277 * green) + (0.136 * blue))
        

    # Return sepia color
    return tRed, tGreen, tBlue, alpha


# Return the color average
def color_average(image, i0, j0, i1, j1):
    # Colors
    red, green, blue, alpha = 0, 0, 0, 255

    # Get size
    width, height = image.size

    # Check size restrictions for width
    i_start, i_end = i0, i1
    if i0 < 0:
        i_start = 0
    if i1 > width:
        i_end = width

    # Check size restrictions for height
    j_start, j_end = j0, j1
    if j0 < 0:
        j_start = 0
    if j1 > height:
        j_end = height

    # This is a lazy approach, we discard half the pixels we are comparing
    # This will not affect the end result, but increase speed
    count = 0
    for i in range(i_start, i_end - 2, 2):
        for j in range(j_start, j_end - 2, 2):
            count+=1
            p = get_pixel(image, i, j)
            red, green, blue = p[0] + red, p[1] + green, p[2] + blue

    # Set color average
    red /= count
    green /= count
    blue /= count

    # Return color average
    return int(red), int(green), int(blue), alpha


# Convert an image to sepia
def convert_sepia(image):
    # Get size
    width, height = image.size

    # Create new Image and a Pixel Map
    new = create_image(width, height)
    pixels = new.load()

    # Convert each pixel to sepia
    for i in range(0, width, 1):
        for j in range(0, height, 1):
            p = get_pixel(image, i, j)
            pixels[i, j] = get_sepia_pixel(p[0], p[1], p[2], 255)

    # Return new image
    return new


# Create a Pointilize version of the image
def convert_pointilize(image):
    # Get size
    width, height = image.size

    # Radius
    radius = 6

    # Intentional error on the positionning of dots to create a wave-like effect
    count = 0
    errors = [1, 0, 1, 1, 2, 3, 3, 1, 2, 1]

    # Create new Image
    new = create_image(width, height)

    # The ImageDraw module provide simple 2D graphics for Image objects
    draw = ImageDraw.Draw(new)

    # Draw circles
    for i in range(0, width, radius+3):
        for j in range(0, height, radius+3):
            # Get the color average
            color = color_average(image, i-radius, j-radius, i+radius, j+radius)
            
            # Set error in positions for I and J
            eI = errors[count % len(errors)]
            count += 1
            eJ = errors[count % len(errors)]

            # Create circle
            draw.ellipse((i-radius+eI, j-radius+eJ, i+radius+eI, j+radius+eJ), fill=(color))

    # Return new image
    return new


# Main
if __name__ == "__main__":
    # Load Image (JPEG/JPG needs libjpeg to load)
    original = open_image('Image_Filters/small.png')

    # Convert to sepia
    new = convert_sepia(original)
    save_image(new, 'Image_Filters/Prinny_Sepia.png')

    # Convert to Pointillism
    new = convert_pointilize(original)
    save_image(new, 'Image_Filters/Seattle_Pointillism.png')

Wrapping Things Up

Image filters allow us to add unique tones and look to a sometimes dull or mundane image. This tutorial showed you two old-school styles that can be used for your headers or social network images.
How about if you mess around with the pointillism one and try to find your very own style?

Have You Try The Following

What happens when you change the background color and space the circles in the pointillism filter?

Did you notice any similarity between the pointillism and the dithering filter?

There are two extra values for sepia. Have you tried them out?

Discover and read more posts from Isai B. Cicourel
get started