Фильтр Numpy для сглаживания нулевых областей

У меня есть массив 2D numpy с целыми числами 0 и выше, где значения представляют метки регионов. Например,

array([[9, 9, 9, 0, 0, 0, 0, 1, 1, 1],
       [9, 9, 9, 9, 0, 7, 1, 1, 1, 1],
       [9, 9, 9, 9, 0, 2, 2, 1, 1, 1],
       [9, 9, 9, 8, 0, 2, 2, 1, 1, 1],
       [9, 9, 9, 8, 0, 2, 2, 2, 1, 1],
       [4, 4, 4, 4, 0, 2, 2, 2, 1, 1],
       [4, 6, 6, 4, 0, 0, 0, 0, 0, 0],
       [4, 6, 6, 4, 0, 0, 0, 0, 0, 0],
       [4, 4, 4, 4, 5, 5, 5, 5, 5, 5],
       [4, 4, 4, 4, 5, 5, 5, 5, 5, 5]])

Я хотел бы, чтобы индексы, равные 0 (т.е. нулевые регионы), принимали значение, наиболее распространенное в их окрестностях. Операция по существу закроет нулевые регионы. Я пробовал несколько вариантов расширения, эрозии, закрытие серым цветом и другие морфологические операции, но я не могу полностью исключить нулевые области (без неуклюжего смешивания других областей). Достойным подходом может быть определение ядра, которое свертывает только нули, и устанавливает значение с наиболее распространенной меткой в ​​области фильтра. Я не уверен, как это реализовать.


person BoltzmannBrain    schedule 09.09.2017    source источник
comment
Готовы ли вы использовать Numba?   -  person John Zwinck    schedule 09.09.2017


Ответы (3)


Здесь предлагается один векторизованный подход. Шаги:

  1. Получите скользящие 2D-окна размером с ядро, ведущие к 4D-массиву. Мы можем использовать skimage's view_as_windows, чтобы получить их как представление и, таким образом, избежать создания дополнительной памяти для этого.

  2. Выберите окна, центрированные по нулям, путем индексации в массиве 4D. Это вызывает копию. Но если предположить, что количество нулей относительно меньше, чем общее количество элементов во входном массиве, все должно быть в порядке.

  3. Для каждого из этих выбранных окон сместите каждое окно с правильным смещением с идеей использования np.bincount для выполнения подсчета. Таким образом, используйте bincount и получите максимальное количество, исключая нули. argmax для максимального количества должен быть нашим парнем!

Вот реализация, охватывающая эти шаги -

from skimage.util import view_as_windows as viewW

def fill_zero_regions(a, kernel_size=3):
    hk = kernel_size//2 # half_kernel_size    

    a4D = viewW(a, (kernel_size,kernel_size))
    sliced_a = a[hk:-hk,hk:-hk]
    zeros_mask = sliced_a==0
    zero_neighs = a4D[zeros_mask].reshape(-1,kernel_size**2)
    n = len(zero_neighs) # num_zeros

    scale = zero_neighs.max()+1
    zno = zero_neighs + scale*np.arange(n)[:,None] # zero_neighs_offsetted

    count = np.bincount(zno.ravel(), minlength=n*scale).reshape(n,-1)
    modevals = count[:,1:].argmax(1)+1
    sliced_a[zeros_mask] = modevals
    return a

Пробный запуск -

In [23]: a
Out[23]: 
array([[9, 9, 9, 0, 0, 0, 0, 1, 1, 1],
       [9, 9, 9, 9, 0, 7, 1, 1, 1, 1],
       [9, 9, 9, 9, 0, 2, 2, 1, 1, 1],
       [9, 9, 9, 8, 0, 2, 2, 1, 1, 1],
       [9, 9, 9, 8, 0, 2, 2, 2, 1, 1],
       [4, 4, 4, 4, 0, 2, 2, 2, 1, 1],
       [4, 6, 6, 4, 0, 0, 0, 0, 0, 0],
       [4, 6, 6, 4, 0, 0, 0, 0, 0, 0],
       [4, 4, 4, 4, 5, 5, 5, 5, 5, 5],
       [4, 4, 4, 4, 5, 5, 5, 5, 5, 5]])

In [24]: fill_zero_regions(a)
Out[24]: 
array([[9, 9, 9, 0, 0, 0, 0, 1, 1, 1],
       [9, 9, 9, 9, 9, 7, 1, 1, 1, 1],
       [9, 9, 9, 9, 2, 2, 2, 1, 1, 1],
       [9, 9, 9, 8, 2, 2, 2, 1, 1, 1],
       [9, 9, 9, 8, 2, 2, 2, 2, 1, 1],
       [4, 4, 4, 4, 2, 2, 2, 2, 1, 1],
       [4, 6, 6, 4, 4, 2, 2, 2, 1, 0],
       [4, 6, 6, 4, 4, 5, 5, 5, 5, 0],
       [4, 4, 4, 4, 5, 5, 5, 5, 5, 5],
       [4, 4, 4, 4, 5, 5, 5, 5, 5, 5]])

Как видно, мы не решаем граничные случаи. Если необходимо, используйте массив с нулевым дополнением в качестве входного массива, что-то вроде этого: np.pad(a, (k//2,k//2), 'constant'), с k в качестве размера ядра (=3 для примера).

person Divakar    schedule 09.09.2017
comment
Отличный ответ, спасибо! +1 за объяснение использования памяти. - person BoltzmannBrain; 11.09.2017

Возможное решение, основанное на идее свертки

from scipy import stats
ar = #Your np array
blank = np.zeros(ar.shape)
#Size to search in for mode values
window_size = 3

for x,y in np.array(np.where(ar == 0)).T:
    window = ar[max(x-window_size,0):x+window_size,max(0,y-window_size):y+window_size]
    oneD = window.flatten()

    #fill blank array with modal value
    blank[x,y] = stats.mode(oneD[oneD != 0])[0]

#fill in the zeros
print ar + blank

Я не уверен, что здесь можно избежать цикла

person DJK    schedule 09.09.2017
comment
zip(np.where(ar == 0)[0],np.where(ar == 0)[1]) может быть просто np.array(np.where(ar == 0)).T. - person John Zwinck; 09.09.2017

Вот рабочее решение с использованием Numba, которое я не профилировал, но должно быть довольно быстрым:

import numba
@numba.njit
def nn(arr):
    res = arr.copy()
    zeros = np.where(arr == 0)
    for n in range(len(zeros[0])):
        i = zeros[0][n]
        j = zeros[1][n]
        left = max(i-1, 0)
        right = min(i+2, arr.shape[1])
        top = max(j-1, 0)
        bottom = min(j+2, arr.shape[0])
        area = arr[left:right,top:bottom].ravel()
        counts = np.bincount(area[area != 0])
        res[i,j] = np.argmax(counts)
    return res

Он производит:

array([[9, 9, 9, 9, 7, 1, 1, 1, 1, 1],
       [9, 9, 9, 9, 9, 7, 1, 1, 1, 1],
       [9, 9, 9, 9, 2, 2, 2, 1, 1, 1],
       [9, 9, 9, 8, 2, 2, 2, 1, 1, 1],
       [9, 9, 9, 8, 2, 2, 2, 2, 1, 1],
       [4, 4, 4, 4, 2, 2, 2, 2, 1, 1],
       [4, 6, 6, 4, 4, 2, 2, 2, 1, 1],
       [4, 6, 6, 4, 4, 5, 5, 5, 5, 5],
       [4, 4, 4, 4, 5, 5, 5, 5, 5, 5],
       [4, 4, 4, 4, 5, 5, 5, 5, 5, 5]])

Размер ядра здесь равен 3x3, что определяется вычитанием 1 и добавлением 2 к i и j (добавление 2, потому что срезы Python занимают один после конца, например, [0:3] дает вам 3 элемента). Граничные условия обрабатываются min и max.

Кредит на идею bincount: https://stackoverflow.com/a/6252400/4323

person John Zwinck    schedule 09.09.2017