How to draw ellipse on an image? - python

I am using this function to draw a circle. How can I modify to draw ellipse?
def circle_points(resolution, center, radius):
"""
Generate points which define a circle on an image.Centre refers to the centre of the circle
"""
radians = np.linspace(0, 2*np.pi, resolution)
c = center[1] + radius*np.cos(radians)#polar co-ordinates
r = center[0] + radius*np.sin(radians)
return np.array([c, r]).T

A simple way is to stretch x-axis and y-axis by a different factor, i.e. to replace radius by x_semiaxis and y_semiaxis, to mean the semi-axes of the ellipse:
def ellipse_points(resolution, center, x_semiaxis, y_semiaxis):
"""
Generate points which define a ellipse on an image.
Centre refers to the centre of the ellipse.
"""
radians = np.linspace(0, 2 * np.pi, resolution)
x = center[0] + x_semiaxis * np.cos(radians)
y = center[1] + y_semiaxis * np.sin(radians)
return np.array([x, y])
Note that I use the more traditional formula to have x by the cosine, and y by the sine, and I avoid including the transposition, just to make plotting easier. You can easily bring it back.
import matplotlib.pyplot as plt
plt.scatter(*ellipse_points(1000, (10, 20), 200, 100))
plt.scatter((10,), (20,)) # plot the center too
plt.gca().set_aspect('equal')

Related

How to use mgrid to interpolate between a rectangle and a circle

I am trying to create a 3D surface that has a 1/4 rectangle for the exterior and 1/4 circle for the interior. I had help before to create the 3D surface with an ellipse as an exterior but I cannot do this for a rectangle for some reason. I have done the math by hand which makes sense, but my code does not. I would greatly appreciate any help with this.
import numpy as np
import pyvista as pv
# parameters for the waveguide
# diameter of the inner circle
waveguide_throat = 30
# axes of the outer ellipse
ellipse_x = 250
ellipse_y = 170
# shape parameters for the z profile
depth_factor = 4
angle_factor = 40
# number of grid points in radial and angular direction
array_length = 100
phase_plug = 0
phase_plug_dia = 20
plug_offset = 5
dome_dia = 28
# theta is angle where x and y intersect
theta = np.arctan(ellipse_x / ellipse_y)
# chi is for x direction and lhi is for y direction
chi = np.linspace(0, theta, 100)
lhi = np.linspace(theta, np.pi/2, 100)
# mgrid to create structured grid
r, phi = np.mgrid[0:1:array_length*1j, 0:np.pi/2:array_length*1j]
# Rectangle exterior, circle interior
x = (ellipse_y * np.tan(chi)) * r + ((waveguide_throat / 2 * (1 - r)) * np.cos(phi))
y = (ellipse_x / np.tan(lhi)) * r + ((waveguide_throat / 2 * (1 - r)) * np.sin(phi))
# compute z profile
angle_factor = angle_factor / 10000
z = (ellipse_x / 2 * r / angle_factor) ** (1 / depth_factor)
plotter = pv.Plotter()
waveguide_mesh = pv.StructuredGrid(x, y, z)
plotter.add_mesh(waveguide_mesh)
plotter.show()
The linear interpolation you're trying to use is a general tool that should work (with one small caveat). So the issue is first with your rectangular edge.
Here's a sanity check which plots your interior and exterior lines:
# debugging: plot interior and exterior
exterior_points = np.array([
ellipse_y * np.tan(chi),
ellipse_x / np.tan(lhi),
np.zeros_like(chi)
]).T
phi_aux = np.linspace(0, np.pi/2, array_length)
interior_points = np.array([
waveguide_throat / 2 * np.cos(phi_aux),
waveguide_throat / 2 * np.sin(phi_aux),
np.zeros_like(phi_aux)
]).T
plotter = pv.Plotter()
plotter.add_mesh(pv.wrap(exterior_points))
plotter.add_mesh(pv.wrap(interior_points))
plotter.show()
The bottom left is your interior circle, looks good. The top right is what's supposed to be a rectangle, but isn't.
To see why your original surface looks the way it does, we have to note one more thing (this is the small caveat I mentioned): the orientation of your curves is also the opposite. This implies that you interpolate the "top" (in the screenshot) point of your interior curve with the "bottom" point of the exterior curve. This explains the weird fan shape.
So you need to fix the exterior curve, and make sure the orientation of the two edges is the same. Note that you can just create the two 1d arrays for the two edges, and then interpolate them. You don't have to come up with a symbolic formula that you plug into the interpolation step. If you have 1d arrays of the same shape x_interior, y_interior, x_exterior, y_exterior then you can then do x_exterior * r + x_interior * (1 - r) and the same for y. This means removing the mgrid call, only using an array r of shape (n, 1), and making use of array broadcasting to do the interpolation. This means doing r = np.linspace(0, 1, array_length)[:, None].
So the question is how to define your rectangle. You need to have the same number of points on the rectangular curve than what you have on the circle (I would strongly recommend using the array_length parameter everywhere to ensure this!). Since you want to span the whole rectangle, I believe you have to choose an array index (i.e. a certain angle in the circular arc) which will map to the corner of the rectangle. Then it's a simple matter of varying only y for the points until that index, and x for the rest (or vice versa).
Here's what I mean: you know that the rectangle's corner is at angle theta in your code (although I think you have x and y mixed up if we assume the conventional relationship between "x", "y" and the tangent of the angle). Since theta goes from 0 to pi/2, and your phi values also go from 0 to pi/2, you should choose index (array_length * (2*theta/np.pi)).round().astype(int) - 1 (or something similar) that will map to the rectangle's corner. If you have a square, this gives you theta = pi/4, and consequently (array_length / 2).round().astype(int) - 1. For array_length = 3 this is index (2 - 1) == 1, which is the middle index for 3-length arrays. (The more points you have along the edge, the less it will matter if you commit an off-by-one error here.)
The only remaining complication then is that we have to explicitly broadcast the 1d z array to the common shape. And we can use the same math you used to get a rectangular edge that is equidistant in angles.
Your code fixed with this suggestion (note that I've added 1 to the corner index because I'm using it as a right-exclusive range index):
import numpy as np
import pyvista as pv
# parameters for the waveguide
# diameter of the inner circle
waveguide_throat = 30
# axes of the outer ellipse
ellipse_x = 250
ellipse_y = 170
# shape parameters for the z profile
depth_factor = 4
angle_factor = 40
# number of grid points in radial and angular direction
array_length = 100
# quarter circle interior line
phi = np.linspace(0, np.pi/2, array_length)
x_interior = waveguide_throat / 2 * np.cos(phi)
y_interior = waveguide_throat / 2 * np.sin(phi)
# theta is angle where x and y intersect
theta = np.arctan2(ellipse_y, ellipse_x)
# find array index which maps to the corner of the rectangle
corner_index = (array_length * (2*theta/np.pi)).round().astype(int)
# construct rectangular coordinates manually
x_exterior = np.zeros_like(x_interior)
y_exterior = x_exterior.copy()
phi_aux = np.linspace(0, theta, corner_index)
x_exterior[:corner_index] = ellipse_x
y_exterior[:corner_index] = ellipse_x * np.tan(phi_aux)
phi_aux = np.linspace(np.pi/2, theta, array_length - corner_index, endpoint=False)[::-1] # mind the reverse!
x_exterior[corner_index:] = ellipse_y / np.tan(phi_aux)
y_exterior[corner_index:] = ellipse_y
# interpolate between two curves
r = np.linspace(0, 1, array_length)[:, None] # shape (array_length, 1) for broadcasting
x = x_exterior * r + x_interior * (1 - r)
y = y_exterior * r + y_interior * (1 - r)
# debugging: plot interior and exterior
exterior_points = np.array([
x_exterior,
y_exterior,
np.zeros_like(x_exterior),
]).T
interior_points = np.array([
x_interior,
y_interior,
np.zeros_like(x_interior),
]).T
plotter = pv.Plotter()
plotter.add_mesh(pv.wrap(exterior_points))
plotter.add_mesh(pv.wrap(interior_points))
plotter.show()
# compute z profile
angle_factor = angle_factor / 10000
z = (ellipse_x / 2 * r / angle_factor) ** (1 / depth_factor)
# explicitly broadcast to the shape of x and y
z = np.broadcast_to(z, x.shape)
plotter = pv.Plotter()
waveguide_mesh = pv.StructuredGrid(x, y, z)
plotter.add_mesh(waveguide_mesh, style='wireframe')
plotter.show()
The curves look reasonable:
As does the interpolated surface:

Why is this randomly generated spherical point cloud not uniformly distributed?

I'm trying to simulate radiation emitting from a point source. To do this, given the coordinates of a source and the desired length of emitted rays, I randomly generate a direction vector in spherical coordinates, convert it to cartesian, and return the correct end point. However, when I run this, and visualize the resulting point cloud (consisting of all the randomly generated end points) in Blender, I see that it's more densely populated at the "poles" of the sphere. I'd like the points to be uniformly distributed along the sphere. How can I achieve this?
The random generation function:
def getRadiationEmissionLineSeg(p, t):
if(p.size == 4):
#polar angle spans [0, pi] from +Z axis to -Z axis
#azimuthal angle spans [0, 2*pi] orthogonal to the zenith (in the XY plane)
theta = math.pi * random.random()
phi = 2 * math.pi * random.random()
#use r = 1 to get a unit direction vector
v = sphericalToCartesian(1, theta, phi)
#parametric vector form: vec = p + tv
#p = point that lies on vector (origin point in case of a ray)
#t = parameter (-inf, inf) for lines, [0, inf) for rays
#v = direction vector (must be normalized)
return p + t * v
The spherical coordinates -> cartesian conversion function:
def sphericalToCartesian(r, theta, phi):
x = r * math.sin(theta) * math.cos(phi)
y = r * math.sin(theta) * math.sin(phi)
z = r * math.cos(theta)
return npy.array([x, y, z, 0])
When you transform points by spherical coordinates and angle theta approaches pi, the circle which is an image of [0,2pi]x{theta} gets smaller and smaller. Since theta is uniformly distributed, there will be more points near poles. It could be seen on image of grid.
If you want to generate uniformly distributed points on sphere, you can use the fact that if you cut a sphere with two parallel planes, the area of the strip of spherical surface between the planes depends only on the distance between the planes. Hence, you can get a uniform distribution on the sphere using two uniformly distributed random variables:
z coordinate between -r and r,
an angle theta between [0, 2pi) corresponding to a longitude.
Then you can easily calculate x and y coordiantes.
Example code:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
r = 1
n = 1000
z = np.random.random(n)*2*r - r
phi = np.random.random(n)*2*np.pi
x = np.sqrt(1 - z**2)*np.cos(phi)
y = np.sqrt(1 - z**2)*np.sin(phi)
fig = plt.figure(figsize=(8, 8))
ax = plt.axes(projection='3d')
ax.scatter(x, y, z)
plt.show()
Results for n=100,250,1000:

Matplotlib: inverse affine transformation to get an equal aspect with different x and y limits

I have the 2D coordinates of a geometric shape as x and y arrays. Using a combination of translation and rotation I can get the shape rotated about its geometric center by a given angle alpha (See below for a minimal example).
As shown in the code below, this can be achieved by first shifting the geometric center of the shape to the origin of the coordinates, then applying the rotation (multiplying by the 2D rotation matrix) then translating it back to its original position.
In this example, let's assume that the shape is a rectangle:
import numpy as np
from numpy import cos, sin, linspace, concatenate
import matplotlib.pyplot as plt
def rotate(x, y, alpha):
"""
Rotate the shape by an angle alpha (given in degrees)
"""
# Get the center of the shape
x_center = (x.max() + x.min()) / 2.0
y_center = (y.max() + y.min()) / 2.0
# Shifting the center of the shape to the origin of coordinates
x0 = x - x_center
y0 = y - y_center
angle_rad = np.deg2rad(alpha)
rot_mat = np.array([
[cos(angle_rad), -sin(angle_rad)],
[sin(angle_rad), cos(angle_rad)]
])
xy = np.vstack((x0, y0))
xnew, ynew = rot_mat # xy
# translate it back to its original location
xnew += x_center
ynew += y_center
return xnew, ynew
z0, z1, z2, z3 = 4 + 0.6*1j, 4 + 0.8*1j, 8 + 0.8*1j, 8 + 0.6*1j
xy = concatenate((
linspace(z0, z1, 10, endpoint=False),
linspace(z1, z2, 10, endpoint=False),
linspace(z2, z3, 10, endpoint=False),
linspace(z3, z0, 10, endpoint=True)
))
x = xy.real
y = xy.imag
xrot, yrot = rotate(x, y, alpha=-45.0)
# The x and y limits
xlow, xup = 0, 10
ylow, yup = -1.5, 3.0
plt.plot(x, y, label='original shape')
plt.plot(xrot, yrot, label='rotated shape')
plt.xlim((xlow, xup))
plt.ylim((ylow, yup))
plt.legend()
plt.show()
We get the following plot:
As you can see, the shape gets rotated but it is stretched/skewed as well because the aspect was not set to equal. we could check that by setting:
plt.gca().set_aspect('equal')
And this shows the rotated shape without being skewed:
The problem is that I am plotting this shape with other data that has an x range much larger than the y range. So, setting an equal aspect is not a solution in this case.
To be more precise, I want the rotated shape (orange color) in the first figure to show up correctly like the second figure. My approach is to find the inverse skew matrix in the first figure (resulting from the difference between x and y limits) and multiply it by the rotated shape to get the expected result.
Unfortunately, Using trial and error I couldn't get the correct skew matrix.
Any help is greatly appreciated.
EDIT
From a linear algebra perspective, how to express that deformation of the rotated shape in the first figure in terms of skewing and scaling transformations?
When performing the desired rotation, the vertices of the rectangle will lose their meaning in data coordinates, and the initial rectangle will become a trapezoid. Apparently this is desired. So the question becomes essentially how to perform a rotation in screen coordinates about a given point center in data coordinates.
The solution might look a little complicated, which is due to a callback being used. This is necessary, to keep the center point in screen coordinates synchronized with possible axis limit changes.
from matplotlib import pyplot as plt
from matplotlib.transforms import Affine2D
x, y = (4, 0.6)
dx, dy = (4, 0.2)
fig, ax = plt.subplots()
# The x and y limits
xlow, xup = 0, 10
ylow, yup = -1.5, 3.0
ax.set(xlim=(xlow, xup), ylim=(ylow, yup))
rect1 = plt.Rectangle((x,y), width=dx, height=dy, facecolor="none", edgecolor="C0")
ax.add_patch(rect1)
rect2 = plt.Rectangle((x,y), width=dx, height=dy, facecolor="none", edgecolor="C1")
ax.add_patch(rect2)
def lim_change(evt=None):
center = (x+dx/2, y+dy/2)
trans = ax.transData + Affine2D().rotate_deg_around(*ax.transData.transform_point(center), -45)
rect2.set_transform(trans)
lim_change()
cid = ax.callbacks.connect("xlim_changed", lim_change)
cid = ax.callbacks.connect("ylim_changed", lim_change)
plt.show()

Generate profiles through a 2D array at an angle without altering pixels

I'd like to plot two profiles through the highest intensity point in a 2D numpy array, which is an image of a blob (i.e. a line through the semi-major axis, and another line through the semi-minor axis). The blob is rotated at an angle theta counterclockwise from the standard x-axis and is asymmetric.
It is a 600x600 array with a max intensity of 1 (at only one pixel) that is located right at the center at (300, 300). The angle rotation from the x-axis (which then gives the location of the semi-major axis when rotated by that angle) is theta = 89.54 degrees. I do not want to use scipy.ndimage.rotate because it uses spline interpolation, and I do not want to change any of my pixel values. But I suppose a nearest-neighbor interpolation method would be okay.
I tried generating lines corresponding to the major and minor axes across the image, but the result was not right at all (the peak was far less than 1), so maybe I did something wrong. The code for this is below:
import numpy as np
import matplotlib.pyplot as plt
from scipy import ndimage
def profiles_at_angle(image, axis, theta):
theta = np.deg2rad(theta)
if axis == 'major':
x_0, y_0 = 0, 300-300*np.tan(theta)
x_1, y_1 = 599, 300+300*np.tan(theta)
elif axis=='minor':
x_0, y_0 = 300-300*np.tan(theta), 599
x_1, y_1 = 300+300*np.tan(theta), -599
num = 600
x, y = np.linspace(x_0, x_1, num), np.linspace(y_0, y_1, num)
z = ndimage.map_coordinates(image, np.vstack((x,y)))
fig, axes = plt.subplots(nrows=2)
axes[0].imshow(image, cmap='gray')
axes[0].axis('image')
axes[1].plot(z)
plt.xlim(250,350)
plt.show()
profiles_at_angle(image, 'major', theta)
Did I do something obviously wrong in my code above? Or how else can I accomplish this? Thank you.
Edit: Here are some example images. Sorry for the bad quality; my browser crashed every time I tried uploading them anywhere so I had to take photos of the screen.
Figure 1: This is the result of my code above, which is clearly wrong since the peak should be at 1. I'm not sure what I did wrong though.
Figure 2: I made this plot below by just taking the profiles through the standard x and y axes, ignoring any rotation (this only looks good coincidentally because the real angle of rotation is so close to 90 degrees, so I was able to just switch the labels and get this). I want my result to look something like this, but taking the correction rotation angle into account.
Edit: It could be useful to run tests on this method using data very much like my own (it's a 2D Gaussian with nearly the same parameters):
image = np.random.random((600,600))
def generate(data_set):
xvec = np.arange(0, np.shape(data_set)[1], 1)
yvec = np.arange(0, np.shape(data_set)[0], 1)
X, Y = np.meshgrid(xvec, yvec)
return X, Y
def gaussian_func(xy, x0, y0, sigma_x, sigma_y, amp, theta, offset):
x, y = xy
a = (np.cos(theta))**2/(2*sigma_x**2) + (np.sin(theta))**2/(2*sigma_y**2)
b = -np.sin(2*theta)/(4*sigma_x**2) + np.sin(2*theta)/(4*sigma_y**2)
c = (np.sin(theta))**2/(2*sigma_x**2) + (np.cos(theta))**2/(2*sigma_y**2)
inner = a * (x-x0)**2
inner += 2*b*(x-x0)*(y-y0)
inner += c * (y-y0)**2
return (offset + amp * np.exp(-inner)).ravel()
xx, yy = generate(image)
image = gaussian_func((xx.ravel(), yy.ravel()), 300, 300, 5, 4, 1, 1.56, 0)
image = np.reshape(image, (600, 600))
This should do it for you. You just did not properly compute your lines.
theta = 65
peak = np.argwhere(image==1)[0]
x = np.linspace(peak[0]-100,peak[0]+100,1000)
y = lambda x: (x-peak[1])*np.tan(np.deg2rad(theta))+peak[0]
y_maj = np.linspace(y(peak[1]-100),y(peak[1]+100),1000)
y = lambda x: -(x-peak[1])/np.tan(np.deg2rad(theta))+peak[0]
y_min = np.linspace(y(peak[1]-100),y(peak[1]+100),1000)
del y
z_min = scipy.ndimage.map_coordinates(image, np.vstack((x,y_min)))
z_maj = scipy.ndimage.map_coordinates(image, np.vstack((x,y_maj)))
fig, axes = plt.subplots(nrows=2)
axes[0].imshow(image)
axes[0].plot(x,y_maj)
axes[0].plot(x,y_min)
axes[0].axis('image')
axes[1].plot(z_min)
axes[1].plot(z_maj)
plt.show()

Elliptic orbit with polar method tracing phases, axes and Earth direction

I would like to represent the elliptical orbit of a binary system of two stars. What I aim to, is something like this:
Where I have a grid of the sizes along the axes, an in-scale star at the focus, and the orbit of the secondary star. The decimal numbers along the orbit are the orbital phases. The arrow at the bottom is the Earth direction, and the thick part at the orbit is related to the observation for that specific case - I don't need it. What I want to change from this plot is:
Orbital phase: instead of numbers along the orbit, I would like "dashed rays" from the focus to the orbit, and the orbital phase above them:
I don't want the cross along (0, 0);
I would like to re-orient the orbit, in order that the 0.0 phase is around the top left part of the plot, and the Earth direction is an upward pointing straight arrow (the parameters of my system are different from the one plotted here).
I tried to look for python examples, but the only thing I came out with (from here), is a polar plot:
which is not really representative of what I want, but still is a beginning:
import numpy as np
import matplotlib.pyplot as plt
cos = np.cos
pi = np.pi
a = 10
e = 0.1
theta = np.linspace(0,2*pi, 360)
r = (a*(1-e**2))/(1+e*cos(theta))
fig = plt.figure()
ax = fig.add_subplot(111, polar=True)
ax.set_yticklabels([])
ax.plot(theta,r)
print(np.c_[r,theta])
plt.show()
Here's something that gets you very close. You do not need polar coordinates to plot a decent ellipse. There is a so-called artist you can readily utilize.
You probably have to customize the axis labels and maybe insert an arrow or two if you want:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
# initializing the figure:
fig = plt.figure()
# the (carthesian) axis:
ax = fig.add_subplot(111,aspect='equal')
ax.grid(True)
# parameters of the ellipse:
a = 5.0
e = 4.0
b = np.sqrt(a**2.0 - e**2.0)
# the center of the ellipse:
x = 6.0
y = 6.0
# the angle by which the ellipse is rotated:
angle = -45.0
#angle = 0.0
# plotting the ellipse, using an artist:
ax.add_artist(Ellipse(xy=[x,y], width=2.0*a, height=2.0*b, \
angle=angle, facecolor='none'))
ax.set_xlim(0,2.0*x)
ax.set_ylim(0,2.0*y)
# marking the focus (actually, both)
# and accounting for the rotation of the ellipse by angle
xf = [x - e*np.cos(angle * np.pi/180.0),
x + e*np.cos(angle * np.pi/180.0)]
yf = [y - e*np.sin(angle * np.pi/180.0),
y + e*np.sin(angle * np.pi/180.0)]
ax.plot(xf,yf,'xr')
# plotting lines from the focus to the ellipse:
# these should be your "rays"
t = np.arange(np.pi,3.0*np.pi,np.pi/5.0)
p = b**2.0 / a
E = e / a
r = [p/(1-E*np.cos(ti)) for ti in t]
# converting the radius based on the focus
# into x,y coordinates on the ellipse:
xr = [ri*np.cos(ti) for ri,ti in zip(r,t)]
yr = [ri*np.sin(ti) for ri,ti in zip(r,t)]
# accounting for the rotation by anlge:
xrp = [xi*np.cos(angle * np.pi/180.0) - \
yi*np.sin(angle * np.pi/180.0) for xi,yi in zip(xr,yr)]
yrp = [xi*np.sin(angle * np.pi/180.0) + \
yi*np.cos(angle * np.pi/180.0) for xi,yi in zip(xr,yr)]
for q in range(0,len(t)):
ax.plot([xf[0], xf[0]+xrp[q]],[yf[0], yf[0]+yrp[q]],'--b')
# put labels outside the "rays"
offset = 0.75
rLabel = [ri+offset for ri in r]
xrl = [ri*np.cos(ti) for ri,ti in zip(rLabel,t)]
yrl = [ri*np.sin(ti) for ri,ti in zip(rLabel,t)]
xrpl = [xi*np.cos(angle * np.pi/180.0) - \
yi*np.sin(angle * np.pi/180.0) for xi,yi in zip(xrl,yrl)]
yrpl = [xi*np.sin(angle * np.pi/180.0) + \
yi*np.cos(angle * np.pi/180.0) for xi,yi in zip(xrl,yrl)]
# for fancy label rotation reduce the range of the angle t:
tlabel = [(ti -np.pi)*180.0/np.pi for ti in t]
for q in range(0,len(tlabel)):
if tlabel[q] >= 180.0:
tlabel[q] -= 180.0
# convert the angle t from radians into degrees:
tl = [(ti-np.pi)*180.0/np.pi for ti in t]
for q in range(0,len(t)):
rotate_label = angle + tlabel[q]
label_text = '%.1f' % tl[q]
ax.text(xf[0]+xrpl[q],yf[0]+yrpl[q],label_text,\
va='center', ha='center',rotation=rotate_label)
plt.show()
The example above will result in this figure:
Explanations:
You can use an artist to plot the ellipse, instead of using polar coordinates
The nomenclature is based on the definitions available on Wikipedia
The angle in the artist setup rotates the ellipse. This angle is later used to rotate coordinates for the rays and labels (this is just math)
The rays are derived from the polar form of the ellipse relative to a focus.
The angle t runs from pi to 3.0*pi because I assumed that this would correspond to your idea of where the rays should start. You get the same rays for 0 to 2.0*pi. I used np.arange instead of linspace because I wanted a defined increment (pi/5.0, or 36 degrees) in this example.
The labels at the end of the rays are placed as text, the variable offset controls the distance between the ellipse and the labels. Adjust this as needed.
For the alignment of the label text orientation with the rays, I reduced the angle, t, to the range 0 to 180 degrees. This makes for better readability compared to the full range of 0 to 360 degrees.
For the label text I just used the angle, t, for simplicity. Replace this with whatever information better suits your purpose.
The angle, t, was converted from radians to degrees before the loop that places the label. Inside the loop, each element of tl is converted to a string. This allows for more formatting control (e.g. %.3f if you needed 3 decimals)

Categories