Search code examples
pythonopencvhierarchycontourfill

Filling contours but leaving contained regions unfilled


I have this python code that supposedly fills the contours of an image, but leaves the holes contained in it unfilled. This is what I want:

what I want (holes unfilled)

But this is what I get:

what I get (the whole thing filled)

I've tried specifying the contour hierarchies for filling with cv2, but I can't get the result I want.

This is what I've tried:


import numpy as np
import cv2

# Load the PNG image
img = cv2.imread('slice.png')

# Convert the image to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Threshold the image to create a binary image
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)

# Find the contours in the binary image
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# Create a blank image with the same dimensions as the original image
filled_img = np.zeros(img.shape[:2], dtype=np.uint8)

# Iterate over the contours and their hierarchies
for i, contour in enumerate(contours):
    # Check if the contour has a parent
    if hierarchy[0][i][3] == -1:
        # If the contour doesn't have a parent, fill it with pixel value 255
        cv2.drawContours(filled_img, [contour], -1, 255, cv2.FILLED)

# Display the result
cv2.imshow('Original Image', img)
cv2.imshow('Filled Regions', filled_img)
cv2.waitKey(0)
cv2.destroyAllWindows()

I've tried modifying the -1, 0, 1 values for the 'if hierarchy[0][i][3] == -1:' part, but it either fills the smaller holes, or fills the entire bigger contour like the first pic I posted.

Update

I also would like to fill with white the inside of lesser hierarchy contours, like this:

enter image description here enter image description here


Solution

  • The issue is that cv2.drawContours fills the entire inner part of a closed contour, regardless if there is an inner contour.

    Instead of filling the contours without a parent with white, we may start with white contour, and fill the contours without a child with black.


    Assuming we know that the inner part should be black, we may apply the following stages:

    • Find contours using cv2.RETR_EXTERNAL, and fill the outer contour with white.
    • Find contours using cv2.RETR_TREE.
    • Iterate the contours hierarchy, and fill with black color only contours that doesn't have a child contour (fill with black the most inner contours).

    Code sample:

    import numpy as np
    import cv2
    
    # Load the PNG image
    img = cv2.imread('slice.png')
    
    # Convert the image to grayscale
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # Threshold the image to create a binary image
    ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
    
    # Find the outer contours in the binary image (using cv2.RETR_EXTERNAL)
    contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    # Create a blank image with the same dimensions as the original image
    filled_img = np.zeros(img.shape[:2], dtype=np.uint8)
    
    # Fill the outer contour with white color
    cv2.drawContours(filled_img, contours, -1, 255, cv2.FILLED)
    
    # Find contours with hierarchy, this time use cv2.RETR_TREE
    contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    
    # Iterate over the contours and their hierarchies
    for i, contour in enumerate(contours):
        # Check if the contour has no child
        if hierarchy[0][i][2] < 0:
            # If contour has no child, fill the contour with black color
            cv2.drawContours(filled_img, [contour], -1, 0, cv2.FILLED)
    
    # Display the result
    cv2.imshow('Original Image', img)
    cv2.imshow('Filled Regions', filled_img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    

    Result filled_img:
    enter image description here

    Note:
    In case we don't know the color of the most inner contour, we may draw a white contour on a black background, and use the result as a mask - use the mask for copying the original content of the input image.


    Update:

    Support contours that don't have a child:

    For supporting both contours that have a child and contours without a child, we may fill with black color, only contours that match both conditions:

    • Contours has no child contour.
    • Contour has a grandparent contour (look for grandparent instead of a parent because an empty contour has an inner contour and its parent is the outer contour).

    Code sample:

    import numpy as np
    import cv2
    
    # Load the PNG image
    img = cv2.imread('slice.png')
    
    # Convert the image to grayscale
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # Threshold the image to create a binary image
    ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
    
    # Find the outer contours in the binary image (using cv2.RETR_EXTERNAL)
    contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    # Create a blank image with the same dimensions as the original image
    filled_img = np.zeros(img.shape[:2], dtype=np.uint8)
    
    # Fill the outer contour with white color
    cv2.drawContours(filled_img, contours, -1, 255, cv2.FILLED)
    
    # Find contours with hierarchy, this time use cv2.RETR_TREE
    contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    
    # Iterate over the contours and their hierarchies
    for i, contour in enumerate(contours):
        has_grandparent = False
        has_parent = hierarchy[0][i][3] >= 0
        if has_parent:
            # Check if contour has a grandparent
            parent_idx = hierarchy[0][i][3]
            has_grandparent = hierarchy[0][parent_idx][3] >= 0
    
        # Check if the contour has no child
        if hierarchy[0][i][2] < 0 and has_grandparent:
            # If contour has no child, fill the contour with black color
            cv2.drawContours(filled_img, [contour], -1, 0, cv2.FILLED)
    
    # Display the result
    cv2.imshow('Original Image', img)
    cv2.imshow('Filled Regions', filled_img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    

    Update:

    Fill with white the inside of lesser hierarchy contours:

    Before filling the contour with black color, we may check is the nominated contour has black pixels inside.
    Fill with black only if it has no child, has a grandparent and has black inside.

    For testing if has black pixels inside we may draw the contour (with white color) over temporary image.
    Then check if the minimum value is 0 (value where drawn contour is white).

    tmp = np.zeros_like(thresh)
    cv2.drawContours(tmp, [contour], -1, 255, cv2.FILLED)
    has_innder_black_pixels = (thresh[tmp==255].min() == 0)
    

    Code sample:

    import numpy as np
    import cv2
    
    # Load the PNG image
    img = cv2.imread('slice.png')
    
    # Convert the image to grayscale
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # Threshold the image to create a binary image
    ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
    
    # Find the outer contours in the binary image (using cv2.RETR_EXTERNAL)
    contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    # Create a blank image with the same dimensions as the original image
    filled_img = np.zeros(img.shape[:2], dtype=np.uint8)
    
    # Fill the outer contour with white color
    cv2.drawContours(filled_img, contours, -1, 255, cv2.FILLED)
    
    # Find contours with hierarchy, this time use cv2.RETR_TREE
    contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    
    # Iterate over the contours and their hierarchies
    for i, contour in enumerate(contours):
        has_grandparent = False
        has_parent = hierarchy[0][i][3] >= 0
        if has_parent:
            # Check if contour has a grandparent
            parent_idx = hierarchy[0][i][3]
            has_grandparent = hierarchy[0][parent_idx][3] >= 0
    
        # Draw the contour over temporary image first (for testing if it has black pixels inside).
        tmp = np.zeros_like(thresh)
        cv2.drawContours(tmp, [contour], -1, 255, cv2.FILLED)
        has_innder_black_pixels = (thresh[tmp==255].min() == 0)  # If the minimum value is 0 (value where draw contour is white) then the contour has black pixels inside
    
        if hierarchy[0][i][2] < 0 and has_grandparent and has_innder_black_pixels:
            # If contour has no child and has a grandparent and it has black inside, fill the contour with black color
            cv2.drawContours(filled_img, [contour], -1, 0, cv2.FILLED)
    
    # Display the result
    cv2.imshow('Original Image', img)
    cv2.imshow('Filled Regions', filled_img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()