Does anyone know if it is possible to calculate the overlapping area of two ellipses using matplotlib.patches.Ellipse.
I have to ellipses like this:
And i would like to calculate the ratio between the overlap area and the are of the individual ellipses.
Is this possible using only the Ellipse from matplotlib.patches
You cannot compute the area of the intersect with matplotlib (at least not to my knowledge), but you can use shapely to do so and then use matplotlib to visualise the result. Here a quick demo:
from matplotlib import pyplot as plt
from shapely.geometry.point import Point
from shapely import affinity
from matplotlib.patches import Polygon
import numpy as np
def create_ellipse(center, lengths, angle=0):
"""
create a shapely ellipse. adapted from
https://gis.stackexchange.com/a/243462
"""
circ = Point(center).buffer(1)
ell = affinity.scale(circ, int(lengths[0]), int(lengths[1]))
ellr = affinity.rotate(ell, angle)
return ellr
fig,ax = plt.subplots()
##these next few lines are pretty important because
##otherwise your ellipses might only be displayed partly
##or may be distorted
ax.set_xlim([-5,5])
ax.set_ylim([-5,5])
ax.set_aspect('equal')
##first ellipse in blue
ellipse1 = create_ellipse((0,0),(2,4),10)
verts1 = np.array(ellipse1.exterior.coords.xy)
patch1 = Polygon(verts1.T, color = 'blue', alpha = 0.5)
ax.add_patch(patch1)
##second ellipse in red
ellipse2 = create_ellipse((1,-1),(3,2),50)
verts2 = np.array(ellipse2.exterior.coords.xy)
patch2 = Polygon(verts2.T,color = 'red', alpha = 0.5)
ax.add_patch(patch2)
##the intersect will be outlined in black
intersect = ellipse1.intersection(ellipse2)
verts3 = np.array(intersect.exterior.coords.xy)
patch3 = Polygon(verts3.T, facecolor = 'none', edgecolor = 'black')
ax.add_patch(patch3)
##compute areas and ratios
print('area of ellipse 1:',ellipse1.area)
print('area of ellipse 2:',ellipse2.area)
print('area of intersect:',intersect.area)
print('intersect/ellipse1:', intersect.area/ellipse1.area)
print('intersect/ellipse2:', intersect.area/ellipse2.area)
plt.show()
The resulting plot looks like this:
And the computed areas (printed out to the terminal) are:
area of ellipse 1: 25.09238792436751
area of ellipse 2: 18.81929094327563
area of intersect: 13.656608779925698
intersect/ellipse1: 0.5442530547945023
intersect/ellipse2: 0.7256707397260032
Note that I adapted the code to generate the ellipse-shaped polygon from this post. Hope this helps.
Related
I would like to annotate a scatterplot with images corresponding to each datapoint. With standard parameters the images end up clashing with each other and other important features such as legend axis, etc. Thus, I would like the images to form a circle or a rectangle around the main scatter plot.
My code looks like this for now and I am struggling to modify it to organise the images around the center point of the plot.
import matplotlib.cbook as cbook
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import seaborn as sns
#Generate n points around a 2d circle
def generate_circle_points(n, centre_x, center_y, radius=1):
"""Generate n points around a circle.
Args:
n (int): Number of points to generate.
centre_x (float): x-coordinate of circle centre.
center_y (float): y-coordinate of circle centre.
radius (float): Radius of circle.
Returns:
list: List of points.
"""
points = []
for i in range(n):
angle = 2 * np.pi * i / n
x = centre_x + radius * np.cos(angle)
y = center_y + radius * np.sin(angle)
points.append([x, y])
return points
fig, ax = plt.subplots(1, 1, figsize=(7.5, 7.5))
data = pd.DataFrame(data={'x': np.random.uniform(0.5, 2.5, 20),
'y': np.random.uniform(10000, 50000, 20)})
with cbook.get_sample_data('grace_hopper.jpg') as image_file:
image = plt.imread(image_file)
# Set logarithmic scale for x and y axis
ax.set(xscale="log", yscale='log')
# Add grid
ax.grid(True, which='major', ls="--", c='gray')
coordianates = generate_circle_points(n=len(data),
centre_x=0, center_y=0, radius=10)
# Plot the scatter plot
scatter = sns.scatterplot(data=data, x='x', y='y', ax=ax)
for index, row in data.iterrows():
imagebox = OffsetImage(image, zoom=0.05)
imagebox.image.axes = ax
xy = np.array([row['x'], row['y']])
xybox = np.array(coordianates[index])
ab = AnnotationBbox(imagebox, xy,
xycoords='data',
boxcoords="offset points",
xybox=xybox,
pad=0)
ax.add_artist(ab)
for the moment the output looks like this:enter image description here
Ideally I would like the output to look to something like this:
enter image description here
Many thanks in advance for your help
Not an answer but a long comment:
You can control the location of the arrows, but sometimes it is easier to export figures as SVGs and edit them in Adobe Illustrator or Inkscape.
R has a dodge argument which is really nice, but even then is not always perfect. Solutions in Python exist but are laborious.
The major issue is that this needs to be done last as alternations to the plot would make it problematic. A few points need mentioning.
Your figures will have to have a fixed size (57mm / 121mm / 184mm for Science, 83mm / 171mm for RSC, 83mm / 178mm for ACS etc.), if you need to scale the figure in Illustrator keep note of the scaling factor, adding it as a textbox outside of the canvas —as the underlying plot will need to be replaced at least once due to Murphy's law. Exporting at the right size the SVG is ideal. Sounds silly, but it helps. Likewise, make sure the font size does not go under the minimum spec (7-9 points).
TL/DR: How to use Wedge() in polar coordinates?
I'm generating a 2D histogram plot in polar coordinates (r, theta). At various values of r there can be different numbers of theta values (to preserve equal area sized bins). To draw the color coded bins I'm currently using pcolormesh() calls for each radial ring. This works ok, but near the center of the plot where there may be only 3 bins (each 120 degrees "wide" in theta space), pcolormesh() draws triangles that don't "sweep" out full arc (just connecting the two outer arc points with a straight line).
I've found a workaround using ax.bar() call, one for each radial ring and passing in arrays of theta values (each bin rendering as an individual bar). But when doing 90 rings with 3 to 360 theta bins in each, it's incredibly slow (minutes).
I tried using Wedge() patches, but can't get them to render correctly in the polar projection. Here is sample code showing both approaches:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Wedge
from matplotlib.collections import PatchCollection
# Theta coordinates in degrees
theta1=45
theta2=80
# Radius coordinates
r1 = 0.4
r2 = 0.5
# Plot using bar()
fig, ax = plt.subplots(figsize=[6,6], subplot_kw={'projection': 'polar'})
theta_mid = np.deg2rad((theta1 + theta2)/2)
theta_width = np.deg2rad(theta2 - theta1)
height = r2 - r1
ax.bar(x=theta_mid, height = height, width=theta_width, bottom=r1)
ax.set_rlim(0, 1)
plt.savefig('bar.png')
# Plot using Wedge()
fig, ax = plt.subplots(figsize=[6,6], subplot_kw={'projection': 'polar'})
patches = []
patches.append( Wedge(center=(0, 0), r = r1, theta1=theta1, theta2=theta2, width = r2-r1, color='blue'))
p = PatchCollection(patches)
ax.add_collection(p)
ax.set_rlim(0, 1)
plt.savefig('wedge.png')
The outputs of each are:
Bar
Wedge
I've tried using radians for the wedge (because polar plots usually want their angle values in radians). That didn't help.
Am I missing something in how I'm using the Wedge? If I add thousands of Wedges to my Patch collection should I have any expectation it will be faster than bar()?
Thinking this was an actual bug, I opened this issue https://github.com/matplotlib/matplotlib/issues/22717 on matplotlib where one of the maintainers nicely pointed out that I should be using Rectangle() instead of Wedge().
The solution they provided is
from matplotlib.patches import Rectangle
fig, ax = plt.subplots(figsize=[6,6], subplot_kw={'projection': 'polar'})
p = PatchCollection([Rectangle((np.deg2rad(theta1), r1), theta_width, height, color='blue')])
ax.add_collection(p)
ax.set_rlim(0, 1)
plt.savefig('wedge.png')
I want to use in matplotlib.patches.Arc the clip_path parameter, but do not succeed.
Next is just an example, where I want to see not the complete orange arc but only the partial orange arc between y-axis and the red circle by using the clip_path parameter, but do no understand how to define the clip_path parameters. Thanks.
import math as m
import matplotlib.pyplot as plt
import matplotlib.patches as pat
plt.figure(figsize=(10,10),dpi=300)
ag=10
plt.axis([-ag,ag,-ag,ag])
plt.grid(True)
circle1 = plt.Circle((0, 2.5), 7, color='r',fill=False)
plt.gcf().gca().add_artist(circle1)
myarc=pat.Arc((0,0),25,18,angle=0,theta1=0,theta2=355,color="orange")
plt.gcf().gca().add_artist(myarc)
plt.savefig("myarc.png")
plt.show()
This is what I got:
Just a further remark: With next modification of theta1 and theta2 angle I get what I need, but for this the two intersections need to be determined first. My intention is to avoid these calculations and just draw an ellipse and defining two clipping paths (the red circle and the y-axis).
myarc=pat.Arc((0,0),25,18,angle=0,theta1=110,theta2=152,color="orange")
To clip the arc by the circle, you can use myarc.set_clip_path(circle1). It is important that both the arc and the circle are previously added to the plot (ax.add_artist()). Note that clipping by the borders of the axes happens automatically.
To create more complicated clipping, the shapely is probably handier.
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
fig, ax = plt.subplots(figsize=(10, 6))
circle1 = plt.Circle((0, 2.5), 7, color='r', fill=False)
ax.add_artist(circle1)
myarc = mpatches.Arc((0, 0), 25, 18, angle=0, theta1=0, theta2=355, color="orange", lw=5)
ax.add_artist(myarc)
myarc.set_clip_path(circle1)
ag = 10
ax.set_xlim(-ag, ag)
ax.set_ylim(-ag, ag)
plt.grid(True)
ax.set_aspect('equal') # set the aspect ratio so circles look like circles
plt.show()
By using the steps of answer (1) I got the wanted result without the need to calculate all the intersections. Steps:
Defining and Plotting series of curves
Defining clipping areas by using clip_path option (e.g. circles or shaping an area by concatenating 1D-arrays through mathematical function results)
Using clip_path to get rid of unwanted portion of curves
# Import python Modules
import math as m
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Polygon
## constants for the circles
Loc=37 # in degree
StepAl=3 # in degree
StepAz=10 # in degree
rAequ=6.3 # Radius
rWkSt=9.6 # Radius
Ze=3.14 # Distance
## red AlCircles, in total 31
AlCircle=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
i=0
while i<=30:
AlCircle[i]=[0,0,0,0]
i=i+1
# Calculating the parameters of the AlCircles
i=0
while i<=30:
AlCircle[i][0]=rAequ*m.tan((-Loc+i*StepAl)/2/180*m.pi) # lower y-Value
AlCircle[i][1]=rAequ*m.tan((-Loc+180-i*StepAl)/2/180*m.pi) # upper y-Value
AlCircle[i][2]=(AlCircle[i][1]-AlCircle[i][0])/2 # Radius
AlCircle[i][3]=AlCircle[i][0]+AlCircle[i][2] # Center
i=i+1
## green AzCircles, in total 18
DZ=rAequ/m.cos(Loc/180*m.pi) # Distance
AzCircle=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
i=0
while i<=17:
AzCircle[i]=[0,0]
i=i+1
# Calculating the parameters of the AzCircles
i=1
while i<=17:
AzCircle[i][0]=DZ*m.tan((-90+i*StepAz)/180*m.pi) # distance Center to y-Axis
AzCircle[i][1]=rAequ/m.cos(Loc/180*m.pi)/m.cos((-90+i*StepAz)/180*m.pi) # Radius of AzCircles
i=i+1
### Generating Plots
plt.figure(figsize=(10,10),dpi=100)
ag=rWkSt
plt.axis([-ag,ag,-ag,ag])
plt.grid(True)
# Plotting blue Circle
circle0=plt.Circle((0,0),rWkSt,color='b',fill=False)
plt.gcf().gca().add_artist(circle0)
# Plotting red AlCircles
i=0
while i<=30:
# defining Cliparea1
myCliparea1=plt.Circle((0,0),rWkSt,color="b",ls="dotted",fill=False)
plt.gcf().gca().add_artist(myCliparea1)
# calculating AlCircles and plotting
circle1=plt.Circle((0,AlCircle[i][3]),AlCircle[i][2],color='r',fill=False)
plt.gcf().gca().add_artist(circle1)
circle1.set_clip_path(myCliparea1) # performing clipping
i=i+1
# Plotting green AzCircles
i=1
while i<=17: # nur bis 17
xA=9.072582 # precalculated Intersection for f1(x) and f2(x)
# f1(x) for lower clipping area border line
x1=np.arange(-xA,+xA,0.1)
y1=(-1)*np.sqrt(AlCircle[0][2]**2-x1**2)+AlCircle[0][3]
# f2(x) for upper clipping area border line
x2=np.arange(xA,-xA,-0.1)
y2=(+1)*np.sqrt(rWkSt**2-x2**2)
# building clipping area
x3=np.concatenate((x1,x2))
y3=np.concatenate((y1,y2))
poly = Polygon(np.column_stack([x3, y3]), animated=True, color="aqua", fill=False)
plt.gcf().gca().add_artist(poly) # plotting of clipping area
# calculating AzCircles and plotting
circle2=plt.Circle((-AzCircle[i][0],Ze-DZ),AzCircle[i][1],color='g',fill=False)
plt.gcf().gca().add_artist(circle2)
circle2.set_clip_path(poly) # performing clipping
i=i+1
plt.savefig("myPlot.png")
plt.show()
myPlot
I have a boolean image, where the zeros are the background, and I want to plot the ellipse that encloses the major and minor axis of an object retrieved from skimage.measure.regionprops. The module skimage.draw.ellipse_perimeter generates the expected ellipse but also two undesired lines.
Code (the input image is here):
import skimage
import skimage.draw
from skimage.measure import label
from skimage.measure import regionprops
import matplotlib.pyplot as plt
# load example image
TP_mask = plt.imread('https://i.stack.imgur.com/UYLE0.png')
# connect region with same integer value
region = label(TP_mask)
# obtain RegionProperties
props = regionprops(region)
props = props[0]
# define centroid
y0,x0 = props.centroid
# draw ellipse perimeter
rr,cc = skimage.draw.ellipse_perimeter(int(x0),int(y0),int(props.minor_axis_length*0.5),int(props.major_axis_length*0.5), orientation = props.orientation)
# plot
plt.plot(rr,cc, color = 'yellow')
plt.imshow(TP_mask, cmap = 'gray')
plt.show()
However, if I create a simplified example as follows, I obtain the expected ellipse. Could someone help me understand what am I doing wrong?
import numpy as np
img = np.zeros((1000,1000))
img[200:800,200:400] = 1
region = label(img)
props = regionprops(region)
props = props[0]
y0,x0 = props.centroid
rr,cc = skimage.draw.ellipse_perimeter(int(x0),int(y0),int(props.minor_axis_length*0.5),int(props.major_axis_length*0.5), orientation = props.orientation)
plt.plot(rr,cc, color = 'yellow')
plt.imshow(img, cmap = 'gray')
plt.show()
It turns out that the coordinates returned by the draw module are designed to index into an array, as shown in this example, rather than plot:
rr, cc = ellipse_perimeter(120, 400, 60, 20, orientation=math.pi / 4.)
img[rr, cc, :] = (1, 0, 1)
To use plt.plot and do a line plot, the coordinates need to be sorted as they go around the circle/ellipse. They are not properly sorted by default because the ellipse is actually drawn in four separate quadrants, which you can find by looking at the relevant part of the source code. (A clue: the lines hit exactly where the ellipse is vertical or horizontal.)
Since you have a convex surface, computing the angle between each point and the centre of the ellipse is enough to sort the points. Try the following:
fig, ax = plt.subplots()
_ = ax.imshow(TP_mask, cmap='gray')
angle = np.arctan2(rr - np.mean(rr), cc - np.mean(cc))
sorted_by_angle = np.argsort(angle)
rrs = rr[sorted_by_angle]
ccs = cc[sorted_by_angle]
_ = ax.plot(rrs, ccs, color='red')
Which gives:
I tried to using the algorithm shown here: https://discuss.leetcode.com/topic/15733/my-java-solution-sum-of-areas-overlapped-area
However, that algorithm only deals with finding the areas of only TWO overlapped rectangles.
How would I go on about finding the area of the intersection of say 3, or 4 or 5, etc number of overlapping rectangles, if I know the length, breadth of each rectangle?
Shapely is a good library for stuff like this.
from shapely.geometry import box
# make some rectangles (for demonstration purposes and intersect with each other)
rect1 = box(0,0,5,2)
rect2 = box(0.5,0.5,3,3)
rect3 = box(1.5,1.5,4,6)
rect_list = [rect1, rect2, rect3]
# find intersection of rectangles (probably a more elegant way to do this)
for rect in rect_list[1:]:
rect1 = rect1.intersection(rect)
intersection = rect1
To visualize what's happening here. I plot the rectangles and their intersection:
from matplotlib import pyplot as plt
from matplotlib.collections import PatchCollection
from matplotlib.patches import Polygon
# plot the rectangles before and after merging
patches = PatchCollection([Polygon(a.exterior) for a in rect_list], facecolor='red', linewidth=.5, alpha=.5)
intersect_patch = PatchCollection([Polygon(intersection.exterior)], facecolor='red', linewidth=.5, alpha=.5)
# make figure
fig, ax = plt.subplots(1,2, subplot_kw=dict(aspect='equal'))
ax[0].add_collection(patches, autolim=True)
ax[0].autoscale_view()
ax[0].set_title('separate polygons')
ax[1].add_collection(intersect_patch, autolim=True)
ax[1].set_title('intersection = single polygon')
ax[1].set_xlim(ax[0].get_xlim())
ax[1].set_ylim(ax[0].get_ylim())
plt.show()