0

Is there a way to perform a process similar to erosion in OpenCV that retains a given pixel if ANY of its neighbors are non-zero, instead of requiring all of its neighbors to be non-zero?

Here, by neighbors, I mean any pixel with abs(x1-x2)+abs(y1-y2)==1, but that is easy to control via the erosion kernel.

Of course, I can always use for loops and implement this behavior from scratch, but I prefer the speed that OpenCV can provide with its libraries.

Will it work to invert the image, perform an erosion, and then invert it back?

The other idea I had would be to convolve with a kernel with an empty center and then clip all values to the range 0 to 1. I would use scipy.ndimage.convolve for this.

I am working with a binary NumPy array with type np.float32 (i.e., values of 0.0 or 1.0) with shape (512,512).

Cris Luengo
  • 55,762
  • 10
  • 62
  • 120
Michael Sohnen
  • 953
  • 7
  • 15

2 Answers2

4

One easy way to accomplish your goal would be to convolve with a square 3x3 kernel of ones. For each pixel, you now know how many foreground pixels there are in its neighborhood (including itself). Threshold this at 2 (>= 2) to get all pixels where there are at least 2 foreground pixels in the neighborhood. Finally, the logical AND with the original image will give all foreground pixels that have at least one foreground neighbor.

Here's an example:

import scipy.ndimage
import numpy as np

img = np.array([[0., 0., 0., 0., 0., 0., 0., 1., 1., 1.],
                [0., 1., 0., 0., 0., 0., 0., 1., 1., 1.],
                [0., 0., 0., 0., 0., 0., 0., 0., 1., 1.],
                [0., 0., 0., 0., 0., 0., 1., 0., 1., 1.],
                [0., 0., 0., 0., 0., 0., 1., 0., 0., 0.],
                [0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
                [0., 1., 1., 0., 0., 0., 0., 0., 0., 0.],
                [0., 1., 1., 0., 1., 0., 0., 0., 0., 0.],
                [0., 1., 1., 0., 0., 0., 0., 0., 0., 0.],
                [0., 1., 1., 0., 0., 0., 0., 0., 0., 0.],
                [0., 1., 1., 0., 0., 0., 0., 0., 0., 0.],
                [0., 0., 0., 1., 0., 0., 0., 1., 1., 0.],
                [0., 0., 0., 0., 1., 1., 0., 0., 0., 0.],
                [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
                [0., 0., 0., 0., 0., 0., 0., 0., 0., 1.],
                [0., 0., 0., 0., 0., 0., 0., 1., 0., 0.]], dtype=np.float32)
tmp = scipy.ndimage.convolve(img, np.ones((3,3)), mode='constant')
out = np.logical_and(tmp >= 2, img).astype(np.float32)

The output is:

[[0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]
 [0. 0. 0. 0. 0. 0. 0. 1. 1. 1.]
 [0. 0. 0. 0. 0. 0. 0. 0. 1. 1.]
 [0. 0. 0. 0. 0. 0. 1. 0. 1. 1.]
 [0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]
 [0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
 [0. 1. 1. 0. 0. 0. 0. 0. 0. 0.]
 [0. 1. 1. 0. 0. 0. 0. 0. 0. 0.]
 [0. 1. 1. 0. 0. 0. 0. 0. 0. 0.]
 [0. 1. 1. 0. 0. 0. 0. 0. 0. 0.]
 [0. 1. 1. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 1. 0. 0. 0. 1. 1. 0.]
 [0. 0. 0. 0. 1. 1. 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. 0. 0. 0. 0. 0. 0. 0.]]

Of course some imaging libraries will have functions designed specifically for this purpose. I don't know if OpenCV or ndimage or scikit-image have such a function, I don't know these libraries well enough. But DIPlib does (disclosure: I'm an author):

import diplib as dip

out = img - dip.GetSinglePixels(img > 0)

The img > 0 part is to convert the floating-point array into a logical array, which DIPlib expects for binary images. This is about 5 times as fast as the other solution for a 512x512 image.

Cris Luengo
  • 55,762
  • 10
  • 62
  • 120
  • are you sure that `scipy.ndimage.convolve(img, np.ones((3,3)), mode='constant')` does not need to have the origin variable specified? I would use `scipy.ndimage.convolve(img, np.ones((3,3)), mode='constant',origin=(1,1))`, am I wrong? – Michael Sohnen Sep 04 '21 at 06:38
  • 1
    @MichaelSohnen Do you get a different output if you do that? I seemed to get the right output with the code I wrote, so I don't think it's necessary to specify the origin. I would expect that the middle of the kernel is the default origin. – Cris Luengo Sep 04 '21 at 07:14
  • 1
    @MichaelSohnen The docs say that `origin=0` (the default) causes the kernel to be centered around the middle pixel. Setting `origin=(1,1)` would shift the kernel to the left and up. – Cris Luengo Sep 04 '21 at 07:16
  • Okay, I read the docs wrong. In a previous project I had used origin=(1,1,1) for a 3d convolution but I guess it was incorrect. Thanks! – Michael Sohnen Sep 04 '21 at 09:05
  • @Chris Luengo I used your first solution (with some adaptation) for a similar task on a 3D numpy array. I got the results I needed with no perceivable performance issues. Thank you for the help. Also I like how your algorithm can be adapted. If instead of using the logical and, you use a comparison (e.g. `img[tmp<3]=0.0`), it is possible to remove pixels with varying levels of strandedness. Kudos! – Michael Sohnen Sep 07 '21 at 09:59
2

let's say you have

src = np.uint8([
       [  0,   0,   0,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0,   0,   0,   0,   0],
       [  0,   0, 255,   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, 255,   0,   0],
       [  0,   0,   0,   0,   0,   0, 255,   0],
       [  0,   0,   0,   0,   0,   0,   0,   0]])

(you can figure out how to get that from your floats)

You are looking for nonzero pixels where not all neighbors are 0. You could construct the desired result from several operations.

You can use MORPH_HITMISS for the positive condition (all neighbors 0), then combine with the negation.

You'd use this kernel:

kernel = np.int8([ # 0 means "don't care", all others have to match
    [-1, -1, -1],
    [-1, +1, -1],
    [-1, -1, -1],
])

neighbors_all_zero = cv.morphologyEx(src=src, op=cv.MORPH_HITMISS, kernel=kernel)

result = src & ~neighbors_all_zero

Result:

array([[  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,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0,   0, 255,   0,   0],
       [  0,   0,   0,   0,   0,   0, 255,   0],
       [  0,   0,   0,   0,   0,   0,   0,   0]], dtype=uint8)

Be careful with the values themselves. OpenCV assumes masks to be 0 or 255 sometimes, and 0 and non-zero at other times. I got some funny results when I used just 0 and 1 instead of 0 and 255. I'm sure those can be worked with though.

Christoph Rackwitz
  • 11,317
  • 4
  • 27
  • 36
  • sure, but what do you think about simply clipping the result of scipy convolution with a kernel with an empty center and 1.0s for the neighbors? – Michael Sohnen Sep 04 '21 at 06:36
  • I can give this solution a try, but I may try the others first since they do not require changing the type of my array – Michael Sohnen Sep 04 '21 at 06:44
  • 1
    his solution assumes that you have exactly 0.0 and 1.0 values. if you don't have exactly that, you **still** need thresholding/comparison to arrive at a **mask**, and then convolution on floats is more expensive than what I do here. in any case, convolution on floats probably costs more than this morphology approach on integers. you should benchmark it if you care about performance. if you care about results, don't. – Christoph Rackwitz Sep 04 '21 at 12:53