Related
Community,
I am trying to align a point cloud with the detected floor using Open3D. So far I implemented the following steps (partly of this answer):
Detecting the floor using Open3D's plane segmentation
Translating the plane to the coordinate center
Calculating rotation angle between plane normal & z-axis
Calculating the axis of rotation
Rotating the point cloud using Open3Ds get_rotation_matrix_from_axis_angle function (see 3)
The results are OK, but I have to use an optimization-factor at the end for better results. Is there a mistake or a simpler/ more precise way for the alignment?
# See functions below
# Get the plane equation of the floor → ax+by+cz+d = 0
floor = get_floor_plane(pcd)
a, b, c, d = floor
# Translate plane to coordinate center
pcd.translate((0,-d/c,0))
# Calculate rotation angle between plane normal & z-axis
plane_normal = tuple(floor[:3])
z_axis = (0,0,1)
rotation_angle = vector_angle(plane_normal, z_axis)
# Calculate rotation axis
plane_normal_length = math.sqrt(a**2 + b**2 + c**2)
u1 = b / plane_normal_length
u2 = -a / plane_normal_length
rotation_axis = (u1, u2, 0)
# Generate axis-angle representation
optimization_factor = 1.4
axis_angle = tuple([x * rotation_angle * optimization_factor for x in rotation_axis])
# Rotate point cloud
R = pcd.get_rotation_matrix_from_axis_angle(axis_angle)
pcd.rotate(R, center=(0,0,0))
# FUNCTIONS
def vector_angle(u, v):
return np.arccos(np.dot(u,v) / (np.linalg.norm(u)* np.linalg.norm(v)))
def get_floor_plane(pcd, dist_threshold=0.02, visualize=False):
plane_model, inliers = pcd.segment_plane(distance_threshold=dist_threshold,
ransac_n=3,
num_iterations=1000)
[a, b, c, d] = plane_model
return plane_model
I'm working on a Python-based data analysis. I have some x-y data points, and some ellipses, and I want to determine whether points are inside any of the ellipses. The way that I've been doing this works, but it's kludgy. As I think about distributing my software to other people, I find myself wanting a cleaner way.
Right now, I'm using matplotlib.patches.Ellipse objects. Matplotlib Ellipses have a useful method called contains_point(). You can work in data coordinates on a Matplotlib Axes object by calling Axes.transData.transform().
The catch is that I have to create a Figure and an Axes object to hold the Ellipses. And when my program runs, an annoying Matplotlib Figure object will get rendered, showing the Ellipses, which I don't actually need to see. I have tried several methods to suppress this output. I have succeeded in deleting the Ellipses from the Axes, using Axes.clear(), resulting in an empty graph. But I can't get Matplolib's pyplot.close(fig_number) to delete the Figure itself before calling pyplot.show().
Any advice is appreciated, thanks!
Inspired by how a carpenter draws an ellipse using two nails and a piece of string, here is a numpy-friendly implementation to test whether points lie inside given ellipses.
One of the definitions of an ellipse, is that the sum of the distances to the two foci is constant, equal to the width (or height if it would be larger) of the ellipse. The distance between the center and the foci is sqrt(a*a - b*b), where a and b are half of the width and height. Using that distance and rotation by the desired angle finds the locations of the foci. numpy.linalg.norm can be used to calculate the distances using numpy's efficient array operations.
After the calculations, a plot is generated to visually check whether everything went correct.
import numpy as np
from numpy.linalg import norm # calculate the length of a vector
x = np.random.uniform(0, 40, 20000)
y = np.random.uniform(0, 20, 20000)
xy = np.dstack((x, y))
el_cent = np.array([20, 10])
el_width = 28
el_height = 17
el_angle = 20
# distance between the center and the foci
foc_dist = np.sqrt(np.abs(el_height * el_height - el_width * el_width) / 4)
# vector from center to one of the foci
foc_vect = np.array([foc_dist * np.cos(el_angle * np.pi / 180), foc_dist * np.sin(el_angle * np.pi / 180)])
# the two foci
el_foc1 = el_cent + foc_vect
el_foc2 = el_cent - foc_vect
# for each x,y: calculate z as the sum of the distances to the foci;
# np.ravel is needed to change the array of arrays (of 1 element) into a single array
z = np.ravel(norm(xy - el_foc1, axis=-1) + norm(xy - el_foc2, axis=-1) )
# points are exactly on the ellipse when the sum of distances is equal to the width
# z = np.where(z <= max(el_width, el_height), 1, 0)
# now create a plot to check whether everything makes sense
from matplotlib import pyplot as plt
from matplotlib import patches as mpatches
fig, ax = plt.subplots()
# show the foci as red dots
plt.plot(*el_foc1, 'ro')
plt.plot(*el_foc2, 'ro')
# create a filter to separate the points inside the ellipse
filter = z <= max(el_width, el_height)
# draw all the points inside the ellipse with the plasma colormap
ax.scatter(x[filter], y[filter], s=5, c=z[filter], cmap='plasma')
# draw all the points outside with the cool colormap
ax.scatter(x[~filter], y[~filter], s=5, c=z[~filter], cmap='cool')
# add the original ellipse to verify that the boundaries match
ellipse = mpatches.Ellipse(xy=el_cent, width=el_width, height=el_height, angle=el_angle,
facecolor='None', edgecolor='black', linewidth=2,
transform=ax.transData)
ax.add_patch(ellipse)
ax.set_aspect('equal', 'box')
ax.autoscale(enable=True, axis='both', tight=True)
plt.show()
The simplest solution here is to use shapely.
If you have an array of shape Nx2 containing a set of vertices (xy) then it is trivial to construct the appropriate shapely.geometry.polygon object and check if an arbitrary point or set of points (points) is contained within -
import shapely.geometry as geom
ellipse = geom.Polygon(xy)
for p in points:
if ellipse.contains(geom.Point(p)):
# ...
Alternatively, if the ellipses are defined by their parameters (i.e. rotation angle, semimajor and semiminor axis) then the array containing the vertices must be constructed and then the same process applied. I would recommend using the polar form relative to center as this is the most compatible with how shapely constructs the polygons.
import shapely.geometry as geom
from shapely import affinity
n = 360
a = 2
b = 1
angle = 45
theta = np.linspace(0, np.pi*2, n)
r = a * b / np.sqrt((b * np.cos(theta))**2 + (a * np.sin(theta))**2)
xy = np.stack([r * np.cos(theta), r * np.sin(theta)], 1)
ellipse = affinity.rotate(geom.Polygon(xy), angle, 'center')
for p in points:
if ellipse.contains(geom.Point(p)):
# ...
This method is advantageous because it supports any properly defined polygons - not just ellipses, it doesn't rely on matplotlib methods to perform the containment checking, and it produces a very readable code (which is often important when "distributing [one's] software to other people").
Here is a complete example (with added plotting to show it working)
import shapely.geometry as geom
from shapely import affinity
import matplotlib.pyplot as plt
import numpy as np
n = 360
theta = np.linspace(0, np.pi*2, n)
a = 2
b = 1
angle = 45.0
r = a * b / np.sqrt((b * np.cos(theta))**2 + (a * np.sin(theta))**2)
xy = np.stack([r * np.cos(theta), r * np.sin(theta)], 1)
ellipse = affinity.rotate(geom.Polygon(xy), angle, 'center')
x, y = ellipse.exterior.xy
# Create a Nx2 array of points at grid coordinates throughout
# the ellipse extent
rnd = np.array([[i,j] for i in np.linspace(min(x),max(x),50)
for j in np.linspace(min(y),max(y),50)])
# Filter for points which are contained in the ellipse
res = np.array([p for p in rnd if ellipse.contains(geom.Point(p))])
plt.plot(x, y, lw = 1, color='k')
plt.scatter(rnd[:,0], rnd[:,1], s = 50, color=(0.68, 0.78, 0.91)
plt.scatter(res[:,0], res[:,1], s = 15, color=(0.12, 0.67, 0.71))
plt.show()
I need to integrate over the arcs that are resulted from the intersection of a circle with a rectangle and fall inside the rectangle. I can find the intersection points using the shapely package. However, I don't know how to obtain integration intervals. For example, in the below figure my code returns [-2.1562, 2.1562] in radians (with respect to the center of the circle), while it should be able to automatically understand that the integration intervals that falls inside the rectangle are [[2.1562, 3.1415],[-3.1415, -2.1562]] (assuming pi = 3.1415).
Here is another example:
My code returns [-0.45036, -0.29576, 0.29576, 0.45036] and the expected intervals will be [[0.29576, 0.45036], [-0.45036, -0.29576]].
The code should also work for any other location that the circle is located (with any radius), whether its center is outside or inside the rectangle.
Here is my code, written using iPython:
import matplotlib.pyplot as plt
import math
import numpy as np
from shapely.geometry import LineString, MultiPoint
from shapely.geometry import Polygon
from shapely.geometry import Point
# Utilities
def cart2pol(xy, center):
x,y = xy
x_0,y_0 = center
rho = np.sqrt((x-x_0)**2 + (y-y_0)**2)
phi = np.arctan2(y-y_0, x-x_0)
return(rho, phi)
def pol2cart(rho, phi, center):
x_0,y_0 = center
x = rho * np.cos(phi)+x_0
y = rho * np.sin(phi)+y_0
return(x, y)
def distance(A,B):
return math.sqrt((A[0]-B[0])**2+(A[1]-B[1])**2)
#######################
rad = 6
center = (-1,5)
p = Point(center)
c = p.buffer(rad).boundary
A = (10,0)
B = (0,0)
C = (0,10)
D = (10,10)
coords = [Point(A), Point(B), Point(C), Point(D)]
poly = MultiPoint(coords).convex_hull
i=c.intersection(poly)
lines = [LineString([A, D]), LineString([D, C]),
LineString([C, B]), LineString([B, A])]
points = []
for l in lines:
i = c.intersection(l)
if not i.is_empty:
if i.geom_type == 'MultiPoint':
for j in range(len(i.geoms)):
points.append(i.geoms[j].coords[0])
else:
points.append(i.coords[0])
# Repeat the tangential points
for k, point in enumerate(points.copy()):
if abs(distance(center, point)**2 + distance(point, B)**2 - distance(B, center)**2) < 1e-4:
points.insert(k+1,point)
elif abs(distance(center, point)**2 + distance(point, D)**2 -distance(D, center)**2) < 1e-4:
points.insert(k+1,point)
# Sort points in polar coordinates
phis = [cart2pol(point,center)[1] for point in points]
phis.sort()
print(phis)
# Plot the shapes
x,y = c.xy
plt.plot(*c.xy)
for l in lines:
plt.plot(*l.xy, 'b')
plt.gca().set_aspect('equal', adjustable='box')
I tried to sort the intersection points according to their angle in a way that each two adjacent items in the list of intersection points corresponds to an arc. The problem is that there will be a jump in the angles from -pi to pi when rotating along the unit circle. Also I don't know how to find that whether an arc is inside the rectangle or not given its 2 end points.
Dealing with angle ranges is not straightforward.
1) select a non-ambiguous representation range, such as [-π, π) radians.
2) write a function that finds the intersections of the circle with a (h/v) half-plane and returns an angle interval. It the interval straddles the ±π border, split it in two.
3) write a function that finds the intersection between two lists of intervals (this is a modified merging problem).
4) process the four edges and intersect the resulting intervals.
5) possibly merge intervals that straddle the ±π border.
I have an irregular 3d object and want to know the surface of this object. The object can be both convex or non convex type. I can get the surface of this object applying any method like marching cube, surface contour, or isosurface.
All this methods give me triangulated mesh which is basically contains edges and vertex.
My task is to generate random and lattice points inside the object.
How should i check whether my point is inside or outside?
Any suggestion?
Thanks a lot.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from skimage import measure, io
from skimage.draw import ellipsoid
import skimage as sk
import random
I=np.zeros((50,50,50),dtype=np.float)
for i in range(50):
for j in range(50):
for k in range(50):
dist=np.linalg.norm([i,j,k]-O)
if dist<8:
I[i,j,k]=0.8#random.random()
dist=np.linalg.norm([i,j,k]-O2)
if dist<16:
I[i,j,k]=1#random.random()
verts, faces, normals, values = measure.marching_cubes_lewiner(I,0.7)
fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(111, projection='3d')
mesh = Poly3DCollection(verts[faces])
mesh.set_edgecolor('k')
ax.add_collection3d(mesh)
plt.show()
%now forget the above code and suppose i have only verts and
%faces information. Now how to generate random points inside this Data
Data=verts[faces]
???????
For random points inside the closed shape:
Select linear density of samples
Make bounding box enclosing the shape
Select entry point on the box
Select exit point, compute direction cosines (wx, wy, wz). Find all segments inside the shape along the ray
Start the ray from entry point
Get to first segment and and set it to pstart
Sample length s from exponential distribution with selected linear density
Find point pend = pstart + s (wx, wy, wz)
If it is in the first segment, store it, and make pstart = pend. Go to step 7.
If it is not, go to the start of another segment, and set it to pstart. Go to step 7. If there is no segment left, you're done with one ray, go to step 3 and generate another ray.
Generate some predefined number of rays, collect all stored points, and you're done
I am sharing the code which I have written. It might be useful for others if anybody is interested for similar kind of problem. This is not the optimize code. As grid spacing value decrease computation time increase. Also depends upon the number of triangle of mesh. Any suggestion for optimizing or improve the code is welcome. Thanks
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import numpy as np
#from mayavi import mlab
verts # numpy array of vertex (triangulated mesh)
faces # numpy array of faces (triangulated mesh)
%This function is taken from here
%https://www.erikrotteveel.com/python/three-dimensional-ray-tracing-in-python/
def ray_intersect_triangle(p0, p1, triangle):
# Tests if a ray starting at point p0, in the direction
# p1 - p0, will intersect with the triangle.
#
# arguments:
# p0, p1: numpy.ndarray, both with shape (3,) for x, y, z.
# triangle: numpy.ndarray, shaped (3,3), with each row
# representing a vertex and three columns for x, y, z.
#
# returns:
# 0.0 if ray does not intersect triangle,
# 1.0 if it will intersect the triangle,
# 2.0 if starting point lies in the triangle.
v0, v1, v2 = triangle
u = v1 - v0
v = v2 - v0
normal = np.cross(u, v)
b = np.inner(normal, p1 - p0)
a = np.inner(normal, v0 - p0)
# Here is the main difference with the code in the link.
# Instead of returning if the ray is in the plane of the
# triangle, we set rI, the parameter at which the ray
# intersects the plane of the triangle, to zero so that
# we can later check if the starting point of the ray
# lies on the triangle. This is important for checking
# if a point is inside a polygon or not.
if (b == 0.0):
# ray is parallel to the plane
if a != 0.0:
# ray is outside but parallel to the plane
return 0
else:
# ray is parallel and lies in the plane
rI = 0.0
else:
rI = a / b
if rI < 0.0:
return 0
w = p0 + rI * (p1 - p0) - v0
denom = np.inner(u, v) * np.inner(u, v) - \
np.inner(u, u) * np.inner(v, v)
si = (np.inner(u, v) * np.inner(w, v) - \
np.inner(v, v) * np.inner(w, u)) / denom
if (si < 0.0) | (si > 1.0):
return 0
ti = (np.inner(u, v) * np.inner(w, u) - \
np.inner(u, u) * np.inner(w, v)) / denom
if (ti < 0.0) | (si + ti > 1.0):
return 0
if (rI == 0.0):
# point 0 lies ON the triangle. If checking for
# point inside polygon, return 2 so that the loop
# over triangles can stop, because it is on the
# polygon, thus inside.
return 2
return 1
def bounding_box_of_mesh(triangle):
return [np.min(triangle[:,0]), np.max(triangle[:,0]), np.min(triangle[:,1]), np.max(triangle[:,1]), np.min(triangle[:,2]), np.max(triangle[:,2])]
def boundingboxoftriangle(triangle,x,y,z):
localbox= [np.min(triangle[:,0]), np.max(triangle[:,0]), np.min(triangle[:,1]), np.max(triangle[:,1]), np.min(triangle[:,2]), np.max(triangle[:,2])]
#print 'local', localbox
for i in range(1,len(x)):
if (x[i-1] <= localbox[0] < x[i]):
x_min=i-1
if (x[i-1] < localbox[1] <= x[i]):
x_max=i
for i in range(1,len(y)):
if (y[i-1] <= localbox[2] < y[i]):
y_min=i-1
if (y[i-1] < localbox[3] <= y[i]):
y_max=i
for i in range(1,len(z)):
if (z[i-1] <= localbox[4] < z[i]):
z_min=i-1
if (z[i-1] < localbox[5] <= z[i]):
z_max=i
return [x_min, x_max, y_min, y_max, z_min, z_max]
spacing=5 # grid spacing
boundary=bounding_box_of_mesh(verts)
print boundary
x=np.arange(boundary[0]-2*spacing,boundary[1]+2*spacing,spacing)
y=np.arange(boundary[2]-2*spacing,boundary[3]+2*spacing,spacing)
z=np.arange(boundary[4]-2*spacing,boundary[5]+2*spacing,spacing)
Grid=np.zeros((len(x),len(y),len(z)),dtype=np.int)
print Grid.shape
data=verts[faces]
xarr=[]
yarr=[]
zarr=[]
# actual number of grid is very high so checking every grid is
# inside or outside is inefficient. So, I am looking for only
# those grid which is near to mesh boundary. This will reduce
#the time and later on internal grid can be interpolate easily.
for i in range(len(data)):
#print '\n', data[i]
AABB=boundingboxoftriangle(data[i],x,y,z) ## axis aligned bounding box
#print AABB
for gx in range(AABB[0],AABB[1]+1):
if gx not in xarr:
xarr.append(gx)
for gy in range(AABB[2],AABB[3]+1):
if gy not in yarr:
yarr.append(gy)
for gz in range(AABB[4],AABB[5]+1):
if gz not in zarr:
zarr.append(gz)
print len(xarr),len(yarr),len(zarr)
center=np.array([np.mean(verts[:,0]), np.mean(verts[:,1]), np.mean(verts[:,2])])
print center
fw=open('Grid_value_output_spacing__.dat','w')
p1=center #np.array([0,0,0])
for i in range(len(xarr)):
for j in range(len(yarr)):
for k in range(len(zarr)):
p0=np.array([x[xarr[i]],y[yarr[j]],z[zarr[k]]])
for go in range(len(data)):
value=ray_intersect_triangle(p0, p1, data[go])
if value>0:
Grid[i,j,k]=value
break
fw.write(str(xarr[i])+'\t'+str(yarr[j])+'\t'+str(zarr[k])+'\t'+str(x[xarr[i]])+'\t'+str(y[yarr[j]])+'\t'+str(z[zarr[k]])+'\t'+str(Grid[i,j,k])+'\n')
print i
fw.close()
#If the grid value is greater than 0 then it is inside the triangulated mesh.
#I am writing the value of only confusing grid near boundary.
#Deeper inside grid of mesh can be interpolate easily with above information.
#If grid spacing is very small then generating random points inside the
#mesh is equivalent to choosing the random grid.
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)