Search code examples
pythonimageopencvtensorflowcrop

Is there a recommended methodology to determine minRadius for finding the 1 circle in an image if using OpenCV's HoughCircles?


I need to figure out the best way to choose a minRadius value for use in the OpenCV cv2.HoughCircles function.

I am working on increasing the accuracy of a Tensorflow CNN that does US rare coin classification. Currently, the CNN is reviewing >10k images of all different sizes from 300x300 to 1024x1024

To increase the accuracy of the model, I am attempting to pull the coin out of the image prior to training, and only train the model on the coin and not its surroundings.

The below code works OK in detecting the coin as a circle but I have to try several minRadius values to get the HoughCircles function to work well.

In some cases, minRadius=270 works on a 600x600 and a 785x1024 and in other cases only r=200 works for a 600x600 but fails on the 785x1024. In other cases only r=318 works but not 317 or 319. I've not found a consistent approach.

Question: Is there a recommended methodology to determine minRadius for finding the 1 circle in an image? assuming the image is of different sizes and the coin is taking up from 50% to 90% of the image

here are examples of typical images: https://i.ebayimg.com/images/g/r5oAAOSwH8VeHNBf/s-l1600.jpg https://i.ebayimg.com/images/g/~JsAAOSwGtdeyFfU/s-l1600.jpg

image = cv2.imread(r"C:\testimages\70a.jpg")
output = image.copy()
height, width = image.shape[:2]

minRadius = 200
maxRadius =0

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
circles = cv2.HoughCircles(image=gray, 
                           method=cv2.HOUGH_GRADIENT, 
                           dp=1.2, 
                           minDist=200*minRadius, #something large since we are looking for 1
                           param1=200,
                           param2=100,
                           minRadius=minRadius,
                           maxRadius=maxRadius
                          )

#Draw the circles detected
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')

Solution

  • Here is a different way to do that by fitting an ellipse to the largest contour extracted from the thresholded image. You can use the ellipse major and minor radii as approximations for your Hough Circles if you want.

    Input:

    enter image description here

    import cv2
    import numpy as np
    
    # read input
    img = cv2.imread('s-l1600.jpg')
    
    # convert to gray
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # threshold
    thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
    
    # apply morphology open and close
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))
    morph = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (21,21))
    morph = cv2.morphologyEx(morph, cv2.MORPH_CLOSE, kernel)
    
    # find largest contour
    contours = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    contours = contours[0] if len(contours) == 2 else contours[1]
    big_contour = max(contours, key=cv2.contourArea)
    
    # fit ellipse to contour and get ellipse center, minor and major diameters and angle in degree 
    ellipse = cv2.fitEllipse(big_contour)
    (xc,yc),(d1,d2),angle = ellipse
    print('center: ',xc,',',yc)
    print('diameters: ',d1,',',d2)
    
    # draw ellipse
    result = img.copy()
    cv2.ellipse(result, ellipse, (0, 0, 255), 2)
    
    # draw circle at center
    xc, yc = ellipse[0]
    cv2.circle(result, (int(xc),int(yc)), 5, (0, 255, 0), -1)
    
    cv2.imwrite("s-l1600_thresh.jpg", thresh)
    cv2.imwrite("s-l1600_morph.jpg", morph)
    cv2.imwrite("s-l1600_ellipse.jpg", result)
    
    cv2.imshow("s-l1600_thresh", thresh)
    cv2.imshow("s-l1600_morph", morph)
    cv2.imshow("s-l1600_ellipse", result)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    


    Otsu thresholded image:

    enter image description here

    Cleaned threshold image:

    enter image description here

    Ellipse draw from contour fitting on input showing ellipse outline and center:

    enter image description here

    Ellipse parameters:

    center:  504.1853332519531 , 524.3350219726562
    diameters:  953.078125 , 990.545654296875
    


    Here is your other image. But here I use color thresholding using inRange().

    Input:

    enter image description here

    import cv2
    import numpy as np
    
    # read input
    img = cv2.imread('s-l1600b.jpg')
    
    # convert to hsv
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    
    # get color bounds of red circle
    lower =(0,0,0) # lower bound for each channel
    upper = (150,150,150) # upper bound for each channel
    
    # create the mask and use it to change the colors
    thresh = cv2.inRange(hsv, lower, upper)
    
    # apply morphology open and close
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))
    morph = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (31,31))
    morph = cv2.morphologyEx(morph, cv2.MORPH_CLOSE, kernel)
    
    # find largest contour
    contours = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    contours = contours[0] if len(contours) == 2 else contours[1]
    big_contour = max(contours, key=cv2.contourArea)
    
    # fit ellipse to contour and get ellipse center, minor and major diameters and angle in degree 
    ellipse = cv2.fitEllipse(big_contour)
    (xc,yc),(d1,d2),angle = ellipse
    print('center: ',xc,',',yc)
    print('diameters: ',d1,',',d2)
    
    # draw ellipse
    result = img.copy()
    cv2.ellipse(result, ellipse, (0, 0, 255), 2)
    
    # draw circle at center
    xc, yc = ellipse[0]
    cv2.circle(result, (int(xc),int(yc)), 5, (0, 255, 0), -1)
    
    cv2.imwrite("s-l1600_thresh.jpg", thresh)
    cv2.imwrite("s-l1600_morph.jpg", morph)
    cv2.imwrite("s-l1600_ellipse.jpg", result)
    
    cv2.imshow("s-l1600b_thresh", thresh)
    cv2.imshow("s-l1600b_morph", morph)
    cv2.imshow("s-l1600b_ellipse", result)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    


    Thresholded image:

    enter image description here

    Morphology cleaned image:

    enter image description here

    Largest external contour fitted to ellipse and drawn on input:

    enter image description here

    Ellipse parameters:

    center:  497.53564453125 , 639.7144165039062
    diameters:  454.8548583984375 , 458.95843505859375