Uso de la función numpy `as_strided` para crear parches, mosaicos, ventanas enrollables o deslizantes de dimensión arbitraria

Resuelto Daniel F asked hace 7 años • 1 respuestas

Pasé un rato esta mañana buscando una pregunta generalizada para señalar duplicados en caso de preguntas sobre as_stridedy/o cómo crear funciones de ventana generalizadas . Parece haber muchas preguntas sobre cómo crear (de forma segura) parches, ventanas deslizantes, ventanas rodantes, mosaicos o vistas en una matriz para aprendizaje automático, convolución, procesamiento de imágenes y/o integración numérica.

Estoy buscando una función generalizada que pueda aceptar un windowparámetro stepy axisdevolver una as_stridedvista para dimensiones más arbitrarias. Daré mi respuesta a continuación, pero me interesa saber si alguien puede crear un método más eficiente, ya que no estoy seguro de que usar np.squeeze()sea el mejor método, no estoy seguro de que mis assertdeclaraciones hagan que la función sea lo suficientemente segura como para escribir en el resultado. vista, y no estoy seguro de cómo manejar el caso límite de axisno estar en orden ascendente.

DEBIDA DILIGENCIA

La función más generalizada que puedo encontrar está sklearn.feature_extraction.image.extract_patchesescrita por @eickenberg (así como la aparentemente equivalente skimage.util.view_as_windows), pero no están bien documentadas en la red y no pueden hacer ventanas en menos ejes de los que hay en la matriz original (por ejemplo , esta pregunta solicita una ventana de cierto tamaño en un solo eje). También muchas veces las preguntas quieren una numpyúnica respuesta.

@Divakar creó una función generalizada numpypara entradas 1-d aquí , pero las entradas de mayor dimensión requieren un poco más de cuidado. He creado una ventana 2D básica sobre un método de entrada 3D , pero no es muy extensible.

Daniel F avatar Aug 30 '17 19:08 Daniel F
Aceptado

EDITAR ENERO DE 2020 : Se cambió el retorno iterable de una lista a un generador para ahorrar memoria.

EDITAR OCTUBRE DE 2020 : coloque el generador en una función separada, ya que mezclar generadores y returndeclaraciones no funciona de manera intuitiva.

Aquí está la receta que tengo hasta ahora:

def window_nd(a, window, steps = None, axis = None, gen_data = False):
        """
        Create a windowed view over `n`-dimensional input that uses an 
        `m`-dimensional window, with `m <= n`
        
        Parameters
        -------------
        a : Array-like
            The array to create the view on
            
        window : tuple or int
            If int, the size of the window in `axis`, or in all dimensions if 
            `axis == None`
            
            If tuple, the shape of the desired window.  `window.size` must be:
                equal to `len(axis)` if `axis != None`, else 
                equal to `len(a.shape)`, or 
                1
                
        steps : tuple, int or None
            The offset between consecutive windows in desired dimension
            If None, offset is one in all dimensions
            If int, the offset for all windows over `axis`
            If tuple, the steps along each `axis`.  
                `len(steps)` must me equal to `len(axis)`
    
        axis : tuple, int or None
            The axes over which to apply the window
            If None, apply over all dimensions
            if tuple or int, the dimensions over which to apply the window

        gen_data : boolean
            returns data needed for a generator
    
        Returns
        -------
        
        a_view : ndarray
            A windowed view on the input array `a`, or `a, wshp`, where `whsp` is the window shape needed for creating the generator
            
        """
        ashp = np.array(a.shape)
        
        if axis != None:
            axs = np.array(axis, ndmin = 1)
            assert np.all(np.in1d(axs, np.arange(ashp.size))), "Axes out of range"
        else:
            axs = np.arange(ashp.size)
            
        window = np.array(window, ndmin = 1)
        assert (window.size == axs.size) | (window.size == 1), "Window dims and axes don't match"
        wshp = ashp.copy()
        wshp[axs] = window
        assert np.all(wshp <= ashp), "Window is bigger than input array in axes"
        
        stp = np.ones_like(ashp)
        if steps:
            steps = np.array(steps, ndmin = 1)
            assert np.all(steps > 0), "Only positive steps allowed"
            assert (steps.size == axs.size) | (steps.size == 1), "Steps and axes don't match"
            stp[axs] = steps
    
        astr = np.array(a.strides)
        
        shape = tuple((ashp - wshp) // stp + 1) + tuple(wshp)
        strides = tuple(astr * stp) + tuple(astr)
        
        as_strided = np.lib.stride_tricks.as_strided
        a_view = np.squeeze(as_strided(a, 
                                     shape = shape, 
                                     strides = strides))
        if gen_data :
            return a_view, shape[:-wshp.size]
        else:
            return a_view

def window_gen(a, window, **kwargs):
    #Same docstring as above, returns a generator
    _ = kwargs.pop(gen_data, False)
    a_view, shp = window_nd(a, window, gen_data  = True, **kwargs)
    for idx in np.ndindex(shp):
        yield a_view[idx]

Algunos casos de prueba:

a = np.arange(1000).reshape(10,10,10)

window_nd(a, 4).shape # sliding (4x4x4) window
Out: (7, 7, 7, 4, 4, 4)

window_nd(a, 2, 2).shape # (2x2x2) blocks
Out: (5, 5, 5, 2, 2, 2)

window_nd(a, 2, 1, 0).shape # sliding window of width 2 over axis 0
Out: (9, 2, 10, 10)

window_nd(a, 2, 2, (0,1)).shape # tiled (2x2) windows over first and second axes
Out: (5, 5, 2, 2, 10)

window_nd(a,(4,3,2)).shape  # arbitrary sliding window
Out: (7, 8, 9, 4, 3, 2)

window_nd(a,(4,3,2),(1,5,2),(0,2,1)).shape #arbitrary windows, steps and axis
Out: (7, 5, 2, 4, 2, 3) # note shape[-3:] != window as axes are out of order
Daniel F avatar Aug 30 '2017 12:08 Daniel F