Search code examples
pythonimageopencvcomputer-visionscikit-image

How to remove small contours attached to another big one


I'm doing cell segmentation, so I'm trying to code a function that removes all minor contours around the main one in order to do a mask. That happens because I load an image with some color markers: Image1

The problem is when I do threshold, it assumes that "box" between the color markers as a part of the main contour.

Image2 Image3

As you may see in my code, I don't directly pass color image to grays because the red turns black but there are other colors too, at least 8, and always different in each image. I've got thousands of images like this where just one cell is displayed, but in most of it, there are always outsiders contours attached. My goal is to come to a function that gives a binary image of a single cell for each image input like this. So I'm starting with this code:

import cv2 as cv
cell1 = cv.imread(image_cell, 0)
imgray = cv.cvtColor(cell1,cv.COLOR_BGR2HSV)
imgray = cv.cvtColor(imgray,cv.COLOR_BGR2GRAY)
ret,thresh_binary = cv.threshold(imgray,107,255,cv.THRESH_BINARY)
cnts= cv.findContours(image =cv.convertScaleAbs(thresh_binary) , mode = 
cv.RETR_TREE,method = cv.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
   cv.drawContours(thresh_binary,[c], 0, (255,255,255), -1)    
kernel = cv.getStructuringElement(cv.MORPH_RECT, (3,3))
opening = cv.morphologyEx(thresh_binary, cv.MORPH_OPEN, kernel, 
iterations=2) # erosion followed by dilation

Summing up, how do I get just the red contour from image 1?


Solution

  • So another approach, without color ranges.

    A couple of things are not going right in your code I think. First, you are drawing the contours on thresh_binary, but that already has the outer lines of the other cells as well - the lines you are trying to get rid off. I think that is why you use opening(?) while in this case you shouldn't.

    To fix things, first a little information on how findContours works. findContours starts looking for white shapes on a black background and then looks for black shapes inside that white contour and so on. That means that the white outline of the cells in the thresh_binary are detected as a contour. Inside of it are other contours, including the one you want. docs with examples

    What you should do is first look only for contours that have no contours inside of them. The findContours also returns a hierarchy of contours. It indicates whether a contour has 'childeren'. If it has none (value: -1) then you look at the size of the contour and disregard the ones that are to small. You could also just look for the largest, as that is probably the one you want. Finally you draw the contour on a black mask.

    Result:
    enter image description here

    Code:

        import cv2 as cv
        import numpy as np
        # load image as grayscale
        cell1 = cv.imread("PjMQR.png",0)
        # threshold image
        ret,thresh_binary = cv.threshold(cell1,107,255,cv.THRESH_BINARY)
        # findcontours
        contours, hierarchy = cv.findContours(image =thresh_binary , mode = cv.RETR_TREE,method = cv.CHAIN_APPROX_SIMPLE)
    
        # create an empty mask
        mask = np.zeros(cell1.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) > 10000:
                                cv.drawContours(mask,[cnt], 0, (255), -1)    
        # display result
        cv2.imshow("Mask", mask)
        cv2.imshow("Img", cell1)
        cv2.waitKey(0)
        cv2.destroyAllWindows()
    

    Note: I used the image you uploaded, your image probably has far fewer pixels, so a smaller contourArea
    Note2: enumerate loops through the contours, and returns both a contour and an index for each loop