Removing isolated pixels using OpenCV
Asked Answered
S

3

10

I'm looking for a way to remove isolated white pixels from a binary image using OpenCV. A similar question (OpenCV get rid of isolated pixels) has a bunch of "answers" but none seem to work for me. I've tried various combinations of opening and closing without success as well.

The article here:

https://homepages.inf.ed.ac.uk/rbf/HIPR2/hitmiss.htm

Suggests I can use the hit-or-miss operation for exactly this purpose:

Structuring elements

1 is used to locate isolated points in a binary image

And that the reason why is that 0s are interpreted differently than when they are used with erosion/dilation directly (where 0s are interpreted as "don't care's" rather than "not white" which is basically what I'm after). However, using this kernel simply renders the original image.

My input image is this:

Calc input image

You'll notice there's a few white pixels near the left-hand side of the image which I'd like to get rid of.

Here's the code:

kernel = np.array([ [0, 0, 0],
                    [0, 1, 0],
                    [0, 0, 0]],np.uint8)

hitormiss = cv2.morphologyEx(input_image, cv2.MORPH_HITMISS, kernel)

cv2.imshow('hitormiss', hitormiss)

What is the right way of removing isolated pixels like these?

Update: Alexander's answer works like a charm and is the fastest solution. The other answer provides a solution too, which is to use the cv2.connectedComponents function, but it is much more processor-intensive. Here's a function that uses this approach:

def remove_isolated_pixels(self, image):
    connectivity = 8

    output = cv2.connectedComponentsWithStats(image, connectivity, cv2.CV_32S)

    num_stats = output[0]
    labels = output[1]
    stats = output[2]

    new_image = image.copy()

    for label in range(num_stats):
        if stats[label,cv2.CC_STAT_AREA] == 1:
            new_image[labels == label] = 0

    return new_image
Samale answered 10/9, 2017 at 17:52 Comment(3)
have you tried using a median blur? Choose the right kernel to avoid losing extra informationJoannajoanne
Yes; blurring the binary image is too destructive, even with the smallest aperture size of 3.Samale
There is something odd going wrong with OpenCV here. I used ImageMagick and applied a Hit-or-Miss morphology with a 0,0,0 0,1,0 0,0,0 kernel and it immediately isolated your noisy pixels. I notice your image is palettised - have you checked the actual values in your input image where you expect to find the noise - I mean printed out their actual values and the 8 surrounding pixels?Initial
A
16

I believe the OpenCV implementation was broken. There was a related issue on OpenCV's GitHub which seems to have merged a pull request to fix; I think it was added to OpenCV 3.3-rc as referenced in the pull request so hopefully this should be fixed by the next time you update OpenCV. I'm not sure if the problem is caused by the same thing or not.

The creative solution from the selected answer is great, but I agree with you: there must be a better way, despite the broken implementation.

On the OpenCV Hit-or-miss tutorial they state:

Therefore, the hit-or-miss operation comprises three steps:

  1. Erode image A with structuring element B1.
  2. Erode the complement of image A (A_c) with structuring element B2.
  3. AND results from step 1 and step 2.

It then goes on to say that this can be accomplished with a single kernel in the hit-or-miss transform, but as we know, it is broken. So let's do those steps instead.

import cv2
import numpy as np

# load image, ensure binary, remove bar on the left
input_image = cv2.imread('calc.png', 0)
input_image = cv2.threshold(input_image, 254, 255, cv2.THRESH_BINARY)[1]
input_image_comp = cv2.bitwise_not(input_image)  # could just use 255-img

kernel1 = np.array([[0, 0, 0],
                    [0, 1, 0],
                    [0, 0, 0]], np.uint8)
kernel2 = np.array([[1, 1, 1],
                    [1, 0, 1],
                    [1, 1, 1]], np.uint8)

hitormiss1 = cv2.morphologyEx(input_image, cv2.MORPH_ERODE, kernel1)
hitormiss2 = cv2.morphologyEx(input_image_comp, cv2.MORPH_ERODE, kernel2)
hitormiss = cv2.bitwise_and(hitormiss1, hitormiss2)

cv2.imshow('isolated.png', hitormiss)
cv2.waitKey()

Isolated pixels

And then to remove, it's as simple as inverting the hitormiss and using that as a mask in cv2.bitwise_and() with the input_image.

hitormiss_comp = cv2.bitwise_not(hitormiss)  # could just use 255-img
del_isolated = cv2.bitwise_and(input_image, input_image, mask=hitormiss_comp)
cv2.imshow('removed.png', del_isolated)
cv2.waitKey()

Removed isolated pixels


Note: as discussed in the comments, erosion with kernel1 in this specific case is identical to the input binary image, so there's no need to do this computation, and this introduces some other unnecessary steps as well in this specific case. However, you could have different kernels than just a single 1 in the middle, so I'm going to keep the code as-is to keep it general for any kernels.

Alcaic answered 10/9, 2017 at 23:42 Comment(4)
Nice piece of sleuthing - thank you for taking the time to investigate and for sharing.Initial
@MarkSetchell Thanks! Yeah, I remember when the question was asked a few months ago and that it was reported as an issue, so I did a quick search on the GitHub and found it.Alcaic
@buckminst I believe you're right---however, my answer was supposed to be general for any kernels, not just for this specific one. Good catch though, I will make a note of it in my answer.Alcaic
Do we know if the bug in opencv has been fixed?Parkman
D
3
  1. Run connected component labeling
  2. Calculate the number of pixels each component have
  3. For each component with less than a min number of pixels convert all the component pixels to zero.
Devitrify answered 10/9, 2017 at 18:20 Comment(2)
Thank you! I have updated my question with a function that implements this suggestion and it is working as intended.Samale
Using connectedComponents is overkill. It requires additional labels matrix (of type int32 I guess) and additional efforts to fill it with values. So no - this can't be correct answer.Requisite
S
3

This is how I solved it:

import cv2 as cv
import numpy as np

# let's say "image" is a thresholded image

kernel = np.array([ [-1, -1, -1],
                    [-1,  1, -1],
                    [-1, -1, -1] ], dtype="int")
single_pixels = cv.morphologyEx(image, cv.MORPH_HITMISS, kernel)
single_pixels_inv = cv.bitwise_not(single_pixels)
image = cv.bitwise_and(image, image, mask=single_pixels_inv)

# now "image" shouldn't have alone pixels
Stockish answered 19/12, 2021 at 9:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.