1. Introduction

While using OpenCV, did you ever get confused about how to totally remove inner or children contours from the output of cv2.findContours. It is not as simple as using cv2.RETR_EXTERNAL in cv2.findContours function as Retrieval method. Let's discuss this in detail.

2. Working of cv2.findContours

As per the docs of OpenCV:

Contours are curves joining all points having same intensity or color. Hence, contour drawing is useful and essential in object detection and recognition of particular object or shape. Also, for better and precise contour finding, the image should be in binary or high contrast form which can be obtained by edge detection(canny or laplacian) or apply appropriate threshold.
In OpenCV, finding contours is like finding white object from black background. So remember, object to be found should be white and background should be black

3. Difficulty of finding only parents

Let us take this sample image: sample image

In this image, there are some blur images and we want to find position of all such blurred images on the black background. We have following two approaches:

  1. Read input image as grayscale and find contours
  2. Thresh the image by selecting a range that suits our purpose.
First approach:
										image_read_bg = cv2.imread(path + input_file, cv2.IMREAD_GRAYSCALE )
										img_output, contours, hierarchy = cv2.findContours(image_read_bg, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
									
Second approach:
										image_read_color = cv2.imread(path + input_file, cv2.IMREAD_GRAYSCALE )
										dst, thresh = cv2.threshold(cv2.cvtColor(img_read_color, cv2.COLOR_BGR2GRAY), 60, 255, cv2.THRESH_BINARY) 
										#here 60 to 255 is the range. Full range is 0-255 for grayscale image.
										img_output, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
									
Here is the result of threshing in the 60-255 range.

thresh image


Now, when we draw contour's coordinate on the real image, it looks like as shown below(along with code):

thresh image


Contouring code:
										temp_img = image_read_color.copy()
										for index, contour in enumerate(contours):
											[x, y, w, h] = cv2.boundingRect(contour)
											cX = round(int(x + int(w/2)))
											cY = round(int(y + int(h/2)))
											
											randomB, randomG, randomR = (random.sample(range(50,255),1)[0], random.sample(range(50,255),1)[0], random.sample(range(50,255),1)[0]) #returns a list
											
											cv2.circle(temp_img, (cX, cY), 4, (randomB, randomG, randomR), -1)#4pixel is radius of circle(dot) and -1 flag means fill the circle with color mentioned
											cv2.rectangle(temp_img, (x, y), (x + w, y + h), (randomB, randomG, randomR), 2)
											cv2.putText(temp_img, str(index),(x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (randomB,randomG,randomR), 4, cv2.LINE_AA)
											
											cv2.rectangle(blank_image_firstImage, (x, y), (x + w, y + h), (255, 255, 255), -1)
										 
										print(hierarchy)	#order: [Next, Previous, First_Child, Parent]
										cv2.imwrite(path + "colored_contour.png", temp_img)
										cv2.imwrite(path + "intermediate.png", blank_image_firstImage)
									
We can see here, that eventhough we used cv2.RETR_EXTERNAL as retrieval method which is supposed to provide only external contours, there are many child contours also drawn. Here is the hierarchy array which follows the format: [Next, Previous, First_Child, Parent]

hierarchy array of cv2.findContours
										#order: [Next, Previous, First_Child, Parent]
										[[[ 1 -1 -1 -1]
										  [ 2  0 -1 -1]
										  [ 3  1 -1 -1]
										  [ 4  2 -1 -1]
										  [ 5  3 -1 -1]
										  [ 6  4 -1 -1]
										  [ 7  5 -1 -1]
										  [ 8  6 -1 -1]
										  [ 9  7 -1 -1]
										  [10  8 -1 -1]
										  [11  9 -1 -1]
										  [12 10 -1 -1]
										  [13 11 -1 -1]
										  [14 12 -1 -1]
										  [15 13 -1 -1]
										  [-1 14 -1 -1]]]
									
We can notice that, "First_Child" for all the rectangles are "-1" means empty while we had it in our image drawn.
So, how to get contours of only outer contours.

4. Solution

One easy and effective workaround is to fill the color(white) in all the contours and get only black background and white foreground(contour) area. Filling a rectangle with white color can be performed using

cv2.rectangle(blank_image_firstImage, (x, y), (x + w, y + h), (255, 255, 255), -1) #filling rectangle with white color in blank image of same size as input image with -1 flag
In this way, we filled white in all parent contours and then again performing cv2.findContours on the image.

Here is the complete code:
										import cv2
										import numpy as np
										import random

										path = "D://testHierarchy//"
										input_file = "test.png"
										output_file = "output.png"
										image_read_bg = cv2.imread(path + input_file, cv2.IMREAD_GRAYSCALE )
										image_read_color =  cv2.imread(path + input_file, cv2.IMREAD_COLOR )
										img_output_intermediate, contours_intermediate, hierarchy_intermediate = cv2.findContours(image_read_bg, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

										temp_img = image_read_color.copy()
										blank_image_firstImage = np.zeros((image_read.shape[0], image_read.shape[1],3), np.uint8) #creating a blank image with Numpy

										for index, contour in enumerate(contours_intermediate):
											[x, y, w, h] = cv2.boundingRect(contour) #x,y,w,h of rectangle
											cX = round(int(x + int(w/2))) #x-coordinate of rectangle's center
											cY = round(int(y + int(h/2))) #y-coordinate of rectangle's center
											
											randomB, randomG, randomR = (random.sample(range(50,255),1)[0], random.sample(range(50,255),1)[0], random.sample(range(50,255),1)[0]) #random Blue, Green and Red color pixel value between 50-255 which are not dark
											
											cv2.circle(temp_img, (cX, cY), 4, (randomB, randomG, randomR), -1) #putting a dot at the center of rectangle
											cv2.rectangle(temp_img, (x, y), (x + w, y + h), (randomB, randomG, randomR), 2) #drawing a rectangle around contour
											cv2.putText(temp_img, str(index),(x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (randomB,randomG,randomR), 4, cv2.LINE_AA) #marking the index number just outside rectangle.
											
											cv2.rectangle(blank_image_firstImage, (x, y), (x + w, y + h), (255, 255, 255), -1) #filling rectangle with white color in blank image of same size as input image with -1 flag

										print(hierarchy_intermediate) #has several inner/children contours too. [Next, Previous, First_Child, Parent]
										cv2.imwrite(path + "colored_contour_drawn.png", temp_img)
										cv2.imwrite(path + "black_background_with_white_fill.png", blank_image_firstImage)

										img_output, contours, hierarchy = cv2.findContours(blank_image_firstImage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

										for index, contour in enumerate(contours):
											[x, y, w, h] = cv2.boundingRect(contour) #x,y,w,h of rectangle
											cX = round(int(x + int(w/2))) #x-coordinate of rectangle's center
											cY = round(int(y + int(h/2))) #y-coordinate of rectangle's center
											randomB, randomG, randomR = (random.sample(range(50,255),1)[0], random.sample(range(50,255),1)[0], random.sample(range(50,255),1)[0]) #random Blue, Green and Red color pixel value between 50-255 which are not dark
											cv2.circle(image_read_color, (cX, cY), 4, (randomB, randomG, randomR), -1)
											cv2.rectangle(image_read_color, (x, y), (x + w, y + h), (randomB, randomG, randomR), 2)
											cv2.putText(image_read_color, str(index),(x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (randomB,randomG,randomR), 4, cv2.LINE_AA)
											
										cv2.imwrite(path + "final_output.png", image_read_color)

										print(hierarchy) #has only parent contours. order: [Next, Previous, First_Child, Parent]
									
Here is the blank image with white fill:

thresh image


and here is the final image with contour drawn:

thresh image


Here is the hierarchy array
										#order: [Next, Previous, First_Child, Parent]
										[[[ 1 -1 -1 -1]
										  [ 2  0 -1 -1]
										  [ 3  1 -1 -1]
										  [ 4  2 -1 -1]
										  [ 5  3 -1 -1]
										  [ 6  4 -1 -1]
										  [ 7  5 -1 -1]
										  [ 8  6 -1 -1]
										  [-1  7 -1 -1]]]
									
We can observe that number of contours reduced from 15 to 8 and also, inner/children contours are eliminated.


Thank you for reading it all along. Please comment if you have any doubt!!


About the author

Prakash snippetnuggets

Prakash

You can contact him at prakash@snippetnuggets.com