提高代码效率:滑动窗口的标准偏差

时间:2013-08-24 14:57:27

标签: python optimization python-2.7 numpy

我正在尝试改进为图像的每个像素计算位于像素附近的像素的标准偏差的函数。我的函数使用两个嵌入式循环来运行矩阵,这是我程序的瓶颈。我想有可能通过numpy摆脱循环来改善它,但我不知道如何继续。 欢迎任何建议!

问候

def sliding_std_dev(image_original,radius=5) :
    height, width = image_original.shape
    result = np.zeros_like(image_original) # initialize the output matrix
    hgt = range(radius,height-radius)
    wdt = range(radius,width-radius)
    for i in hgt:
        for j in wdt:
            result[i,j] = np.std(image_original[i-radius:i+radius,j-radius:j+radius])
    return result

4 个答案:

答案 0 :(得分:26)

酷技巧:你可以只计算平方值之和和窗口中值的总和来计算标准偏差。

因此,您可以使用数据上的统一过滤器非常快速地计算标准偏差:

from scipy.ndimage.filters import uniform_filter

def window_stdev(arr, radius):
    c1 = uniform_filter(arr, radius*2, mode='constant', origin=-radius)
    c2 = uniform_filter(arr*arr, radius*2, mode='constant', origin=-radius)
    return ((c2 - c1*c1)**.5)[:-radius*2+1,:-radius*2+1]

这比原始功能荒谬更快。对于1024x1024阵列和半径20,旧功能需要34.11秒,新功能需要 0.11秒,速度提高300倍。


这如何以数学方式工作?它计算每个窗口的数量sqrt(mean(x^2) - mean(x)^2)。我们可以从标准差sqrt(mean((x - mean(x))^2))推导出这个数量,如下所示:

E成为期望运算符(基本上是mean()),X是数据的随机变量。然后:

E[(X - E[X])^2]
= E[X^2 - 2X*E[X] + E[X]^2]
= E[X^2] - E[2X*E[X]] + E[E[X]^2](通过期望运算符的线性度)
= E[X^2] - 2E[X]*E[X] + E[X]^2(再次通过线性,以及E[X]是常数的事实)
= E[X^2] - E[X]^2

证明使用这种技术计算的数量在数学上等于标准偏差。

答案 1 :(得分:12)

在图像处理中最常用的方法是使用求和区域表,这是1984年在this paper中引入的一个想法。想法是,当你通过添加一个窗口来计算数量时,并移动窗口,例如右边一个像素,您不需要在新窗口中添加所有项目,只需从总数中减去最左侧的列,并添加新的最右侧列。因此,如果您在数组的两个维度上创建累积和数组,则可以在窗口上获得具有几个总和和减法的总和。如果为数组及其正方形保留求和区域表,则很容易得到这两者的差异。这是一个实现:

def windowed_sum(a, win):
    table = np.cumsum(np.cumsum(a, axis=0), axis=1)
    win_sum = np.empty(tuple(np.subtract(a.shape, win-1)))
    win_sum[0,0] = table[win-1, win-1]
    win_sum[0, 1:] = table[win-1, win:] - table[win-1, :-win]
    win_sum[1:, 0] = table[win:, win-1] - table[:-win, win-1]
    win_sum[1:, 1:] = (table[win:, win:] + table[:-win, :-win] -
                       table[win:, :-win] - table[:-win, win:])
    return win_sum

def windowed_var(a, win):
    win_a = windowed_sum(a, win)
    win_a2 = windowed_sum(a*a, win)
    return (win_a2 - win_a * win_a / win/ win) / win / win

要看到这个有效:

>>> a = np.arange(25).reshape(5,5)
>>> windowed_var(a, 3)
array([[ 17.33333333,  17.33333333,  17.33333333],
       [ 17.33333333,  17.33333333,  17.33333333],
       [ 17.33333333,  17.33333333,  17.33333333]])
>>> np.var(a[:3, :3])
17.333333333333332
>>> np.var(a[-3:, -3:])
17.333333333333332

这比基于卷积的方法要快几行。

答案 2 :(得分:3)

首先,有多种方法可以做到这一点。

这不是最有效的速度,但使用scipy.ndimage.generic_filter将允许您在移动窗口上轻松应用任意python函数。

作为一个简单的例子:

result = scipy.ndimage.generic_filter(data, np.std, size=2*radius)

请注意,边界条件可由mode kwarg控制。


另一种方法是使用一些不同的跨步技巧来查看有效移动窗口的数组,然后沿最后一个轴应用np.std。 (注意:这取自我之前的一个答案:https://stackoverflow.com/a/4947453/325565

def strided_sliding_std_dev(data, radius=5):
    windowed = rolling_window(data, (2*radius, 2*radius))
    shape = windowed.shape
    windowed = windowed.reshape(shape[0], shape[1], -1)
    return windowed.std(axis=-1)

def rolling_window(a, window):
    """Takes a numpy array *a* and a sequence of (or single) *window* lengths
    and returns a view of *a* that represents a moving window."""
    if not hasattr(window, '__iter__'):
        return rolling_window_lastaxis(a, window)
    for i, win in enumerate(window):
        if win > 1:
            a = a.swapaxes(i, -1)
            a = rolling_window_lastaxis(a, win)
            a = a.swapaxes(-2, i)
    return a

def rolling_window_lastaxis(a, window):
    """Directly taken from Erik Rigtorp's post to numpy-discussion.
    <http://www.mail-archive.com/numpy-discussion@scipy.org/msg29450.html>"""
    if window < 1:
       raise ValueError, "`window` must be at least 1."
    if window > a.shape[-1]:
       raise ValueError, "`window` is too long."
    shape = a.shape[:-1] + (a.shape[-1] - window + 1, window)
    strides = a.strides + (a.strides[-1],)
    return np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)

乍看起来有点难以理解这里发生了什么。不要插入我自己的答案之一,但我不想重新输入解释,所以看看这里:https://stackoverflow.com/a/4924433/325565如果你之前没有看到这种“跨越式”的技巧。

如果我们将时序与100x100随机浮点数组进行比较,radius为5,则比原始版本或generic_filter版本快10倍。但是,此版本的边界条件没有灵活性。 (它与您目前正在进行的操作相同,而generic_filter版本以牺牲速度为代价为您提供了很大的灵活性。)

# Your original function with nested loops
In [21]: %timeit sliding_std_dev(data)
1 loops, best of 3: 237 ms per loop

# Using scipy.ndimage.generic_filter
In [22]: %timeit ndimage_std_dev(data)
1 loops, best of 3: 244 ms per loop

# The "stride-tricks" version above
In [23]: %timeit strided_sliding_std_dev(data)
100 loops, best of 3: 15.4 ms per loop

# Ophion's version that uses `np.take`
In [24]: %timeit new_std_dev(data)
100 loops, best of 3: 19.3 ms per loop

“stride-tricks”版本的缺点是,与“普通”跨步滚动窗口技巧不同,此版本制作副本,并且 大于原始数组。如果在大型阵列上使用它,遇到内存问题! (另一方面,它基本上相当于@Ophion在内存使用和速度方面的答案。这只是做同样事情的另一种方法。)

答案 3 :(得分:1)

您可以先获取索引,然后使用np.take形成新数组:

def new_std_dev(image_original,radius=5):
    cols,rows=image_original.shape

    #First obtain the indices for the top left position
    diameter=np.arange(radius*2)
    x,y=np.meshgrid(diameter,diameter)
    index=np.ravel_multi_index((y,x),(cols,rows)).ravel()

    #Cast this in two dimesions and take the stdev
    index=index+np.arange(rows-radius*2)[:,None]+np.arange(cols-radius*2)[:,None,None]*(rows)
    data=np.std(np.take(image_original,index),-1)

    #Add the zeros back to the output array
    top=np.zeros((radius,rows-radius*2))
    sides=np.zeros((cols,radius))

    data=np.vstack((top,data,top))
    data=np.hstack((sides,data,sides))
    return data

首先生成一些随机数据并检查时间:

a=np.random.rand(50,20)

print np.allclose(new_std_dev(a),sliding_std_dev(a))
True

%timeit sliding_std_dev(a)
100 loops, best of 3: 18 ms per loop

%timeit new_std_dev(a)
1000 loops, best of 3: 472 us per loop

对于较大的数组,只要你有足够的内存,它总是更快:

a=np.random.rand(200,200)

print np.allclose(new_std_dev(a),sliding_std_dev(a))
True

%timeit sliding_std_dev(a)
1 loops, best of 3: 1.58 s per loop

%timeit new_std_dev(a)
10 loops, best of 3: 52.3 ms per loop

对于非常小的数组,原始函数更快,看起来收敛点是hgt*wdt >50时的。需要注意的是你的函数是采用方框并将std dev放在右下角的索引中,而不是在索引周围进行采样。这是故意的吗?