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
- Pillow is a fork of PIL (Python Imaging Library)
- Pillow and PIL cannot co-exist in the same environment. Before installing Pillow, uninstall PIL.
libjpeg-dev
is required to be able to process jpeg's with Pillow.- Pillow >= 2.0.0 supports Python versions 2.6, 2.7, 3.2, 3.3, and 3.4
- Pillow >= 1.0 no longer supports
import Image
. Usefrom 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.
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,
- Sodium sulfide toners.
- Thiourea toners.
- Polysulfide or 'direct' toners.
- 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.
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.
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?