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.
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
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
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
CSS you can apply an scale to the filter. After searching a lot I
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)]]
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)
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.