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:
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:
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
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:
- Erode image A with structuring element B1.
- Erode the complement of image A (A_c) with structuring element B2.
- 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()
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()
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With