Understanding the ellipse parameters in Open CV using Python - python

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

Related

Opencv ellipse() bug?

cv.ellipse( img, center, axes, angle, startAngle, endAngle, color[, thickness[, lineType[, shift]]] )
The startAngle is the starting angle of the elliptic arc in degrees.If we set angele=30 and startAngle=60,the angle of the starting line of the ellipse may be 30+60=90.Is that right?
But this is not the case.
My code:
img2 = np.zeros((500, 500))
img2 = cv2.ellipse(img2, (250, 250), (120, 40), 30, 60, 180, 255, -1)
cv2.imshow('2', img2)
cv2.waitKey(0)
Lets look at the result, the angle of the starting line(red line) is obviously not 90 degrees.
enter image description here
Why?Is it a bug?
I got it.
startAngle is not the starting angle based on the ellpise.It is based on the circumcircle of that ellipse.Lets look at the following figure
It does a mapping operation.The blue curve is the output.

To cut a piece from a circle, I have to do the math manually first. Is there a smarter way to do the job using Python along with opencv?

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)

Pillow: How to gradient fill drawn shapes?

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.)

Blur a region shaped like a rounded rectangle inside an Image

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.

Customizing cv2.circle

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.

Categories