Finding perimeter coordinates of a mask

Question:

      [[  0,   0,   0,   0, 255,   0,   0,   0,   0],
       [  0,   0, 255, 255, 255, 255, 255,   0,   0],
       [  0, 255, 255, 255, 255, 255, 255, 255,   0],
       [  0, 255, 255, 255, 255, 255, 255, 255,   0],
       [255, 255, 255, 255, 255, 255, 255, 255, 255],
       [  0, 255, 255, 255, 255, 255, 255, 255,   0],
       [  0, 255, 255, 255, 255, 255, 255, 255,   0],
       [  0,   0, 255, 255, 255, 255, 255,   0,   0],
       [  0,   0,   0,   0, 255,   0,   0,   0,   0]]

I have a mask array like the one above. I would like to get the x and y coordinates belonging to the perimeter of the mask. The perimeter points are the ones shown in the array below:

  [[  0,   0,   0,   0, 255,   0,   0,   0,   0],
   [  0,   0, 255, 255,   0, 255, 255,   0,   0],
   [  0, 255,   0,   0,   0,   0,   0, 255,   0],
   [  0, 255,   0,   0,   0,   0,   0, 255,   0],
   [255,   0,   0,   0,   0,   0,   0,   0, 255],
   [  0, 255,   0,   0,   0,   0,   0, 255,   0],
   [  0, 255,   0,   0,   0,   0,   0, 255,   0],
   [  0,   0, 255, 255,   0, 255, 255,   0,   0],
   [  0,   0,   0,   0, 255,   0,   0,   0,   0]]

In the array above, I could just use numpy.nonzero() but I was unable to apply this logic to the original array because it returns a tuple of arrays each containing all the x or y values of all non zero elements without partitioning by row.

I wrote the code below which works but seems inefficient:

height = mask.shape[0]
width = mask.shape[1]

y_coords = []
x_coords = []
for y in range(1,height-1,1):
           for x in range(0,width-1,1):
               val = mask[y,x]
               prev_val = mask[y,(x-1)]
               next_val = mask[y, (x+1)]
               top_val = mask[y-1, x]
               bot_val = mask[y+1, x]
               if (val != 0 and prev_val == 0) or (val != 0 and next_val == 0) or (val != 0 and top_val == 0) or (val != 0 and bot_val == 0):
                   y_coords.append(y)
                   x_coords.append(x)

I am new to python and would like to learn a better way to do this. Perhaps using Numpy?

Asked By: bonzoon

||

Answers:

I think this would work, I edit my answer based on my new understanding of the problem you want the outer pixel of 255 circle
What I did here is getting all the axis of where pixels are 255 items (print it to see) and then selecting the first occurrence and last occurrence that’s it easy

result=np.where(pixel==255)
items=list(zip(result[0],result[1]))
unique=[]
perimiter=[]
for index in range(len(items)-1):

    if items[index][0]!=items[index+1][0] or items[index][0] not in unique:
        unique.append(items[index][0])
        perimiter.append(items[index])
perimiter.append(items[-1])  

Output

[(0, 4),
 (1, 2),
 (1, 6),
 (2, 1),
 (2, 7),
 (3, 1),
 (3, 7),
 (4, 0),
 (4, 8),
 (5, 1),
 (5, 7),
 (6, 1),
 (6, 7),
 (7, 2),
 (7, 6),
 (8, 4)]
Answered By: Mohamed Fathallah

I played a bit with your problem and found a solution and I realized you could use convolutions to count the number of neighboring 255s for each cell, and then perform a filtering of points based on the appropriate values of neighbors.

I am giving a detailed explanation below, although one part was trial and error and you could potentially skip it and get directly to the code if you understand that convolutions can count neighbors in binary images.


First observation: When does a point belong to the perimeter of the mask?
Well, that point has to have a value of 255 and "around" it, there must be at least one (and possibly more) 0.

Next: What is the definition of "around"?
We could consider all four cardinal (i.e. North, East, South, West) neighbors. In this case, a point of the perimeter must have at least one cardinal neighbor which is 0.
You have already done that, and truly, I cannot thing of a faster way by this definition.

What if we extended the definition of "around"?
Now, let’s consider the neighbors of a point at (i,j) all points along an N x N square centered on (i,j). I.e. all points (x,y) such that i-N/2 <= x <= i+N/2 and j-N/2 <= y <= j+N/2 (where N is odd and ignoring out of bounds for the moment).
This is more useful from a performance point of view, because the operation of sliding "windows" along the points of 2D arrays is called a "convolution" operation. There are built in functions to perform such operations on numpy arrays really fast. The scipy.ndimage.convolve works great.
I won’t attempt to fully explain convolutions here (the internet is ful of nice visuals), but the main idea is that the convolution essentially replaces the value of each cell with the weighted sum of the values of all its neighboring cells. Depending on what weight matrix, (or kernel) you specify, the convolution does different things.

Now, if your mask was 1s and 0s, to count the number of neighboring ones around a cell, you would need a kernel matrix of 1s everywhere (since the weighted sum will simply add the original ones of your mask and cancel the 0s). So we will scale the values from [0, 255] to [0,1].

Great, we know how to quickly count the neighbors of a point within an area, but the two questions are

  1. What area size should we choose?
  2. How many neighbors do the points in the perimeter have, now that we are including diagonal and more faraway neighbors?

I suppose there is an explicit answer to that, but I did some trial and error. It turns out, we need N=5, at which case the number of neighbors being one for each point in the original mask is the following:

[[ 3  5  8 10 11 10  8  5  3]
 [ 5  8 12 15 16 15 12  8  5]
 [ 8 12 17 20 21 20 17 12  8]
 [10 15 20 24 25 24 20 15 10]
 [11 16 21 25 25 25 21 16 11]
 [10 15 20 24 25 24 20 15 10]
 [ 8 12 17 20 21 20 17 12  8]
 [ 5  8 12 15 16 15 12  8  5]
 [ 3  5  8 10 11 10  8  5  3]]

Comparing that matrix with your original mask, the points on the perimeter are the ones having values between 11 and 15 (inclusive) [1]. So we simply filter out the rest using np.where().

A final caveat: We need to explicilty say to the convolve function how to treat points near the edges, where an N x N window won’t fit. In those cases, we tell it to treat out of bounds values as 0s.

The full code is following:

from scipy import ndimage as ndi

mask   //= 255
kernel = np.ones((5,5))
C      = ndi.convolve(mask, kernel, mode='constant', cval=0)

#print(C) # The C matrix contains the number of neighbors for each cell.

outer  = np.where( (C>=11) & (C<=15 ), 255, 0)
print(outer)



[[  0   0   0   0 255   0   0   0   0]
 [  0   0 255 255   0 255 255   0   0]
 [  0 255   0   0   0   0   0 255   0]
 [  0 255   0   0   0   0   0 255   0]
 [255   0   0   0   0   0   0   0 255]
 [  0 255   0   0   0   0   0 255   0]
 [  0 255   0   0   0   0   0 255   0]
 [  0   0 255 255   0 255 255   0   0]
 [  0   0   0   0 255   0   0   0   0]]

[1] Note that we are also counting the point itself as one of its own neighbors. That’s alright.

Answered By: kyriakosSt
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.