2

I am trying to count the number of neighbours for each element in a 2d numpy array that differ from the element itself (4-neighbourhood in this case, but 8-neighbourhood is also interesting).

Something like this:

input labels:
 [[1 1 1 2 2 2 2]
 [1 1 1 2 2 2 2]
 [1 1 1 2 2 2 2]
 [1 1 3 3 3 5 5]
 [4 4 4 3 3 5 5]
 [4 4 4 3 3 5 5]] (6, 7)

count of unique neighbour labels:
 [[0 0 1 1 0 0 0]
 [0 0 1 1 0 0 0]
 [0 0 2 2 1 1 1]
 [1 2 2 1 2 2 1]
 [1 1 1 1 1 1 0]
 [0 0 1 1 1 1 0]] (6, 7)

I have the code below, and out of curiosity I am wondering if there is a better way to achieve this, perhaps without the for loops?

import numpy as np
import cv2


labels_image = np.array([
    [1,1,1,2,2,2,2],
    [1,1,1,2,2,2,2],
    [1,1,1,2,2,2,2],
    [1,1,3,3,3,5,5],
    [4,4,4,3,3,5,5],
    [4,4,4,3,3,5,5]])

print('input labels:\n', labels_image, labels_image.shape)

# Make a border, otherwise neighbours are counted as wrapped values from the other side
labels_image = cv2.copyMakeBorder(labels_image, 1, 1, 1, 1, cv2.BORDER_REPLICATE)

offsets = [(-1, 0), (0, -1), (0, 1), (1, 0)] # 4 neighbourhood

# Stack labels_image with one shifted per offset so we get a 3d array
# where each z-value corresponds to one of the neighbours
stacked = np.dstack(np.roll(np.roll(labels_image, i, axis=0), j, axis=1) for i, j in offsets)

# count number of unique neighbours, also take the border away again
labels_image = np.array([[(len(np.unique(stacked[i,j])) - 1)
                        for j in range(1, labels_image.shape[1] - 1)]
                        for i in range(1, labels_image.shape[0] - 1)])

print('count of unique neighbour labels:\n', labels_image, labels_image.shape)

I tried using np.unique with the return_counts and axis arguments, but could not get it to work.

ikkjo
  • 735
  • 1
  • 9
  • 18

1 Answers1

2

Here's one approach -

import itertools

def count_nunique_neighbors(ar):
    a = np.pad(ar, (1,1), mode='reflect')
    c = a[1:-1,1:-1]

    top = a[:-2,1:-1]
    bottom = a[2:,1:-1]
    left = a[1:-1,:-2] 
    right = a[1:-1,2:]

    ineq = [top!= c,bottom!= c, left!= c, right!= c]
    count = ineq[0].astype(int) + ineq[1] + ineq[2] + ineq[3] 

    blck = [top, bottom, left, right]
    for i,j in list(itertools.combinations(range(4), r=2)):
        count -= ((blck[i] == blck[j]) & ineq[j])
    return count

Sample run -

In [22]: a
Out[22]: 
array([[1, 1, 1, 2, 2, 2, 2],
       [1, 1, 1, 2, 2, 2, 2],
       [1, 1, 1, 2, 2, 2, 2],
       [1, 1, 3, 3, 3, 5, 5],
       [4, 4, 4, 3, 3, 5, 5],
       [4, 4, 4, 3, 3, 5, 5]])

In [23]: count_nunique_neighbors(a)
Out[23]: 
array([[0, 0, 1, 1, 0, 0, 0],
       [0, 0, 1, 1, 0, 0, 0],
       [0, 0, 2, 2, 1, 1, 1],
       [1, 2, 2, 1, 2, 2, 1],
       [1, 1, 1, 1, 1, 1, 0],
       [0, 0, 1, 1, 1, 1, 0]])
Divakar
  • 218,885
  • 19
  • 262
  • 358
  • Great! Thank you very much! Your code takes a third of the time of mine which is good. Since I am still keen to learn, and purely out of curiosity, can you think of a way achieving the same using np.unique? – ikkjo Jan 15 '18 at 19:28
  • @ikkjo `np.unique` to find unique neighbours? Maybe if you put each sliding window as a col each - https://stackoverflow.com/questions/30109068/implement-matlabs-im2col-sliding-in-python. But don't think that would be any more efficient than this one. – Divakar Jan 16 '18 at 07:20
  • Many thanks for your help, as well as the additional pointer. – ikkjo Jan 16 '18 at 19:33