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')
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:
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:
Cleaned threshold image:
Ellipse draw from contour fitting on input showing ellipse outline and center:
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:
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:
Morphology cleaned image:
Largest external contour fitted to ellipse and drawn on input:
Ellipse parameters:
center: 497.53564453125 , 639.7144165039062
diameters: 454.8548583984375 , 458.95843505859375