У меня есть квадратный двумерный массив N {N0}} и массив нулей B с одинаковой формой.

Для каждого индекса (i, j) в A, кроме первой и последней строк и столбцов, я хочу присвоить B[i, j] значение np.sum(A[i - 1:i + 2, j - 1:j + 2].

Примере:

A =
array([[0, 0, 0, 0, 0],
       [0, 1, 0, 1, 0],
       [0, 1, 1, 0, 0],
       [0, 1, 0, 1, 0],
       [0, 0, 0, 0, 0])

B =
array([[0, 0, 0, 0, 0],
       [0, 3, 4, 2, 0],
       [0, 4, 6, 3, 0],
       [0, 3, 4, 2, 0],
       [0, 0, 0, 0, 0])

Есть ли эффективный способ сделать это? Или я должен просто использовать цикл for?

5
Travis Black 6 Июл 2019 в 03:00

3 ответа

Лучший ответ

Существует умный (читай «пограничный умник») способ сделать это с помощью np.lib.stride_tricks.as_strided. as_strided позволяет вам создавать представления в вашем буфере, которые имитируют окна, добавляя другое измерение в представление. Например, если у вас был 1D массив как

>>> x = np.arange(10)
>>> np.lib.stride_tricks.as_strided(x, shape=(3, x.shape[0] - 2), strides=x.strides * 2)
array([[0, 1, 2, 3, 4, 5, 6, 7],
       [1, 2, 3, 4, 5, 6, 7, 8],
       [2, 3, 4, 5, 6, 7, 8, 9]])

Надеюсь, ясно, что вы можете просто суммировать по axis=0, чтобы получить сумму для каждого окна размера 3. Нет причины, по которой вы не могли бы расширить это до двух или более измерений. Я написал форму и индекс предыдущего примера таким образом, который предлагает решение:

A = np.array([[0, 0, 0, 0, 0],
              [0, 1, 0, 1, 0],
              [0, 1, 1, 0, 0],
              [0, 1, 0, 1, 0],
              [0, 0, 0, 0, 0]])
view = np.lib.stride_tricks.as_strided(A,
    shape=(3, 3, A.shape[0] - 2, A.shape[1] - 2),
    strides=A.strides * 2
)
B[1:-1, 1:-1] = view.sum(axis=(0, 1))

Суммирование по нескольким осям одновременно поддерживается в np.sum начиная с v1.7.0. Для более старых версий numpy просто суммируйте многократно (дважды) вдоль axis=0.

Заполнение краев B оставлено читателю как упражнение (так как на самом деле это не часть вопроса).

Кроме того, решение здесь однострочное, если вы хотите, чтобы это было. Лично я думаю, что что-то с as_strided уже достаточно неразборчиво и не нуждается в дальнейшей запутанности. Я не уверен, будет ли цикл for достаточно плохим с точки зрения производительности, чтобы оправдать этот метод на самом деле.

Для дальнейшего использования вот общая функция создания окон, которая может использоваться для решения такого рода проблем:

def window_view(a, window=3):
    """
    Create a (read-only) view into `a` that defines window dimensions.

    The first ``a.ndim`` dimensions of the returned view will be sized according to `window`.
    The remaining ``a.ndim`` dimensions will be the original dimensions of `a`, truncated by `window - 1`.

    The result can be post-precessed by reducing the leading dimensions. For example, a multi-dimensional moving average could look something like ::


         window_view(a, window).sum(axis=tuple(range(a.ndim))) / window**a.ndim

    If the window size were different for each dimension (`window` were a sequence rather than a scalar), the normalization would be ``np.prod(window)`` instead of ``window**a.ndim``.

    Parameters
    -----------
    a : array-like
        The array to window into. Due to numpy dimension constraints, can not have > 16 dims.
    window :
        Either a scalar indicating the window size for all dimensions, or a sequence of length `a.ndim` providing one size for each dimension.

    Return
    ------
    view : numpy.ndarray
         A read-only view into `a` whose leading dimensions represent the requested windows into `a`.
         ``view.ndim == 2 * a.ndim``.
    """
    a = np.array(a, copy=False, subok=True)
    window = np.array(window, copy=False, subok=False, dtype=np.int)
    if window.size == 1:
        window = np.full(a.ndim, window)
    elif window.size == a.ndim:
        window = window.ravel()
    else:
        raise ValueError('Number of window sizes must match number of array dimensions')

    shape = np.concatenate((window, a.shape))
    shape[a.ndim:] -= window - 1
    strides = a.strides * 2

    return np.lib.stride_tricks.as_strided(a, shake=shape, strides=strides)
6
Mad Physicist 6 Июл 2019 в 06:55

Я не нашел «простых» способов сделать это. Но вот два пути:


  • Все еще включает в себя цикл
# Basically, get the sum for each location and then pad the result with 0's
B = [[np.sum(A[j-1:j+2,i-1:i+2]) for i in range(1,len(A)-1)] for j in range(1,len(A[0])-1)]
B = np.pad(B, ((1,1)), "constant", constant_values=(0))

  • Это длиннее, но не для циклов (это будет намного эффективнее на больших массивах):
# Roll basically slides the array in the desired direction
A_right = np.roll(A, -1, 1)
A_left = np.roll(A, 1, 1)
A_top = np.roll(A, 1, 0)
A_bottom = np.roll(A, -1, 0)
A_bot_right = np.roll(A_bottom, -1, 1)
A_bot_left = np.roll(A_bottom, 1, 1)
A_top_right = np.roll(A_top, -1, 1)
A_top_left = np.roll(A_top, 1, 1)

# After doing that, you can just add all those arrays and these operations
# are handled better directly by numpy compared to when you use for loops
B = A_right + A_left + A_top + A_bottom + A_top_left + A_top_right + A_bot_left + A_bot_right + A

# You can then return the edges to 0 or whatever you like
B[0:len(B),0] = 0
B[0:len(B),len(B[0])-1] = 0
B[0,0:len(B)] = 0
B[len(B[0])-1,0:len(B)] = 0
2
Akaisteph7 6 Июл 2019 в 01:13

Вы можете просто сложить 9 массивов, которые составляют блок, каждый из которых смещен на 1 w.r.t. предыдущий в любом измерении. Используя обозначение среза, это можно сделать сразу для всего массива A:

B = np.zeros_like(A)
B[1:-1, 1:-1] = sum(A[i:A.shape[0]-2+i, j:A.shape[1]-2+j]
                    for i in range(0, 3) for j in range(0, 3))

Общий вариант для произвольных прямоугольных окон

def sliding_window_sum(a, size):
    """Compute the sum of elements of a rectangular sliding window over the input array.

    Parameters
    ----------
    a : array_like
        Two-dimensional input array.
    size : int or tuple of int
        The size of the window in row and column dimension; if int then a quadratic window is used.

    Returns
    -------
    array
        Shape is ``(a.shape[0] - size[0] + 1, a.shape[1] - size[1] + 1)``.
    """
    if isinstance(size, int):
        size = (size, size)
    m = a.shape[0] - size[0] + 1
    n = a.shape[1] - size[1] + 1
    return sum(A[i:m+i, j:n+j] for i in range(0, size[0]) for j in range(0, size[1]))
0
a_guest 6 Июл 2019 в 16:33