How to get all array edges?

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]])

A relatively straight forward way of doing this - in clockwise order is:

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])

In a sense it's a loop, in that I have to build 4 slices. But if 4 is small compared to n, that's a small price. It has to concatenate at some level.

If order doesn't matter we could simplify the slices some (e.g. forgetting the reverse order, etc).

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

If I didn't care about order, or double counting the corners I could use:

np.array([arr[[0,n],:], arr[:,[0,n]].T]).ravel()

eliminating the duplicate corners

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])

Here's one vectorized approach to create a mask of such edge pixels/elements and then simply indexing into the array to get those -

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]

Again, this not exactly meant for performance, but more for cases when you might to vary the edge-width or just create such a mask of such edge elements. The mask would be : np.minimum(r[:,None],r)<W as created at the last step.

Sample run -

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])

For generic shape, we can extend like so -

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 convolution based solution for generic shape

Here's another with 2D convolution that takes care of generic 2D shape -

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]

Sample run -

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])