Быстрое обнаружение пиков и центрирование в python

Я пытаюсь разработать быстрый алгоритм на питоне для поиска пиков на изображении, а затем найти центр тяжести этих пиков. Я написал следующий код, используя scipy.ndimage.label и ndimage.find_objects для поиска объектов. Это кажется узким местом в коде, и поиск 20 объектов на изображении 500x500 занимает около 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: введите здесь описание изображения

РЕДАКТИРОВАТЬ: если количество пиков велико (~ 100), а размер изображения мал, то узким местом на самом деле является центральная часть. Так что, возможно, скорость этой части тоже нужно оптимизировать.


person DanHickstein    schedule 01.10.2013    source источник
comment
См. github.com/tacaswell/trackpy и github.com/nkeim/trackpy Может представлять интерес. (отказ от ответственности: один из них — мой код, а другой — форк моего кода, созданный бывшим сотрудником лаборатории)   -  person tacaswell    schedule 01.10.2013
comment
О, выглядит очень интересно! Вероятно, мне следует проверить функции identity.find_local_max и identity.subpixel_centroid, верно?   -  person DanHickstein    schedule 02.10.2013


Ответы (4)


Ваш метод нахождения пиков (простое определение порога), конечно, очень чувствителен к выбору порога: установите его слишком низким, и вы «обнаружите» вещи, которые не являются пиками; установите его слишком высоко, и вы пропустите действительные пики.

Существуют более надежные альтернативы, которые будут обнаруживать все локальные максимумы интенсивности изображения независимо от значения их интенсивности. Я предпочитаю применять расширение с небольшим (5x5 или 7x7) структурирующим элементом, а затем находить пиксели, в которых исходное изображение и его расширенная версия имеют одинаковое значение. Это работает, потому что, по определению, dilation(x, y, E, img) = {максимум изображений в пределах E с центром в пикселе (x,y)}, и, следовательно, dilation(x, y, E, img) = img(x , y) всякий раз, когда (x, y) является местоположением локального максимума в масштабе E.

Благодаря быстрой реализации морфологических операторов (например, в OpenCV) этот алгоритм является линейным по размеру изображения как в пространстве, так и во времени (один дополнительный буфер размера изображения для расширенного изображения и один проход для обоих). В крайнем случае, это также может быть реализовано в режиме онлайн без дополнительного буфера и небольшой дополнительной сложности, и это все еще линейное время.

Чтобы еще больше усилить его в присутствии «соли и перца» или подобного шума, который может привести к множеству ложных максимумов, вы можете применить метод дважды, со структурирующими элементами разного размера (скажем, 5x5 и 7x7), а затем оставить только стабильный максимумы, где стабильность может быть определена неизменным положением максимумов или положением, не изменяющимся более чем на один пиксель, и т. д. Кроме того, вы можете захотеть подавить низкие соседние максимумы, если у вас есть основания полагать, что они вызваны шумом. Эффективный способ сделать это - сначала обнаружить все локальные максимумы, как указано выше, отсортировать их по убыванию по высоте, затем перейти вниз по отсортированному списку и сохранить их, если их значение на изображении не изменилось, и, если они сохраняются, установить на обнулить все пиксели в окрестности (2d+1) x (2d+1) от них, где d — минимальное расстояние между соседними максимумами, которое вы готовы допустить.

person Francesco Callari    schedule 02.10.2013
comment
Спасибо за предложение! Я думаю, что предложенный вами алгоритм очень похож на функцию find_local_maxima, предложенную @tcaswell в его коде trackpy: github.com/tacaswell/trackpy/blob/master/trackpy/ Этот подход работает очень хорошо и действительно находит истинные пики, даже если они частично перекрываются или если некоторые пики значительно меньше. Однако код намного медленнее: для изображения размером 500x500 требуется ~200 мс. Я раньше не использовал OpenCV, но попробую. Можете ли вы более конкретно указать, какие функции OpenCV мне следует использовать? - person DanHickstein; 02.10.2013
comment
Расширение OpenCV: docs.opencv.org/modules/imgproc /doc/ (или более быстрый docs.opencv .org/modules/ocl/doc/image_filtering.html#ocl-dilate, если вы можете использовать OCL), используя docs.opencv.org/modules/imgproc/doc/, если вам лень. Для быстрого сравнения с оригиналом используйте docs.opencv.org/modules/core /док/ - person Francesco Callari; 02.10.2013

Если у вас много пиков, быстрее использовать 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 раз быстрее. Таким образом, ваш лучший вариант будет зависеть от ваших фактических данных.

person Jaime    schedule 01.10.2013
comment
Ах, чище код и быстрее для многих пиков, отлично! Спасибо! Но мне также очень хотелось бы ускорить процессы маркировки изображения и поиска объектов. - person DanHickstein; 02.10.2013
comment
Я серьезно сомневаюсь, что есть более быстрый способ, чем функции ndimage, которые вы можете кодировать в Python... - person Jaime; 02.10.2013

Другой подход — избегать всех 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

Это может быть израсходовано и в более высоком измерении. 60% ускорения по сравнению с centroid()

person CT Zhu    schedule 01.10.2013

Следующий расчет центроида выполняется быстрее обоих, особенно для больших данных:

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)
person CyxAndr    schedule 28.01.2018
comment
любые комментарии о том, почему это быстрее? - person Palu Macil; 28.01.2018