This time, I would like to demonstrate the combined knowledge of edge detection and convolution and use these powers for image analysis rather than just processing. I shall use the force to detect coins in a picture and draw a circle around them!
Those of you that are more familiar with computer vision might have an “I see” moment thinking of the existing function that would save me a lot of time...but since I am a promising padawan and wanted to apply all the hard-earned knowledge to practice, I went the hard way, taking the “Never tell me the odds” (Solo, Ep V) sort of approach. Haha.
If we go backwards, to draw a circle around the coins, I need to know their radius and coordinates of their centres. To obtain that, I used convolution, that I am explaining here, to compare the coins to reference circles of increasing sizes until more than a certain percentage of the edge pixels were aligned with the circle. Lastly, to be able to convolute the circles, I needed to find the edge of the coins, which I have explored in this article. Sounds simple enough? Nevertheless, allow me to break down the approach to you in more detail, in case you would like to try it yourself, this time in the correct chronological order. As always, everything is available on GitHub.
Draw circles on a separate image from the smallest to largest (I checked the radius of the smallest and largest coin in the image to establish the boundaries). With each iteration increase the radius by one pixel. Using another loop, save the coordinates of the edge pixels to a list.
For each reference circle generated before, move the center of reference circle through the coin image. Optimize for coin image boundaries, as there is no point to align the center of reference circle to the first corner pixel of the coin image.
For each reference circle position in the original coin image, iterate through the list of circle coordinates and align their position to the original image. Here, we need to establish two threshold values. First is the edge threshold, i.e. how many edge pixels need to match the reference circle to be considered enough. The second threshold is the intensity threshold, i.e. the minimal witness of the edge pixel, to be still considered a part of an edge. If both thresholds are passed, then consider the alignment as an identified coin.
For all the identified coins' radiuses and center coordinates, draw a circle in the original image.
The first time I ran the code, I set the parameters for min and max radius to be 40 till 57 and had a constant stream of coordinates and radiuses printing out to see the progress. It was late so I went to bed thinking that I will see beautiful circles around coins in the morning. When I got up and went to check, the program was just passing radius 44. I mean, I expected the function to run slower, with Python being an interpreted language, but this was a bit extreme.
So I optimised. Some variables were taken out of loops and were pre-processed and therefore not calculated through each iteration (e.g. circumference of reference circle). Another drastic optimisation was resizing the image to half its size. This led to a 4 times faster run as the loops had fewer pixels to go through!
As I hinted at the beginning, there is, of course, an easier and much more accurate way of detecting circles in OpenCV, the so-called Hough Circle Transformation (here is a link to an alternative explanation). Not only the function gives one result per circle (unlike the manual way, as you can see below), but because OpenCV has the implementation in C++, which is a compiled language, it also runs much faster.
def hough_circle_detection(coins, min_r, max_r): # turn original image to grayscale gray = cv2.cvtColor(coins, cv2.COLOR_BGR2GRAY) # blur grayscale image blurred = cv2.medianBlur(gray, 5) return cv2.HoughCircles( blurred, # source image (blurred and grayscaled) cv2.HOUGH_GRADIENT, # type of detection 1, # inverse ratio of accumulator res. to image res. 40, # minimum distance between the centers of circles param1=50, # Gradient value passed to edge detection param2=30, # accumulator threshold for the circle centers minRadius=min_r*2, # min circle radius maxRadius=max_r*2, # max circle radius )
The biggest challenge for me was to wrap my head around all the nested loops. What helped most was to literally draw the loops out to visually see the logic behind. Figuring out how to exactly do the alignment of comparison circle on the original image was another toughie. Of course, there is tons of room for improvement, but overall I am very pleased with the results. Hope you enjoyed the post and as usual, would love some feedback :) May the Python be with you.