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 threads
Avoid 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 sectors i falls. It will do all 12 if i 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)