3

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

Nico
  • 75
  • 5

1 Answers1

2

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()
Rotem
  • 30,366
  • 4
  • 32
  • 65
  • Thanks! that seems to work in contours that do have childs, but I also want it to fill contours that don't have childs (like filling up a square). Is there a way to do both? – Nico Mar 27 '23 at 10:56
  • Sure, I updated the answer. Please note that the answer may not cover all possible cases. – Rotem Mar 27 '23 at 12:20
  • Done! Am I am solving your exercise, or are you giving me exercises? – Rotem Apr 20 '23 at 18:47
  • thanks :) i will try it later. sorry if i bother you, it's just that i am working on a project that involves scanning and slicing some 3D objetcs, and i encounter with these problems – Nico Apr 20 '23 at 20:18
  • sorry, but i think i keep getting the same problem. I also added an skeletonized image, an example of what i work with – Nico Apr 20 '23 at 20:30
  • Can you ignore very small contours (use cv2.contourArea)? – Rotem Apr 20 '23 at 20:36
  • i think not, my project requires plotting small areas, actually. But i also get the error on the images before the last one I posted, too (that got bigger contours), with your last code – Nico Apr 20 '23 at 20:39
  • 1
    I can't come up with a simple fix... I recommend you to post a new question. Make sure to note that this is a followed up question (post a reference to this question). Include some background about the slicing so others know it's not an exercise. Make it very clear which contours should be filled, and which should be kept empty (this stuff is very confusing). When posting a screenshot with red markings, don't forget to add the original (clean) image. – Rotem Apr 20 '23 at 21:52
  • okay, thanks for all your help, i've progressed a lot thanks to it :) does your last code work for you (filled contour, then unfilled, then filled again)? At least with bigger contours, not pixel-sized ones like in my skeletonized image (which are also somewhat uncommon). – Nico Apr 20 '23 at 22:19