Reputation: 135
I'm trying to get an algorithm that fills the contours of an image with alternating colors: first white, then black, then white again, then black again... like how the following picture shows:
What I've achieved so far was getting to fill the contours of an image with white, and then leaving the contours inside them with black, with the following code:
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()
But once I get several contours, it doesn't do the job
I've seen another question about something similar (Find all pixels bounded between two contours in a binary image in Python), but I can't get to find a generalisation of the problem
Upvotes: 2
Views: 422
Reputation: 32144
Different approach:
We may use cv2.floodFill
operations instead of cv2.findContours
:
Using flood filling the corner with white color, we can "peel" the contours from the outer contour toward the inner ones.
Since we want to keep every second contour with white color, we may apply the following two steps each iteration:
First step: fill, XOR, negate
cv2.floodFill(thresh, None, (0, 0), 255, loDiff=0, upDiff=0) # Fill the outer background with white color.
out = out ^ thresh
thresh = 255 - thresh # Invert thresh
Second step: fill negate, XOR
cv2.floodFill(thresh, None, (0, 0), 255, loDiff=0, upDiff=0) # Fill the outer background with white color.
thresh = 255 - thresh # Invert thresh
out = out ^ thresh
Using XOR operation turns white and white into black... that way we the result is alternating black and white.
At the end, we have to negate the output if the number of iterations is odd.
Code sample:
import numpy as np
import cv2
img = cv2.imread('slice.png') # Load the PNG image
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Convert the image to grayscale
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)[1] # Threshold the image to create a binary image
orig_thresh = thresh.copy() # Save original thresh.
out = thresh.copy() # Copy thresh to out (out is the final result).
i = 0
while np.any(thresh != 0):
# First step: fill, XOR, negate
cv2.floodFill(thresh, None, (0, 0), 255, loDiff=0, upDiff=0) # Fill the outer background with white color.
out = out ^ thresh
thresh = 255 - thresh # Invert thresh
# Second step: fill negate, XOR
cv2.floodFill(thresh, None, (0, 0), 255, loDiff=0, upDiff=0) # Fill the outer background with white color.
thresh = 255 - thresh # Invert thresh
out = out ^ thresh
i += 1 # Count iterations
if i % 2 == 1:
out = 255 - out # Negate output if number of iterations is odd
out[orig_thresh == 255] = 255 # Set all original white pixels to white
cv2.imshow('out', out) # Show the output for testing
cv2.waitKey()
cv2.destroyAllWindows()
Example for inputs and outputs:
Upvotes: 2
Reputation: 15575
I am basing this answer on a previous one I gave: Find all pixels bounded between two contours in a binary image in Python
Basically:
findContours
We have boundaries, which are those closed white lines. Each boundary has an outer contour ("outline") and inner contour ("inline").
The space between two boundaries I'll call a "lumen". It is bounded by the inline of the outer boundary and the outline of the inner boundary.
Contour hierarchy:
You want to fill alternating lumina (0, 2, 4, ...). That'll require contours 1+2, 5+6, 9+10, ...
If you want to fill all lumina with something, just select the right ones and give them an appropriate color.
contours, hierarchy = cv.findContours(im, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
hierarchy = hierarchy.squeeze(0) # vector<Vec4i> -> row vector of Vec4i, so (1,N,4), weird shape
ncontours = len(contours)
# extend hierarchy to include nesting level
(NEXT, PREV, CHILD, PARENT, LEVEL) = range(5)
# first four are defined by OpenCV in the docs of `findContours`
# fifth is my own addition
hierarchy = np.hstack((
hierarchy[:,:4],
np.full((ncontours, 1), -1, dtype=np.int32) # level
))
def get_level(i):
if hierarchy[i, LEVEL] != -1:
return hierarchy[i, LEVEL]
else:
level = 0 if (hierarchy[i, PARENT] == -1) else 1 + get_level(hierarchy[i, PARENT])
hierarchy[i, LEVEL] = level
return level
for i in range(ncontours):
hierarchy[i, LEVEL] = get_level(i)
#predicate to decide which contours to keep
def do_include(index, mod=2, offset=0):
level = hierarchy[index, LEVEL]
# level: 1+2, 5+6, 9+10, ...
# lumen: 0, 2, 4, ...
lumen = (level-1) // 2
if lumen < 0: return False # outermost contour, not the outline of any lumen (is inline of background)
return lumen % mod == offset
even_lumina = [ contours[i] for i in range(len(contours)) if do_include(i, 2, 0) ]
odd_lumina = [ contours[i] for i in range(len(contours)) if do_include(i, 2, 1) ]
composite = cv.cvtColor(im, cv.COLOR_GRAY2BGR)
cv.drawContours(image=composite, contours=even_lumina, contourIdx=-1, color=(0,0,255), thickness=cv.FILLED)
cv.drawContours(image=composite, contours=odd_lumina, contourIdx=-1, color=(255,0,0), thickness=cv.FILLED)
composite[im > 0] = 255 # overlay boundaries for neatness
Upvotes: 4