使用步幅进行有效的移动平均滤波器

时间:2011-02-08 18:05:38

标签: python image-processing filter numpy

我最近在strides了解了answer to this post,并想知道如何使用它们来计算移动平均滤波器的效率高于我在this post中提出的算法(使用卷积滤波器) )。

这是我到目前为止所拥有的。它接受原始数组的视图然后将其滚动必要的量并将内核值相加以计算平均值。我知道边缘没有正确处理,但我可以在以后处理...有更好更快的方法吗?目标是过滤大小为5000x5000 x 16层的大型浮点数组,scipy.ndimage.filters.convolve的任务相当慢。

请注意,我正在寻找8邻居连接,即3x3滤镜取9个像素的平均值(焦点像素周围8个)并将该值分配给新图像中的像素。

import numpy, scipy

filtsize = 3
a = numpy.arange(100).reshape((10,10))
b = numpy.lib.stride_tricks.as_strided(a, shape=(a.size,filtsize), strides=(a.itemsize, a.itemsize))
for i in range(0, filtsize-1):
    if i > 0:
        b += numpy.roll(b, -(pow(filtsize,2)+1)*i, 0)
filtered = (numpy.sum(b, 1) / pow(filtsize,2)).reshape((a.shape[0],a.shape[1]))
scipy.misc.imsave("average.jpg", filtered)

编辑关于我如何看待这项工作的澄清:

当前代码:

  1. 使用stride_tricks生成一个像[[0,1,2],[1,2,3],[2,3,4] ...]这样的数组,它对应于过滤器内核的顶行。 / LI>
  2. 沿垂直轴滚动以获得内核[[10,11,12],[11,12,13],[13,14,15] ...]的中间行并将其添加到数组中我进入了1)
  3. 重复以获得内核的最后一行[[20,21,22],[21,22,23],[22,23,24] ...]。此时,我取每行的总和除以滤波器中的元素数量,给出每个像素的平均值,(移动1行和1列,边缘有一些奇怪,但我可以稍后再照顾。)
  4. 我所希望的是更好地使用stride_tricks直接获取9个值或内核元素的总和,对于整个数组,或者有人可以说服我另一个更有效的方法......

4 个答案:

答案 0 :(得分:26)

对于它的价值,这里是你如何使用“花哨的”跨步技巧来做到这一点。我昨天要发布这个帖子,但实际工作让我分心了! :)

@Paul& @eat都有很好的实现,使用各种其他方法来做到这一点。只是为了继续前面的问题,我想我会发布N维等价物。

然而,对于> 1D数组,您无法显着超越scipy.ndimage函数。 (scipy.ndimage.uniform_filter应该击败scipy.ndimage.convolve

此外,如果您正在尝试获取多维移动窗口,那么每当您无意中制作阵列副本时,就有可能导致内存使用量爆炸。虽然最初的“滚动”数组只是原始数组内存的一个视图,但复制数组的任何中间步骤都会产生比原始数组大数量级的副本(即Let's假设您正在使用100x100原始阵列...其中的视图(对于过滤器大小为(3,3))将为98x98x3x3,但使用与原始相同的内存。但是,任何副本都将使用该数量一个完整 98x98x3x3阵列的内存!!)

基本上,当您想要在ndarray的单轴上矢量化移动窗口操作时,使用疯狂的跨步技巧非常有用。它可以很容易地计算诸如移动标准偏差之类的东西,而且开销很小。当你想沿着多个轴开始这样做时,它是可能的,但你通常会有更专业的功能。 (例如scipy.ndimage等)

无论如何,这是你如何做到的:

import numpy as np

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)

def rolling_window(a, 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

filtsize = (3, 3)
a = np.zeros((10,10), dtype=np.float)
a[5:7,5] = 1

b = rolling_window(a, filtsize)
blurred = b.mean(axis=-1).mean(axis=-1)

所以当我们做b = rolling_window(a, filtsize)时得到的是一个8x8x3x3阵列,它实际上是与原始10x10阵列相同的内存视图。我们可以像沿着不同的轴一样容易地使用不同的滤波器尺寸,或者仅沿着N维阵列的选定轴操作(即,在4维阵列上的filtsize = (0,3,0,3)将给出6维视图)。

然后我们可以重复地将任意函数应用于最后一个轴,以有效地计算移动窗口中的内容。

但是,因为我们在mean(或std或其他)的每一步中存储比原始数组大得多的临时数组,所以这根本不是内存效率!它也不会非常快。

ndimage的等价物只是:

blurred = scipy.ndimage.uniform_filter(a, filtsize, output=a)

这将处理各种边界条件,就地“模糊”而不需要数组的临时副本,并且非常快。沿着一个轴将一个函数应用到移动窗口是一个很好的方法,但它们不是沿多个轴执行它的好方法,通常......

无论如何,只需我0.02美元......

答案 1 :(得分:7)

我不熟悉Python为此编写代码,但加速卷积的两种最佳方法是分离滤波器或使用傅里叶变换。

分离滤波器:卷积为O(M * N),其中M和N分别是图像和滤波器中的像素数。由于使用3乘3内核进行平均过滤相当于首先使用3乘1内核进行过滤,然后使用1乘3内核过滤,因此连续速度可以提高(3+3)/(3*3) =〜30%使用两个1-d内核进行卷积(随着内核变大,这显然会变得更好)。当然,你仍然可以在这里使用大步技巧。

傅里叶变换conv(A,B)相当于ifft(fft(A)*fft(B)),即直接空间中的卷积成为傅立叶空间中的乘法,其中A是您的图像, B是您的过滤器。由于傅立叶变换的(逐元素)乘法要求A和B的大小相同,B是size(A)的数组,其中内核位于图像的中心,其他地方为零。要将3乘3内核放置在数组的中心,您可能需要将A填充到奇数大小。根据你的傅立叶变换的实现,这可能比卷积快得多(如果你多次应用相同的滤波器,你可以预先计算fft(B),节省另外30%的计算时间。) / p>

答案 2 :(得分:4)

我有信心需要解决的一件事是你的视图数组b

它有一些来自未分配内存的项目,因此您将崩溃。

鉴于您对算法的新描述,需要修复的第一件事是您正在超越a的分配:

bshape = (a.size-filtsize+1, filtsize)
bstrides = (a.itemsize, a.itemsize)
b = numpy.lib.stride_tricks.as_strided(a, shape=bshape, strides=bstrides)

<强>更新

因为我还没有完全掌握这个方法,而且似乎有更简单的方法来解决这个问题,我只想把它放在这里:

A = numpy.arange(100).reshape((10,10))

shifts = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
B = A[1:-1, 1:-1].copy()
for dx,dy in shifts:
    xstop = -1+dx or None
    ystop = -1+dy or None
    B += A[1+dx:xstop, 1+dy:ystop]
B /= 9

......这似乎是直截了当的方法。唯一无关的操作是它只分配和填充B一次。无论如何,所有的添加,划分和索引都必须完成。如果您正在执行16个波段,如果您的目的是保存图像,则仍然只需要分配B一次。即使这没有帮助,也可能澄清为什么我不理解这个问题,或者至少作为计算其他方法加速时间的基准。在我的笔记本电脑上以5k x 5k的float64阵列运行2.6秒,其中0.5是B

的创建

答案 3 :(得分:4)

让我们看看:

你的问题不是那么清楚,但我现在假设你想要显着改善这种平均值。

import numpy as np
from numpy.lib import stride_tricks as st

def mf(A, k_shape= (3, 3)):
    m= A.shape[0]- 2
    n= A.shape[1]- 2
    strides= A.strides+ A.strides
    new_shape= (m, n, k_shape[0], k_shape[1])
    A= st.as_strided(A, shape= new_shape, strides= strides)
    return np.sum(np.sum(A, -1), -1)/ np.prod(k_shape)

if __name__ == '__main__':
    A= np.arange(100).reshape((10, 10))
    print mf(A)

现在,您实际期望实现哪些性能改进?

<强>更新
首先,一个警告:它当前状态的代码不能正确适应'内核'形状。然而,这不是我现在主要关心的问题(无论如何,这个想法已经完全适应了)。

我刚刚直观地选择了4D A的新形状,对我而言,考虑将2D'内核'中心置于原始2D A的每个网格位置的中心是非常有意义的。

但是4D整形可能实际上并不是“最好的”。我认为这里真正的问题是求和的表现。一个人应该能够找到(最佳订单)(4D A)以充分利用您的机器缓存架构。然而,对于那些与你的机器缓存“合作”的“小”数组和那些没有(与此不那么简单的方式)的大型数组,这个顺序可能不一样。

更新2:
以下是mf的略微修改版本。显然,最好先重塑为3D数组,然后再进行求和,而不是总结点数产品(这具有优势,所以内核可以是任意的)。然而,它仍然比Pauls更新功能慢了3倍(在我的机器上)。

def mf(A):
    k_shape= (3, 3)
    k= np.prod(k_shape)
    m= A.shape[0]- 2
    n= A.shape[1]- 2
    strides= A.strides* 2
    new_shape= (m, n)+ k_shape
    A= st.as_strided(A, shape= new_shape, strides= strides)
    w= np.ones(k)/ k
    return np.dot(A.reshape((m, n, -1)), w)