提高 getpixel 和 putpixel 的速度

Improve speed of getpixel and putpixel

使用 PIL,我使用 getpixelsetpixel 将彩虹滤镜应用于给定图像。一个问题,这种方法很慢。完成一张图像大约需要 10 秒。

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

你能帮助我改进我的特定算法,使用专门的 Numpy 或 Pillow 功能来解决提到的问题吗?

您可以将 image 转换为 NumPy.array,然后使用 numba 来提高速度,如下所示:

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

我对此很感兴趣,并决定尝试优化@I'mahdi 的代码。

我的想法如下:

  • 创建并归零输出图像up-front并避免在主循环中写入已经归零的元素

  • 只对 outer 循环使用并行化的 nb.prange() 因为,如果你有 12 个 CPU 核心,那将已经创建 12线程

  • 避免在每次迭代中创建新的 3 元素 Numpy 数组以分配给输出数组的 RGB 元素 - 只需直接分配两个 non-zero 值

  • 大幅减少 if 语句中的测试数量。原始代码使用多达 12 次测试来确定 i 属于 6 个扇区中的哪个扇区。如果 i 在最后一个扇区,它将执行所有 12 个操作。我的代码在 2-4 次测试中完成,更像是二进制搜索。


#!/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')

时间安排如下:

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)