Related
I researched quite a bit already, and all that I found was how to apply gradients to text generated with Pillow. However, I wanted to know how can I apply a gradient instead of a regular single color fill to a drawn shape (specifically a polygon).
image = Image.new('RGBA', (50, 50))
draw = ImageDraw.Draw(image)
draw.polygon([10, 10, 20, 40, 40, 20], fill=(255, 50, 210), outline=None)
Here's my attempt to create something, which might fit your use case. It has limited functionality – specifically, only supports linear and radial gradients – and the linear gradient itself is also kind of limited. But, first of all, let's see an exemplary output:
Basically, there are two methods
linear_gradient(i, poly, p1, p2, c1, c2)
and
radial_gradient(i, poly, p, c1, c2)
which both get an Pillow Image object i, a list of vertices describing a polygon poly, start and stop colors for the gradient c1 and c2, and either two points p1 and p2 describing the direction of the linear gradient (from one vertex to a second one) or a single point p describing the center of the radial gradient.
In both methods, the initial polygon is drawn on an empty canvas of the final image's size, solely using the alpha channel.
For the linear gradient, the angle between p1 and p2, is calculated. The drawn polygon is rotated by that angle, and cropped to get the needed dimensions for a proper linear gradient. That one is simply created by np.linspace. The gradient is rotated by the known angle, but in the opposite direction, and finally translated to fit the actual polygon. The gradient image is pasted on the intermediate polygon image to get the polygon with the linear gradient, and the result is pasted onto the actual image.
Limitation for the linear gradient: You better pick two vertices of the polygon "on opposite sides", or better: Such that all points inside the polygon are within the virtual space spanned by that two points. Otherwise, the current implementation might fail, e.g. when choosing two neighbouring vertices.
The radial gradient method works slightly different. The maximum distance from p to all polygon vertices is determined. Then, for all points in an intermediate image of the actual image size, the distance to p is calculated and normalized by the calculated maximum distance. We get values in the range [0.0 ... 1.0] for all points inside the polygon. The values are used to calculate appropriate colors ranging from c1 to c2. As for the linear gradient, that gradient image is pasted on the intermediate polygon image, and the result is pasted onto the actual image.
Hopefully, the code is self-explanatory using the comments. But if there are questions, please don't hesitate to ask!
Here's the full code:
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image, ImageDraw
# Draw polygon with linear gradient from point 1 to point 2 and ranging
# from color 1 to color 2 on given image
def linear_gradient(i, poly, p1, p2, c1, c2):
# Draw initial polygon, alpha channel only, on an empty canvas of image size
ii = Image.new('RGBA', i.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(ii)
draw.polygon(poly, fill=(0, 0, 0, 255), outline=None)
# Calculate angle between point 1 and 2
p1 = np.array(p1)
p2 = np.array(p2)
angle = np.arctan2(p2[1] - p1[1], p2[0] - p1[0]) / np.pi * 180
# Rotate and crop shape
temp = ii.rotate(angle, expand=True)
temp = temp.crop(temp.getbbox())
wt, ht = temp.size
# Create gradient from color 1 to 2 of appropriate size
gradient = np.linspace(c1, c2, wt, True).astype(np.uint8)
gradient = np.tile(gradient, [2 * h, 1, 1])
gradient = Image.fromarray(gradient)
# Paste gradient on blank canvas of sufficient size
temp = Image.new('RGBA', (max(i.size[0], gradient.size[0]),
max(i.size[1], gradient.size[1])), (0, 0, 0, 0))
temp.paste(gradient)
gradient = temp
# Rotate and translate gradient appropriately
x = np.sin(angle * np.pi / 180) * ht
y = np.cos(angle * np.pi / 180) * ht
gradient = gradient.rotate(-angle, center=(0, 0),
translate=(p1[0] + x, p1[1] - y))
# Paste gradient on temporary image
ii.paste(gradient.crop((0, 0, ii.size[0], ii.size[1])), mask=ii)
# Paste temporary image on actual image
i.paste(ii, mask=ii)
return i
# Draw polygon with radial gradient from point to the polygon border
# ranging from color 1 to color 2 on given image
def radial_gradient(i, poly, p, c1, c2):
# Draw initial polygon, alpha channel only, on an empty canvas of image size
ii = Image.new('RGBA', i.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(ii)
draw.polygon(poly, fill=(0, 0, 0, 255), outline=None)
# Use polygon vertex with highest distance to given point as end of gradient
p = np.array(p)
max_dist = max([np.linalg.norm(np.array(v) - p) for v in poly])
# Calculate color values (gradient) for the whole canvas
x, y = np.meshgrid(np.arange(i.size[0]), np.arange(i.size[1]))
c = np.linalg.norm(np.stack((x, y), axis=2) - p, axis=2) / max_dist
c = np.tile(np.expand_dims(c, axis=2), [1, 1, 3])
c = (c1 * (1 - c) + c2 * c).astype(np.uint8)
c = Image.fromarray(c)
# Paste gradient on temporary image
ii.paste(c, mask=ii)
# Paste temporary image on actual image
i.paste(ii, mask=ii)
return i
# Create blank canvas with zero alpha channel
w, h = (800, 600)
image = Image.new('RGBA', (w, h), (0, 0, 0, 0))
# Draw first polygon with radial gradient
polygon = [(100, 200), (320, 130), (460, 300), (700, 500), (350, 550), (200, 400)]
point = (350, 350)
color1 = (255, 0, 0)
color2 = (0, 255, 0)
image = radial_gradient(image, polygon, point, color1, color2)
# Draw second polygon with linear gradient
polygon = [(500, 50), (650, 250), (775, 150), (700, 25)]
point1 = (700, 25)
point2 = (650, 250)
color1 = (255, 255, 0)
color2 = (0, 0, 255)
image = linear_gradient(image, polygon, point1, point2, color1, color2)
# Draw third polygon with linear gradient
polygon = [(50, 550), (200, 575), (200, 500), (100, 300), (25, 450)]
point1 = (100, 300)
point2 = (200, 575)
color1 = (255, 255, 255)
color2 = (255, 128, 0)
image = linear_gradient(image, polygon, point1, point2, color1, color2)
# Save image
image.save('image.png')
----------------------------------------
System information
----------------------------------------
Platform: Windows-10-10.0.16299-SP0
Python: 3.9.1
Matplotlib: 3.4.0
NumPy: 1.20.2
Pillow: 8.1.2
----------------------------------------
I'm still not sure I understand your question fully. But it sounds like you want to have a bunch of shapes with their own gradients? One approach would be to generate the gradients of each shape separately then combine the shapes after the fact.
Piggy-backing off the answer already referred to by #HansHirse, you can do something like:
from PIL import Image, ImageDraw
def channel(i, c, size, startFill, stopFill):
"""calculate the value of a single color channel for a single pixel"""
return startFill[c] + int((i * 1.0 / size) * (stopFill[c] - startFill[c]))
def color(i, size, startFill, stopFill):
"""calculate the RGB value of a single pixel"""
return tuple([channel(i, c, size, startFill, stopFill) for c in range(3)])
def round_corner(radius):
"""Draw a round corner"""
corner = Image.new("RGBA", (radius, radius), (0, 0, 0, 0))
draw = ImageDraw.Draw(corner)
draw.pieslice((0, 0, radius * 2, radius * 2), 180, 270, fill="blue")
return corner
def apply_grad_to_corner(corner, gradient, backwards=False, topBottom=False):
width, height = corner.size
widthIter = range(width)
if backwards:
widthIter = reversed(widthIter)
for i in range(height):
gradPos = 0
for j in widthIter:
if topBottom:
pos = (i, j)
else:
pos = (j, i)
pix = corner.getpixel(pos)
gradPos += 1
if pix[3] != 0:
corner.putpixel(pos, gradient[gradPos])
return corner
def round_rectangle(size, radius, startFill, stopFill, runTopBottom=False):
"""Draw a rounded rectangle"""
width, height = size
rectangle = Image.new("RGBA", size)
if runTopBottom:
si = height
else:
si = width
gradient = [color(i, width, startFill, stopFill) for i in range(si)]
if runTopBottom:
modGrad = []
for i in range(height):
modGrad += [gradient[i]] * width
rectangle.putdata(modGrad)
else:
rectangle.putdata(gradient * height)
origCorner = round_corner(radius)
# upper left
corner = origCorner
apply_grad_to_corner(corner, gradient, False, runTopBottom)
rectangle.paste(corner, (0, 0))
# lower left
if runTopBottom:
gradient.reverse()
backwards = True
else:
backwards = False
corner = origCorner.rotate(90)
apply_grad_to_corner(corner, gradient, backwards, runTopBottom)
rectangle.paste(corner, (0, height - radius))
# lower right
if not runTopBottom:
gradient.reverse()
corner = origCorner.rotate(180)
apply_grad_to_corner(corner, gradient, True, runTopBottom)
rectangle.paste(corner, (width - radius, height - radius))
# upper right
if runTopBottom:
gradient.reverse()
backwards = False
else:
backwards = True
corner = origCorner.rotate(270)
apply_grad_to_corner(corner, gradient, backwards, runTopBottom)
rectangle.paste(corner, (width - radius, 0))
return rectangle
def get_concat_h(im1, im2):
dst = Image.new("RGB", (im1.width + im2.width, im1.height))
dst.paste(im1, (0, 0))
dst.paste(im2, (im1.width, 0))
return dst
def get_concat_v(im1, im2):
dst = Image.new("RGB", (im1.width, im1.height + im2.height))
dst.paste(im1, (0, 0))
dst.paste(im2, (0, im1.height))
return dst
img1 = round_rectangle((200, 200), 70, (255, 0, 0), (0, 255, 0), True)
img2 = round_rectangle((200, 200), 70, (0, 255, 0), (0, 0, 255), True)
get_concat_h(img1, img2).save("testcombo.png")
The result looks something like this:
The only "new" stuff comes in at the end: the images are just combined. If you want to get wild you can rotate the individual shapes or allow them to overlap (by tweaking the position of the images in get_concat_h + playing around with the final image size.)
I am trying to draw an arc using Open CV, using cv2.ellipse function
I tried reading the documentation for the same, but I m finding it very confusing. It is an arc in my case so axes_x and axes_y are same, i.e the radius. What should be my axis, In which direction should I calculate the Start and the End angle? And what is this angle of rotation?
Given is the function -
cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color[, thickness[, lineType[, shift]]])
import cv2
import numpy as np
def create_blank(height, width, color):
blank_image = np.zeros((int(height), int(width), 3), np.uint8)
blank_image[:, :] = color
return blank_image
def draw_arc(image):
height, width = image.shape[0:2]
# Ellipse parameters
radius = 100
center = (width / 2, height/2)
axes = (radius, radius)
angle = 0
startAngle = 135
endAngle = 180
cv2.line(image, (0, 150), (300, 150), (0, 0, 0), 2, cv2.CV_AA)
cv2.line(image, (150, 0), (150, 300), (0, 0, 0), 2, cv2.CV_AA)
cv2.ellipse(image, center, axes, angle, startAngle, endAngle, (0, 0, 0), 2, cv2.CV_AA)
cv2.imshow("ellipse", image)
# Create new blank 300x150 white image
width, height = 300, 300
image = create_blank(width, height, color=WHITE)
draw_arc(image)
cv2.waitKey(0)
cv2.destroyAllWindows()
When my startAngle is 135 and endAngle is 180, the result looks like
whereas when the startAngle is 0 and endAngle is 90, the result looks like
So this makes it confusing, in which direction is the arc rotating.
You can really easily view how the change of parameters affect the drawing of the ellipse. Here is a simple code for it:
import numpy as np
import cv2
center = (200, 200) # x,y
axes = (100, 75) # first, second
angle = 0. # clockwise, first axis, starts horizontal
for i in range(360):
image = np.zeros((400, 400, 3)) # creates a black image
image = cv2.ellipse(image, center, axes, angle, 0., 360, (0,0,255))
image = cv2.ellipse(image, center, axes, angle, 0., i, (0,255,0))
cv2.imshow("image", image)
cv2.waitKey(5)
cv2.waitKey(0)
cv2.destroyAllWindows()
This will do something like:
Lets go through the parameters:
center -> x and y tuple where the center of the ellipse is.
axes -> first and second axes radius (half the total size). The first one is the horizontal one if angle 0 is applied, the second one will be the vertical one.
angle -> The angle of the whole ellipse, i.e. if you move clockwise the first axis
startAngle -> where you want to start drawing your arc, for example 0 will be like my example image (in the first axis), but if the angle has a value, then the 0 will rotate the same way.
endAngle -> where you want to stop drawing, you can see that I vary it in my example to draw an increasing ellipse.
If you want an arc of a circle of radius 50px, lets say from 60 degrees up to 120 degrees, but in counterclockwise (360 - start/endAngle) you can do:
image = cv2.ellipse(image, (100,100), (50,50), 0.0, 360-120, 360-60, (0,255,0))
If you have doubts with any of them, feel free to ask in a comment
The picture and code below is a toy example that should reflect an experiment I am running.
I would like to extract a disk corresponding to the boundary in the picture where the pixels intensities are the same or similar (in this example the bluish disk)
Using HoughCircles procedure, I can extract the center of the most probable circle of the picture.
From there I would like to probe 360° from the center at the various radius (higher or lower) from the detected center to define the boundaries (max radius and min radius) of the bluish color in the picture below.
How can I do that?
I try to analyze the histogram by applying multiple masks without success.
The green circle is the one detected with HoughCircles, the blue and red circle are the +/- 15% radius circle.
import cv2
import numpy as np
from matplotlib import pyplot as plt
image = cv2.imread("./picture.jpg")
output = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 2, 800,
param1=300,
param2=1,
minRadius=100,
maxRadius=0)
if circles is not None:
# convert the (x, y) coordinates and radius of the circles to integers
circles = np.round(circles[0, :]).astype("int")
output = image.copy()
# loop over the (x, y) coordinates and radius of the circles
for (x, y, r) in circles:
# draw the circle in the output image, then draw a rectangle
# corresponding to the center of the circle
cv2.circle(output, (x, y), r, (0, 255, 0), 2)
cv2.rectangle(output, (x - 2, y - 2), (x + 2, y + 2), (0, 128, 255), -1)
# create the mask and explore histograms
# height,width,depth = output.shape
# mask = np.zeros((height,width), np.uint8)
# cv2.circle(mask, (x, y), int(round(r - (r* .15))), 1, thickness=-1)
# output = cv2.bitwise_and(output, output, mask=mask)
# hist_full = cv2.calcHist([output],[0],None,[256],[0,256])
# hist_mask = cv2.calcHist([output],[0],mask,[256],[0,256])
# plt.hist(image.ravel(),256,[0,256]); plt.show()
# plt.plot(hist_full),
# plt.plot(hist_mask)
# plt.xlim([0,256])
# plt.show()
cv2.circle(output, (x, y), int(round(r * 1.15)), (255, 0, 0), 2)
cv2.circle(output, (x, y), int(round(r - (r* .15))), (0, 0, 255), 2)
# show the output image
cv2.imshow("output", np.hstack([image, output]))
cv2.waitKey(0)
I resized the disk image, because the origin is too large. So you may modify the parameters in the function.
The source:
I found in S(HSV), the disk is more clear, so I did canny in "S".
The result:
You can reproduce the result using the code.
#!/usr/bin/python3
# 2017.11.21 21:03:09 CST
# 2017.11.22 23:21:42 CST
# 2017.11.25 16:32:46 CST
import cv2
import numpy as np
img = cv2.imread("disk2.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
## Canny edge in S(HSV)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h,s,v = cv2.split(hsv)
canny = cv2.Canny(s, 30, 200)
## The inner circle using gray
circles1 = cv2.HoughCircles(gray, method = cv2.HOUGH_GRADIENT,
dp = 2, minDist = 100,
param1=200, param2=100,
minRadius=80, maxRadius=200)
## The outer circle using canny
circles2 = cv2.HoughCircles(canny, method = cv2.HOUGH_GRADIENT,
dp = 2, minDist = 100,
param1=200, param2=100,
minRadius=200, maxRadius=0)
x1,y1, r1 = circles1[0][0]
x2,y2, r2 = circles2[0][0]
## create the mask
mask = np.zeros_like(canny)
cv2.circle(mask, (x2, y2), r2, 255, -1)
cv2.circle(mask, (x1, y1), r1, 0, -1)
## crop
imask = mask > 0
masked = np.zeros_like(img)
masked[imask] = img[imask]
cv2.imshow("canny", canny)
cv2.imshow("mask", mask)
cv2.imshow("croped", masked)
cv2.waitKey()
cv2.destroyAllWindows()
Im using following code to draw circle around face.
for (x, y, w, h) in faces:
cv2.circle(img, ( int((x + x + w )/2), int((y + y + h)/2 )), int (h / 2), (0, 255, 0), 5)
however the thickness of drawn circle is entirely green. Is there any ways to make few percentage (say 30% )of the circle to be pink?
As I suggested in the comments, you could use cv2.ellipse() to draw the two arcs individually. For example:
import numpy as np
import cv2
img = np.ones((400,400,3), np.uint8) * 255
# See:
# http://docs.opencv.org/2.4/modules/core/doc/drawing_functions.html#cv2.ellipse
# http://docs.opencv.org/3.1.0/dc/da5/tutorial_py_drawing_functions.html
circ_center = (200,200)
circ_radius = 150
circ_thick = 12
circ_axes = (circ_radius,circ_radius)
# cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color[, thickness[, lineType[, shift]]])
cv2.ellipse(img, circ_center, circ_axes, 0, 0, 90, (255,0,0), circ_thick, cv2.LINE_AA)
cv2.ellipse(img, circ_center, circ_axes, 0, 90, 360, (0,255,0), circ_thick, cv2.LINE_AA)
cv2.imshow("Image", img)
cv2.imwrite("circ1.png", img)
cv2.waitKey()
Produces:
Now, the arcs have rounded edges. This may or may not be an issue for you. I'm not sure if there's a better way in OpenCV, but one way that I've created thick lines with flat edges is to build the thick lines out of many thin lines.
For example:
import numpy as np
import cv2
img = np.ones((400,400,3), np.uint8) * 255
# See:
# http://docs.opencv.org/2.4/modules/core/doc/drawing_functions.html#cv2.ellipse
# http://docs.opencv.org/3.1.0/dc/da5/tutorial_py_drawing_functions.html
circ_center = (200,200)
circ_radius = 150
circ_thick = 12
def draw_arc(img, center, rad, angle, startAngle, endAngle, color, thickness, lineType, thick=1):
for r in range(rad,rad+thickness):
cv2.ellipse(img, center, (r,r), angle, startAngle, endAngle, color, thick, lineType)
draw_arc(img, circ_center, circ_radius, 0, 0, 90, (255,0,0), circ_thick, cv2.LINE_AA)
draw_arc(img, circ_center, circ_radius, 0, 90, 360, (0,255,0), circ_thick, cv2.LINE_AA)
cv2.imshow("Image", img)
cv2.imwrite("circ2.png", img)
cv2.waitKey()
Produces:
You can adjust the starting and ending points of the coloring by adjusting the startAngle and endAngle parameters. There are a few other parameters you may want to adjust in there, but this should give you an idea of one approach.
You could also just draw a complete circle and layer an arc on top of it corresponding to what you want colored different, which may be easier in the end.
I am new here and a little bit newbie in programming.
I have one question. I have picture of Sun in bmp file and 16 bit. The picture look as white circle with black backround.
I want to find a circle and identify its center in x,y coordinates.
I have this script
import cv
import numpy as np
orig = cv.LoadImage('sun0016.bmp')
grey_scale = cv.CreateImage(cv.GetSize(orig), 8, 1)
processed = cv.CreateImage(cv.GetSize(orig), 8, 1)
cv.Smooth(orig, orig, cv.CV_GAUSSIAN, 5, 5)
cv.CvtColor(orig, grey_scale, cv.CV_RGB2GRAY)
cv.Erode(grey_scale, processed, None, 10)
cv.Dilate(processed, processed, None, 10)
cv.Canny(processed, processed, 5, 70, 3)
cv.Smooth(processed, processed, cv.CV_GAUSSIAN, 15, 15)
storage = cv.CreateMat(orig.width, 1, cv.CV_32FC3)
cv.HoughCircles(processed, storage, cv.CV_HOUGH_GRADIENT, 1, 16.0, 10, 140)
for i in range(0, len(np.asarray(storage))):
print "circle #%d" %i
Radius = int(np.asarray(storage)[i][0][2])
x = int(np.asarray(storage)[i][0][0])
y = int(np.asarray(storage)[i][0][1])
center = (x, y)
print x,y
cv.Circle(orig, center, 1, cv.CV_RGB(0, 255, 0), 1, 8, 0)
cv.Circle(orig, center, Radius, cv.CV_RGB(255, 0, 0), 1, 8, 0)
cv.Circle(processed, center, 1, cv.CV_RGB(0, 0, 0), -1, 8, 0)
cv.Circle(processed, center, Radius, cv.CV_RGB(255, 0, 0), 3, 8, 0)
cv.ShowImage("sun0016", orig)
cv.ShowImage("processed", processed)
cv_key = cv.WaitKey(0)
And when I run this I find edge of Sun which is circle with center but very inaccurately.
Pls know you setting of parameters HoughCircles module for precise search circles.
Thanks
The main problem here is finding a good range for your radius.
You may have a look at your picture and guess the Radius.
From the Picture you have given I would guess 180 - 220 would be a good range.
Your code would look like:
cv.HoughCircles(processed, storage, cv.CV_HOUGH_GRADIENT, 1, 16.0, 180, 220)
Just try to find good Values for minRadius and maxRadius and this should work fine.
here is solution of my problem
import numpy as np
import cv2
im = cv2.imread('sun0016.bmp')
height, width, depth = im.shape
print height, width, depth
thresh = 132
imgray = cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(imgray,(5,5),0)
edges = cv2.Canny(blur,thresh,thresh*2)
contours, hierarchy = cv2.findContours(edges,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
cnt = contours[0]
cv2.drawContours(im,contours,-1,(0,255,0),-1)
#centroid_x = M10/M00 and centroid_y = M01/M00
M = cv2.moments(cnt)
x = int(M['m10']/M['m00'])
y = int(M['m01']/M['m00'])
print x,y
print width/2.0,height/2.0
print width/2-x,height/2-y
cv2.circle(im,(x,y),1,(0,0,255),2)
cv2.putText(im,"center of Sun contour", (x,y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255))
cv2.circle(im,(width/2,height/2),1,(255,0,0),2)
cv2.putText(im,"center of image", (width/2,height/2), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,0,0))
cv2.imshow('contour',im)
cv2.waitKey(0)
Thought I would chime in with an alternative solution in case anyone stumbles upon this question in the future.
The following function uses cv2.inRange instead of cv2.Canny, and cv2.minEnclosingCircle instead of cv2.moments. It selects the largest contour found by cv2.findContours by measuring the radius of candidates' minimum enclosing circle. This filtering helps reject false positives from e.g. watermarks or dust, but depending on your requirements you might want to go about this step differently or omit it entirely.
The function returns both the x,y coordinates as well as the radius of the detected disk, a requirement for the project I was working on.
import cv2
def find_disk(img, threshold=10):
"""Finds the center and radius of a single solar disk present in the supplied image.
Uses cv2.inRange, cv2.findContours and cv2.minEnclosingCircle to determine the centre and
radius of the solar disk present in the supplied image.
Args:
img (numpy.ndarray): greyscale image containing a solar disk against a background that is below `threshold`.
threshold (int): threshold of min pixel value to consider as part of the solar disk
Returns:
tuple: center coordinates in x,y form (int)
int: radius
"""
if img is None:
raise TypeError("img argument is None - check that the path of the loaded image is correct.")
if len(img.shape) > 2:
raise TypeError("Expected single channel (grayscale) image.")
blurred = cv2.GaussianBlur(img, (5, 5), 0)
mask = cv2.inRange(blurred, threshold, 255)
img_mod, contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Find and use the biggest contour
r = 0
for cnt in contours:
(c_x, c_y), c_r = cv2.minEnclosingCircle(cnt)
# cv2.circle(img, (round(c_x), round(c_y)), round(c_r), (255, 255, 255), 2)
if c_r > r:
x = c_x
y = c_y
r = c_r
# print("Number of contours found: {}".format(len(contours)))
# cv2.imwrite("mask.jpg", mask)
# cv2.imwrite("circled_contours.jpg", img)
if x is None:
raise RuntimeError("No disks detected in the image.")
return (round(x), round(y)), round(r)
if __name__ == "__main__":
image = cv2.imread("path/to/your/image.jpg")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
center, radius = find_disk(img=gray, threshold=20)
print("circle x,y: {},{}".format(center[0], center[1]))
print("circle radius: {}".format(radius))
# Output the original image with the detected disk superimposed
cv2.circle(image, center, radius, (0, 0, 255), 1)
cv2.rectangle(image, (center[0] - 2, center[1] - 2), (center[0] + 2, center[1] + 2), (0, 0, 255), -1)
cv2.imwrite("disk_superimposed.jpg", image)
I have left in some commented debug statements that may come in handy if you find the need to tinker with this further.
You might want to use a higher threshold if your images include a lot of glare.