7

I have a 2D numpy.ndarray. Given a list of positions, I want to find the positions of first non-zero elements to the right of the given elements in the same row. Is it possible to vectorize this? I have a huge array and looping is taking too much time.

Eg:

matrix = numpy.array([
    [1, 0, 0, 1, 1], 
    [1, 1, 0, 0, 1], 
    [1, 0, 0, 0, 1], 
    [1, 1, 1, 1, 1], 
    [1, 0, 0, 0, 1]
])
query = numpy.array([[0,2], [2,1], [1,3], [0,1]])

Expected Result:

>> [[0,3], [2,4], [1,4], [0,3]]

Currently I'm doing this using for loops as follows

for query_point in query:
    y, x = query_point
    result_point = numpy.min(numpy.argwhere(self.matrix[y, x + 1:] == 1)) + x + 1
    print(f'{y}, {result_point}')

PS: I also want to find the first non-zero element to the left as well. I guess, the solution to find the right point can be easily tqeaked to find the left point.

Nagabhushan S N
  • 6,407
  • 8
  • 44
  • 87

2 Answers2

6

If your query array is sufficiently dense, you can reverse the computation: find an array of the same size as matrix that gives the index of the next nonzero element in the same row for each location. Then your problem becomes one of just one of applying query to this index array, which numpy supports directly.

It is actually much easier to find the left index, so let's start with that. We can transform matrix into an array of indices like this:

r, c = np.nonzero(matrix)
left_ind = np.zeros(matrix.shape, dtype=int)
left_ind[r, c] = c

Now you can find the indices of the preceding nonzero element by using np.maximum similarly to how it is done in this answer: https://stackoverflow.com/a/48252024/2988730:

np.maximum.accumulate(left_ind, axis=1, out=left_ind)

Now you can index directly into ind to get the previous nonzero column index:

left_ind[query[:, 0], query[:, 1]]

or

left_ind[tuple(query.T)]

Now to do the same thing with the right index, you need to reverse the array. But then your indices are no longer ascending, and you risk overwriting any zeros you have in the first column. To solve that, in addition to just reversing the array, you need to reverse the order of the indices:

right_ind = np.zeros(matrix.shape, dtype=int)
right_ind[r, c] = matrix.shape[1] - c

You can use any number larger than matrix.shape[1] as your constant as well. The important thing is that the reversed indices all come out greater than zero so np.maximum.accumulate overwrites the zeros. Now you can use np.maximum.accumulate in the same way on the reversed array:

right_ind = matrix.shape[1] - np.maximum.accumulate(right_ind[:, ::-1], axis=1)[:, ::-1]

In this case, I would recommend against using out=right_ind, since right_ind[:, ::-1] is a view into the same buffer. The operation is buffered, but if your line size is big enough, you may overwrite data unintentionally.

Now you can index the array in the same way as before:

right_ind[(*query.T,)]

In both cases, you need to stack with the first column of query, since that's the row key:

>>> row, col = query.T
>>> np.stack((row, left_ind[row, col]), -1)
array([[0, 0],
       [2, 0],
       [1, 1],
       [0, 0]])
>>> np.stack((row, right_ind[row, col]), -1)
array([[0, 3],
       [2, 4],
       [1, 4],
       [0, 3]])
>>> np.stack((row, left_ind[row, col], right_ind[row, col]), -1)
array([[0, 0, 3],
       [2, 0, 4],
       [1, 1, 4],
       [0, 0, 3]])

If you plan on sampling most of the rows in the array, either at once, or throughout your program, this will help you speed things up. If, on the other hand, you only need to access a small subset, you can apply this technique only to the rows you need.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • Query array is not that dense. For a `1920x1080` matrix, there are about `6000` query points. I'll try applying on the rows like you suggested, to see if it improves time. The idea/approach is cool. I think it'll help me a lot in the future also. Thank you very much. I'll try it and update. I'll accept your answer after that. – Nagabhushan S N Mar 05 '21 at 04:02
  • Btw, the algorithm is an iterative one (iterative image inpainting). So, my matrix gets updated slightly after every iteration. Any suggestions to update the index array without computing it afresh? – Nagabhushan S N Mar 05 '21 at 04:05
  • 1
    @NagabhushanSN. Probably do it per row. But you'd have to ask a separate question for that really – Mad Physicist Mar 05 '21 at 06:34
  • I implemented this. Also, in every iteration, instead of computing `left_ind` and `right_ind` from scratch, I'm updating them in the patches where `matrix` changed. This reduced time taken by 300 times!!! Instead if I compute `left_ind` and `right_ind` from scratch in every iteration, it reduced time by 200 times! (Just adding so that someone who comes here in future can know the difference). And, thanks a lot again! – Nagabhushan S N Mar 07 '21 at 18:05
  • I'm glad you saw some dramatic improvement! – Mad Physicist Mar 07 '21 at 18:05
1

I came up with a solution to get both your wanted indices, i.e. to the left and to the right from the indicated position.

First define the following function, to get the row number and both indices:

def inds(r, c, arr):
    ind = np.nonzero(arr[r])[0]
    indSlice = ind[ind < c]
    iLeft = indSlice[-1] if indSlice.size > 0 else None
    indSlice = ind[ind > c]
    iRight = indSlice[0] if indSlice.size > 0 else None
    return r, iLeft, iRight

Parameters:

  • r and c are row number (in the source array) and the "starting" index in this row,
  • arr is the array to look in (matrix will be passed here).

Then define the vectorized version of this function:

indsVec = np.vectorize(inds, excluded=['arr'])

And to get the result, run:

result = np.vstack(indsVec(query[:, 0], query[:, 1], arr=matrix)).T

The result is:

array([[0, 0, 3],
       [2, 0, 4],
       [1, 1, 4],
       [0, 0, 3]], dtype=int64)

Your expected result is the left and right column (row number and the index of first non-zero element after the "starting" position.

The middle column is the index of last non-zero element before the "starting" position.

This solution is resistant to "non-existing" case (if there are no any "before" or "after" non-zero element). In such case the respective index is returned as None.

Valdi_Bo
  • 30,023
  • 4
  • 23
  • 41
  • 2
    The problem with this solution is that it uses `np.vectorize`, which is a python-level `for` loop, unfortunately. You will find that the timing here is not much better than OP's original post. – Mad Physicist Mar 04 '21 at 19:19
  • 1
    I'm afraid that "normal" vectorization (over *matrix*) is impossible. The reason is that the code to write does not operate on consecutive rows of *matrix*, but takes its rows in an arbitrary sequence, even with repetitions, as defined by the left column of *query*. – Valdi_Bo Mar 04 '21 at 19:24
  • 1
    I've posted a vectorized solution. – Mad Physicist Mar 04 '21 at 20:23