Wykorzystanie kroków do wydajnego filtra średniej ruchomej

Ostatnio dowiedziałem się o krokach w odpowiedzi na ten post i zastanawiałem się, jak mogę ich użyć, aby obliczyć filtr średniej ruchomej bardziej efektywnie niż to, co zaproponowałem w ten post (używając filtrów splotowych).

To jest to, co mam do tej pory. Pobiera widok oryginalnej tablicy, a następnie zwija ją o niezbędną ilość i sumuje wartości jądra, aby obliczyć średnią. Zdaję sobie sprawę, że krawędzie nie są obsługiwane prawidłowo, ale mogę zadbać o potem... Czy jest lepszy i szybszy sposób? Celem jest filtrowanie dużych macierzy zmiennoprzecinkowych o rozmiarze do 5000x5000 x 16 warstw, zadanie, które scipy.ndimage.filters.convolve jest dość powolne.

Zauważ, że szukam połączenia 8-sąsiadującego, czyli filtr 3x3 pobiera średnio 9 pikseli (8 wokół ogniskowego piksela) i przypisuje tę wartość pikselowi na nowym obrazie.

import numpy, scipy

filtsize = 3
a = numpy.arange(100).reshape((10,10))
b = numpy.lib.stride_tricks.as_strided(a, shape=(a.size,filtsize), strides=(a.itemsize, a.itemsize))
for i in range(0, filtsize-1):
    if i > 0:
        b += numpy.roll(b, -(pow(filtsize,2)+1)*i, 0)
filtered = (numpy.sum(b, 1) / pow(filtsize,2)).reshape((a.shape[0],a.shape[1]))
scipy.misc.imsave("average.jpg", filtered)

Edytuj Wyjaśnienie, jak to widzę działa:

Aktualny kod:

  1. użycie stride_tricks do wygenerowania tablicy jak [[0,1,2],[1,2,3],[2,3,4]...] co odpowiada Najwyższemu rzędowi jądra filtra.
  2. Obróć się wzdłuż osi pionowej, aby uzyskać środkowy rząd jądra [[10,11,12],[11,12,13],[13,14,15]...] i dodać go do tablicy, którą dostałem w 1)
  3. powtórz, aby uzyskać dolny wiersz jądra [[20,21,22],[21,22,23],[22,23,24]...]. W tym momencie biorę sumę każdego wiersza i dzielę ją przez liczbę elementów w filtrze, dając mi średnią dla każdego piksel, (przesunięty o 1 rząd i 1 kol, z pewnymi dziwactwami wokół krawędzi, ale mogę się tym zająć później).

Liczyłem na lepsze wykorzystanie stride_tricks, aby uzyskać 9 wartości lub sumę elementów jądra bezpośrednio, dla całej tablicy, lub że ktoś może mnie przekonać do innej bardziej wydajnej metody...

Author: Community, 2011-02-08

4 answers

Jeśli to coś warte, oto jak to zrobisz używając "fantazyjnych" sztuczek. Miałem zamiar to opublikować wczoraj, ale rozproszyła mnie prawdziwa praca! :)

@Paul & @eat obie mają ładne implementacje, używając różnych innych sposobów. Kontynuując wcześniejsze pytanie, pomyślałem, że wrzucę N-wymiarowy odpowiednik.

Nie będziesz jednak w stanie znacząco pokonać scipy.ndimage funkcji dla > tablic 1D. (scipy.ndimage.uniform_filter powinien pokonać scipy.ndimage.convolve, chociaż)

Ponadto, jeśli próbujesz uzyskać wielowymiarowe przesuwające się okno, ryzykujesz, że użycie pamięci wybuchnie, gdy nieumyślnie wykonasz kopię tablicy. Podczas gdy początkowa tablica "rolling" jest tylko widokiem do pamięci oryginalnej tablicy, wszelkie pośrednie kroki kopiowania tablicy sprawią, że kopia jest rzędy wielkości większa niż oryginalna tablica (powiedzmy, że pracujesz z oryginalną tablicą 100x100... Widok na nią (dla rozmiar filtra (3,3)) będzie wynosił 98x98x3x3, ale będzie używał tej samej pamięci co oryginał. Jednak wszelkie kopie będą zużywać taką ilość pamięci, jaką miałaby tablica full 98x98x3x3!!)

Zasadniczo, używanie szalonych trików jest świetne, gdy chcesz wektoryzować operacje ruchomego okna na pojedynczej osi ndarray. To sprawia, że bardzo łatwo obliczyć rzeczy, takie jak ruchome odchylenie standardowe, itp z bardzo małym obciążeniem. Kiedy chcesz zacząć to robić wiele osi, to możliwe, ale zazwyczaj lepiej jest z bardziej wyspecjalizowanymi funkcjami. (Np. scipy.ndimage, itp.)

W każdym razie, oto jak to zrobić:

import numpy as np

def rolling_window_lastaxis(a, window):
    """Directly taken from Erik Rigtorp's post to numpy-discussion.
    <http://www.mail-archive.com/[email protected]/msg29450.html>"""
    if window < 1:
       raise ValueError, "`window` must be at least 1."
    if window > a.shape[-1]:
       raise ValueError, "`window` is too long."
    shape = a.shape[:-1] + (a.shape[-1] - window + 1, window)
    strides = a.strides + (a.strides[-1],)
    return np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)

def rolling_window(a, window):
    if not hasattr(window, '__iter__'):
        return rolling_window_lastaxis(a, window)
    for i, win in enumerate(window):
        if win > 1:
            a = a.swapaxes(i, -1)
            a = rolling_window_lastaxis(a, win)
            a = a.swapaxes(-2, i)
    return a

filtsize = (3, 3)
a = np.zeros((10,10), dtype=np.float)
a[5:7,5] = 1

b = rolling_window(a, filtsize)
blurred = b.mean(axis=-1).mean(axis=-1)

Więc to, co otrzymujemy, kiedy robimy b = rolling_window(a, filtsize) to tablica 8x8x3x3, która jest w rzeczywistości widokiem do tej samej pamięci, co oryginalna tablica 10x10. Moglibyśmy równie łatwo użyć różnej wielkości filtra wzdłuż różnych osi lub operować tylko wzdłuż wybranych osi tablicy N-wymiarowej (np. filtsize = (0,3,0,3) na tablicy 4-wymiarowej dałoby nas Widok 6 wymiarowy).

Możemy następnie wielokrotnie zastosować dowolną funkcję do ostatniej osi, aby skutecznie obliczyć rzeczy w ruchomym oknie.

Jednakże, ponieważ przechowujemy tymczasowe tablice, które są znacznie większe niż nasza oryginalna tablica na każdym kroku mean (lub std lub cokolwiek innego), nie jest to w ogóle efektywne w pamięci! Nie będzie też strasznie szybko.

Odpowiednikiem dla {[10] } jest właśnie:

blurred = scipy.ndimage.uniform_filter(a, filtsize, output=a)

To poradzi sobie z różnymi warunki brzegowe, czy "rozmycie" w miejscu bez konieczności tymczasowej kopii tablicy, i być bardzo szybko. Triki z przesuwaniem są dobrym sposobem na zastosowanie funkcji do poruszającego się okna wzdłuż jednej osi, ale zazwyczaj nie są dobrym sposobem na zrobienie tego wzdłuż wielu osi....

Tylko moje $ 0.02, w każdym razie...
 24
Author: Joe Kington,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-12 16:59:56

Nie znam Pythona na tyle, aby napisać do tego Kod, ale dwa najlepsze sposoby na przyspieszenie splotów to albo oddzielenie filtra, albo użycie transformaty Fouriera.

Separated filter: splot to O (M*N), gdzie M I N to odpowiednio liczba pikseli obrazu i filtra. Ponieważ średnie filtrowanie z jądrem 3 na 3 jest równoważne filtrowaniu najpierw z jądrem 3 na 1, a następnie z jądrem 1 na 3, można uzyskać (3+3)/(3*3) = ~30% poprawę prędkości przez kolejne splot z dwoma jądrami 1-d (to oczywiście staje się lepsze, gdy jądro się powiększa). Oczywiście nadal możesz używać trików z krokami.

transformata Fouriera : conv(A,B) jest równoważne ifft(fft(A)*fft(B)), tzn. splot w przestrzeni bezpośredniej staje się mnożeniem w przestrzeni Fouriera, gdzie A jest twoim obrazem, a {[4] } filtrem. Ponieważ mnożenie transformat Fouriera wymaga, aby a i B były tej samej wielkości, B jest tablicą size(A) z jądrem w samym centrum obrazu i zerami wszędzie indziej. Aby umieścić jądro 3 na 3 na środku tablicy, być może będziesz musiał użyć pad A do nieparzystego rozmiaru. W zależności od implementacji transformaty Fouriera może to być dużo szybsze niż splot (a jeśli zastosujesz ten sam filtr wiele razy, możesz wstępnie obliczyć fft(B), oszczędzając kolejne 30% czasu obliczeń).

 7
Author: Jonas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-09 16:09:17

Jedna rzecz, którą jestem pewien, że należy naprawić, to Twoja tablica widoków b.

Ma kilka elementów z nieprzydzielonej pamięci, więc będziesz miał awarie.

Biorąc pod uwagę nowy opis algorytmu, pierwszą rzeczą, która wymaga naprawy, jest fakt, że stąpasz poza alokacją a:

bshape = (a.size-filtsize+1, filtsize)
bstrides = (a.itemsize, a.itemsize)
b = numpy.lib.stride_tricks.as_strided(a, shape=bshape, strides=bstrides)

Update

Ponieważ wciąż nie do końca rozumiem metodę i wydaje się, że są prostsze sposoby rozwiązania problemu, zamierzam umieścić to tutaj:

A = numpy.arange(100).reshape((10,10))

shifts = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
B = A[1:-1, 1:-1].copy()
for dx,dy in shifts:
    xstop = -1+dx or None
    ystop = -1+dy or None
    B += A[1+dx:xstop, 1+dy:ystop]
B /= 9

...co wydaje się prostym podejściem. Jedyną operacją zewnętrzną jest to, że przydziela i wypełnia B tylko raz. Wszystkie dodawanie, dzielenie i indeksowanie musi odbywać się niezależnie. Jeśli wykonujesz 16 pasm, musisz przydzielić B tylko raz, jeśli chcesz zapisać obraz. Nawet jeśli to nie pomoże, może to wyjaśnić, dlaczego nie rozumiem problemu, lub przynajmniej służyć jako punkt odniesienia do czasu przyspieszenia innych metod. To działa w 2.6 s na moim laptopie na macierzy 5K x 5K float64, z których 0.5 jest tworzeniem B

 4
Author: Paul,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-08 23:38:50

Zobaczmy:

To nie jest tak jasne, jak twoje pytanie, ale zakładam, że teraz będziesz chciał znacznie poprawić ten rodzaj uśredniania.
import numpy as np
from numpy.lib import stride_tricks as st

def mf(A, k_shape= (3, 3)):
    m= A.shape[0]- 2
    n= A.shape[1]- 2
    strides= A.strides+ A.strides
    new_shape= (m, n, k_shape[0], k_shape[1])
    A= st.as_strided(A, shape= new_shape, strides= strides)
    return np.sum(np.sum(A, -1), -1)/ np.prod(k_shape)

if __name__ == '__main__':
    A= np.arange(100).reshape((10, 10))
    print mf(A)

Teraz, jakiego rodzaju poprawy wydajności można się spodziewać?

Update:
Przede wszystkim Ostrzeżenie: kod w obecnym stanie nie dostosowuje się właściwie do kształtu 'jądra'. Jednak to nie jest moim głównym zmartwieniem w tej chwili (w każdym razie pomysł jest już gotowy, jak prawidłowo się dostosować).

Właśnie wybrałem nowy kształt 4D a intuicyjnie, dla mnie naprawdę sensowne jest myślenie o centrum jądra 2D, które będzie wyśrodkowane do każdej pozycji siatki oryginalnego 2D A.

Ale to kształtowanie 4D może nie być tak naprawdę "najlepsze". Myślę, że prawdziwym problemem jest tutaj wydajność sumowania. Należy być w stanie znaleźć "najlepszy porządek" (z 4D A) w celu pełnego wykorzystania architektury pamięci podręcznej maszyn. Jednak kolejność ta może nie być taka sama dla "małych" tablic, które rodzaj "współpracuje" z pamięci podręcznej maszyn i tych większych, które nie (przynajmniej nie tak prosty sposób).

Aktualizacja 2:
Oto nieco zmodyfikowana wersja mf. Oczywiście lepiej najpierw przekształcić tablicę 3D, a następnie zamiast sumować po prostu zrób produkt dot (ma to tę zaletę, że jądro może być dowolne). Jednak nadal jest trochę 3x wolniejszy (na moim komputerze) niż zaktualizowana funkcja Paulsa.

def mf(A):
    k_shape= (3, 3)
    k= np.prod(k_shape)
    m= A.shape[0]- 2
    n= A.shape[1]- 2
    strides= A.strides* 2
    new_shape= (m, n)+ k_shape
    A= st.as_strided(A, shape= new_shape, strides= strides)
    w= np.ones(k)/ k
    return np.dot(A.reshape((m, n, -1)), w)
 4
Author: eat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-02-09 15:26:47