如何获取数组的所有边缘?

9
我有一个 n x n 的数组,想要获取其轮廓值。例如:
[4,5,6,7] [2,2,6,3] [4,4,9,4] [8,1,6,1]
从中,我将得到以下结果:
[4,5,6,7,3,4,1,6,1,8,4,2]
[4,5,6,7,3,4,1,6,1,8,4,2]

基本上,如何以最高效的方式获取一个二维数组边缘所有值的一维数组? 我问这个问题是因为我假设有一个NumPy函数可以帮助实现这一点(但我还没有找到!),而不是使用循环手动实现。


1
顺序重要吗? - José Sánchez
不,幸运的是我的情况不是这样的 :) - harry lakins
6个回答

5
In [1]: arr=np.arange(16).reshape(4,4)
In [2]: arr
Out[2]: 
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15]])

相对简单的方法 - 顺时针顺序如下:
In [5]: alist=[arr[0,:-1], arr[:-1,-1], arr[-1,::-1], arr[-2:0:-1,0]]
In [6]: alist
Out[6]: [array([0, 1, 2]), array([ 3,  7, 11]), array([15, 14, 13, 12]), array([8, 4])]
In [7]: np.concatenate(alist)
Out[7]: array([ 0,  1,  2,  3,  7, 11, 15, 14, 13, 12,  8,  4])

从某种意义上说,这是一个循环,因为我必须构建4个切片。但是如果4与n相比较小,那就是一个小代价。它必须在某个级别上连接。

如果顺序不重要,我们可以简化一些切片(例如忘记反转顺序等)。

alist=[arr[0,:], arr[1:,-1], arr[-1,:-1], arr[1:-1,0]]

如果我不关心顺序,或者重复计算角落,我可以使用以下代码:
np.array([arr[[0,n],:], arr[:,[0,n]].T]).ravel()

消除重复的角落。
In [18]: np.concatenate((arr[[0,n],:].ravel(), arr[1:-1,[0,n]].ravel()))
Out[18]: array([ 0,  1,  2,  3, 12, 13, 14, 15,  4,  7,  8, 11])

2
这里有一种向量化方法来创建这些边缘像素/元素的掩码,然后只需索引数组即可获取它们 -
def border_elems(a, W): # Input array : a, Edgewidth : W
    n = a.shape[0]
    r = np.minimum(np.arange(n)[::-1], np.arange(n))
    return a[np.minimum(r[:,None],r)<W]

再次强调,这不完全是为了性能而设计的,而是为了在需要变化边缘宽度或创建此类边缘元素的掩码时使用。该掩码将是:np.minimum(r[:,None],r)<W,如上一步所创建。

示例运行 -

In [89]: a
Out[89]: 
array([[49, 49, 12, 90, 42],
       [91, 58, 92, 16, 78],
       [97, 19, 58, 84, 84],
       [86, 31, 80, 78, 69],
       [29, 95, 38, 51, 92]])

In [90]: border_elems(a,1)
Out[90]: array([49, 49, 12, 90, 42, 91, 78, 97, 84, 86, 69, 29, 95, 38, 51, 92])

In [91]: border_elems(a,2) # Note this will select all but the center one : 58
Out[91]: 
array([49, 49, 12, 90, 42, 91, 58, 92, 16, 78, 97, 19, 84, 84, 86, 31, 80,
       78, 69, 29, 95, 38, 51, 92])

对于通用形状,我们可以这样扩展 -

def border_elems_generic(a, W): # Input array : a, Edgewidth : W
    n1 = a.shape[0]
    r1 = np.minimum(np.arange(n1)[::-1], np.arange(n1))
    n2 = a.shape[1]
    r2 = np.minimum(np.arange(n2)[::-1], np.arange(n2))
    return a[np.minimum(r1[:,None],r2)<W]

2D卷积基于通用形状的解决方案

这是另一种使用2D卷积来处理通用二维形状的解决方案 -

from scipy.signal import convolve2d

k = np.ones((3,3),dtype=int) # kernel
boundary_elements = a[convolve2d(np.ones(a.shape,dtype=int),k,'same')<9]

示例运行 -

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

In [38]: k = np.ones((3,3),dtype=int)

In [39]: a[convolve2d(np.ones(a.shape,dtype=int),k,'same')<9]
Out[39]: array([4, 3, 8, 3, 1, 1, 7, 9, 9, 2, 2, 8, 4, 7])

这种方法仅适用于维度长度相等的数组。例如,在 a = np.ones(10,11) 上测试会失败。 - TomNorway
@TomNorway 为通用形状添加了另一种选择。 - Divakar

1
假设你的列表格式如下:
l = [
     [4, 5, 6, 7],
     [2, 2, 6, 3],
     [4, 4, 9, 4],
     [8, 1, 6, 1]
    ]

您可以使用列表推导式来快速实现所需的功能:

out = list(l[0]) +  # [4, 5, 6, 7]
      list([i[-1] for i in l[1:-1]]) +  # [3, 4]
      list(reversed(l[-1])) +  # [1, 6, 1, 8]
      list(reversed([i[0] for i in l[1:-1]])) # [4, 2]

print(out)  # gives [4, 5, 6, 7, 3, 4, 1, 6, 1, 8, 4, 2]

无论您有一个普通的Python列表还是一个NumPy数组,此方法都可以使用。
关于效率,在一个20000x20000矩阵上使用%timeit,这种方法需要16.4毫秒。
l = np.random.random(20000, 20000)
%timeit list(l[0]) + list(...) + list(...) + list(...)
100 loops, best of 3: 16.4 ms per loop

我相信有更有效的方法来完成这个任务,但我认为这是一个很好的一行解决方案。


1
您可以像下面的示例一样使用 itertools.groupbylist comprehension

a = [
        [4,5,6,7],
        [2,2,6,3],
        [4,4,9,4],
        [8,1,6,1],
    ]

from itertools import groupby

def edges(a = list):
    final, i = [], []
    for k, _ in groupby(a[1:-1], lambda x : [x[0], x[-1]]):
        i += k

    return a[0] + [k for n in range(1,len(i), 2) for k in i[n:n+1]] + a[-1][::-1] + [k for n in range(0, len(i), 2) for k in i[n:n+1] ][::-1]

输出:

print(edges(a))
>>> [4, 5, 6, 7, 3, 4, 1, 6, 1, 8, 4, 2]

测试使用timeit

a = [
        [4,5,6,7],
        [2,2,6,3],
        [4,4,9,4],
        [8,1,6,1],
    ]

from itertools import groupby

def edges():
    final, i = [], []
    for k, _ in groupby(a[1:-1], lambda x : [x[0], x[-1]]):
        i += k

    return a[0] + [k for n in range(1,len(i), 2) for k in i[n:n+1]] + a[-1][::-1] + [k for n in range(0, len(i), 2) for k in i[n:n+1] ][::-1]


if __name__ == '__main__':
    import timeit
    print(timeit.timeit("edges()", setup="from __main__ import edges", number = 100))

最佳时间是0.0006266489999688929

1
它可能比其他答案中提到的替代方案慢,因为它正在创建一个掩码(这是我的用例),但它可以在您的情况下使用:
def mask_borders(arr, num=1):
    mask = np.zeros(arr.shape, bool)
    for dim in range(arr.ndim):
        mask[tuple(slice(0, num) if idx == dim else slice(None) for idx in range(arr.ndim))] = True  
        mask[tuple(slice(-num, None) if idx == dim else slice(None) for idx in range(arr.ndim))] = True  
    return mask

正如前面所说,这将创建并返回一个mask,其中边框被遮蔽(True):
>>> mask_borders(np.ones((5,5)))
array([[ True,  True,  True,  True,  True],
       [ True, False, False, False,  True],
       [ True, False, False, False,  True],
       [ True, False, False, False,  True],
       [ True,  True,  True,  True,  True]], dtype=bool)

>>> # Besides supporting arbitary dimensional input it can mask multiple border rows/cols
>>> mask_borders(np.ones((5,5)), 2)
array([[ True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True],
       [ True,  True, False,  True,  True],
       [ True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True]], dtype=bool)

要获取“border”值,需要使用布尔索引将其应用于您的数组:
>>> arr = np.array([[4,5,6,7], [2,2,6,3], [4,4,9,4], [8,1,6,1]])

>>> arr[mask_borders(arr)]
array([4, 5, 6, 7, 2, 3, 4, 4, 8, 1, 6, 1])

不错!我也有类似的想法,只是以向量化的方式实现。 - Divakar
@Divakar 如果在所有维度中形状不相同,我在向量化方面遇到了一些麻烦。我一定会查看您的答案! - MSeifert
是的,所以如果a的形状为(m,n),我的解决方案将会改变为:r1 = np.minimum(np.arange(n)[::-1], np.arange(n)),并且类似地对于r2使用m,最后使用np.minimum(r2[:,None],r1)生成布尔掩码,如果我正确理解了这些维度。 - Divakar

0

请查看这个完整的解决方案。这是源代码

def border(array, corner=0, direction='cw'):
"""
Extract the values arround the border of a 2d array.

Default settings start from top left corner and move clockwise. 
Corners are only used once.

Parameters
----------
array : array_like
    A 2d array
corner : {0, 1, 2, 3}
    Specify the corner to start at.
    0 - start at top left corner (default)
    1 - start at top right corner
    2 - start at bottom right corner
    3 - start at bottom left corner
direction : {'cw', 'ccw'}
    Specify the direction to walk around the array
    cw  - clockwise (default)
    ccw - counter-clockwise

Returns
-------
border : ndarray
    Values around the border of `array`.

Examples
--------
>>> x, y = np.meshgrid(range(1,6), range(5))
>>> array=x*y
>>> array[0,0]=999
array([[999,   0,   0,   0,   0],
       [  1,   2,   3,   4,   5],
       [  2,   4,   6,   8,  10],
       [  3,   6,   9,  12,  15],
       [  4,   8,  12,  16,  20]])
>>> border(array)
array([999,   0,   0,   0,   0,   5,  10,  15,  20,  16,  12,   8,   4,
         3,   2,   1, 999])
>> border(array, corner=2)
array([ 20,  16,  12,   8,   4,   3,   2,   1, 999,   0,   0,   0,   0,
         5,  10,  15,  20])
>>> border(array, direction='ccw')
array([999,   1,   2,   3,   4,   8,  12,  16,  20,  15,  10,   5,   0,
         0,   0,   0, 999])
>>> border(array, corner=2, direction='ccw')
array([ 20,  15,  10,   5,   0,   0,   0,   0, 999,   1,   2,   3,   4,
         8,  12,  16,  20])
"""
if corner > 0:
    # Rotate the array so we start on a different corner
    array = np.rot90(array, k=corner)
if direction is 'ccw':
    # Transpose the array so we march around counter-clockwise
    array = array.T

border = []
border += list(array[0, :-1])    # Top row (left to right), not the last element.
border +=list(array[:-1, -1])    # Right column (top to bottom), not the last element.
border +=list(array[-1, :0:-1])  # Bottom row (right to left), not the last element.
border +=list(array[::-1, 0])    # Left column (bottom to top), all elements element.
# NOTE: in that last statement, we include the last element to close the path.

return np.array(border)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接