The title might be slightly misleading, because we will not be talking about pizza slices. But let's say you are in a situation where you have an image containing different types of pizzas with different shapes. Now, somebody has taken a slice out of one of those pizzas. How would we automatically identify this?
We cannot take the approach we took earlier because we don't know what the shape looks like. So we don't have any template. We are not even sure what shape we are looking for, so we cannot build a template based on any prior information. All we know is the fact that a slice has been taken from one of the pizzas. Let's consider the following image:
It's not exactly a real image, but you get the idea. You know what shape we are talking about. Since we don't know what we are looking for, we need to use some of the properties of these shapes to identify the sliced pizza. If you notice, all the other shapes are nicely closed. As in, you can take any two points within those shapes and draw a line between them, and that line will always lie within that shape. These kinds of shapes are called convex shapes.
If you look at the sliced pizza shape, we can choose two points such that the line between them goes outside the shape as shown in the figure that follows:
So, all we need to do is detect the non-convex shape in the image and we'll be done. Let's go ahead and do that:
import sys import cv2 import numpy as np # Input is a color image def get_contours(img): # Convert the image to grayscale img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Threshold the input image ret, thresh = cv2.threshold(img_gray, 127, 255, 0) # Find the contours in the above image contours, hierarchy = cv2.findContours(thresh, 2, 1) return contours if __name__=='__main__': img = cv2.imread(sys.argv[1]) # Iterate over the extracted contours for contour in get_contours(img): # Extract convex hull from the contour hull = cv2.convexHull(contour, returnPoints=False) # Extract convexity defects from the above hull defects = cv2.convexityDefects(contour, hull) if defects is None: continue # Draw lines and circles to show the defects for i in range(defects.shape[0]): start_defect, end_defect, far_defect, _ = defects[i,0] start = tuple(contour[start_defect][0]) end = tuple(contour[end_defect][0]) far = tuple(contour[far_defect][0]) cv2.circle(img, far, 5, [128,0,0], -1) cv2.drawContours(img, [contour], -1, (0,0,0), 3) cv2.imshow('Convexity defects',img) cv2.waitKey(0) cv2.destroyAllWindows()
If you run the above code, you will see something like this:
Wait a minute, what happened here? It looks so cluttered. Did we do something wrong? As it turns out, the curves are not really smooth. If you observe closely, there are tiny ridges everywhere along the curves. So, if you just run your convexity detector, it's not going to work. This is where contour approximation comes in really handy. Once we've detected the contours, we need to smoothen them so that the ridges do not affect them. Let's go ahead and do that:
import sys import cv2 import numpy as np # Input is a color image def get_contours(img): img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, thresh = cv2.threshold(img_gray, 127, 255, 0) contours, hierarchy = cv2.findContours(thresh, 2, 1) return contours if __name__=='__main__': img = cv2.imread(sys.argv[1]) # Iterate over the extracted contours for contour in get_contours(img): orig_contour = contour epsilon = 0.01 * cv2.arcLength(contour, True) contour = cv2.approxPolyDP(contour, epsilon, True) # Extract convex hull and the convexity defects hull = cv2.convexHull(contour, returnPoints=False) defects = cv2.convexityDefects(contour,hull) if defects is None: continue # Draw lines and circles to show the defects for i in range(defects.shape[0]): start_defect, end_defect, far_defect, _ = defects[i,0] start = tuple(contour[start_defect][0]) end = tuple(contour[end_defect][0]) far = tuple(contour[far_defect][0]) cv2.circle(img, far, 7, [255,0,0], -1) cv2.drawContours(img, [orig_contour], -1, (0,0,0), 3) cv2.imshow('Convexity defects',img) cv2.waitKey(0) cv2.destroyAllWindows()
If you run the preceding code, the output will look like the following: