Home > Blockchain >  Improve speed of getpixel and putpixel
Improve speed of getpixel and putpixel

Time:04-04

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')

Input image Result 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 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)
  • Related