Using PIL, I'm applying a rainbow filter to the given image using getpixel
and setpixel
. One issue, this method is very slow. It takes around 10 seconds to finish one image.
def Rainbow(i):
x = 1 - abs(((i / 60) % 2) - 1)
i %= 360
if (i >= 0 and i < 60 ): r,g,b = 1, x, 0
if (i >= 60 and i < 120): r,g,b = x, 1, 0
if (i >= 120 and i < 180): r,g,b = 0, 1, x
if (i >= 180 and i < 240): r,g,b = 0, x, 1
if (i >= 240 and i < 300): r,g,b = x, 0, 1
if (i >= 300 and i < 360): r,g,b = 1, 0, x
res = (int(r * 255), int(g * 255), int(b * 255))
return res
def RainbowFilter(img):
for x in range(img.size[0]):
for y in range(img.size[1]):
intensity = sum(img.getpixel((x, y)))
img.putpixel((x, y), Rainbow(intensity x y))
return img
im = Image.open('cat.jpg')
rainbow_im = RainbowFilter(im)
rainbow_im.save('rainbow_im.png')
Can you help me improve my specific algorithm, using exclusively Numpy or Pillow features, to resolve the issue mentioned?
CodePudding user response:
You can convert image
to NumPy.array
then use numba
for improving speed like below:
from PIL import Image
import numba as nb
import numpy as np
@nb.njit(parallel=True)
def new_RainbowFilter(img):
intensity = img.sum(axis=-1)
row , col = img.shape[:2]
for r in nb.prange(row):
for c in nb.prange(col):
i = (intensity[r,c] r c)
x = 1 - abs(((i / 60) % 2) - 1)
i %= 360
res = np.zeros(3)
if (i >= 0 and i < 60 ): res = np.array([1, x, 0])
elif (i >= 60 and i < 120): res = np.array([x, 1, 0])
elif (i >= 120 and i < 180): res = np.array([0, 1, x])
elif (i >= 180 and i < 240): res = np.array([0, x, 1])
elif (i >= 240 and i < 300): res = np.array([x, 0, 1])
elif (i >= 300 and i < 360): res = np.array([1, 0, x])
img[r,c] = res * 255
return img
im = Image.open('cat.jpg')
img = np.asarray(im)
img = new_RainbowFilter(img)
im = Image.fromarray(img)
im.save('rainbow_im.png')
CodePudding user response:
I was intrigued by this and decided to have a go at optimising the code from @I'mahdi.
My ideas were as follows:
Create and zero the output image up-front and avoid writing the already zeroed elements in the main loops
Only use parallelised
nb.prange()
for the outer loop since, if you have 12 CPU cores, that will already create 12 threadsAvoid creating a new 3-element Numpy array in each iteration to assign to the RGB elements of the output array - just assign the two non-zero values directly
Drastically reduce the number of tests in the
if
statements. The original code uses up to 12 tests to determine in which of the 6 sectorsi
falls. It will do all 12 ifi
is in the last sector. My code does it in 2-4 tests, more like a binary search.
#!/usr/bin/env python3
from PIL import Image
import numba as nb
import numpy as np
def Rainbow(i):
x = 1 - abs(((i / 60) % 2) - 1)
i %= 360
if (i >= 0 and i < 60 ): r,g,b = 1, x, 0
if (i >= 60 and i < 120): r,g,b = x, 1, 0
if (i >= 120 and i < 180): r,g,b = 0, 1, x
if (i >= 180 and i < 240): r,g,b = 0, x, 1
if (i >= 240 and i < 300): r,g,b = x, 0, 1
if (i >= 300 and i < 360): r,g,b = 1, 0, x
res = (int(r * 255), int(g * 255), int(b * 255))
return res
def RainbowFilter(img):
for x in range(img.size[0]):
for y in range(img.size[1]):
intensity = sum(img.getpixel((x, y)))
img.putpixel((x, y), Rainbow(intensity x y))
return img
@nb.njit(parallel=True)
def imahdi(img):
intensity = img.sum(axis=-1)
row , col = img.shape[:2]
for r in nb.prange(row):
for c in nb.prange(col):
i = (intensity[r,c] r c)
x = 1 - abs(((i / 60) % 2) - 1)
i %= 360
res = np.zeros(3)
if (i >= 0 and i < 60 ): res = np.array([1, x, 0])
elif (i >= 60 and i < 120): res = np.array([x, 1, 0])
elif (i >= 120 and i < 180): res = np.array([0, 1, x])
elif (i >= 180 and i < 240): res = np.array([0, x, 1])
elif (i >= 240 and i < 300): res = np.array([x, 0, 1])
elif (i >= 300 and i < 360): res = np.array([1, 0, x])
img[r,c] = res * 255
return img
@nb.njit(parallel=True)
def mark(img):
intensity = img.sum(axis=-1)
row , col = img.shape[:2]
# Create zeroed result image
res = np.zeros_like(img)
for r in nb.prange(row):
# Only make outer loop parallel else inner one will make more threads than cores
for c in range(col):
i = (intensity[r,c] r c)
x = 1 - abs(((i / 60) % 2) - 1)
x = int(x * 255)
i %= 360
# Split the problem space in half in one test - like binary search
if i < 180:
if i < 60:
# Don't create whole new array here
# Don't assign zeroes, array is empty already
res[r,c,0] = 255
res[r,c,1] = x
elif i < 120:
res[r,c,0] = x
res[r,c,1] = 255
else:
res[r,c,1] = 255
res[r,c,2] = x
else:
if i < 240:
res[r,c,1] = x
res[r,c,2] = 255
elif i < 300:
res[r,c,0] = x
res[r,c,2] = 255
else:
res[r,c,0] = 255
res[r,c,2] = x
return res
orig = Image.open('cat.jpg')
res = RainbowFilter(orig)
res.save('result.png')
im = np.asarray(orig)
res = imahdi(im)
Image.fromarray(res).save('imahdi.ppm')
res = mark(im)
Image.fromarray(res).save('mark.ppm')
Here are the timings:
In [17]: %timeit res = RainbowFilter(orig)
11.7 s ± 80 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [18]: %timeit res = imahdi(im)
1.52 s ± 4.81 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [13]: %timeit res=mark(im)
35.6 ms ± 928 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)