8

enter image description here

I have looked at several pages regarding optimizing circle detection using opencv in python. All seem to be specific to the individual circumstances of a given picture. What are some starting points for each of the parameters for cv2.HoughCircles? Since I am not sure what recommended values are, I have attempted looping over ranges but this is not producing any promising results. Why can't I detect any of the circles in this image?

import cv2
import numpy as np
image = cv2.imread('IMG_stack.png')
output = image.copy()
height, width = image.shape[:2]
maxWidth = int(width/10)
minWidth = int(width/20)

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 1.2, 20,param1=50,param2=50,minRadius=minWidth,maxRadius=maxWidth)


if circles is not None:
    # convert the (x, y) coordinates and radius of the circles to integers
    circlesRound = np.round(circles[0, :]).astype("int")
    # loop over the (x, y) coordinates and radius of the circles
    for (x, y, r) in circlesRound:
        cv2.circle(output, (x, y), r, (0, 255, 0), 4)
    cv2.imwrite(filename = 'test.circleDraw.png', img = output)
    cv2.imwrite(filename = 'test.circleDrawGray.png', img = gray)
else:
    print ('No circles found')

This should be a straight forward circle detection, but all of the circles detected are not even close.

nathancy
  • 42,661
  • 14
  • 115
  • 137
Rob
  • 395
  • 1
  • 2
  • 15
  • Can you share the original image you want to detect the circles on? And indicate where those circles should be detected. – Jop Knoppers Sep 26 '19 at 06:41
  • threshold all the dark/black regions; use findContours to select the inner-contours; use ellipse-fitting or minEnclosingCircle to get the circle outline. Have a look at this question/answer for additional ideas: https://stackoverflow.com/questions/34650697/opencv-divide-contacted-circles-into-single/34653138#34653138 – Micka Sep 26 '19 at 07:31

2 Answers2

5

Normally circle detection can be done using traditional image processing methods such as thresholding + contour detection, hough circles, or contour fitting but since your circles are overlapping/touching, watershed segmentation may be better. Here's a good resource.

enter image description here

import cv2
import numpy as np
from skimage.feature import peak_local_max
from skimage.morphology import watershed
from scipy import ndimage

# Load in image, convert to gray scale, and Otsu's threshold
image = cv2.imread('1.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

# Remove small noise by filtering using contour area
cnts = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]

for c in cnts:
    if cv2.contourArea(c) < 1000:
        cv2.drawContours(thresh,[c], 0, (0,0,0), -1)

cv2.imshow('thresh', thresh)
# Compute Euclidean distance from every binary pixel
# to the nearest zero pixel then find peaks
distance_map = ndimage.distance_transform_edt(thresh)
local_max = peak_local_max(distance_map, indices=False, min_distance=20, labels=thresh)

# Perform connected component analysis then apply Watershed
markers = ndimage.label(local_max, structure=np.ones((3, 3)))[0]
labels = watershed(-distance_map, markers, mask=thresh)

# Iterate through unique labels
for label in np.unique(labels):
    if label == 0:
        continue

    # Create a mask
    mask = np.zeros(gray.shape, dtype="uint8")
    mask[labels == label] = 255

    # Find contours and determine contour area
    cnts = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    cnts = cnts[0] if len(cnts) == 2 else cnts[1]
    c = max(cnts, key=cv2.contourArea)
    cv2.drawContours(image, [c], -1, (36,255,12), -1)

cv2.imshow('image', image)
cv2.waitKey()
nathancy
  • 42,661
  • 14
  • 115
  • 137
  • Nice solution, nathancy! – Cris Luengo Sep 26 '19 at 20:41
  • @CrisLuengo, thanks! I'm looking forward to your solution :) – nathancy Sep 26 '19 at 20:51
  • Ah, I would have posted something similar to yours. :) – Cris Luengo Sep 26 '19 at 21:15
  • Wow awesome, I had not heard of watershed segmentation before. Cheers! – Rob Sep 27 '19 at 04:03
  • 1
    Actually, Im confused. While very informative and much appreciated, it seems that what this code actually does is highlight the black portion in an image, and NOT just the circles. For example, this code also effeciently detects black letters on a white page, or even rectangles in an image. Importantly, if i were to invert the colors in this picture, it would highlight literally the entire image except the circles, obviously not what is intended. – Rob Sep 27 '19 at 04:26
  • Furthermore, how would one parse the detected circles? For example, the x and y coordinates of each circle? – Rob Sep 27 '19 at 04:47
  • If you don't want it to color in the circles change `cv2.drawContours(image, [c], -1, (36,255,12), -1)` to `cv2.drawContours(image, [c], -1, (36,255,12), 2)`. If you invert the color in the image, you have to invert the threshold to get the same result. To parse the detected circles, you can iterate through each contour and use either bounding rect for (x,y) coordinates or compute moments for the center of each circle – nathancy Sep 27 '19 at 20:06
  • 1
    This is a very interesting piece of code and I am learning lots off of it. However, it is not necessarily detecting circles, just black lines. Like I said above, if there is text or any kind of other shape in the image, as long as it is a black line, this code is not able to differentiate between them and actual circles. I was hoping to learn how to optimize the ability to detect circles in order to rule out false positives. I am not concerned with whether lines around the circles are colored or not, just detecting circles. – Rob Sep 28 '19 at 05:22
  • Instead of using `from skimage.morphology import watershed` ; use `from skimage.segmentation import watershed`. Watershed has been moved to Segmentation. – shreyans jain Apr 20 '22 at 07:54
5

The main parameters that you should pay attention are minDist, minRadius and maxRadius.

Analyzing the radius first: you have an image that is 12 circles wide and 8 circles tall, which gives you a diameter of roughly width/12 for each circle, or a radius of (width/12)/2. The constraints that you have used allowed the algorithm to detect circles way bigger or smaller than necessary, therefore you should use a parameterization that is better fit for your image. In this case, I have used an interval [0.9 * radius, 1.1 * radius].

As there is no overlapping, you could say that the distance between two circles is at least the diameter, so minDist could be set to something like 2*minRadius.

This implementation is basically the same as yours, just updating those 3 parameters:

%matplotlib inline
import cv2
import numpy as np
import matplotlib.pyplot as plt

image = cv2.imread('data/balls.jpg')
output = image.copy()
height, width = image.shape[:2]
maxRadius = int(1.1*(width/12)/2)
minRadius = int(0.9*(width/12)/2)

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
circles = cv2.HoughCircles(image=gray, 
                           method=cv2.HOUGH_GRADIENT, 
                           dp=1.2, 
                           minDist=2*minRadius,
                           param1=50,
                           param2=50,
                           minRadius=minRadius,
                           maxRadius=maxRadius                           
                          )

if circles is not None:
    # convert the (x, y) coordinates and radius of the circles to integers
    circlesRound = np.round(circles[0, :]).astype("int")
    # loop over the (x, y) coordinates and radius of the circles
    for (x, y, r) in circlesRound:
        cv2.circle(output, (x, y), r, (0, 255, 0), 4)

    plt.imshow(output)
else:
    print ('No circles found')

The result is:

enter image description here

Ricardo
  • 581
  • 4
  • 11
  • 1
    Thank you Ricardo! I tried it on various file sizes code can often lead to different results. Your code largely works the same regardless. I also tweaked the radii in various ways before resorting to stackoverflow and had odd results, I guess your logic makes more sense. I ended up using a minRadius of 'int(0.8*(width/12)/2)' as in larger images there was more overlap with a larger minRadius and some circles were not detected. This seems to help with that. Cheers! – Rob Nov 06 '19 at 04:54