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.)
Related
im currently trying to implement a paper of reading pressure gauges. There is a step where i dont know what to do. It says: "The display contour is then rotated to vertically align the long
axis of the ellipse and inscribed in a rectangle used to crop the gauge image." So im not sure how i can rotate this image vertically, for better understanding i will show you a example what the current image is and what it needs to be.
Im currently at step C and need to get the image in position of step D. The text detection in the example is currently not important.
At the moment i have the correct contours and ellipse for the display.
(cnts, boundingBoxes) = sort_contours(cnts)
#find correct contours and fitellipse
if len(cnts) != 0:
for i in range(len(cnts)):
if len(cnts[i]) >= 5: #if contours has more than 5 points
# cv2.drawContours(image,cnts[0],-1,(150,10,255),3)
ellipse = cv2.fitEllipse(cnts[i])
finalElps.append(ellipse) #(centx,centy), (width,height), angle
for i in range(len(finalElps)):
centx = finalElps[i][0][0]
centy = finalElps[i][0][1]
eWidth = finalElps[i][1][0]
eHeight = finalElps[i][1][1]
sfRes = Sf(eWidth, eHeight)
cfRes = Cf(centx, imgCenterX, centy, imgCenterY)
afRes = Af(eWidth,eHeight,imgWidth,imgHeight)
print("SF: " + str(sfRes) + "| " + "CF: " + str(cfRes) + "| Af: " + str(afRes))
if(sfRes < 0.4 and cfRes < 6 and afRes < 0.9):
print(finalElps[i])
cv2.ellipse(image, finalElps[i], (255,0,0), 2)
plt.imshow(image)
sfRes, cfRes and afRes are just calculations to find the right ellipse.
What should be my next step to reach the vertical rotation? I think the correct name for it is "image rectification" but im not 100% sure about it
Here is how to get the image D from the image C:
Find the binary mask
Find an ellipse (center, width, height, angle)
Find 4 points on opposite sides of the ellipse
Use these 4 points to get perspective transform that can be used to warp the rotated gauge image to rectangular image. The final result:
Code:
import cv2
import numpy as np
image = cv2.imread("gauge.png")
# find the parameters of the ellipse
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
mask = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY)[1]
points = np.stack(np.nonzero(mask.T)).T
hull = cv2.convexHull(points)
(cx, cy), (width, height), angle = cv2.fitEllipse(hull)
# for visualization
# cv2.ellipse(image, (int(cx), int(cy)), (int(width/2), int(height/2)), angle, 0, 360, (0, 0, 255), 2)
# find the points on the opposite sides of the ellipse
# define rectangular homogenuous coordinates and rotate them using a rotation matrix
mat = cv2.getRotationMatrix2D((cx, cy), -angle, 1)
mat = np.vstack((mat, [0, 0, 1]))
coords = np.array(
[
[cx + width // 2, cy, 1],
[cx - width // 2, cy, 1],
[cx, cy + height // 2, 1],
[cx, cy - height // 2, 1],
]
)
points = (mat # coords.T)[:2].T # drop the homogenuos part
# for visualization
# for px, py in points.astype(int)[:2]:
# cv2.circle(image, (px, py), 10, (0, 0, 255), -1)
# for px, py in points.astype(int)[2:]:
# cv2.circle(image, (px, py), 10, (255, 0, 0), -1)
# define points on the target image to which the ellipse points should be mapped
size = 300
target = np.float32(
[
[size, size // 2],
[0, size // 2],
[size // 2, size],
[size // 2, 0],
]
)
mat = cv2.getPerspectiveTransform(points.astype(np.float32), target)
rect_image = cv2.warpPerspective(image, mat, (size, size))
cv2.imwrite("rect_gauge.png", rect_image)
I'm trying to cut a piece from a circle using Python along with opencv, here is the code
firstly, I constructed the circle
layer1 = np.zeros((48, 48, 4))
cv2.circle(layer1, (24, 24), 23, (0, 0, 0, 255), -1)
res = layer1[:]
and I got
and then, I drew a smaller square on it
start_point = (24, 0); end_point = (48, 24); color = (255, 0, 0)
cv2.rectangle(res, start_point, end_point, color, -1)
which gives
similarly, I drew a triangle on the circle
pt1 = (24, 0); pt2 = (48, 0); pt3 = (24, 24)
triangle_cnt = np.array( [pt1, pt2, pt3] )
cv2.drawContours(res, [triangle_cnt], 0, (255,0,0), -1)
which gives
I can go along this way to draw a smaller triangle, 1/16, 1/32 and so on.
I have to do the math manually to get the vertices.
Is there a smarter (more elegant) way to do the job?
import cv2
import numpy as np
# Colors (B, G, R)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
# Create new blank 300x150 white image
width, height = 800, 500
img = np.zeros((height, width, 3), np.uint8)
img[...] = BLACK
center = (width//2, height//2)
axes = (200, 200) # axes radius, keep equal to draw circle.
angle = 0 #clockwise first axis
startAngle = 0
endAngle = 90
color = WHITE
img = cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color, thickness=-1)
cv2.imshow('image', img)
cv2.waitKey(-1)
You can play with startAngle and endAngle to change the position of the white part.
Another option is to change the angle option (to -90 for example to rotate counter clockwise).
EDIT to show the different end angles add
img = cv2.ellipse(img, center, axes, angle, startAngle, endAngle/2, (255, 0, 0), thickness=-1)
img = cv2.ellipse(img, center, axes, angle, startAngle, endAngle/4, (0, 255, 0), thickness=-1)
img = cv2.ellipse(img, center, axes, angle, startAngle, endAngle/8, (0, 0, 255), thickness=-1)
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
I want to blur a rectangle (with rounded corners) in an image using python pillow. I already found a way to blur only a certain part of a picture.
img = Image.open('assets/images/image.png')
x, y = 300, 1600
cropped_img = img.crop((x, y, 1000, 2600))
blurred_img = cropped_img.filter(ImageFilter.GaussianBlur(20))
img.paste(blurred_img, (x, y))
img.save('assets/images/new.png')
img.show()
Furthermore I found a method to add rounded corners on a rectangle(Transparency issues drawing a rectangle with rounded corners)
def round_corner(radius):
corner = Image.new('RGBA', (radius, radius), (0, 0, 0, 0))
draw = ImageDraw.Draw(corner)
draw.pieslice((0, 0, radius * 2, radius * 2), 180, 270)
return corner
def round_rectangle(rectangle, radius):
corner = round_corner(radius)
rectangle.paste(corner, (0, 0))
rectangle.paste(corner.rotate(90), (0, rectangle.size[1] - radius))
rectangle.paste(corner.rotate(180), (rectangle.size[0] - radius, rectangle.size[1] - radius))
rectangle.paste(corner.rotate(270), (rectangle.size[0] - radius, 0))
return rectangle
Unfortunately, I can't find a way to combine these two source codes so that they work.
My Example Image:
What you need to do, is essentially to create a mask for the Image.paste() that only pastes those parts of the blurred image that lie inside the rounded rectangle.
import PIL
from PIL import Image
from PIL import ImageFilter
from PIL import ImageDraw
# when using an image as mask only the alpha channel is important
solid_fill = (50,50,50,255)
def create_rounded_rectangle_mask(rectangle, radius):
# create mask image. all pixels set to translucent
i = Image.new("RGBA",rectangle.size,(0,0,0,0))
# create corner
corner = Image.new('RGBA', (radius, radius), (0, 0, 0, 0))
draw = ImageDraw.Draw(corner)
# added the fill = .. you only drew a line, no fill
draw.pieslice((0, 0, radius * 2, radius * 2), 180, 270, fill = solid_fill)
# max_x, max_y
mx,my = rectangle.size
# paste corner rotated as needed
# use corners alpha channel as mask
i.paste(corner, (0, 0), corner)
i.paste(corner.rotate(90), (0, my - radius),corner.rotate(90))
i.paste(corner.rotate(180), (mx - radius, my - radius),corner.rotate(180))
i.paste(corner.rotate(270), (mx - radius, 0),corner.rotate(270))
# draw both inner rects
draw = ImageDraw.Draw(i)
draw.rectangle( [(radius,0),(mx-radius,my)],fill=solid_fill)
draw.rectangle( [(0,radius),(mx,my-radius)],fill=solid_fill)
return i
Mask:
Apply the mask to your image:
img = Image.open('pic.jpg')
x, y = 300, 160
radius = 75
cropped_img = img.crop((x, y, 600, 600))
# the filter removes the alpha, you need to add it again by converting to RGBA
blurred_img = cropped_img.filter(ImageFilter.GaussianBlur(20),).convert("RGBA")
# paste blurred, uses alphachannel of create_rounded_rectangle_mask() as mask
# only those parts of the mask that have a non-zero alpha gets pasted
img.paste(blurred_img, (x, y), create_rounded_rectangle_mask(cropped_img,radius))
img.save('new2.png')
img.show()
I changed some dimensions and paths. Your code lacked the imports, I completed it to a minimal verifyable complete example.
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.