快速在Python中找到峰值并计算重心

10
我正在尝试开发一种快速算法,用Python在图像中查找峰值并找到这些峰值的质心。我使用scipy.ndimage.label和ndimage.find_objects编写了以下代码来定位对象。这似乎是代码的瓶颈,在一个500x500的图像中定位20个对象需要大约7毫秒。我想将其扩展到更大的(2000x2000)图像,但时间增加到近100毫秒。因此,我想知道是否有更快的选项。
以下是我目前拥有的代码,它能够工作,但速度很慢。首先,我使用一些高斯峰模拟我的数据。这部分很慢,但在实践中,我将使用真实数据,所以我不太关心加速那部分。我希望能够快速找到峰值。
import time
import numpy as np
import matplotlib.pyplot as plt
import scipy.ndimage
import matplotlib.patches 

plt.figure(figsize=(10,10))
ax1 = plt.subplot(221)
ax2 = plt.subplot(222)
ax3 = plt.subplot(223)
ax4 = plt.subplot(224)

size        = 500 #width and height of image in pixels
peak_height = 100 # define the height of the peaks
num_peaks   = 20
noise_level = 50
threshold   = 60

np.random.seed(3)

#set up a simple, blank image (Z)
x = np.linspace(0,size,size)
y = np.linspace(0,size,size)

X,Y = np.meshgrid(x,y)
Z = X*0

#now add some peaks
def gaussian(X,Y,xo,yo,amp=100,sigmax=4,sigmay=4):
    return amp*np.exp(-(X-xo)**2/(2*sigmax**2) - (Y-yo)**2/(2*sigmay**2))

for xo,yo in size*np.random.rand(num_peaks,2):
    widthx = 5 + np.random.randn(1)
    widthy = 5 + np.random.randn(1)
    Z += gaussian(X,Y,xo,yo,amp=peak_height,sigmax=widthx,sigmay=widthy)

#of course, add some noise:
Z = Z + scipy.ndimage.gaussian_filter(0.5*noise_level*np.random.rand(size,size),sigma=5)    
Z = Z + scipy.ndimage.gaussian_filter(0.5*noise_level*np.random.rand(size,size),sigma=1)    

t = time.time() #Start timing the peak-finding algorithm

#Set everything below the threshold to zero:
Z_thresh = np.copy(Z)
Z_thresh[Z_thresh<threshold] = 0
print 'Time after thresholding: %.5f seconds'%(time.time()-t)

#now find the objects
labeled_image, number_of_objects = scipy.ndimage.label(Z_thresh)
print 'Time after labeling: %.5f seconds'%(time.time()-t)

peak_slices = scipy.ndimage.find_objects(labeled_image)
print 'Time after finding objects: %.5f seconds'%(time.time()-t)

def centroid(data):
    h,w = np.shape(data)   
    x = np.arange(0,w)
    y = np.arange(0,h)

    X,Y = np.meshgrid(x,y)

    cx = np.sum(X*data)/np.sum(data)
    cy = np.sum(Y*data)/np.sum(data)

    return cx,cy

centroids = []

for peak_slice in peak_slices:
    dy,dx  = peak_slice
    x,y = dx.start, dy.start
    cx,cy = centroid(Z_thresh[peak_slice])
    centroids.append((x+cx,y+cy))

print 'Total time: %.5f seconds\n'%(time.time()-t)

###########################################
#Now make the plots:
for ax in (ax1,ax2,ax3,ax4): ax.clear()
ax1.set_title('Original image')
ax1.imshow(Z,origin='lower')

ax2.set_title('Thresholded image')
ax2.imshow(Z_thresh,origin='lower')

ax3.set_title('Labeled image')
ax3.imshow(labeled_image,origin='lower') #display the color-coded regions

for peak_slice in peak_slices:  #Draw some rectangles around the objects
    dy,dx  = peak_slice
    xy     = (dx.start, dy.start)
    width  = (dx.stop - dx.start + 1)
    height = (dy.stop - dy.start + 1)
    rect = matplotlib.patches.Rectangle(xy,width,height,fc='none',ec='red')
    ax3.add_patch(rect,)

ax4.set_title('Centroids on original image')
ax4.imshow(Z,origin='lower')

for x,y in centroids:
    ax4.plot(x,y,'kx',ms=10)

ax4.set_xlim(0,size)
ax4.set_ylim(0,size)

plt.tight_layout
plt.show()

size=500的结果如下: enter image description here 编辑:如果峰数量较大(~100)且图像尺寸较小,则瓶颈实际上是重心定位部分。因此,也许需要优化这一部分的速度。

2
请查看https://github.com/tacaswell/trackpy和https://github.com/nkeim/trackpy,可能会感兴趣。(免责声明:其中一个是我的代码,另一个是我的前同事对我的代码进行的分支) - tacaswell
哦,看起来非常有趣!我应该检查一下identification.find_local_max和identification.subpixel_centroid函数,对吧? - DanHickstein
4个回答

9
你寻找峰值的方法(简单阈值法)非常依赖于阈值的选择:如果设置得太低,则会“检测”出不是峰值的东西;如果设置得太高,则会错过有效峰值。有更加稳健的替代方法,可以检测图像强度中所有的局部最大值,无论其强度值如何。我倾向于应用一个小的形态学元素(5x5或7x7)进行膨胀,然后找到原始图像和其膨胀版本具有相同值的像素点。这种方法可行的原因在于定义上,膨胀(x,y,E,img)= {在以像素(x,y)为中心的E内img的最大值},因此当(x,y)是E尺度内的局部最大值时,膨胀(x,y,E,img)= img(x,y)。
使用形态学运算符的快速实现(例如OpenCV中的运算符),该算法在空间和时间方面都是图像大小的线性(膨胀图像需要额外的与图像一样大小的缓冲区,并且需要分别进行一次操作)。在紧急情况下,它也可以在线实现,不需要额外的缓冲区和稍微复杂的处理,仍然是线性时间。
为了进一步稳健地处理椒盐噪声或类似噪声,可能会引入许多错误的极值点,可以使用不同尺寸(例如5x5和7x7)的形态学元素两次应用该方法,然后仅保留稳定的最大值,其中“稳定”可以定义为最大值位置不变或者位置变化不超过一个像素。此外,如果你有理由认为它们是由于噪声造成的,则可能想要抑制低附近的最大值。实现这样的高效方法的方式是首先如上所述检测所有局部最大值,按高度降序排序,然后向下移动已排序列表,并仅在其像素值未发生变化时保留它们,如果保留它们,则将它们周围(2d +1)x(2d +1)的像素设为零,其中d是你愿意容忍的附近最大值之间的最小距离。

谢谢您的建议!我认为您所建议的算法与@tcaswell在他的“trackpy”代码中建议的“find_local_maxima”函数非常相似:https://github.com/tacaswell/trackpy/blob/master/trackpy/identification.py 这种方法效果非常好,确实可以找到真正的峰值,即使它们部分重叠,或者某些峰值要小得多。然而,该代码速度较慢,对于一个500x500的图像需要大约200毫秒。我以前没有使用过OpenCV,但我会尝试一下。您能具体建议我应该使用哪些OpenCV函数吗? - DanHickstein
OpenCV 膨胀操作:使用 http://docs.opencv.org/modules/imgproc/doc/filtering.html?highlight=dilate#dilate(或者更快的 http://docs.opencv.org/modules/ocl/doc/image_filtering.html#ocl-dilate,如果您可以使用 OCL),如果您感到懒惰,可以使用 http://docs.opencv.org/modules/imgproc/doc/filtering.html#getstructuringelement。为了与原始内容进行快速比较,请使用 http://docs.opencv.org/modules/core/doc/operations_on_arrays.html#compare。 - Francesco Callari

5

如果你有很多峰值,使用scipy.ndimage.center_of_mass会更快。你可以用下面两行代码替换从peak_slices定义开始,直到打印总时间的代码:

centroids = scipy.ndimage.center_of_mass(Z_thresh, labeled_image,
                                         np.arange(1, number_of_objects + 1))
centroids = [(j, i) for i, j in centroids]

对于 num_peaks = 20,这种方法运行速度比您的方法慢了约3倍,但对于 num_peaks = 100,它的运行速度却快了约10倍。因此,你最好的选择取决于你的实际数据。

啊,代码更简洁,对于许多高峰期来说速度更快,太完美了!谢谢!但是我也非常希望能找到一种加快图像标记和物体检测过程的方法。 - DanHickstein
1
我真的怀疑除了在Python中编写的ndimage函数之外,是否还有更快的方法... - Jaime

2
另一种方法是避免使用所有的sum()meshgrid()等函数,将所有内容替换为直接线性代数计算。
>>> def centroid2(data):
    h,w=data.shape
    x=np.arange(h)
    y=np.arange(w)
    x1=np.ones((1,h))
    y1=np.ones((w,1))
    return ((np.dot(np.dot(x1, data), y))/(np.dot(np.dot(x1, data), y1)),
            (np.dot(np.dot(x, data), y1))/(np.dot(np.dot(x1, data), y1)))
#be careful, it returns two arrays

这也可以扩展到更高的维度。相较于 centroid(),速度提升了60%。


0

以下的质心计算比两者都快,特别是对于大数据而言:

def centroidnp(data):
    h,w = data.shape
    x = np.arange(w)
    y = np.arange(h)
    vx = data.sum(axis=0)
    vx /= vx.sum()
    vy = data.sum(axis=1)
    vy /= vy.sum()    
    return np.dot(vx,x),np.dot(vy,y)

1
为什么它更快?对此有任何评论吗? - Palu Macil

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