Reputation: 695
I am just starting to play with OpenCV and I have found some very strange behaviour from the contourArea function.
See this image.
It has three non connected areas, the left is a grouping of long strokes and on the top center there is a single dot and finally a big square on the right.
When I run my function, I get this result
Contour[0] Area: 221, Length: 70, Colour: Red
Contour[1] Area: 13772, Length: 480, Colour: Green
Contour[2] Area: 150, Length: 2370, Colour: Blue
While I havent actually counted the area of the left part, It seems as if it encompasses much more than 150 pixels and would certainly have a higher value than the dot in the top center, I would say that dot should be able to fit in to the left part at least 10 times. The area of the square does work out.
Square Area
width = 118
height = 116
118 * 116 = 13,688
13,688 is really close to what opencv gave as the area (13,772), the difference is likely measurement error on my behalf. I manually calculated the area of the dot
Dot Area
width = 27
height = 6
27*6 = 162
Not too far off from what opencv said it would be (221)
Reading from the OpenCV docs page on contourArea it says that it will give wrong results for contours with self intersections. Not really understanding what self intersections are, I made a test image.
As you can see I have a rectangle on the left and a cross in the middle and another cross rotated 45 deg. I would expect the cross to have slightly less than double the area of the rectangle due to the overlap in the center.
Contour[0] Area: 1805, Length: 423, Colour: Red
Contour[1] Area: 947, Length: 227, Colour: Green
Contour[2] Area: 1825, Length: 415, Colour: Blue
As you can see the area of the two crosses are slightly less than double the area of the rectangle. As expected.
I am not interested in capturing the inside of the square or getting a box drawn around the shape on the left and the dot (though it would be tangentially interesting) it's not specifically what I'm asking about in this question.
So my question: Why is the area of my irregular shape severly underestimated?
I copied most of this code from this tutorial
I have stripped down my code to this self contained example below.
def contour_test(name):
import cv2 as cv
colours = [{'name': 'Red ', 'bgr': (0, 0, 255)},
{'name': 'Green ', 'bgr': (0, 255, 0)},
{'name': 'Blue ', 'bgr': (255, 0, 0)}]
src = cv.imread(cv.samples.findFile(name))
src_gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
src_gray = cv.blur(src_gray, (3,3))
threshold = 100
canny_output = cv.Canny(src_gray, threshold, threshold * 2)
contours, _ = cv.findContours(canny_output, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
# Get the moments
mu = [None for i in contours]
for i in range(len(contours)):
mu[i] = cv.moments(contours[i])
# Get the mass centers
mc = [None for i in contours]
for i in range(len(contours)):
mc[i] = (mu[i]['m10'] / (mu[i]['m00'] + 1e-5), mu[i]['m01'] / (mu[i]['m00'] + 1e-5))
# Draw contours
drawing = np.zeros((canny_output.shape[0], canny_output.shape[1], 3), dtype=np.uint8)
for i, j in enumerate(contours):
colour = colours[i]['bgr']
cv.drawContours(drawing, contours, i, colour, 2)
area = int(cv.contourArea(contours[i]))
length = int(cv.arcLength(contours[i], True))
print('Contour[{0}] Area: {1}, Length: {2}, Colour: {3}'.format(i, area, length, colours[i]['name']))
Upvotes: 8
Views: 4330
Reputation: 32144
The inner part of the contours the findContours
finds is supposed to be of filled with white color.
cv.Canny
before findContours
(cv.blur
is also not required). cv.threshold
with cv.THRESH_BINARY_INV
option for inverting polarity.cv.THRESH_OTSU
option for automatic threshold. You may replace cv.blur
and cv.Canny
and cv.findContours(canny_output...
with:
_, src_thresh = cv.threshold(src_gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)
contours, _ = cv.findContours(src_thresh, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
Result (of top image):
Contour[0] Area: 13531, Length: 476, Colour: Red
Contour[1] Area: 184, Length: 71, Colour: Green
Contour[2] Area: 4321, Length: 1202, Colour: Blue
Here is the complete (updated) code:
import numpy as np
def contour_test(name):
import cv2 as cv
colours = [{'name': 'Red ', 'bgr': (0, 0, 255)},
{'name': 'Green ', 'bgr': (0, 255, 0)},
{'name': 'Blue ', 'bgr': (255, 0, 0)}]
src = cv.imread(cv.samples.findFile(name))
src_gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
#src_gray = cv.blur(src_gray, (3,3))
#threshold = 100
#canny_output = cv.Canny(src_gray, threshold, threshold * 2)
#contours, _ = cv.findContours(canny_output, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
_, src_thresh = cv.threshold(src_gray, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU)
cv.imshow('src_thresh', src_thresh);cv.waitKey(0);cv.destroyAllWindows() # Show src_thresh for testing
contours, _ = cv.findContours(src_thresh, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
# Get the moments
mu = [None for i in contours]
for i in range(len(contours)):
mu[i] = cv.moments(contours[i])
# Get the mass centers
mc = [None for i in contours]
for i in range(len(contours)):
mc[i] = (mu[i]['m10'] / (mu[i]['m00'] + 1e-5), mu[i]['m01'] / (mu[i]['m00'] + 1e-5))
# Draw contours
drawing = np.zeros((src_thresh.shape[0], src_thresh.shape[1], 3), dtype=np.uint8)
for i, j in enumerate(contours):
colour = colours[i]['bgr']
cv.drawContours(drawing, contours, i, colour, 2)
area = int(cv.contourArea(contours[i]))
length = int(cv.arcLength(contours[i], True))
print('Contour[{0}] Area: {1}, Length: {2}, Colour: {3}'.format(i, area, length, colours[i]['name']))
cv.imshow('drawing', drawing);cv.waitKey(0);cv.destroyAllWindows() # Show drawing for testing
contour_test('img.jpg')
I added cv.imshow
in two places for testing.
Upvotes: 6