github twitter mastodon linkedin email
Creating a sepia filter with python
Feb 17, 2019
4 minutes read

Introduction

Last week I wanted to emulate the css effects in python on images. The filter that caught my attention was the sepia filter. I write this because I couldn’t find much information about the topic on the web.

Basics

A quick search on your favourite search engine leads to this page where as mentioned in stackoverflow (1) what you have to do is:

outputRed = (inputRed * .393) + (inputGreen *.769) + (inputBlue * .189)
outputGreen = (inputRed * .349) + (inputGreen *.686) + (inputBlue * .168)
outputBlue = (inputRed * .272) + (inputGreen *.534) + (inputBlue * .131)

The result is (Image by Andrea Ranalleta on Unsplash):

Coding this on python

With this information what you have to do is to ignore the alpha layer and for each pixel calculate a bunch of numbers.

You can use Pillow for this and then on your program:

from PIL import Image

def sepia(image_path:str)->Image:
    img = Image.open(image_path)
    width, height = img.size

    pixels = img.load() # create the pixel map

    for py in range(height):
     for px in range(width):
         r, g, b = img.getpixel((px, py))

         tr = int(0.393 * r + 0.769 * g + 0.189 * b)
         tg = int(0.349 * r + 0.686 * g + 0.168 * b)
         tb = int(0.272 * r + 0.534 * g + 0.131 * b)

         if tr > 255:
             tr = 255

         if tg > 255:
             tg = 255

         if tb > 255:
             tb = 255

         pixels[px, py] = (tr,tg,tb)

    return img

Here I’m assuming that the image is a jpeg, but in case of a png is similar but taking care of the alpha layer. This is easy, but very slow, so … how can this be faster?

My next search was for applying filters to images and I discovered cv2. Scrolling fast the docs I wrote this:

import cv2
import numpy as np
from PIL import Image

def sepia_cv(image_path:str)->Image:
    """
    Optimization on the sepia filter using cv2 
    """

    image = Image.open(image_path)

    # Load the image as an array so cv knows how to work with it
    img = np.array(image)

    # Apply a transformation where we multiply each pixel rgb 
    # with the matrix for the sepia

    filt = cv2.transform( img, np.matrix([[ 0.393, 0.769, 0.189],
                                          [ 0.349, 0.686, 0.168],
                                          [ 0.272, 0.534, 0.131]                                  
    ]) )

    # Check wich entries have a value greather than 255 and set it to 255
    filt[np.where(filt>255)] = 255

    # Create an image from the array 
    return Image.fromarray(filt)

What is happening here is that the previous sums and products are no more than a linear transformation between subspaces of the reals, so we can represent this as a matrix. But loading cv2 and numpy for this is too much so let’s just use numpy.

def sepia_np(image_path:str)->Image:
    """
    Optimization on the sepia filter using numpy 
    """

    image = Image.open(image_path)

    # Load the image as an array so cv knows how to work with it
    img = np.array(image)

    # Apply a transformation where we multiply each pixel 
    # rgb with the matrix transformation for the sepia

    lmap = np.matrix([[ 0.393, 0.769, 0.189],
                      [ 0.349, 0.686, 0.168],
                      [ 0.272, 0.534, 0.131]                                  
    ])

    filt = np.array([x * lmap.T for x in img] )

    # Check wich entries have a value greather than 255 and set it to 255
    filt[np.where(filt>255)] = 255

    # Create an image from the array
    return Image.fromarray(filt.astype('uint8'))

In this solution take care of (2)

Improving the filter

In CSS you can apply an scale to the filter. After searching a lot I found this link where it specifies how to apply the filter using this scale. The thing is that aside the linear map, you are using a movement. The transformation matrix is:

    matrix = [[ 0.393 + 0.607 * (1 - k), 0.769 - 0.769 * (1 - k), 0.189 - 0.189 * (1 - k)],
    [ 0.349 - 0.349 * (1 - k), 0.686 + 0.314 * (1 - k), 0.168 - 0.168 * (1 - k)],
    [ 0.272 - 0.349 * (1 - k), 0.534 - 0.534* (1 - k), 0.131 + 0.869 * (1 - k)]]

where k is the interval [0,1].

Here are some examples:

  • Sepia with amount = 0 (original image)

  • Sepia with amount = 0.3

  • Sepia with amount = 0.7

  • Sepia with amount = 1 (the same filter we created initally)

Measuring times

Calling python -m cProfile test.py (3) with my Pentium G3258 on the image used in this post (2453x2453 pixels):

    ncalls  tottime  percall  cumtime  percall filename:lineno(function)

         1    0.007    0.007    0.293    0.293 test.py:34(sepia_cv)
         1    0.033    0.033    0.499    0.499 test.py:56(sepia_np)
         1    7.857    7.857   16.634   16.634 test.py:7(sepia)

This times don’t count the time importing the modules used.


Back to posts