1

First of all I'm a beginner in programming so please be indulgent.

I'm trying to detect an unique circle in a picture with HoughCircles from opencv.

detected_circles = cv2.HoughCircles(bw_contoured_img8, 
                   cv2.HOUGH_GRADIENT, 1, 20, param1 = 255,
               param2 = 30, minRadius = 25, maxRadius = 80)

By taking the hyperparameters up there, the circle is being pretty well detected. The problem is that this handmade tuning was pretty hazardous and poorly reproducible. Moreover, it was necessary to see the result each time and adapt in consequence.

enter image description here

The wider goal is to do that on multiple very similar pictures, but with few differences like maybe little translations/rotations, and the pre-processed picture is changing a little bit from one picture to another. So I wanted to find a way to "automatize" this hyperparameter research.

I found this post with this answer that seemed to perfectly match my expectations. But for some reason, the following code gave me a way too big circle, worse than the manually tuned one.

# load the image, clone it for output, and then convert it to grayscale
image = bw_contoured_img8
orig_image = np.copy(image)
output = image.copy()

circles = None

min_circle_size = 30
maximum_circle_size = 70 # Maximum possible circle size we're willing to find in pixels
guess_dp = 1.0
number_of_circles_expected = 1 # We expect to find just one circle
breakout = False

# Hand tuning
max_guess_accumulator_array_threshold = 100 # Minimum of 1, no maximum, the quantity of votes needed to qualify for a circle to be found

circleLog = []

guess_accumulator_array_threshold = max_guess_accumulator_array_threshold

while guess_accumulator_array_threshold > 1 and breakout == False:
    # Start out with smallest resolution possible, to find the most precise circle, then creep bigger if none found
    guess_dp = 1.0
    # print("ressetting guess_dp:" + str(guess_dp))
    
    while guess_dp < 9 and breakout == False:
        guess_radius = maximum_circle_size
        # print("setting guess_radius: " + str(guess_radius))
        # print(circles is None)
        
        while True:
            # print("guessing radius: " + str(guess_radius) + 
            #         " and dp: " + str(guess_dp) + " vote threshold: " + 
            #         str(guess_accumulator_array_threshold))    
            
            circles = cv2.HoughCircles(orig_image, 
                cv2.HOUGH_GRADIENT, 
                dp=guess_dp,  # Resolution of accumulator array
                minDist=10,  # Number of pixels center of circles should be from each other
                param1=255,
                param2=guess_accumulator_array_threshold,
                minRadius=(guess_radius-3),    #HoughCircles will look for circles at minimum this size
                maxRadius=(guess_radius+3)     #HoughCircles will look for circles at maximum this size
                )   
            
            if circles is not None:
                if len(circles[0]) == number_of_circles_expected:
                    # print("len of circles: " + str(len(circles)))
                    circleLog.append(copy.copy(circles))
                    # print("k1")
                break
                circles = None
            guess_radius -= 5 
            if guess_radius < min_circle_size:
                break;

        guess_dp += 1.5

    guess_accumulator_array_threshold -= 2   

# Return the circleLog with the highest accumulator threshold
# Ensure at least some circles were found
for cir in circleLog:
    # Convert the (x, y) coordinates and radius of the circles to integers
    output = np.copy(orig_image)
    
    if (len(cir) > 1):
        print("FAIL before")
        exit()

    # print(cir[0, :])

    cir = np.round(cir[0, :]).astype("int")

    # loop over the (x, y) coordinates and radius of the circles
    if (len(cir) > 1):
        print("FAIL after")
        exit()

    for (x, y, r) in cir:
        # Draw the circle in the output image, then draw a rectangle corresponding to the center of the circle
        output = cv2.circle(output, (x, y), r, (255, 255, 255), 2)
        output = cv2.rectangle(output, (x - 5, y - 5), (x + 5, y + 5), (255, 255, 255), -1)

# Show the output image
plt.imshow(output)

enter image description here

PS. I'm not using cv2.imshow because it makes my Kernel crash (don't really know why but plt.imshow is making the job).

lpe
  • 15
  • 8
  • Post your image that is failing – fmw42 Feb 14 '23 at 17:42
  • @fmw42 just added both images – lpe Feb 15 '23 at 06:49
  • Why `max_guess_accumulator_array_threshold = 100` ? I guess this value may be too small. – fana Feb 15 '23 at 09:50
  • @fana I have tested with 500 and 300 but it's giving the same result. – lpe Feb 15 '23 at 09:55
  • I don't understand the validity of the values your procedure uses (e.g. `guess_dp += 1.5`, `guess_accumulator_array_threshold -= 2`). Are those values good? – fana Feb 15 '23 at 10:06
  • @fana It's just to define a step, but we sure have could use 0.5 for example in order to do a more detailed search (i think ?) Take a look at the original answer where the author is saying "[...] you start out with a large "minimum distance parameter", a very fine dp resolution, and a very high voting threshold. [...] The loop keeps iterates and creeping up on the optimal settings. The first circle you find will be the pixel perfect largest and best circle in the image." – lpe Feb 15 '23 at 10:20

1 Answers1

1

I think that a good accumulator threshold has a strong relationship with the radius. In other words, by examining the radius of the detected circle and the vote value for that circle, you may be able to reject unnecessary circles.

You can only specify one accumulator threshold for HoughCircles, but if the returned results contain vote values, you can use them to select the good circles yourself.

For example, it is possible to check whether or not edge points exist in more than 65% of the circumference. (It looks like that the rate (=votes/circumference) of your bad result will be very poor.)

fana
  • 1,370
  • 2
  • 7
  • Thank you for your answer, it seems clear that it would be a great indicator of the validity of the circle. Unfortunately I don't know how to acceed to these values as `HoughCircles` only return the parameters of the detected circles or `None` if no circle detected – lpe Feb 15 '23 at 10:51
  • OpenCV's reference manual said : "Output vector of found circles. Each vector is encoded as 3 or 4 element floating-point vector (x,y,radius) or (x,y,radius, **votes** )." – fana Feb 15 '23 at 10:55
  • But I've never used OpenCV with python, so I don't know which side will be returned (and how it's determined)... – fana Feb 15 '23 at 10:57
  • by searching a little bit I saw this might not be implemented in Python However, I may be doing something wrong elsewhere regarding the fact that it worked very well in the reference answer. By changing the max value for the radius to 50 and being more restrictive, the circle is pretty good. But as my images may change a bit from one to another, I wanted to have something a little wide to avoid missing the circles in some pictures. – lpe Feb 15 '23 at 11:03
  • 1
    Anyway, I think that your real purpose is not adjusting `HoughCircles` to return a single result. What you need to do is select the best one from one or more circle candidates obtained with single or multiple`HoughCircles` calls. Even if you don't know the vote values, you should be able to check the validity of (x,y,radius) based on your input image. – fana Feb 16 '23 at 01:41
  • For example... you can perform detection process independently for each narrow radius range. (Of course, you may be able to use some procedure like the referenced method for individual ranges.) And then, select one circle from results (based on your original evaluation). – fana Feb 16 '23 at 02:05
  • Yes finally just doing some comparison with the values of the pixels in my original pictures to select the best one Thank you for your help ! – lpe Feb 17 '23 at 08:48