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
:

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()