关于python:噪声二维数组中的峰值检测

Peak detection in a noisy 2d array

我正试图让python尽可能靠近图像中最明显群集的中心,如下所示:

image

在上一个问题中,我问了如何获得二维数组的全局最大值和局部最大值,给出的答案非常有效。问题是,我可以通过求取不同垃圾箱大小下获得的全局最大值的平均值得到的中心估计值总是略低于我用眼睛设定的值,因为我只计算最大的垃圾箱而不是一组最大的垃圾箱(就像用眼睛计算一样)。

我试着根据我的问题调整这个问题的答案,但结果发现我的图像太吵了,算法无法工作。下面是实现该答案的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import numpy as np
from scipy.ndimage.filters import maximum_filter
from scipy.ndimage.morphology import generate_binary_structure, binary_erosion
import matplotlib.pyplot as pp

from os import getcwd
from os.path import join, realpath, dirname

# Save path to dir where this code exists.
mypath = realpath(join(getcwd(), dirname(__file__)))
myfile = 'data_file.dat'

x, y = np.loadtxt(join(mypath,myfile), usecols=(1, 2), unpack=True)
xmin, xmax = min(x), max(x)
ymin, ymax = min(y), max(y)

rang = [[xmin, xmax], [ymin, ymax]]
paws = []

for d_b in range(25, 110, 25):
    # Number of bins in x,y given the bin width 'd_b'
    binsxy = [int((xmax - xmin) / d_b), int((ymax - ymin) / d_b)]

    H, xedges, yedges = np.histogram2d(x, y, range=rang, bins=binsxy)
    paws.append(H)


def detect_peaks(image):
   """
    Takes an image and detect the peaks usingthe local maximum filter.
    Returns a boolean mask of the peaks (i.e. 1 when
    the pixel's value is the neighborhood maximum, 0 otherwise)
   """


    # define an 8-connected neighborhood
    neighborhood = generate_binary_structure(2,2)

    #apply the local maximum filter; all pixel of maximal value
    #in their neighborhood are set to 1
    local_max = maximum_filter(image, footprint=neighborhood)==image
    #local_max is a mask that contains the peaks we are
    #looking for, but also the background.
    #In order to isolate the peaks we must remove the background from the mask.

    #we create the mask of the background
    background = (image==0)

    #a little technicality: we must erode the background in order to
    #successfully subtract it form local_max, otherwise a line will
    #appear along the background border (artifact of the local maximum filter)
    eroded_background = binary_erosion(background, structure=neighborhood, border_value=1)

    #we obtain the final mask, containing only peaks,
    #by removing the background from the local_max mask
    detected_peaks = local_max - eroded_background

    return detected_peaks


#applying the detection and plotting results
for i, paw in enumerate(paws):
    detected_peaks = detect_peaks(paw)
    pp.subplot(4,2,(2*i+1))
    pp.imshow(paw)
    pp.subplot(4,2,(2*i+2) )
    pp.imshow(detected_peaks)

pp.show()

结果是(改变垃圾箱的大小):

enter image description here

很明显,我的背景太吵了,算法无法运行,所以问题是:我如何才能降低算法的敏感度?如果有其他解决方案,请告诉我。

编辑

按照bi-rico的建议,我尝试在将二维数组传递给本地最大查找器之前对其进行平滑处理,如下所示:

1
2
3
H, xedges, yedges = np.histogram2d(x, y, range=rang, bins=binsxy)
H1 = gaussian_filter(H, 2, mode='nearest')
paws.append(H1)

这些是sigma的结果,分别为2、4和8:

enter image description here

编辑2

一个mode ='constant'似乎比nearest工作得更好。对于最大的垃圾箱尺寸,它通过一个sigma=2收敛到右中心:

enter image description here

那么,如何得到最后一幅图中最大值的坐标呢?


堆栈上的n00b溢出太多,无法评论Alejandro在此处的其他地方的答案。我会对他的代码进行一些改进,以使用预分配的numpy数组进行输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def get_max(image,sigma,alpha=3,size=10):
    from copy import deepcopy
    import numpy as np
    # preallocate a lot of peak storage
    k_arr = np.zeros((10000,2))
    image_temp = deepcopy(image)
    peak_ct=0
    while True:
        k = np.argmax(image_temp)
        j,i = np.unravel_index(k, image_temp.shape)
        if(image_temp[j,i] >= alpha*sigma):
            k_arr[peak_ct]=[j,i]
            # this is the part that masks already-found peaks.
            x = np.arange(i-size, i+size)
            y = np.arange(j-size, j+size)
            xv,yv = np.meshgrid(x,y)
            # the clip here handles edge cases where the peak is near the
            #    image edge
            image_temp[yv.clip(0,image_temp.shape[0]-1),
                               xv.clip(0,image_temp.shape[1]-1) ] = 0
            peak_ct+=1
        else:
            break
    # trim the output for only what we've actually found
    return k_arr[:peak_ct]

在使用示例图像分析此代码和Alejandro的代码时,此代码的速度大约快33%(Alejandro的代码为0.03秒,我的代码为0.02秒)。我预计在峰值数量较大的图像上,它会更快-将输出附加到列表中会越来越慢,对于更多的峰值。


回答问题的最后一部分,总是在图像中有点,你可以通过搜索图像的局部最大值来找到它们的坐标。如果您的数据不是点源,您可以对每个峰值应用一个遮罩,以避免在将来执行搜索时峰值邻域达到最大值。我建议采用以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import matplotlib.image as mpimg
import matplotlib.pyplot as plt
import numpy as np
import copy

def get_std(image):
    return np.std(image)

def get_max(image,sigma,alpha=20,size=10):
    i_out = []
    j_out = []
    image_temp = copy.deepcopy(image)
    while True:
        k = np.argmax(image_temp)
        j,i = np.unravel_index(k, image_temp.shape)
        if(image_temp[j,i] >= alpha*sigma):
            i_out.append(i)
            j_out.append(j)
            x = np.arange(i-size, i+size)
            y = np.arange(j-size, j+size)
            xv,yv = np.meshgrid(x,y)
            image_temp[yv.clip(0,image_temp.shape[0]-1),
                                   xv.clip(0,image_temp.shape[1]-1) ] = 0
            print xv
        else:
            break
    return i_out,j_out

#reading the image  
image = mpimg.imread('ggd4.jpg')
#computing the standard deviation of the image
sigma = get_std(image)
#getting the peaks
i,j = get_max(image[:,:,0],sigma, alpha=10, size=10)

#let's see the results
plt.imshow(image, origin='lower')
plt.plot(i,j,'ro', markersize=10, alpha=0.5)
plt.show()

测试图像GGD4可从以下网站下载:

网址:http://www.ipac.caltech.edu/2mass/gallery/spr99/ggd4.jpg

第一部分是获取图像中噪声的一些信息。我是通过计算全图像的标准偏差来实现的(实际上选择一个没有信号的小矩形更好)。这告诉我们图像中有多少噪声。获得峰值的想法是要求连续的最大值,它高于某个阈值(例如,噪音的3、4、5、10或20倍)。这就是函数get_max实际执行的操作。它执行对最大值的搜索,直到其中一个值低于噪声所施加的阈值。为了避免多次找到相同的最大值,需要从图像中删除峰值。一般来说,要做到这一点,遮罩的形状很大程度上取决于人们想要解决的问题。对于恒星来说,最好使用高斯函数或类似的方法来去除恒星。为了简单起见,我选择了一个方形函数,函数的大小(以像素为单位)是可变的"大小"。我认为从这个例子中,任何人都可以通过添加更一般的东西来改进代码。

编辑:

原始图像如下:

enter image description here

识别发光点后的图像如下:

enter image description here


我认为这里需要的第一步是用h表示字段标准偏差的值:

1
2
import numpy as np
H = H / np.std(H)

现在,您可以在这个h的值上设置一个阈值。如果假设噪声是高斯的,选择一个阈值3,您可以非常确定(99.7%)这个像素可以与一个真正的峰值相关联,而不是噪声。请看这里。

现在可以开始进一步的选择。我不清楚你到底想找什么。您想知道峰值的确切位置吗?或者您想要一个位于这个集群中间的峰值集群的位置?无论如何,从这一点开始,所有像素值都以字段的标准偏差表示,您应该能够得到您想要的。如果您想找到集群,可以在>3西格玛网格点上执行最近邻搜索,并在距离上设置一个阈值。也就是说,只有当它们彼此足够接近时才连接它们。如果连接了多个网格点,您可以将其定义为一个组/集群,并计算一些网格点(sigma加权?)集群的中心。希望我在StackOverflow上的第一个贡献对您有用!


我添加这个答案是因为它是我最终使用的解决方案。这是比里科在这里的评论(5月30日18:54)和这个问题给出的答案的组合:找到二维柱状图的峰值。

事实证明,在二维阵列中使用这个问题的峰值检测算法,只会使问题复杂化。在对图像应用高斯滤波器之后,需要做的就是请求最大的bin(正如bi-rico指出的那样),然后在坐标中获得最大值。

因此,在获得高斯二维柱状图后,我不再像上面那样使用"检测峰值"功能,只需添加以下代码:

1
2
3
4
5
6
7
8
# Get 2D histogram.
H, xedges, yedges = np.histogram2d(x, y, range=rang, bins=binsxy)
# Get Gaussian filtered 2D histogram.
H1 = gaussian_filter(H, 2, mode='nearest')
# Get center of maximum in bin coordinates.
x_cent_bin, y_cent_bin = np.unravel_index(H1.argmax(), H1.shape)
# Get center in x,y coordinates.
x_cent_coor , y_cent_coord = np.average(xedges[x_cent_bin:x_cent_bin + 2]), np.average(yedges[y_cent_g:y_cent_g + 2])

我的方式是:

1)使h在0和1之间正常化。

2)选择阈值,如tcaswell建议的那样。例如,它可能在.9和.99之间。

3)使用遮罩数组仅保留x,y坐标,h高于阈值:

1
2
3
import numpy.ma as ma
x_masked=ma.masked_array(x, mask= H < thresold)
y_masked=ma.masked_array(y, mask= H < thresold)

4)现在,您可以在遮罩坐标上加权平均值,重量类似于(H-阈值)^2,或者任何其他大于或等于1的功率,这取决于您的口味/测试。

评论:1)对于您所拥有的峰值类型,这是不可靠的,因为您可能需要调整thresheld。这是个小问题;2)这不适用于两个峰值,如果第二个峰值高于阈值,则会产生错误的结果。

尽管如此,它还是会给你一个答案而不会死机(包括事情的利弊)。