Vanhanen_
Vanhanen_

Reputation: 135

Fill contours with alternating colors in Python

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:

enter image description here

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

enter image description here

But once I get several contours, it doesn't do the job

enter image description here

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

Answers (2)

Rotem
Rotem

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:

enter image description here => enter image description here


enter image description here => enter image description here


enter image description here ==> enter image description here


enter image description here ==> enter image description here

Upvotes: 2

Christoph Rackwitz
Christoph Rackwitz

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:

  • apply findContours
  • build new set of contours, selectively

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:

  • level 0 : outline boundary 1
  • level 1 : inline boundary 1 = lumen 0 outline
  • level 2 : outline boundary 2 = lumen 0 inline
  • level 3 : inline boundary 2 = lumen 1 outline
  • level 4 : outline boundary 3 = lumen 1 inline
  • level 5 : inline boundary 3 = lumen 2 outline
  • level 6 : outline boundary 4 = lumen 2 inline
  • ...

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

composite

Upvotes: 4

Related Questions