加速" for-loop"在迭代次数高达40,000的图像分析中

时间:2015-11-09 15:32:19

标签: python performance for-loop numpy equality

此代码的先决条件的详细信息很长,所以我会尽力总结。 WB / RG / BYColor是基本图像,FIDO是应用于此基本图像的叠加。 S_wb / rg / by是最终输出图像。 WB / RG / BYColor与FIDO的大小相同。

对于FIDO中的每个唯一元素,我们要计算基本图像中该区域的平均颜色。下面的代码执行此操作,但因为numFIDO非常大(最多40,000),这需要长时间

计算三个独立RGB通道的平均值。

sX=200
sY=200
S_wb = np.zeros((sX, sY))
S_rg = np.zeros((sX, sY))
S_by = np.zeros((sX, sY))
uniqueFIDOs, unique_counts = np.unique(FIDO, return_counts=True) 
numFIDOs = uniqueFIDOs.shape  
for i in np.arange(0,numFIDOs[0]):
    Lookup = FIDO==uniqueFIDOs[i]
    # Get average of color signals for this FIDO
    S_wb[Lookup] = np.sum(WBColor[Lookup])/unique_counts[i]
    S_rg[Lookup] = np.sum(RGColor[Lookup])/unique_counts[i]
    S_by[Lookup] = np.sum(BYColor[Lookup])/unique_counts[i]

这需要大约7.89秒才能运行,不会这么长,但这将包含在另一个循环中,所以它会建立起来!

我尝试了矢量化(如下所示),但我无法做到

FIDOsize = unique_counts[0:numFIDOs[0]:1]
Lookup = FIDO ==uniqueFIDOs[0:numFIDOs[0]:1]
S_wb[Lookup] = np.sum(WBColor[Lookup])/FIDOsize
S_rg[Lookup] = np.sum(RGColor[Lookup])/FIDOsize
S_by[Lookup] = np.sum(BYColor[Lookup])/FIDOsize

数组大小匹配错误

5 个答案:

答案 0 :(得分:5)

根据我的时间,这比原始方法快10倍。我测试了这些数组:

import numpy as np

sX=200
sY=200

FIDO = np.random.randint(0, sX*sY, (sX, sY))
WBColor = np.random.randint(0, sX*sY, (sX, sY))
RGColor = np.random.randint(0, sX*sY, (sX, sY))
BYColor = np.random.randint(0, sX*sY, (sX, sY))

这是我定时的部分:

import collections

colors = {'wb': WBColor, 'rg': RGColor, 'by': BYColor}
planes = colors.keys()
S = {plane: np.zeros((sX, sY)) for plane in planes}

for plane in planes:
    counts = collections.defaultdict(int)
    sums = collections.defaultdict(int)
    for (i, j), f in np.ndenumerate(FIDO):
        counts[f] += 1
        sums[f] += colors[plane][i, j]
    for (i, j), f in np.ndenumerate(FIDO):
        S[plane][i, j] = sums[f]/counts[f]

可能是因为即使Python中的循环很慢,这也会减少数据的遍历。

请注意,如果FIDO中存在少量唯一值,则原始版本会更快。对于大多数情况,这大致需要相同的时间。

答案 1 :(得分:4)

您的代码不是最佳的,因为您扫描FIDO中每个区域的所有图像。更好的方法是对每个区域的像素进行分组并首先计算均值。 pandas为这样的计算提供了很好的工具(这里只有一条运河)。然后你跨越区域的手段:

import numpy as np
import pandas as pd     
sX=200
sY=200
Nreg=sX*sY
WBColor=np.random.randint(0,256,(sX,sY))
FIDO=np.random.randint(0,Nreg,(sX,sY))


def oldloop():
    S_wb = np.zeros((sX, sY))
    uniqueFIDOs, unique_counts = np.unique(FIDO, return_counts=True) 
    numFIDOs = uniqueFIDOs.shape 
    for i in np.arange(0,numFIDOs[0]):
        Lookup = FIDO==uniqueFIDOs[i]
        S_wb[Lookup] = np.sum(WBColor[Lookup])/unique_counts[i]
    return S_wb

def newloop():
    index=pd.Index(FIDO.flatten(),name='region')
    means= pd.DataFrame(WBColor.flatten(),index).groupby(level='region').mean()
    lookup=np.zeros(Nreg)
    lookup[means.index]=means.values
    return lookup[FIDO]

在这种情况下,这大约快200倍:

In [32]: np.allclose(oldloop(),newloop())
Out[32]: True

In [33]: %timeit -n1 oldloop()
1 loops, best of 3: 3.92 s per loop

In [34]: %timeit -n100 newloop()
100 loops, best of 3: 20.5 ms per loop    

修改

另一种很酷的现代方法是使用numba。你编写(非常)基本的python代码以接近C的速度运行:

from numba import jit

@jit
def numbaloops():
    counts=np.zeros(Nreg)
    sums=np.zeros(Nreg)
    S = np.empty((sX, sY))
    for x in range(sX):
        for y in range(sY):
            region=FIDO[x,y]
            value=WBColor[x,y]
            counts[region]+=1
            sums[region]+=value
    for x in range(sX):
        for y in range(sY):
            region=FIDO[x,y]
            S[x,y]=sums[region]/counts[region]
    return S                

现在你快了大约4000倍:

In [45]: np.allclose(oldloop(),numbaloops())
Out[45]: True

In [46]: %timeit -n1000 numbaloops()
1000 loops, best of 3: 1.06 ms per loop 

答案 2 :(得分:4)

正如@lejlot之前建议的那样,代码很难进行矢量化。除非您事先知道哪些像素属于每个FIDO,否则它不能并行运行。我不知道你是否把FIDO称为超像素,但我通常会处理这类问题,而我迄今为止找到的最佳解决方案如下:

  • 展平数据:

    data = data.reshape(-1, 3)
    labels = FIDO.copy()
    

    此处data是您的(Width, Height, 3)图片,而不是您拥有的单独3个图片。它变得扁平化为(Width * Height, 3)

  • Relabel FIDO到0..N-1范围,其中N = num unique FIDO:

    from skimage.segmentation import relabel_sequential
    
    labels = relabel_sequential(labels)[0]
    labels -= labels.min()
    

    上述内容从scikit-image开始,将您的FIDO数组转换为[0, N-1]范围,以后更容易使用。

  • 最后, cython 中的代码是计算每个FIDO的平均值的简单函数;因为它们从0到N排序,你可以在一维数组中完成长度为N):

    def fmeans(double[:, ::1] data, long[::1] labels, long nsp):
        cdef long n,  N = labels.shape[0]
        cdef int K = data.shape[1]
        cdef double[:, ::1] F = np.zeros((nsp, K), np.float64)
        cdef int[::1] sizes = np.zeros(nsp, np.int32)
        cdef long l, b
        cdef double t
    
        for n in range(N):
            l = labels[n]
            sizes[l] += 1
    
            for z in range(K):
                t = data[n, z]
                F[l, z] += t
    
        for n in range(nsp):
            for z in range(K):
                F[n, z] /= sizes[n]
    
    return np.asarray(F)
    

您可以稍后调用该函数(一旦使用cython编译),就像:

一样简单
mean_colors = fmeans(data, labels.flatten(), labels.max()+1) # labels.max()+1 == N

然后可以将平均颜色的图像恢复为:

mean_img = mean_colors[labels]

如果您不想在cython中编码,scikit-image也通过使用图形结构和networkx为此提供绑定,但速度要慢得多:

http://scikit-image.org/docs/dev/auto_examples/plot_rag_mean_color.html

以上示例包含使用每个超像素的平均颜色为labels1(您的FIDO)获取图像所需的函数调用。

注意:cython方法要快得多,因为它不是迭代唯一FIDO N的数量,而是为每个人扫描图像(大小M = Width x Height)这个只迭代图像ONCE。因此,计算成本大约为O(M+N)而不是原始方法的O(M*N)

示例测试:

import numpy as np
from skimage.segmentation import relabel_sequential

sX=200
sY=200

FIDO = np.random.randint(0, sX*sY, (sX, sY))
data = np.random.rand(sX, sY, 3) # Your image

扁平化和重新标记:

data = data.reshape(-1, 3)
labels = relabel_sequential(FIDO)[0]
labels -= labels.min()

获得平均值:

>>> %timeit color_means = fmeans(data, labels.flatten(), labels.max()+1)
1000 loops, best of 3: 520 µs per loop

对于200x200图像,需要 0.5ms (半毫秒):

print labels.max()+1 # --> 25787 unique FIDO
print color_means.shape # --> (25287, 3), the mean color of each FIDO

您可以使用智能索引恢复平均颜色的图像:

mean_image = color_means[labels]
print mean_image.shape # --> (200, 200, 3)

我怀疑你能用原始的python方法获得这种速度(或者至少,我没有找到方法)。

答案 3 :(得分:3)

简而言之:python中的循环很慢。您应该执行以下操作之一:

  • 矢量化(你尝试过,但你声称“它不起作用”),你的意思是什么,但不工作?矢量化(如果可能)始终有效
  • 切换到Cython,并将迭代器值声明为{{1}}

以上两种方法均基于将瓶颈环转换为C环。

答案 4 :(得分:3)

这已经在Scipy中实现了,所以你可以这样做:

from scipy.ndimage.measurements import mean as labeled_mean

labels = np.arange(FIDO.max()+1, dtype=int)
S_wb = labeled_mean(WBColor, FIDO, labels)[FIDO]
S_rg = labeled_mean(RGColor, FIDO, labels)[FIDO]
S_by = labeled_mean(BYColor, FIDO, labels)[FIDO]

这假设FIDO包含相对较小的整数。如果不是这种情况,您可以通过np.unique(FIDO, return_inverse=True)转换它。

这个简单的代码比原始代码快约1000倍,200x200图像和FIDO包含从0到40,000的随机整数。