Search code examples
pythonopencvimage-processingndimage

Remove all pixels with all-zero neighbors


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


Solution

  • 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.