web-dev-qa-db-fra.com

Utiliser des projetés pour un filtre moyen en mouvement efficace

J'ai récemment appris sur Strides dans la réponse à ce poste , et était Je me demandais comment je pourrais les utiliser pour calculer plus efficacement un filtre moyen en mouvement que ce que j'ai proposé dans cet article (Utilisation des filtres de convolution).

C'est ce que j'ai jusqu'à présent. Il faut une vue sur le tableau d'origine, puis la roule par la quantité nécessaire et résume les valeurs du noyau pour calculer la moyenne. Je suis conscient que les bords ne sont pas traités correctement, mais je peux prendre soin de cela après ... Y a-t-il un moyen meilleur et plus rapide? L'objectif est de filtrer de grandes matrices à virgule flottante jusqu'à 5000x5000 x 16 couches de taille, une tâche que scipy.ndimage.filters.convolve est assez lent à.

Notez que je recherche une connectivité à 8 voisins, c'est-à-dire qu'un filtre 3x3 prend la moyenne de 9 pixels (8 autour du pixel focal) et attribue cette valeur au pixel de la nouvelle image.

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)

Éditer des éclaircissements sur la façon dont je vois ce travail:

Code actuel:

  1. utilisez stride_tricks pour générer un tableau comme [[0,1,2], [1,2,3], [2,3,4], ...] qui correspond à la rangée supérieure du noyau de filtre.
  2. Rouler le long de l'axe vertical pour obtenir la rangée du milieu du noyau [[10,11,12], [11,12,13], [13,14,15], ...] et l'ajouter à la matrice que j'ai obtenue 1)
  3. Répéter pour obtenir la rangée inférieure du noyau [[20,21,22], [21,22,23], [22,23,24] ...]. À ce stade, je prends la somme de chaque ligne et la divisez par le nombre d'éléments dans le filtre, me donnant la moyenne pour chaque pixel (déplacé par 1 rangée et 1 col, et avec certaines bizarreries autour des bords, mais je peux prendre soin de cela plus tard).

Ce que j'espérais, c'est une meilleure utilisation des strides_tricks pour obtenir les 9 valeurs ou la somme des éléments du noyau directement, pour l'ensemble de la matrice ou que quelqu'un peut me convaincre d'une autre méthode plus efficace ...

29
Benjamin

Pour ce que ça vaut la peine, voici comment vous le feriez en utilisant des astuces strictes "fantaisie". J'allais poster cela hier, mais j'ai été distrait par le travail réel! :)

@Paul et @eat ont toutes deux de bonnes implémentations utilisant diverses autres façons de le faire. Juste pour continuer les choses de la question précédente, je pensais que je posterais l'équivalent n-dimensionnel.

Vous n'allez pas être capable de battre considérablement scipy.ndimage fonctions pour> 1D de matrices, cependant. (scipy.ndimage.uniform_filter devrait battre scipy.ndimage.convolve, cependant)

De plus, si vous essayez d'obtenir une fenêtre mobile multidimensionnelle, vous risquez d'avoir une utilisation de la mémoire exploser chaque fois que vous effectuez une copie de votre tableau par inadvertance. Bien que le tableau initial "roulant" soit juste une vue dans la mémoire de votre tableau d'origine, toutes les étapes intermédiaires qui copient la matrice effectueront une copie des commandes de magnitude Plus grand que votre tableau d'origine (disons-vous que vous travaillez avec un tableau d'origine 100x100 ... la vue sur celui-ci (pour une taille de filtre de (3,3)) sera de 98x98x3x3 mais utilisez la même mémoire que L'original. Toutefois, toute copie utilisera la quantité de mémoire qu'un complet 98x98x3x3 tableau serait !!)

Fondamentalement, l'utilisation de tours de contournement fou est idéal pour que vous souhaitiez vectoriser des opérations de fenêtre en déplacement sur un axe unique d'un ndarray. Il est très facile de calculer des choses comme une déviation type en mouvement, etc. avec très peu de frais généraux. Lorsque vous souhaitez commencer à faire cela le long de plusieurs axes, il est possible, mais vous êtes généralement mieux à partir de fonctions plus spécialisées. (Tels que scipy.ndimage, etc.)

En tout cas, voici comment vous le faites:

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)

Donc, ce que nous obtenons lorsque nous le faisons b = rolling_window(a, filtsize) est un tableau 8x8x3x3, c'est en fait une vue dans la même mémoire que le tableau d'origine 10x10. Nous aurions pu utiliser tout aussi facilement la taille du filtre différente selon différents axes ou actionné uniquement le long des axes sélectionnés d'une matrice N-dimensionnelle (I.e. filtsize = (0,3,0,3) sur une matrice à 4 dimensions vous donnerait une vue en 6 dimensions).

Nous pouvons ensuite appliquer une fonction arbitraire au dernier axe à plusieurs reprises pour calculer efficacement les choses dans une fenêtre en mouvement.

Cependant, parce que nous stockons des tableaux temporaires beaucoup plus gros que notre tableau d'origine à chaque étape de mean (ou std ou autre), ce n'est pas à tout moment efficace de mémoire! Cela ne va pas non plus être terriblement rapide, non plus.

L'équivalent pour ndimage est juste:

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

Cela traitera une variété de conditions limites, faites le "floue" en place sans nécessiter une copie temporaire de la matrice et être très rapide. Les tours stridantes sont un bon moyen d'appliquer une fonction à une fenêtre en mouvement le long un axe, mais ils ne sont pas un bon moyen de le faire sur plusieurs axes , d'habitude....

Juste ma 0,02 $, à tout prix ...

28
Joe Kington

Je ne suis pas assez familier avec Python= pour écrire le code pour cela, mais les deux meilleurs moyens d'accélérer les convolutions consiste à séparer le filtre ou à utiliser la transformée de Fourier.

filtre séparé: la convolution est O (m * n), où m et n sont respectivement nombre de pixels dans l'image et le filtre, respectivement. Comme le filtrage moyen avec un noyau de 3 by-3 équivaut à filtrer d'abord avec un noyau de 3 by-1, puis un noyau de 1 by-3, vous pouvez obtenir (3+3)/(3*3) = ~ 30% d'amélioration de la vitesse par convolution consécutive avec deux noyaux 1-D (cela s'améliore évidemment comme le noyau devient plus grand). Vous pouvez toujours utiliser des tours de foulée ici, bien sûr.

transformée de Fourier: conv(A,B) est équivalente à ifft(fft(A)*fft(B)), c'est-à-dire qu'une convolution dans l'espace direct devient une multiplication dans un espace de Fourier, où A est votre image et B est votre filtre. Étant donné que la multiplication (Element-Wise) des transformations de Fourier nécessite que A et B sont la même taille, B est une matrice de size(A) avec votre noyau au centre même de l'image et des zéros partout ailleurs. Pour placer un noyau de 3 by-3 au centre d'un tableau, vous devrez peut-être avoir à clavier A à la taille impaire. Selon votre implémentation de la transformée de Fourier, cela peut être beaucoup plus rapide que la convolution (et si vous appliquez le même filtre plusieurs fois, vous pouvez pré-calculer fft(B), économiser 30% de temps de calcul) .

8
Jonas

Une chose que je suis confiant doit être corrigée est votre tableau d'affichage b.

Il a quelques éléments de mémoire non allouée, vous obtiendrez donc des accidents.

Compte tenu de votre nouvelle description de votre algorithme, la première chose à laquelle il faut la fixation est le fait que vous ressemblez à l'affectation de a:

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

Mise à jour

Parce que je ne sais toujours pas vraiment la méthode et il semble y avoir des moyens plus simples de résoudre le problème, je vais juste mettre cela ici:

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

... qui semble juste comme l'approche simple. La seule opération étrangère est qu'elle a alloué et peupler B une seule fois. Tous les adjonctions, la division et l'indexation doivent être effectuées indépendamment. Si vous faites 16 bandes, vous n'avez toujours besoin que d'allouer B une fois si votre intention est d'enregistrer une image. Même si cela n'est pas d'aide, cela pourrait préciser pourquoi je ne comprends pas le problème, ni au moins servir de point de repère à temps les écarts d'autres méthodes. Cela fonctionne en 2,6 secondes sur mon ordinateur portable sur un ensemble de 5K x 5k de Float64's, 0,5 dont la création de B

4
Paul

Voyons:

Ce n'est pas si claire forme votre question, mais je suppose maintenant que vous souhaitez améliorer considérablement ce type de moyen de la moyenne.

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)

Maintenant, quel type d'amélioration de la performance vous attendriez-vous réellement?

Mise à jour :
Tout d'abord, un avertissement: le code de son état actuel ne s'adapte pas correctement à la forme "noyau". Cependant, ce n'est pas ma principale préoccupation en ce moment (de toute façon que l'idée est là que vous devez vous adapter correctement).

Je viens de choisir la nouvelle forme d'un 4D un 4D une intuitivement, pour moi, il est vraiment logique de penser à un centre de "noyau" 2D à centrer à chaque position de la grille d'origine 2D A.

Mais cette formation 4D peut ne pas être le "meilleur". Je pense que le vrai problème ici est la performance du sommation. Il faut pouvoir trouver "le meilleur ordre" (du 4D a) inondé d'utiliser complètement votre architecture de cache de machines. Cependant, cet ordre peut ne pas être le même pour les "petits" tableaux qui "cooporent" avec votre cache de machines et ces plus grandes, qui ne le font pas (au moins pas si simples).

Mise à jour 2 :
[.____] Voici une version légèrement modifiée de mf. Il est clair qu'il vaut mieux remodeler à un tableau 3D d'abord, puis au lieu de résumer juste le produit DOT (cela présente l'avantage, ce noyau peut être arbitraire). Cependant, il est encore quelque 3 fois plus lent (sur ma machine) que la fonction mise à jour de Pauls.

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
eat