5

This is my code, I am trying to delete the mask (noise) from the binary image. What I am getting is white lines left around the noise. I am aware that there is a contour around that noise creating the final white line in the results. any help?

Original Image

Original Image

Mask and results

Mask and results

Code

import numpy as np
import cv2
from skimage import util

img = cv2.imread('11_otsu.png')
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(imgray, 127, 255, 0, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
#cv2.drawContours(img, contours, -1, (0,255,0), 2)

# create an empty mask
mask = np.zeros(img.shape[:2], dtype=np.uint8)

# loop through the contours
for i, cnt in enumerate(contours):
    # if the contour has no other contours inside of it
    if hierarchy[0][i][2] == -1:
        # if the size of the contour is greater than a threshold
        if cv2.contourArea(cnt) <70:
            cv2.drawContours(mask, [cnt], 0, (255), -1)
            # display result

cv2.imshow("Mask", mask)

cv2.imshow("Img", img)
image = cv2.bitwise_not(img, img, mask=mask)
cv2.imshow("Mask", mask)
cv2.imshow("After", image)

cv2.waitKey()
cv2.destroyAllWindows()

Jeru Luke
  • 20,118
  • 13
  • 80
  • 87
Vincent Liow
  • 101
  • 1
  • 6
  • Just add morphology close to your command to fill the black holes inside the white regions. Then just get external contours. See https://docs.opencv.org/4.1.1/d4/d86/group__imgproc__filter.html#ga67493776e3ad1a3df63883829375201f – fmw42 Nov 17 '20 at 16:33

3 Answers3

4

Instead of trying to find inner contours and filling those in, may I suggest using cv2.floodFill instead? The flood fill operation is commonly used to fill in holes inside closed objects. Specifically, if you set the seed pixel to be the top left corner of the image then flood fill the image, what will get filled is the background while closed objects are left alone. If you invert this image, you will find all of the pixels that are interior to the closed objects that have "holes". If you take this inverted image and use the non-zero locations to directly set the original image, you will thus fill in the holes.

Therefore:

im = cv2.imread('8AdUp.png', 0)
h, w = im.shape[:2]
mask = np.zeros((h+2, w+2), dtype=np.uint8)
holes = cv2.floodFill(im.copy(), mask, (0, 0), 255)[1]
holes = ~holes
im[holes == 255] = 255
cv2.imshow('Holes Filled', im)
cv2.waitKey(0)
cv2.destroyAllWindows()

First we read in the image that you've provided which is thresholded and before the "noise filtering", then get the height and width of it. We also use an input mask to tell us which pixels to operate on the flood fill. Using a mask of all zeroes means you will operate on the entire image. It's also important to note that the mask needs to have a 1 pixel border surrounding it before using it. We flood fill the image using the top left corner as the initial point, invert it, set any "hole" pixels to 255 and show it. Take note that the input image is mutated once the method finishes so you need to pass in a copy to leave the input image untouched. Also, cv2.floodFill (using OpenCV 4) returns a tuple of four elements. I'll let you look at the documentation but you need the second element of this tuple, which is the filled in image.

We thus get:

enter image description here

rayryeng
  • 102,964
  • 22
  • 184
  • 193
  • 1
    your solution works but misses a very important subtlety - size of the noise. In some cases it might happen so that some of these bacteria (I am assuming these are bacteria images, but I could be wrong) might have a formation where holes are larger and yet are not part of noise. So somehow size filtering on holes should be applied to somewhat mitigate that issue. – Knight Forked Nov 17 '20 at 10:23
  • 1
    @KnightForked thanks. Haven't dealt with these images before and didn't notice this point. – rayryeng Nov 17 '20 at 15:55
  • 1
    Appreciate the answer, as @KnightForked mentioned there is a subtlety to my problem. I will certainly use your answer somewhere when needed. – Vincent Liow Nov 18 '20 at 00:32
3

Your code is perfectly fine just make these adjustments and it should work:

contours, hierarchy = cv2.findContours(thresh, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) # Use cv2.CCOMP for two level hierarchy
if hierarchy[0][i][3] != -1: # basically look for holes
    # if the size of the contour is less than a threshold (noise)
    if cv2.contourArea(cnt) < 70:
        # Fill the holes in the original image
        cv2.drawContours(img, [cnt], 0, (255), -1)
Knight Forked
  • 1,529
  • 12
  • 14
  • Works perfectly, exactly what I needed. Good eyes, I needed to fill noise and not gaps between grains. I adjusted the code cv2.drawContours(img, [cnt], 0, (255,255,255), -1) as yours was filling it blue (0,0,255) for some reasons. – Vincent Liow Nov 18 '20 at 00:31
0

I think using cv2.GaussianBlur() method might help you. After you convert the image to gray-scale, blur it using this method (as the name suggests, this is a Gaussian filter). Here is the documentation: https://docs.opencv.org/4.3.0/d4/d86/group__imgproc__filter.html

Elyas Karimi
  • 292
  • 4
  • 11