如何直接将函数映射到列表的列表上?

8
我已经为图像构建了一个像素分类器,对于每个像素,我想定义它属于哪个预定义颜色簇。这个分类器可以工作,但是处理每张图片的时间大约是5分钟,我认为有些不符合Python语言特性的地方需要优化。
我们如何能够直接在列表的列表上映射该函数?
#First I convert my image to a list
#Below list represents a true image size
list1=[[255, 114, 70],
[120, 89, 15],
[247, 190, 6],
[41, 38, 37],
[102, 102, 10],
[255,255,255]]*3583180

然后,我们定义要映射颜色的聚类以及执行此操作的函数(该函数来自PIL库)。

#Define colors of interest
#Colors of interest 
RED=[255, 114, 70]
DARK_YELLOW=[120, 89, 15]
LIGHT_YELLOW=[247, 190, 6]
BLACK=[41, 38, 37]
GREY=[102, 102, 10]
WHITE=[255,255,255]

Colors=[RED, DARK_YELLOW, LIGHT_YELLOW, GREY, BLACK, WHITE]

#Function to find closes cluster by root and squareroot distance of RGB
def distance(c1, c2):
    (r1,g1,b1) = c1
    (r2,g2,b2) = c2
    return math.sqrt((r1 - r2)**2 + (g1 - g2) ** 2 + (b1 - b2) **2)

现在需要做的是匹配每个颜色,并使用原始颜色列表中匹配的索引创建一个新列表:

Filt_lab=[]

#Match colors and make new list with indexed colors
for pixel in tqdm(list1):
    closest_colors = sorted(Colors, key=lambda color: distance(color, pixel))
    closest_color = closest_colors[0]

    for num, clust in enumerate(Colors):
        if list(clust) == list(closest_color):
            Filt_lab.append(num)

运行单个图像大约需要5分钟,这很好,但很可能有一种方法可以大大缩短这个时间?

36% | ███▌ | 7691707/21499080 [01:50 <03:18,69721.86it/s]

Filt_lab的预期结果:

[0, 1, 2, 4, 3, 5]*3583180

1
虽然不是高性能计算专家,但我的直觉是:放弃列表嵌套列表。你应该使用平坦的数组。你的图像应该是[r1, g1, b1, r2, g2, b2, ...]或者3个数组[r1, r2, ...], [g1, g2, ...], [b1, b2, ...]或者一个多路复用器(利用 int 的前几位表示 r,接下来的 8 位表示 g...)。然后,不要在 for 循环中逐像素计算差异,因为这样会使用高度非连续的内存区域。对于每个类别,计算红色数组、绿色数组和蓝色数组的差异。然后求和差异,然后分类。并且使用 numpy 来实现向量化运算。 - GPI
谢谢。现在我已经有了每种颜色的距离值数组。现在需要解决的问题是如何从这些10个像素数组中找到具有最低值的数组,并将其映射到字典中? - Rivered
最好使用Fortran来解决这类问题,而不是Python。 - Tejas Shetty
关于改进代码(而不是修复问题)的问题,请前往CodeReview.SE网站。SO适用于无法工作的代码。 - outis
请注意,newbedev是Stack Overflow的爬虫程序,请不要链接到它。相反,使用谷歌搜索文本(可选加上“site:stackoverflow.com”),找到正确的站内链接,而不是给予爬虫更多不应得的流量。 - Zoe stands with Ukraine
7个回答

10

您可以使用Numba的即时编译器大大加快代码速度。思路是通过迭代每个像素的颜色来实时构建classified_pixels。颜色存储在一个Numpy数组中,其中索引是颜色键。整个计算过程可以并行运行,避免了创建和读写许多临时数组以及分配大量内存。此外,可以调整数据类型,使得结果数组在内存中更小(因此写入/读取更快)。以下是最终脚本:

import numpy as np
import numba as nb

@nb.njit('int32[:,::1](int32[:,:,::1], int32[:,::1])', parallel=True)
def classify(image, colors):
    classified_pixels = np.empty((image.shape[0], image.shape[1]), dtype=np.int32)
    for i in nb.prange(image.shape[0]):
        for j in range(image.shape[1]):
            minId = -1
            minValue = 256*256 # The initial value is the maximum possible value
            ir, ig, ib = image[i, j]
            # Find the color index with the minimum difference
            for k in range(len(colors)):
                cr, cg, cb = colors[k]
                total = (ir-cr)**2 + (ig-cg)**2 + (ib-cb)**2
                if total < minValue:
                    minValue = total
                    minId = k
            classified_pixels[i, j] = minId
    return classified_pixels

# Representative image
np.random.seed(42)
imarray = np.random.rand(3650,2000,3) * 255
image = imarray.astype(np.int32)

# Colors of interest
RED = [255, 0, 0]
DARK_YELLOW = [120, 89, 15]
LIGHT_YELLOW = [247, 190, 6]
BLACK = [41, 38, 37]
GREY = [102, 102, 10]
WHITE = [255, 255, 255]

# Build a Numpy array rather than a dict
colors = np.array([RED, DARK_YELLOW, LIGHT_YELLOW, GREY, BLACK, WHITE], dtype=np.int32)

# Actual classification
classified_pixels = classify(image, colors)

# Convert array to list
cl_pixel_list = classified_pixels.reshape(classified_pixels.shape[0] * classified_pixels.shape[1]).tolist()

# Print
print(cl_pixel_list[0:10])

在我的 6 核机器上,这个实现大约需要0.19 秒。它比迄今为止提供的上一个答案快15 倍,比最初的实现快 1000 多倍。请注意,由于 classify 函数非常快,因此大约有一半的时间花费在 tolist()上。


我在不支持 Numba 的 Linux 版本上安装 Numba 遇到了困难。当我运行您的脚本时,出现以下信息:NumbaWarning: The TBB threading layer requires TBB version 2019.5 or later i.e 。另外,cl_pixel_list[0:10] 的输出为 [5, 1, 3, 1, 0, 4, 1, 4, 3, 3] ,与 [4, 1, 4, 3, 3, 4, 0, 2, 4, 4] 不同。 - Rivered
1
Numba支持多个线程后端。我认为使用OpenMP更好,因为它往往被广泛支持和高效。所以,如果您在TBB上遇到问题,请考虑使用OpenMP。如果您在OpenMP上遇到问题,则有一个默认的嵌入式线程运行时。如果您仍然遇到所有其他后端的问题,那么您可以只需删除“parallel=True”,并将“prange”替换为“range”,从而得到一个顺序实现。生成的代码仍然比当前备选解决方案快得多。 - Jérôme Richard
1
我检查了代码,并得到了与您上次回答(8月4日发布)使用相同的种子相同的结果。请注意,我手动将种子设置为42以获得可重复的结果。因此,如果您想将此代码的结果与另一个代码进行比较,则需要设置种子(为相同的值)或对两个代码都使用相同的输入值 - Jérôme Richard
谢谢,无论如何,你的回答都很周到和快速! - Rivered

3

看起来你的电脑非常快 :)

这是我系统上代码的中间输出:

  0%|          | 5635/21499080 [00:44<46:51:14, 127.43it/s]

但我已经使用 TensorFlow 重写了你的代码,现在它运行大约需要3秒钟 :)

import math
import os
from time import time

import numpy as np
from tqdm import tqdm

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'  # or any {'0', '1', '2'}
import tensorflow as tf

list1 = [[255, 114, 70],
         [120, 89, 15],
         [247, 190, 6],
         [41, 38, 37],
         [102, 102, 10],
         [255, 255, 255]] * 3583180
list1_np = tf.constant(list1)
RED = [255, 114, 70]
DARK_YELLOW = [120, 89, 15]
LIGHT_YELLOW = [247, 190, 6]
BLACK = [41, 38, 37]
GREY = [102, 102, 10]
WHITE = [255, 255, 255]
Colors = tf.constant([RED, DARK_YELLOW, LIGHT_YELLOW, GREY, BLACK, WHITE])
t = time()
ans = tf.argmin(np.array([tf.math.reduce_sum((list1_np - c) ** 2, axis=1) for c in Colors]), axis=0)
print(time() - t)
print(ans)


# and now your code

def distance(c1, c2):
    (r1, g1, b1) = c1
    (r2, g2, b2) = c2
    return math.sqrt((r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2)


t = time()
Filt_lab = []

# Match colors and make new list with indexed colors
for pixel in tqdm(list1):
    closest_colors = sorted(Colors, key=lambda color: distance(color, pixel))
    closest_color = closest_colors[0]

    for num, clust in enumerate(Colors):
        if list(clust) == list(closest_color):
            Filt_lab.append(num)
print(time() - t)

输出结果为:

3.1714584827423096
tf.Tensor([0 1 2 ... 4 3 5], shape=(21499080,), dtype=int64)
  0%|          | 951/21499080 [00:07<47:36:50, 125.42it/s]

注意1:如果删除第二部分,可以省略一些导入。

注意2:比较距离时,无需使用平方根。


3

使用numpy:

import numpy as np

#Representative image
imarray = np.uint64(np.random.rand(3583180*6,3) * 255)
#Or make with np.uint64(your_list_of_lists) if you already have that list lists; Axes: pixel, color_channels

RED=[255, 114, 70]
DARK_YELLOW=[120, 89, 15]
LIGHT_YELLOW=[247, 190, 6]
BLACK=[41, 38, 37]
GREY=[102, 102, 10]
WHITE=[255,255,255]
#your list of colors
Colors=[RED, DARK_YELLOW, LIGHT_YELLOW, GREY, BLACK, WHITE]
#again converted to numpy
Colors_np = np.uint64(Colors) #axes: colors, color_channels

#Compute all distance, or rather the squares at that has no
#effect on which is minimal and we can drop the sqrt computation then
#Extend both numpy arrays to be haves axes [pixel, color, color_channels] with `np.newaxis`, 
#take the difference, 
#then the square, 
#and then the sum across color channels
distances = np.sum((imarray[:,np.newaxis, :] - Colors_np[np.newaxis, :, :])**2, 2)
#difference has axes [pixel, color, color_channels], summed over axes 2 => [pixel, color] axes remain
#You want index of minimum over color axis, so:
closest_color_indices = np.argmin(distances, 1)

#written as one line and timed with %timeit in ipython (on a single core):
#%timeit np.argmin(np.sum((imarray[:,np.newaxis, :] - Colors_np[np.newaxis, :, :])**2, 2), 1)
#6.11 s +- 79.4 ms per loop (mean +- std. dev. of 7 runs, 1 loop each)

这需要大约6.11秒来处理3583180*6=21499080个像素和6种可能的颜色。


1
你可以尝试创建并使用一个具有256 * 256 * 256个元素的查找表。
import numpy as np
from scipy.spatial import cKDTree
imarray = np.uint8(np.random.rand(3583180*6,3) * 255)

code=np.array([1, 256, 256*256])
RED=[255, 114, 70]
DARK_YELLOW=[120, 89, 15]
LIGHT_YELLOW=[247, 190, 6]
GREY=[102, 102, 10]
BLACK=[41, 38, 37]
WHITE=[255,255,255]
#your list of colors
Colors=[RED, DARK_YELLOW, LIGHT_YELLOW, GREY, BLACK, WHITE]
#again converted to numpy
Colors_np = np.uint8(Colors) #axes: colors, color_channels

x=np.arange(256, dtype=np.uint8)
rgb=np.array(np.meshgrid(x, x, x)).T.reshape(-1,3)
rgb[:,[0, 1, 2]]=rgb[:,[1, 0, 2]] #swap columns
# rgb is table all colors

voronoi_kdtree = cKDTree(Colors_np) # Voronoi by base Colors

_, test_point_regions = voronoi_kdtree.query(rgb)
# test_point_regions is lookup table (LUT)

result=test_point_regions[np.dot(imarray, code)]

assert np.all(test_point_regions[np.dot(Colors_np, code)]==np.array([0, 1, 2, 3, 4, 5]))

1

只是快速加速:

  1. 您可以省略 math.sqrt()
  2. 创建颜色字典而不是列表(这样您就不必在每次迭代中搜索索引)
  3. 使用 min() 而不是 sorted()
from tqdm import tqdm

list1 = [
    [255, 114, 70],
    [120, 89, 15], 
    [247, 190, 6],
    [41, 38, 37],
    [102, 102, 10],
    [255, 255, 255],
] * 3583180


RED = [255, 0, 0]
DARK_YELLOW = [120, 89, 15]
LIGHT_YELLOW = [247, 190, 6]
BLACK = [41, 38, 37]
GREY = [102, 102, 10]
WHITE = [255, 255, 255]

# create a dictionary instead of a list:
Colors = {
    i: c
    for i, c in enumerate([RED, DARK_YELLOW, LIGHT_YELLOW, GREY, BLACK, WHITE])
}


# Function to find closes cluster by root and squareroot distance of RGB - EDIT: squareroot omitted 
def distance(c1, c2):
    (r1, g1, b1) = c1
    (r2, g2, b2) = c2
    return (r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2   # <-- you can ommit math.sqrt


Filt_lab = []

# Match colors and make new list with indexed colors
for pixel in tqdm(list1):
    # use min() instead of sorted:
    closest_color = min(
        Colors, key=lambda color: distance(Colors[color], pixel)
    )
    Filt_lab.append(closest_color)

在我的电脑上,速度从约108000.0it/s提高到约155000.00it/s。
注意:对于这种任务最好使用numpy库。

1
谢谢,我从每秒60k次迭代提高到了80k,看看是否还有可以改进的地方。 - Rivered

0

基本上,我认真考虑了@GPI的评论:“虽然不是高性能计算专家,但我的直觉是:放弃列表。这应该是平坦的数组。您的图像应为[r1,g1,b1,r2,g2,b2,...]或3个数组[r1,r2,...],[g1,g2,...],[b1,b2,...]或mux(使用int的前几位表示r,接下来的8位表示g...)。然后,您不应在for循环中逐像素计算差异,因为您正在使用高度非连续的内存区域。对于每个类别,计算红色数组的差异,然后是绿色,然后是蓝色。然后求和差异,然后分类。并使用numpy将其全部向量化。- GPI”

我已经完全重新绘制了脚本,并将图像保留为原样,而不是首先将其重塑为列表。只应用numpy操作,现在处理一个完整的图像需要几秒钟(与之前的5分钟相比)。可能仍然可以进行一些小的优化步骤。

from tqdm import tqdm
import numpy as np

#Representative image
imarray = np.random.rand(3650,2000,3) * 255
image=imarray.astype(np.uint64)

#Colors of interest
RED = [255, 0, 0]
DARK_YELLOW = [120, 89, 15]
LIGHT_YELLOW = [247, 190, 6]
BLACK = [41, 38, 37]
GREY = [102, 102, 10]
WHITE = [255, 255, 255]

# create a dictionary instead of a list:
Colors = {i: c for i, c in enumerate([RED, DARK_YELLOW, LIGHT_YELLOW, GREY, BLACK, WHITE])}

#Create a dictionary which will be filled with key of color and value of cumulative array with differences for every color
img_dictionary={}
for key, value in Colors.items():
    R=(image[:,:,0].astype(np.uint64)-value[0])**2
    G=(image[:,:,1].astype(np.uint64)-value[1])**2
    B=(image[:,:,2].astype(np.uint64)-value[2])**2
    Total=R+G+B
    img_dictionary[key] = Total

#Stack all arrays from dictionary
arr = np.stack(list(img_dictionary.values()))

#Find array with lowest difference and map it to new array
classified_pixels=np.argmin(arr, axis=0)

#Convert array to list
cl_pixel_list = classified_pixels.reshape((classified_pixels.shape[0] * classified_pixels.shape[1])).tolist()

#Print
print(cl_pixel_list[0:10])
>>>[4, 1, 4, 3, 3, 4, 0, 2, 4, 4]

1
两种可能的添加方式:您确定需要uint64吗?在我看来,您有从0到255的离散int值。一个字节就足够了np.ubyte,计算速度会快得多。我还会比较您的R/G/B数组计算速度:现在,您每个数组都要删除一个常量。相反,我会尝试将每个参考颜色变成与图像相同大小的数组(例如完全红色或深黄色),并通过numpy直接进行差异操作(例如从另一个堆栈中减去)。不确定这是否有帮助,但值得一试。 - GPI

0

我使用了列表推导式,本以为这样会更快,但最终结果却稍微慢了一些:

from tqdm import tqdm

#Representative image
list1 = [[255, 114, 70],
        [120, 89, 15], 
        [247, 190, 6],
        [41, 38, 37],
        [102, 102, 10],
        [255, 255, 255],] * 3583180

#Colors of interest
RED = [255, 0, 0]
DARK_YELLOW = [120, 89, 15]
LIGHT_YELLOW = [247, 190, 6]
BLACK = [41, 38, 37]
GREY = [102, 102, 10]
WHITE = [255, 255, 255]

# create a dictionary instead of a list:
Colors = {i: c for i, c in enumerate([RED, DARK_YELLOW, LIGHT_YELLOW, GREY, BLACK, WHITE])}


# Function to find closes cluster by root and squareroot distance of RGB - EDIT: squareroot omitted 
def distance(c1, c2):
    (r1, g1, b1) = c1
    (r2, g2, b2) = c2
    return (r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2   

#Attempt with list comprehension
Filt_lab = tqdm([(min(Colors, key=lambda color: distance(Colors[color], x))) for x in tqdm(list1)])

列表推导式之前: 59%|█████▉ | 12738323/21499080 [01:49<01:15, 116309.97it/s]

列表推导式之后: 100%|██████████| 21499080/21499080 [03:10<00:00, 112848.76it/s]


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接