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:
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:
- Read input image as grayscale and find contours
- Thresh the image by selecting a range that suits our purpose.
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.
Now, when we draw contour's coordinate on the real image, it looks like as shown below(along with code):
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 flagIn 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:
and here is the final image with contour drawn:
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!!