Make everything zeros in a numpy 2d-array outside of bounding box - python

Say I have a numpy 2d-array like:
>>> ar
array([[1, 2, 3, 1, 2, 3, 1, 7, 2, 3],
[4, 3, 2, 4, 5, 5, 6, 5, 2, 1],
[5, 4, 2, 4, 6, 2, 4, 2, 1, 4],
[1, 5, 6, 1, 4, 2, 2, 4, 1, 4],
[7, 4, 5, 6, 2, 5, 3, 5, 6, 7]])
I define a bounding box like follows, and I want that all the items in ar which are not in the bounding box all become zeros:
>>> my_bbox = ((2, 7), (1, 3))
>>> make_zeros(ar, bounding_box)
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 2, 4, 5, 5, 6, 5, 0, 0],
[0, 0, 2, 4, 6, 2, 4, 2, 0, 0],
[0, 0, 6, 1, 4, 2, 2, 4, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
Beyond the "obvious" (and also ugly and inefficient) looping methods, is there a slice/vector-type operation to achieve that? I believe that array masks might work, but not sure how specifically.

You might like to create a copy of array and then write needed submatrix on top of it:
ar_zeroed = np.zeros(ar.shape).astype(int)
slice1, slice2 = slice(my_bbox[0][0], my_bbox[0][1]+1),
slice(my_bbox[1][0], my_bbox[1][1]+1)
ar_zeroed[slice2, slice1] = ar[slice2, slice1]
Note that dimensions are swapped in your bounding box

How about something like this?
import numpy as np
arr = np.array([[1, 2, 3, 1, 2, 3, 1, 7, 2, 3],
[4, 3, 2, 4, 5, 5, 6, 5, 2, 1],
[5, 4, 2, 4, 6, 2, 4, 2, 1, 4],
[1, 5, 6, 1, 4, 2, 2, 4, 1, 4],
[7, 4, 5, 6, 2, 5, 3, 5, 6, 7]])
print(arr, end='\n\n')
first_idx = (1, 3)
second_idx = (2, 7)
first_slice = slice(first_idx[0], first_idx[1] + 1)
second_slice = slice(second_idx[0], second_idx[1] + 1)
res = np.zeros_like(arr)
res[first_slice, second_slice] = arr[first_slice, second_slice]
print(res)
Output:
[[1 2 3 1 2 3 1 7 2 3]
[4 3 2 4 5 5 6 5 2 1]
[5 4 2 4 6 2 4 2 1 4]
[1 5 6 1 4 2 2 4 1 4]
[7 4 5 6 2 5 3 5 6 7]]
[[0 0 0 0 0 0 0 0 0 0]
[0 0 2 4 5 5 6 5 0 0]
[0 0 2 4 6 2 4 2 0 0]
[0 0 6 1 4 2 2 4 0 0]
[0 0 0 0 0 0 0 0 0 0]]
I'm almost certain it can still be improved, though.

You are trying to do "something" to the slices ar[1:3+1, 2:7+1]. The expression in square brackets is turned into a tuple of slice objects by the interpreter. You can programatically create the same sequence of slices and pass them in as the index to an array. This can be used in a number of approaches.
The "simplest" is to create a new array of zeros and assign the required swath:
index = tuple(slice(start, stop + 1) for start, stop in bounding_box)
result = np.zeros_like(ar)
result[index] = ar[index]
Another would be to make a mask, invert it, and set the elements it corresponds to to zero:
mask = np.ones(ar.shape, dtype=np.bool)
mask[index] = zero
ar[mask] = 0
Finally, you could just set the appropriate swaths to zeros directly. This is probably less optimal than the other approaches, unless you code it very carefully to not have overlaps in the corners:
index = [slice(None) for _ in range(ar.ndim)]
for dim, (start, stop) in enumerate(bounding_box):
# Blank out initial portion
index[dim] = slice(None, start)
ar[tuple(index)] = 0
# Blank out trailing portion
index[dim] = slice(stop + 1, None)
ar[tuple(index)] = 0
# Reinstate ":"
index[dim] = slice(None)

I was just thinking of this solution
a = np.random.uniform(1, 10, [7, 8]).astype(int)
print(a)
b = np.zeros([7, 8]).astype(int)
b[1:4, 2:5] = 1
print(a * b)
Outputs
[[8 3 8 5 8 3 5 7]
[8 8 2 8 5 4 3 5]
[5 2 2 2 5 1 4 7]
[5 8 9 3 5 6 4 1]
[3 6 1 1 4 6 4 8]
[9 5 2 7 8 2 1 1]
[1 4 5 4 2 6 2 4]]
[[0 0 0 0 0 0 0 0]
[0 0 2 8 5 0 0 0]
[0 0 2 2 5 0 0 0]
[0 0 9 3 5 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]]
Not totally satisfied though because it works only if we want 0 out of the bounding box. If we want any other value, then it doesn't work as is.

Guess it was already answered, but I will still give a try:
import numpy as np
bbox = ((2, 7), (1, 3))
arr = np.array([[1, 2, 3, 1, 2, 3, 1, 7, 2, 3],
[4, 3, 2, 4, 5, 5, 6, 5, 2, 1],
[5, 4, 2, 4, 6, 2, 4, 2, 1, 4],
[1, 5, 6, 1, 4, 2, 2, 4, 1, 4],
[7, 4, 5, 6, 2, 5, 3, 5, 6, 7]])
zeros = np.zeros(arr.shape)
zeros[bbox[1][0]:bbox[1][1]+1,
bbox[0][0]:bbox[0][1]+1] = arr[bbox[1][0]:bbox[1][1]+1,
bbox[0][0]:bbox[0][1]+1]
print(zeros)
Out:
[[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 2. 4. 5. 5. 6. 5. 0. 0.]
[0. 0. 2. 4. 6. 2. 4. 2. 0. 0.]
[0. 0. 6. 1. 4. 2. 2. 4. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]

Related

How to replace repeated items in the row of my array with zeros

I am trying to write a code that replaces all rows of three or more continuous values for zeros. so the three threes on the first row should become zero. I wrote this code which in my mind should work but when I execute my code it seems to me that I am stuck in an infinite loop.
import numpy as np
A = np.array([[1, 2, 3, 3, 3, 4],
[1, 3, 2, 4, 2, 4],
[1, 2, 4, 2, 4, 4],
[1, 2, 3, 5, 5, 5],
[1, 2, 1, 3, 4, 4]])
row_nmbr,column_nmbr = (A.shape)
row = 0
column = 0
while column < column_nmbr:
next_col = column + 1
next_col2 = next_col + 1
if A[row][column] == A[row][next_col] and A[row][next_col] == A[row][next_col2]:
A[row][column] = 0
column =+ 1
print(A)
Don't use if-else. It gets messy easily. Here's an approach without if-else.
Iterate over each row, and find unique element and their counts in it.
If an element occurs three or more times, filter that into an array.
Start iteration for each filtered element (val)
Find the indices of val in the given row
Do a groupby on the indices from step 4 to find blocks of contiguous indices.
Check if contiguous indices are three or more in number
If yes, do replacement.
The following sample code is scalable and works for multiple contiguous elements.
from functools import partial
from operator import itemgetter
A = np.array([[3, 3, 5, 3, 3, 3, 5, 5, 5, 6, 6, 5, 5, 5],
[1, 8, 8, 4, 7, 4, 7, 7, 7, 7, 1, 2, 3, 9],
[1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5],
[1, 2, 3, 3, 3, 3, 3, 2, 1, 1, 1, 2, 2, 2],
[1, 2, 1, 3, 4, 4, 9, 8, 8, 8, 8, 9, 9, 8]])
def func1d(row, replacement):
# find and filter elements which occurs three or more times
vals, count = np.unique(row, return_counts=True)
vals = vals[count >= 3]
# Iteration for each filtered element (val)
for val in vals:
# get indices of val from row
indices = (row == val).nonzero()[0]
# find contiguous indices
for k, g in groupby(enumerate(indices), lambda t: t[1] - t[0]):
l = list(map(itemgetter(1), g))
# if grouped indices are three or more, do replacement
if len(l) >=3:
row[l] = replacement
return row
wrapper = partial(func1d, replacement=0)
np.apply_along_axis(wrapper, 1, A)
Output, when compared with A:
# original array
[[3 3 5 3 3 3 5 5 5 6 6 5 5 5]
[1 8 8 4 7 4 7 7 7 7 1 2 3 9]
[1 1 1 2 2 2 3 3 3 4 4 4 4 5]
[1 2 3 3 3 3 3 2 1 1 1 2 2 2]
[1 2 1 3 4 4 9 8 8 8 8 9 9 8]]
# array with replaced values
[[3 3 5 0 0 0 0 0 0 6 6 0 0 0]
[1 8 8 4 7 4 0 0 0 0 1 2 3 9]
[0 0 0 0 0 0 0 0 0 0 0 0 0 5]
[1 2 0 0 0 0 0 2 0 0 0 0 0 0]
[1 2 1 3 4 4 9 0 0 0 0 9 9 8]]
Your loop will be infinite since column will always be 0 and less than column_nmbr.
Do it right like this:
for i in range(row_nmbr):
m, k = np.unique(A[i], return_inverse=True)
val = m[np.bincount(k) > 2]
if len(val) > 0:
aaa = A[i]
aaa[A[i] == val] = 0
print(A)
Output:
[[1 2 0 0 0 4]
[1 3 2 4 2 4]
[1 2 0 2 0 0]
[1 2 3 0 0 0]
[1 2 1 3 4 4]]

Compare 3 columns of a 2-D List and Replace based on conditions

I have a 2-D List as follows:
[
[6 4 4 2 5 5 4 5 4 1 3 5]
[4 3 6 5 4 4 5 1 5 5 2 4]
[2 5 2 0 4 5 4 4 2 3 2 6]
[5 5 4 3 5 4 6 7 3 4 4 4]
[3 5 6 5 6 5 3 5 3 4 7 4]
[4 5 5 4 5 4 7 5 3 5 4 1]
[2 5 3 3 5 3 4 4 3 3 1 3]
[2 5 5 2 5 4 6 2 5 6 2 5]
]
Conditions:
compare column 1,5 and 9 (in steps of 4) - row-wise and process them in the following order
If one of them is zero - do nothing. Go to Step 2
(6,5,4) - none of them zero so go to step 2
If they are all equal - change all of them to zero. If not go Step 3
Take the lowest of the three and subtract each by this minimum
Repeat this with next three elements (2,6,10) until (4,8,12)
How to do efficiently this in python using pandas or numpy or even list operation.
Any help appreciated. Thanks!
You could write a custom function and then apply that functions to every element in the array.
def check_conditions(x):
for i in range(4):
if x[i] == 0 or x[i+4] == 0 or x[i+8] == 0:
continue
elif x[i] == x[i+4] == x[i+8]:
x[i] = 0
x[i+4] = 0
x[i+8] = 0
else:
min_val = min(x[i], x[i+4], x[i+8])
x[i] -= min_val
x[i+4] -= min_val
x[i+8] -= min_val
return x
new_arr = [check_conditions(x) for x in arr]
To get the following result.
print(new_arr)
[[2, 3, 1, 2, 1, 4, 1, 5, 0, 0, 0, 5],
[0, 0, 4, 5, 0, 1, 3, 1, 1, 2, 0, 4],
[0, 2, 0, 0, 2, 2, 2, 4, 0, 0, 0, 6],
[2, 1, 0, 3, 2, 0, 2, 7, 0, 0, 0, 4],
[0, 1, 3, 5, 3, 1, 0, 5, 0, 0, 4, 4],
[1, 1, 1, 4, 2, 0, 3, 5, 0, 1, 0, 1],
[0, 2, 2, 3, 3, 0, 3, 4, 1, 0, 0, 3],
[0, 1, 3, 2, 3, 0, 4, 2, 3, 2, 0, 5]]

Numpy 2D array shuffle elements between rows and not columns

I have following two dimensional array:
seq_length = 5
x = np.array([[0, 2, 0, 4], [5,6,7,8]])
x_repeated = np.repeat(x, seq_length, axis=1)
[[0 0 0 0 0 2 2 2 2 2 0 0 0 0 0 4 4 4 4 4]
[5 5 5 5 5 6 6 6 6 6 7 7 7 7 7 8 8 8 8 8]]
I want to shuffle x_repeated according to seq_length that all items of seq will be shuffled together.
For example, possible shuffle:
[[0 0 0 0 0 6 6 6 6 6 0 0 0 0 0 8 8 8 8 8]
[5 5 5 5 5 2 2 2 2 2 7 7 7 7 7 4 4 4 4 4]]
Thanks
You can do something like this:
import numpy as np
seq_length = 5
x = np.array([[0, 2, 0, 4], [5, 6, 7, 8]])
swaps = np.random.choice([False, True], size=4)
for swap_index, swap in enumerate(swaps):
if swap:
x[[0, 1], swap_index] = x[[1, 0], swap_index]
x_repeated = np.repeat(x, seq_length, axis=1)
You can also rely on the fact that True is non-zero, and replace the for with:
for swap_index in swaps.nonzero()[0]:
x[[0, 1], swap_index] = x[[1, 0], swap_index]
The key is that I did the shuffling/swapping before the np.repeat call, which will be much more efficient compared to doing it afterwards (while meeting your requirement of sequences of values needing to be swapped). There is a 50% chance for each pair of sequences of the same values to be swapped.
Managed to solve it following way:
items_count = x.shape[-1]
swap_flags = np.repeat(np.random.choice([0, 1], size=items_count), single_item_length)
gives:
[1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1]
for idx, flag in enumerate(swap_flags):
if flag:
x_repeated[0,idx], x_repeated[1,idx] = x_repeated[1,idx], x_repeated[0,idx]
Result:
[[5 5 5 5 5 6 6 6 6 6 0 0 0 0 0 8 8 8 8 8]
[0 0 0 0 0 2 2 2 2 2 7 7 7 7 7 4 4 4 4 4]]
Still not so elegant numpy way.
Here's my attempt:
def randomize_blocks(arr):
""" Shuffles an n-dimensional array given consecutive blocks of numbers.
"""
groups = (np.diff(arr.ravel(), prepend=0) != 0).cumsum().reshape(arr.shape)
u, c = np.unique(groups, return_counts=True)
np.random.shuffle(u)
o = np.argsort(u)
return arr.ravel()[np.argsort(np.repeat(u, c[o]))].reshape(arr.shape)
Breakdown
First we get the groups
groups = (np.diff(arr.ravel(), prepend=0) != 0).cumsum().reshape(arr.shape)
array([[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3],
[4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7]])
Then, we get unique and counts for each group.
u, c = np.unique(groups, return_counts=True)
>>> print(u, c)
(array([6, 0, 3, 5, 2, 4, 7, 1]),
array([5, 5, 5, 5, 5, 5, 5, 5]))
Finally, we shuffle our unique groups, reconstruct the array and use argsort to re-order the shuffled unique groups.
o = np.argsort(u)
arr.ravel()[np.argsort(np.repeat(u, c[o]))].reshape(arr.shape)
Example usage:
>>> randomize_blocks(arr)
array([[0, 0, 0, 0, 0, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5],
[7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 2, 2, 2, 2, 2, 6, 6, 6, 6, 6]])
>>> randomize_blocks(arr)
array([[6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 2, 2, 2, 2, 2]])
Here is a solution that does is completely in-place and does not require allocating and generating random indices:
import numpy as np
def row_block_shuffle(a: np.ndarray, seq_len: int):
cols = a.shape[1]
rng = np.random.default_rng()
for block in x_repeated.T.reshape(cols // seq_len, seq_length, -1).transpose(0, 2, 1):
rng.shuffle(block)
if __name__ == "__main__":
seq_length = 5
x = np.array([[0, 2, 0, 4], [5, 6, 7, 8]])
x_repeated = np.repeat(x, seq_length, axis=1)
row_block_shuffle(x_repeated, seq_length)
print(x_repeated)
Output:
[[5 5 5 5 5 6 6 6 6 6 7 7 7 7 7 8 8 8 8 8]
[0 0 0 0 0 2 2 2 2 2 0 0 0 0 0 4 4 4 4 4]]
What I do is to create "blocks" that shares memory with the original array:
>>> x_repeated.T.reshape(cols // seq_len, seq_length, -1).transpose(0, 2, 1)
[[[0 0 0 0 0]
[5 5 5 5 5]]
[[2 2 2 2 2]
[6 6 6 6 6]]
[[0 0 0 0 0]
[7 7 7 7 7]]
[[4 4 4 4 4]
[8 8 8 8 8]]]
Then I shuffle each "block", which will in turn shuffles the original array as well. I believe this is the most effective solution for large arrays as this solution is as in-place as it can be. This answer at least backs up my hypothesis:
https://stackoverflow.com/a/5044364/13091658
Also! The general problem you are facing is sorting "sliding window views" of your array, so if you would like to sort "windows" within your array that both moves horizontally and vertically you can for example see my previous answers for problems related to sliding windows here:
https://stackoverflow.com/a/67416335/13091658
https://stackoverflow.com/a/69924828/13091658
import numpy as np
m = np.array([[0, 2, 0, 4], [5, 6, 7, 8]])
def np_shuffle(m, m_rows = len(m), m_cols = len(m[0]), n_duplicate = 5):
# Flatten the numpy matrix
m = m.flatten()
# Randomize the flattened matrix m
np.random.shuffle(m)
# Duplicate elements
m = np.repeat(m, n_duplicate, axis=0)
# Return reshape numpy array
return (np.reshape(m, (m_rows, n_duplicate*m_cols)))
r = np_shuffle(m)
print(r)
# [[8 8 8 8 8 5 5 5 5 5 2 2 2 2 2 0 0 0 0 0]
# [0 0 0 0 0 7 7 7 7 7 4 4 4 4 4 6 6 6 6 6]]

numpy replace 2d bool array with sum of consecutive elements across an axis efficiently

I have a bool array (bool_arr) that I want to replace the consecutive non-zero numbers along the columns with their count (consecutive_count) (which is also the max/last number of the consecutive group)
bool_arr = consecutive_count =
[[1 1 1 1 0 1] [[3 6 1 6 0 1]
[1 1 0 1 1 0] [3 6 0 6 5 0]
[1 1 1 1 1 1] [3 6 3 6 5 2]
[0 1 1 1 1 1] [0 6 3 6 5 2]
[1 1 1 1 1 0] [2 6 3 6 5 0]
[1 1 0 1 1 1]] [2 6 0 6 5 1]]
I've created my own function that gets the cumulative sum of consecutive non-zero elements along the columns
consecutive_cumsum =
[[1 1 1 1 0 1]
[2 2 0 2 1 0]
[3 3 1 3 2 1]
[0 4 2 4 3 2]
[1 5 3 5 4 0]
[2 6 0 6 5 1]]
I currently use the following to get consecutive_count:
bool_arr = np.array([[1,1,1,1,0,1],
[1,1,0,1,1,0],
[1,1,1,1,1,1],
[0,1,1,1,1,1],
[1,1,1,1,1,0],
[1,1,0,1,1,1]])
consecutive_cumsum = np.array([[1,1,1,1,0,1],
[2,2,0,2,1,0],
[3,3,1,3,2,1],
[0,4,2,4,3,2],
[1,5,3,5,4,0],
[2,6,0,6,5,1]])
consecutive_count = consecutive_cumsum.copy()
for x in range(consecutive_count.shape[1]):
maximum = 0
for y in range(consecutive_count.shape[0]-1, -1, -1):
if consecutive_cumsum[y,x] > 0:
if consecutive_cumsum[y,x] < maximum: consecutive_count[y,x] = maximum
else: maximum = consecutive_cumsum[y,x]
else: maximum = 0
print(consecutive_count)
It works great but I am iterating over every element to replace with the max, between zeros.
Is there a way to use numpy to vectorize this instead of looping over all elements. And as a bonus, specify which axis (row vs column) it will perform it on
The new (v1.15.0 I believe) append and prepend keywords of np.diff make this easy:
bnd = np.diff(bool_arr, axis=0, prepend=0, append=0)
x, y = np.where(bnd.T)
bnd.T[x, y] *= (y[1::2]-y[::2]).repeat(2)
bnd[:-1].cumsum(axis=0)
# array([[3, 6, 1, 6, 0, 1],
# [3, 6, 0, 6, 5, 0],
# [3, 6, 3, 6, 5, 2],
# [0, 6, 3, 6, 5, 2],
# [2, 6, 3, 6, 5, 0],
# [2, 6, 0, 6, 5, 1]])
With selectable axis:
def count_ones(a, axis=-1):
a = a.swapaxes(-1, axis)
bnd = np.diff(a, axis=-1, prepend=0, append=0)
*idx, last = np.where(bnd)
bnd[(*idx, last)] *= (last[1::2]-last[::2]).repeat(2)
return bnd[..., :-1].cumsum(axis=-1).swapaxes(-1, axis)
UPDATE: and a version that works with general (not just 0/1) entries:
def sum_stretches(a, axis=-1):
a = a.swapaxes(-1, axis)
dtype = np.result_type(a, 'i1')
bnd = np.diff((a!=0).astype(dtype), axis=-1, prepend=0, append=0)
*idx, last = np.where(bnd)
A = np.concatenate([np.zeros((*a.shape[:-1], 1), a.dtype), a.cumsum(axis=-1)], -1)[(*idx, last)]
bnd[(*idx, last)] *= (A[1::2]-A[::2]).repeat(2)
return bnd[..., :-1].cumsum(axis=-1).swapaxes(-1, axis)
Using itertools.groupby:
import itertools
for i in range(b.shape[1]):
counts = []
for k,v in itertools.groupby(b[:,i]):
g = list(v)
counts.extend([sum(g)] * len(g))
b[:,i] = counts
Output:
array([[3, 6, 1, 6, 0, 1],
[3, 6, 0, 6, 5, 0],
[3, 6, 3, 6, 5, 2],
[0, 6, 3, 6, 5, 2],
[2, 6, 3, 6, 5, 0],
[2, 6, 0, 6, 5, 1]])
building on paulpanzer's answer for poor souls (like me) who dont have numpy v1.15+
def sum_stretches(a, axis=-1):
a = a.swapaxes(-1, axis)
padding = [[0,0].copy()]*a.ndim
padding[-1] = [1,1]
padded = np.pad((a!=0), padding, 'constant', constant_values=0).astype('int32')
bnd = np.diff(padded, axis=-1)
*idx, last = np.where(bnd)
A = np.concatenate([np.zeros((*a.shape[:-1], 1), 'int32'), a.cumsum(axis=-1)], -1)[(*idx, last)]
bnd[(*idx, last)] *= (A[1::2]-A[::2]).repeat(2)
return bnd[..., :-1].cumsum(axis=-1).swapaxes(-1, axis)

Numpy, counting unique neighbours in 2d array

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

Categories