用Python计算方差图像

7
有没有使用Python/NumPy/Scipy计算图像的滑动方差滤波器的简单方法?所谓的滑动方差图像是指对于图像中的每个子窗口I,计算sum((I-mean(I))^2)/nPixels的结果。
由于图像很大(12000x12000像素),我希望避免在不同库之间转换数组的开销,然后再进行转换。
我想我可以通过查找均值来手动完成此操作,例如:
kernel = np.ones((winSize, winSize))/winSize**2
image_mean = scipy.ndimage.convolve(image, kernel)
diff = (image - image_mean)**2
# Calculate sum over winSize*winSize sub-images
# Subsample result

但是如果有类似于Matlab中的stdfilt函数会更好。

有没有人能够指向一个具有此功能并支持NumPy数组的库,或者提供一种在NumPy/SciPy中实现此功能的方法?


也许这个问题是一个起点。 - tiago
4个回答

9
更简单且更快的解决方案:使用SciPy的ndimage.uniform_filter函数。请参考ndimage.uniform_filter文档。
import numpy as np
from scipy import ndimage 
rows, cols = 500, 500
win_rows, win_cols = 5, 5

img = np.random.rand(rows, cols)
win_mean = ndimage.uniform_filter(img, (win_rows, win_cols))
win_sqr_mean = ndimage.uniform_filter(img**2, (win_rows, win_cols))
win_var = win_sqr_mean - win_mean**2

“步幅技巧”是一种优美的技巧,但速度较慢且不易读懂。与步幅相比,generic_filter要慢20倍...


1
不错的解决方案!基于OpenCV的解决方案甚至比基于“ndimage”的更快。我已经用我的OpenCV代码对比了一下你的代码,结果在这个例子中OpenCV版本快了4.1倍。 - Ulrich Stern

6

您可以使用numpy.lib.stride_tricks.as_strided来获取图像的窗口视图:

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

rows, cols = 500, 500
win_rows, win_cols = 5, 5

img = np.random.rand(rows, cols)

win_img = as_strided(img, shape=(rows-win_rows+1, cols-win_cols+1,
                                 win_rows, win_cols),
                     strides=img.strides*2)

现在,win_img[i, j]是以位置[i,j]为左上角的(win_rows,win_cols)数组:

>>> img[100:105, 100:105]
array([[ 0.34150754,  0.17888323,  0.67222354,  0.9020784 ,  0.48826682],
       [ 0.68451774,  0.14887515,  0.44892615,  0.33352743,  0.22090103],
       [ 0.41114758,  0.82608407,  0.77190533,  0.42830363,  0.57300759],
       [ 0.68435626,  0.94874394,  0.55238567,  0.40367885,  0.42955156],
       [ 0.59359203,  0.62237553,  0.58428725,  0.58608119,  0.29157555]])
>>> win_img[100,100]
array([[ 0.34150754,  0.17888323,  0.67222354,  0.9020784 ,  0.48826682],
       [ 0.68451774,  0.14887515,  0.44892615,  0.33352743,  0.22090103],
       [ 0.41114758,  0.82608407,  0.77190533,  0.42830363,  0.57300759],
       [ 0.68435626,  0.94874394,  0.55238567,  0.40367885,  0.42955156],
       [ 0.59359203,  0.62237553,  0.58428725,  0.58608119,  0.29157555]])

然而,你需要小心,不要将图像的窗口视图转换为窗口副本:在我的例子中,这将需要25倍的存储空间。我相信numpy 1.7可以让你选择多个轴,因此你可以简单地执行以下操作:

>>> np.var(win_img, axis=(-1, -2))

我被困在numpy 1.6.2上,所以无法进行测试。另一个选项是,如果我记得我的数学没错的话,也许不能处理太大的窗口:

>>> win_mean = np.sum(np.sum(win_img, axis=-1), axis=-1)/win_rows/win_cols
>>> win_sqr_mean = np.sum(np.sum(win_img**2, axis=-1), axis=-1)/win_rows/win_cols
>>> win_var = win_sqr_mean - win_mean**2

现在win_var是一个数组,其形状为

>>> win_var.shape
(496, 496)

win_var[i, j]保存的是以[i, j]为左上角的(5, 5)窗口的方差。


img.shape = (1,1) 时,我得到了一个空数组 win_img - mLstudent33
例如,我如何在每个角落获取一个像素的窗口? - mLstudent33

4
经过一些优化,我们为通用的3D图像编写了以下函数:
def variance_filter( img, VAR_FILTER_SIZE ):
  from numpy.lib.stride_tricks import as_strided

  WIN_SIZE=(2*VAR_FILTER_SIZE)+1
  if ~ VAR_FILTER_SIZE%2==1:
      print 'Warning, VAR_FILTER_SIZE must be ODD Integer number  '
  # hack -- this could probably be an input to the function but Alessandro is lazy
  WIN_DIMS = [ WIN_SIZE, WIN_SIZE, WIN_SIZE ]


  # Check that there is a 3D image input.
  if len( img.shape ) != 3:
      print "\t variance_filter: Are you sure that you passed me a 3D image?"
      return -1
  else:
      DIMS = img.shape

  # Set up a windowed view on the data... this will have a border removed compared to the img_in
  img_strided = as_strided(img, shape=(DIMS[0]-WIN_DIMS[0]+1, DIMS[1]-WIN_DIMS[1]+1, DIMS[2]-WIN_DIMS[2]+1, WIN_DIMS[0], WIN_DIMS[1], WIN_DIMS[2] ), strides=img.strides*2)

  # Calculate variance, vectorially
  win_mean = numpy.sum(numpy.sum(numpy.sum(img_strided, axis=-1), axis=-1), axis=-1) / (WIN_DIMS[0]*WIN_DIMS[1]*WIN_DIMS[2])

  # As per http://en.wikipedia.org/wiki/Variance, we are removing the mean from every window,
  #   then squaring the result.
  # Casting to 64 bit float inside, because the numbers (at least for our images) get pretty big
  win_var = numpy.sum(numpy.sum(numpy.sum((( img_strided.T.astype('<f8') - win_mean.T.astype('<f8') )**2).T, axis=-1), axis=-1), axis=-1) / (WIN_DIMS[0]*WIN_DIMS[1]*WIN_DIMS[2])

  # Prepare an output image of the right size, in order to replace the border removed with the windowed view call
  out_img = numpy.zeros( DIMS, dtype='<f8' )
  # copy borders out...
  out_img[ WIN_DIMS[0]/2:DIMS[0]-WIN_DIMS[0]+1+WIN_DIMS[0]/2, WIN_DIMS[1]/2:DIMS[1]-WIN_DIMS[1]+1+WIN_DIMS[1]/2, WIN_DIMS[2]/2:DIMS[2]-WIN_DIMS[2]+1+WIN_DIMS[2]/2, ] = win_var

  # output
  return out_img.astype('>f4')

2
你可以使用 scipy.ndimage.generic_filter。我无法使用matlab进行测试,但也许这能满足你的需求。
import numpy as np
import scipy.ndimage as ndimage     
subs = 10  # this is the size of the (square) sub-windows
img = np.random.rand(500, 500)
img_std = ndimage.filters.generic_filter(img, np.std, size=subs)

您可以使用footprint关键字来制作任意大小的子窗口。请参阅此问题以获取示例。

1
虽然轻松访问所有良好的边缘处理功能很好,但这是那些具有欺骗性的numpy函数之一,例如vectorize,它基本上在图像上运行python循环:在您的(500, 500)示例中,使用(10, 10)窗口,generic_filter比我回答中的窗口视图方法慢了10倍。 - Jaime
@Jaime:没错。我也不太明白为什么这么慢。但是,问题的提出者想要一个单一的命令;-) - tiago

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