Related
The problem - given a list of planar points [p_1, ..., p_n] and the dimensions of some rectangle w, h, find the minimal set of rectangles w, h that cover all points (edit - the rectangles are not rotated).
My inital solution was:
find the bounding-box of all points
divide the width and height of the bounding-box by the w, h of the given rectangle and round the number up to get the number of instances of the rectangle in x and y
to further optimize, go through all rectangles and delete the ones that have zero points inside them.
An example in Python:
def tile_rect(points, rect):
w, h = rect
xs = [p.x for p in points]
ys = [p.y for p in points]
bbox_w = abs(max(xs) - min(xs))
bbox_h = abs(max(ys) - min(ys))
n_x, n_y = ceil(bbox_w / w), ceil(bbox_h / h)
rect_xs = [(min(xs) + n * w for n in range(n_x)]
rect_ys = [(min(ys) + n * h for n in range(n_y)]
rects = remove_empty(rect_xs, rect_ys)
return rects
How can I do better? What algorithm can I use to decrease the number of rectangles?
To discretize the problem for integer programming, observe that given a rectangle we can slide it in the +x and +y directions without decreasing the coverage until the min x and the min y lines both have a point on them. Thus the integer program is just the standard min cover:
minimize sum_R x_R
subject to
for every point p, sum_{R contains p} x_R >= 1
x_R in {0, 1}
where R ranges over all rectangles whose min x is the x of some point and whose min y is the y of some point (not necessarily the same point).
Demo Python:
import random
from ortools.linear_solver import pywraplp
w = 0.1
h = 0.1
points = [(random.random(), random.random()) for _ in range(100)]
rectangles = [(x, y) for (x, _) in points for (_, y) in points]
solver = pywraplp.Solver.CreateSolver("min cover", "SCIP")
objective = solver.Objective()
constraints = [solver.RowConstraint(1, pywraplp.inf, str(p)) for p in points]
variables = [solver.BoolVar(str(r)) for r in rectangles]
for (x, y), var in zip(rectangles, variables):
objective.SetCoefficient(var, 1)
for (px, py), con in zip(points, constraints):
if x <= px <= x + w and y <= py <= y + h:
con.SetCoefficient(var, 1)
solver.Objective().SetMinimization()
solver.Solve()
scale = 6 * 72
margin = 72
print(
'<svg width="{}" height="{}">'.format(
margin + scale + margin, margin + scale + margin
)
)
print(
'<text x="{}" y="{}">{} rectangles</text>'.format(
margin // 2, margin // 2, round(objective.Value())
)
)
for x, y in points:
print(
'<circle cx="{}" cy="{}" r="3" fill="none" stroke="black"/>'.format(
margin + x * scale, margin + y * scale
)
)
for (x, y), var in zip(rectangles, variables):
if var.solution_value():
print(
'<rect x="{}" y="{}" width="{}" height="{}" fill="none" stroke="rgb({},{},{})"/>'.format(
margin + x * scale,
margin + y * scale,
w * scale,
h * scale,
random.randrange(192),
random.randrange(192),
random.randrange(192),
)
)
print("</svg>")
Example output:
Assuming an approximate, rather than optimal solution is acceptable, how about a routine generally like:
Until no points are left:
(1) Find the convex hull of the remaining points.
(2) Cover each point/s on the hull so the
covering rectangles extend "inward."
(Perhaps examine neighbouring hull points
to see if a rectangle can cover more than one.)
(3) Remove the newly covered points.
Clearly, the orientation of the covering rectangles has an effect on the procedure and result. I think there is a way to combine (1) and (3), or possibly rely on a nested convex hull, but I don't have too much experience with those.
This is can be transformed into a mostly standard set cover problem. The general steps are as follows, given n points in the plane.
First, generate all possible maximally inclusive rectangles, of which there are at most n^2, named R. The key insight is that given a point p1 with coordinates (x1, y1), use x1 as the leftmost bound for a set of rectangles. For all other points p2 with (x2,y2) where x1 <= x2 <= x1+w and where y1-h <= y2 <= y1+h, generate a rectangle ((x1, y2), (x1+w, y2+h)).
For each rectangle r generated, count the points included in that rectangle cover(r).
Choose a subset of the rectangles R, s, such that all points are in Union(r in s) cover(r)
Now, the tricky part is that last step. Fortunately, it is a standard problem and there are many algorithms suggested in the literature. For example, combinatorial optimization solvers (such as SAT solvers, MIP solvers, and Constraint programming solvers) can be used.
Note that the above re-formulation only works if it is ok for rectangles to cover each other. It might be the case that the generated set of rectangles is not enough to find the least set of rectangles that do not overlap.
A spiral is a flat curve that rotates around a central axis.
I have a drawing of a parkinsonian patient that is based on a spiral.
As you can see, this image of the patient's drawing oscillates around the base spiral. What I would like to do is the following: "unroll" the spiral so that both the oscillation of the drawing and the spiral itself is based on a straight line, that is, to linearize the spiral. How can I do this?
Here is a possible approach in two parts.
The first part tries to align a spiral with the image. The simplest spiral is an Archimedian spiral where the radius and the angle are linearly coupled. By plotting and looking at the coordinates, the limits for an approximate scale for the x and the y directions are found. The result isn't perfect. Maybe the given image isn't nicely scanned, but just a photo giving rise to deformations or the original spiral wasn't a perfect Archimedian spiral. (Also, a png file would be strongly preferred instead of the given jpg). Anyway, the scale is good enough to give an idea how the algorithm would work, preferably starting from an exact scan.
The next part goes through each pixel of the image and finds it corresponding angle and distance to the center (using the scaling found in the first part). The next step is to find how many times the angle has gone around (in multiples of 2 pi), choosing the closest match. Subtracting the radius from the angle would straighten the spiral.
Some code to illustrate the idea.
import numpy as np
from matplotlib import pyplot as plt
import imageio
fig, ax = plt.subplots()
img = imageio.imread('spiral_follow.jpg')
# the image extent is set using trial and error to align with the spiral equation;
# the center of the spiral should end up with coordinates 0,0
x0, x1, y0, y1 = extent = [-17.8, 16, 13, -16.8]
ax.imshow(img, extent=extent)
# t=17.4 is about where the spiral ends; the exact value is not important
t = np.linspace(0, 17.4, 1000)
r = t
theta = t
sx = r * np.sin(theta)
sy = r * np.cos(theta)
ax.plot(sx, sy, c='lime', lw=6, alpha=0.4) # plot the spiral over the image
# r_p and theta_p are the polar coordinates of the white pixels
r_p = []
theta_p = []
for i in range(img.shape[0]):
for j in range(img.shape[1]):
if img[i,j] > 127: # the black pixels are 0, the white are 255; 127 is the middle
# transform from pixel coordinates (i,j) to the coordinatets of the spiral (x,y)
x = x0 + j * (x1 - x0) / (img.shape[1] - 1)
y = y1 + i * (y0 - y1) / (img.shape[0] - 1)
# transform from carthesian (x,y) to polar coordinates (theta,r)
theta = np.arctan2(x, y)
r = np.sqrt(x*x+y*y)
# the following code tries to find out how many times 2pi should be added to theta
# in order to correspond best to r
k = np.round((r - theta) / (2 * np.pi))
nearest_theta = theta + 2 * k * np.pi
theta_p.append(nearest_theta)
r_p.append(nearest_theta - r)
plt.show()
fig, ax = plt.subplots()
ax.set_facecolor('black')
ax.scatter(theta_p, r_p, s=1, lw=0, color='white')
plt.show()
The aligned spiral:
The straightened spiral:
I have several data points in 3 dimensional space (x, y, z) and have interpolated them using scipy.interpolate.Rbf. This gives me a spline nicely representing the surface of my 3D object. I would now like to determine several x and y pairs that have the same, arbitrary z value. I would like to do that in order to compute the cross section of my 3D object at any given value of z. Does someone know how to do that? Maybe there is also a better way to do that instead of using scipy.interpolate.Rbf.
Up to now I have evaluated the cross sections by making a contour plot using matplotlib.pyplot and extracting the displayed segments. 3D points and interpolated spline
segments extracted using a contour plot
I was able to solve the problem. I have calculated the area by triangulating the x-y data and cutting the triangles with the z-plane I wanted to calculate the cross-sectional area of (z=z0). Specifically, I have searched for those triangles whose z-values are both above and below z0. Then I have calculated the x and y values of the sides of these triangles where the sides are equal to z0. Then I use scipy.spatial.ConvexHull to sort the intersected points. Using the shoelace formula I can then determine the area.
I have attached the example code here:
import numpy as np
from scipy import spatial
import matplotlib.pyplot as plt
# Generation of random test data
n = 500
x = np.random.random(n)
y = np.random.random(n)
z = np.exp(-2*(x-.5)**2-4*(y-.5)**2)
z0 = .75
# Triangulation of the test data
triang= spatial.Delaunay(np.array([x, y]).T)
# Determine all triangles where not all points are above or below z0, i.e. the triangles that intersect z0
tri_inter=np.zeros_like(triang.simplices, dtype=np.int) # The triangles which intersect the plane at z0, filled below
i = 0
for tri in triang.simplices:
if ~np.all(z[tri] > z0) and ~np.all(z[tri] < z0):
tri_inter[i,:] = tri
i += 1
tri_inter = tri_inter[~np.all(tri_inter==0, axis=1)] # Remove all rows with only 0
# The number of interpolated values for x and y has twice the length of the triangles
# Because each triangle intersects the plane at z0 twice
x_inter = np.zeros(tri_inter.shape[0]*2)
y_inter = np.zeros(tri_inter.shape[0]*2)
for j, tri in enumerate(tri_inter):
# Determine which of the three points are above and which are below z0
points_above = []
points_below = []
for i in tri:
if z[i] > z0:
points_above.append(i)
else:
points_below.append(i)
# Calculate the intersections and put the values into x_inter and y_inter
t = (z0-z[points_below[0]])/(z[points_above[0]]-z[points_below[0]])
x_new = t * (x[points_above[0]]-x[points_below[0]]) + x[points_below[0]]
y_new = t * (y[points_above[0]]-y[points_below[0]]) + y[points_below[0]]
x_inter[j*2] = x_new
y_inter[j*2] = y_new
if len(points_above) > len(points_below):
t = (z0-z[points_below[0]])/(z[points_above[1]]-z[points_below[0]])
x_new = t * (x[points_above[1]]-x[points_below[0]]) + x[points_below[0]]
y_new = t * (y[points_above[1]]-y[points_below[0]]) + y[points_below[0]]
else:
t = (z0-z[points_below[1]])/(z[points_above[0]]-z[points_below[1]])
x_new = t * (x[points_above[0]]-x[points_below[1]]) + x[points_below[1]]
y_new = t * (y[points_above[0]]-y[points_below[1]]) + y[points_below[1]]
x_inter[j*2+1] = x_new
y_inter[j*2+1] = y_new
# sort points to calculate area
hull = spatial.ConvexHull(np.array([x_inter, y_inter]).T)
x_hull, y_hull = x_inter[hull.vertices], y_inter[hull.vertices]
# Calculation of are using the shoelace formula
area = 0.5*np.abs(np.dot(x_hull,np.roll(y_hull,1))-np.dot(y_hull,np.roll(x_hull,1)))
print('Area:', area)
plt.figure()
plt.plot(x_inter, y_inter, 'ro')
plt.plot(x_hull, y_hull, 'b--')
plt.triplot(x, y, triangles=tri_inter, color='k')
plt.show()
I'm trying to work out how best to locate the centroid of an arbitrary shape draped over a unit sphere, with the input being ordered (clockwise or anti-cw) vertices for the shape boundary. The density of vertices is irregular along the boundary, so the arc-lengths between them are not generally equal. Because the shapes may be very large (half a hemisphere) it is generally not possible to simply project the vertices to a plane and use planar methods, as detailed on Wikipedia (sorry I'm not allowed more than 2 hyperlinks as a newcomer). A slightly better approach involves the use of planar geometry manipulated in spherical coordinates, but again, with large polygons this method fails, as nicely illustrated here. On that same page, 'Cffk' highlighted this paper which describes a method for calculating the centroid of spherical triangles. I've tried to implement this method, but without success, and I'm hoping someone can spot the problem?
I have kept the variable definitions similar to those in the paper to make it easier to compare. The input (data) is a list of longitude/latitude coordinates, converted to [x,y,z] coordinates by the code. For each of the triangles I have arbitrarily fixed one point to be the +z-pole, the other two vertices being composed of a pair of neighboring points along the polygon boundary. The code steps along the boundary (starting at an arbitrary point), using each boundary segment of the polygon as a triangle side in turn. A sub-centroid is determined for each of these individual spherical triangles and they are weighted according to triangle area and added to calculate the total polygon centroid. I don't get any errors when running the code, but the total centroids returned are clearly wrong (I have run some very basic shapes where the centroid location is unambiguous). I haven't found any sensible pattern in the location of the centroids returned...so at the moment I'm not sure what is going wrong, either in the math or code (although, the suspicion is the math).
The code below should work copy-paste as is if you would like to try it. If you have matplotlib and numpy installed, it will plot the results (it will ignore plotting if you don't). You just have to put the longitude/latitude data below the code into a text file called example.txt.
from math import *
try:
import matplotlib as mpl
import matplotlib.pyplot
from mpl_toolkits.mplot3d import Axes3D
import numpy
plotting_enabled = True
except ImportError:
plotting_enabled = False
def sph_car(point):
if len(point) == 2:
point.append(1.0)
rlon = radians(float(point[0]))
rlat = radians(float(point[1]))
x = cos(rlat) * cos(rlon) * point[2]
y = cos(rlat) * sin(rlon) * point[2]
z = sin(rlat) * point[2]
return [x, y, z]
def xprod(v1, v2):
x = v1[1] * v2[2] - v1[2] * v2[1]
y = v1[2] * v2[0] - v1[0] * v2[2]
z = v1[0] * v2[1] - v1[1] * v2[0]
return [x, y, z]
def dprod(v1, v2):
dot = 0
for i in range(3):
dot += v1[i] * v2[i]
return dot
def plot(poly_xyz, g_xyz):
fig = mpl.pyplot.figure()
ax = fig.add_subplot(111, projection='3d')
# plot the unit sphere
u = numpy.linspace(0, 2 * numpy.pi, 100)
v = numpy.linspace(-1 * numpy.pi / 2, numpy.pi / 2, 100)
x = numpy.outer(numpy.cos(u), numpy.sin(v))
y = numpy.outer(numpy.sin(u), numpy.sin(v))
z = numpy.outer(numpy.ones(numpy.size(u)), numpy.cos(v))
ax.plot_surface(x, y, z, rstride=4, cstride=4, color='w', linewidth=0,
alpha=0.3)
# plot 3d and flattened polygon
x, y, z = zip(*poly_xyz)
ax.plot(x, y, z)
ax.plot(x, y, zs=0)
# plot the alleged 3d and flattened centroid
x, y, z = g_xyz
ax.scatter(x, y, z, c='r')
ax.scatter(x, y, 0, c='r')
# display
ax.set_xlim3d(-1, 1)
ax.set_ylim3d(-1, 1)
ax.set_zlim3d(0, 1)
mpl.pyplot.show()
lons, lats, v = list(), list(), list()
# put the two-column data at the bottom of the question into a file called
# example.txt in the same directory as this script
with open('example.txt') as f:
for line in f.readlines():
sep = line.split()
lons.append(float(sep[0]))
lats.append(float(sep[1]))
# convert spherical coordinates to cartesian
for lon, lat in zip(lons, lats):
v.append(sph_car([lon, lat, 1.0]))
# z unit vector/pole ('north pole'). This is an arbitrary point selected to act as one
#(fixed) vertex of the summed spherical triangles. The other two vertices of any
#triangle are composed of neighboring vertices from the polygon boundary.
np = [0.0, 0.0, 1.0]
# Gx,Gy,Gz are the cartesian coordinates of the calculated centroid
Gx, Gy, Gz = 0.0, 0.0, 0.0
for i in range(-1, len(v) - 1):
# cycle through the boundary vertices of the polygon, from 0 to n
if all((v[i][0] != v[i+1][0],
v[i][1] != v[i+1][1],
v[i][2] != v[i+1][2])):
# this just ignores redundant points which are common in my larger input files
# A,B,C are the internal angles in the triangle: 'np-v[i]-v[i+1]-np'
A = asin(sqrt((dprod(np, xprod(v[i], v[i+1])))**2
/ ((1 - (dprod(v[i+1], np))**2) * (1 - (dprod(np, v[i]))**2))))
B = asin(sqrt((dprod(v[i], xprod(v[i+1], np)))**2
/ ((1 - (dprod(np , v[i]))**2) * (1 - (dprod(v[i], v[i+1]))**2))))
C = asin(sqrt((dprod(v[i + 1], xprod(np, v[i])))**2
/ ((1 - (dprod(v[i], v[i+1]))**2) * (1 - (dprod(v[i+1], np))**2))))
# A/B/Cbar are the vertex angles, such that if 'O' is the sphere center, Abar
# is the angle (v[i]-O-v[i+1])
Abar = acos(dprod(v[i], v[i+1]))
Bbar = acos(dprod(v[i+1], np))
Cbar = acos(dprod(np, v[i]))
# e is the 'spherical excess', as defined on wikipedia
e = A + B + C - pi
# mag1/2/3 are the magnitudes of vectors np,v[i] and v[i+1].
mag1 = 1.0
mag2 = float(sqrt(v[i][0]**2 + v[i][1]**2 + v[i][2]**2))
mag3 = float(sqrt(v[i+1][0]**2 + v[i+1][1]**2 + v[i+1][2]**2))
# vec1/2/3 are cross products, defined here to simplify the equation below.
vec1 = xprod(np, v[i])
vec2 = xprod(v[i], v[i+1])
vec3 = xprod(v[i+1], np)
# multiplying vec1/2/3 by e and respective internal angles, according to the
#posted paper
for x in range(3):
vec1[x] *= Cbar / (2 * e * mag1 * mag2
* sqrt(1 - (dprod(np, v[i])**2)))
vec2[x] *= Abar / (2 * e * mag2 * mag3
* sqrt(1 - (dprod(v[i], v[i+1])**2)))
vec3[x] *= Bbar / (2 * e * mag3 * mag1
* sqrt(1 - (dprod(v[i+1], np)**2)))
Gx += vec1[0] + vec2[0] + vec3[0]
Gy += vec1[1] + vec2[1] + vec3[1]
Gz += vec1[2] + vec2[2] + vec3[2]
approx_expected_Gxyz = (0.78, -0.56, 0.27)
print('Approximate Expected Gxyz: {0}\n'
' Actual Gxyz: {1}'
''.format(approx_expected_Gxyz, (Gx, Gy, Gz)))
if plotting_enabled:
plot(v, (Gx, Gy, Gz))
Thanks in advance for any suggestions or insight.
EDIT: Here is a figure that shows a projection of the unit sphere with a polygon and the resulting centroid I calculate from the code. Clearly, the centroid is wrong as the polygon is rather small and convex but yet the centroid falls outside its perimeter.
EDIT: Here is a highly-similar set of coordinates to those above, but in the original [lon,lat] format I normally use (which is now converted to [x,y,z] by the updated code).
-39.366295 -1.633460
-47.282630 -0.740433
-53.912136 0.741380
-59.004217 2.759183
-63.489005 5.426812
-68.566001 8.712068
-71.394853 11.659135
-66.629580 15.362600
-67.632276 16.827507
-66.459524 19.069327
-63.819523 21.446736
-61.672712 23.532143
-57.538431 25.947815
-52.519889 28.691766
-48.606227 30.646295
-45.000447 31.089437
-41.549866 32.139873
-36.605156 32.956277
-32.010080 34.156692
-29.730629 33.756566
-26.158767 33.714080
-25.821513 34.179648
-23.614658 36.173719
-20.896869 36.977645
-17.991994 35.600074
-13.375742 32.581447
-9.554027 28.675497
-7.825604 26.535234
-7.825604 26.535234
-9.094304 23.363132
-9.564002 22.527385
-9.713885 22.217165
-9.948596 20.367878
-10.496531 16.486580
-11.151919 12.666850
-12.350144 8.800367
-15.446347 4.993373
-20.366139 1.132118
-24.784805 -0.927448
-31.532135 -1.910227
-39.366295 -1.633460
EDIT: A couple more examples...with 4 vertices defining a perfect square centered at [1,0,0] I get the expected result:
However, from a non-symmetric triangle I get a centroid that is nowhere close...the centroid actually falls on the far side of the sphere (here projected onto the front side as the antipode):
Interestingly, the centroid estimation appears 'stable' in the sense that if I invert the list (go from clockwise to counterclockwise order or vice-versa) the centroid correspondingly inverts exactly.
Anybody finding this, make sure to check Don Hatch's answer which is probably better.
I think this will do it. You should be able to reproduce this result by just copy-pasting the code below.
You will need to have the latitude and longitude data in a file called longitude and latitude.txt. You can copy-paste the original sample data which is included below the code.
If you have mplotlib it will additionally produce the plot below
For non-obvious calculations, I included a link that explains what is going on
In the graph below, the reference vector is very short (r = 1/10) so that the 3d-centroids are easier to see. You can easily remove the scaling to maximize accuracy.
Note to op: I rewrote almost everything so I'm not sure exactly where the original code was not working. However, at least I think it was not taking into consideration the need to handle clockwise / counterclockwise triangle vertices.
Legend:
(black line) reference vector
(small red dots) spherical triangle 3d-centroids
(large red / blue / green dot) 3d-centroid / projected to the surface / projected to the xy plane
(blue / green lines) the spherical polygon and the projection onto the xy plane
from math import *
try:
import matplotlib as mpl
import matplotlib.pyplot
from mpl_toolkits.mplot3d import Axes3D
import numpy
plotting_enabled = True
except ImportError:
plotting_enabled = False
def main():
# get base polygon data based on unit sphere
r = 1.0
polygon = get_cartesian_polygon_data(r)
point_count = len(polygon)
reference = ok_reference_for_polygon(polygon)
# decompose the polygon into triangles and record each area and 3d centroid
areas, subcentroids = list(), list()
for ia, a in enumerate(polygon):
# build an a-b-c point set
ib = (ia + 1) % point_count
b, c = polygon[ib], reference
if points_are_equivalent(a, b, 0.001):
continue # skip nearly identical points
# store the area and 3d centroid
areas.append(area_of_spherical_triangle(r, a, b, c))
tx, ty, tz = zip(a, b, c)
subcentroids.append((sum(tx)/3.0,
sum(ty)/3.0,
sum(tz)/3.0))
# combine all the centroids, weighted by their areas
total_area = sum(areas)
subxs, subys, subzs = zip(*subcentroids)
_3d_centroid = (sum(a*subx for a, subx in zip(areas, subxs))/total_area,
sum(a*suby for a, suby in zip(areas, subys))/total_area,
sum(a*subz for a, subz in zip(areas, subzs))/total_area)
# shift the final centroid to the surface
surface_centroid = scale_v(1.0 / mag(_3d_centroid), _3d_centroid)
plot(polygon, reference, _3d_centroid, surface_centroid, subcentroids)
def get_cartesian_polygon_data(fixed_radius):
cartesians = list()
with open('longitude and latitude.txt') as f:
for line in f.readlines():
spherical_point = [float(v) for v in line.split()]
if len(spherical_point) == 2:
spherical_point.append(fixed_radius)
cartesians.append(degree_spherical_to_cartesian(spherical_point))
return cartesians
def ok_reference_for_polygon(polygon):
point_count = len(polygon)
# fix the average of all vectors to minimize float skew
polyx, polyy, polyz = zip(*polygon)
# /10 is for visualization. Remove it to maximize accuracy
return (sum(polyx)/(point_count*10.0),
sum(polyy)/(point_count*10.0),
sum(polyz)/(point_count*10.0))
def points_are_equivalent(a, b, vague_tolerance):
# vague tolerance is something like a percentage tolerance (1% = 0.01)
(ax, ay, az), (bx, by, bz) = a, b
return all(((ax-bx)/ax < vague_tolerance,
(ay-by)/ay < vague_tolerance,
(az-bz)/az < vague_tolerance))
def degree_spherical_to_cartesian(point):
rad_lon, rad_lat, r = radians(point[0]), radians(point[1]), point[2]
x = r * cos(rad_lat) * cos(rad_lon)
y = r * cos(rad_lat) * sin(rad_lon)
z = r * sin(rad_lat)
return x, y, z
def area_of_spherical_triangle(r, a, b, c):
# points abc
# build an angle set: A(CAB), B(ABC), C(BCA)
# http://math.stackexchange.com/a/66731/25581
A, B, C = surface_points_to_surface_radians(a, b, c)
E = A + B + C - pi # E is called the spherical excess
area = r**2 * E
# add or subtract area based on clockwise-ness of a-b-c
# http://stackoverflow.com/a/10032657/377366
if clockwise_or_counter(a, b, c) == 'counter':
area *= -1.0
return area
def surface_points_to_surface_radians(a, b, c):
"""build an angle set: A(cab), B(abc), C(bca)"""
points = a, b, c
angles = list()
for i, mid in enumerate(points):
start, end = points[(i - 1) % 3], points[(i + 1) % 3]
x_startmid, x_endmid = xprod(start, mid), xprod(end, mid)
ratio = (dprod(x_startmid, x_endmid)
/ ((mag(x_startmid) * mag(x_endmid))))
angles.append(acos(ratio))
return angles
def clockwise_or_counter(a, b, c):
ab = diff_cartesians(b, a)
bc = diff_cartesians(c, b)
x = xprod(ab, bc)
if x < 0:
return 'clockwise'
elif x > 0:
return 'counter'
else:
raise RuntimeError('The reference point is in the polygon.')
def diff_cartesians(positive, negative):
return tuple(p - n for p, n in zip(positive, negative))
def xprod(v1, v2):
x = v1[1] * v2[2] - v1[2] * v2[1]
y = v1[2] * v2[0] - v1[0] * v2[2]
z = v1[0] * v2[1] - v1[1] * v2[0]
return [x, y, z]
def dprod(v1, v2):
dot = 0
for i in range(3):
dot += v1[i] * v2[i]
return dot
def mag(v1):
return sqrt(v1[0]**2 + v1[1]**2 + v1[2]**2)
def scale_v(scalar, v):
return tuple(scalar * vi for vi in v)
def plot(polygon, reference, _3d_centroid, surface_centroid, subcentroids):
fig = mpl.pyplot.figure()
ax = fig.add_subplot(111, projection='3d')
# plot the unit sphere
u = numpy.linspace(0, 2 * numpy.pi, 100)
v = numpy.linspace(-1 * numpy.pi / 2, numpy.pi / 2, 100)
x = numpy.outer(numpy.cos(u), numpy.sin(v))
y = numpy.outer(numpy.sin(u), numpy.sin(v))
z = numpy.outer(numpy.ones(numpy.size(u)), numpy.cos(v))
ax.plot_surface(x, y, z, rstride=4, cstride=4, color='w', linewidth=0,
alpha=0.3)
# plot 3d and flattened polygon
x, y, z = zip(*polygon)
ax.plot(x, y, z, c='b')
ax.plot(x, y, zs=0, c='g')
# plot the 3d centroid
x, y, z = _3d_centroid
ax.scatter(x, y, z, c='r', s=20)
# plot the spherical surface centroid and flattened centroid
x, y, z = surface_centroid
ax.scatter(x, y, z, c='b', s=20)
ax.scatter(x, y, 0, c='g', s=20)
# plot the full set of triangular centroids
x, y, z = zip(*subcentroids)
ax.scatter(x, y, z, c='r', s=4)
# plot the reference vector used to findsub centroids
x, y, z = reference
ax.plot((0, x), (0, y), (0, z), c='k')
ax.scatter(x, y, z, c='k', marker='^')
# display
ax.set_xlim3d(-1, 1)
ax.set_ylim3d(-1, 1)
ax.set_zlim3d(0, 1)
mpl.pyplot.show()
# run it in a function so the main code can appear at the top
main()
Here is the longitude and latitude data you can paste into longitude and latitude.txt
-39.366295 -1.633460
-47.282630 -0.740433
-53.912136 0.741380
-59.004217 2.759183
-63.489005 5.426812
-68.566001 8.712068
-71.394853 11.659135
-66.629580 15.362600
-67.632276 16.827507
-66.459524 19.069327
-63.819523 21.446736
-61.672712 23.532143
-57.538431 25.947815
-52.519889 28.691766
-48.606227 30.646295
-45.000447 31.089437
-41.549866 32.139873
-36.605156 32.956277
-32.010080 34.156692
-29.730629 33.756566
-26.158767 33.714080
-25.821513 34.179648
-23.614658 36.173719
-20.896869 36.977645
-17.991994 35.600074
-13.375742 32.581447
-9.554027 28.675497
-7.825604 26.535234
-7.825604 26.535234
-9.094304 23.363132
-9.564002 22.527385
-9.713885 22.217165
-9.948596 20.367878
-10.496531 16.486580
-11.151919 12.666850
-12.350144 8.800367
-15.446347 4.993373
-20.366139 1.132118
-24.784805 -0.927448
-31.532135 -1.910227
-39.366295 -1.633460
To clarify: the quantity of interest is the projection of the true 3d centroid
(i.e. 3d center-of-mass, i.e. 3d center-of-area) onto the unit sphere.
Since all you care about is the direction from the origin to the 3d centroid,
you don't need to bother with areas at all;
it's easier to just compute the moment (i.e. 3d centroid times area).
The moment of the region to the left of a closed path on the unit sphere
is half the integral of the leftward unit vector as you walk around the path.
This follows from a non-obvious application of Stokes' theorem; see Frank Jones's vector calculus book, chapter 13 Problem 13-12.
In particular, for a spherical polygon, the moment is half the sum of
(a x b) / ||a x b|| * (angle between a and b) for each pair of consecutive vertices a,b.
(That's for the region to the left of the path;
negate it for the region to the right of the path.)
(And if you really did want the 3d centroid, just compute the area and divide the moment by it. Comparing areas might also be useful in choosing which of the two regions to call "the polygon".)
Here's some code; it's really simple:
#!/usr/bin/python
import math
def plus(a,b): return [x+y for x,y in zip(a,b)]
def minus(a,b): return [x-y for x,y in zip(a,b)]
def cross(a,b): return [a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]]
def dot(a,b): return sum([x*y for x,y in zip(a,b)])
def length(v): return math.sqrt(dot(v,v))
def normalized(v): l = length(v); return [1,0,0] if l==0 else [x/l for x in v]
def addVectorTimesScalar(accumulator, vector, scalar):
for i in xrange(len(accumulator)): accumulator[i] += vector[i] * scalar
def angleBetweenUnitVectors(a,b):
# https://www.plunk.org/~hatch/rightway.html
if dot(a,b) < 0:
return math.pi - 2*math.asin(length(plus(a,b))/2.)
else:
return 2*math.asin(length(minus(a,b))/2.)
def sphericalPolygonMoment(verts):
moment = [0.,0.,0.]
for i in xrange(len(verts)):
a = verts[i]
b = verts[(i+1)%len(verts)]
addVectorTimesScalar(moment, normalized(cross(a,b)),
angleBetweenUnitVectors(a,b) / 2.)
return moment
if __name__ == '__main__':
import sys
def lonlat_degrees_to_xyz(lon_degrees,lat_degrees):
lon = lon_degrees*(math.pi/180)
lat = lat_degrees*(math.pi/180)
coslat = math.cos(lat)
return [coslat*math.cos(lon), coslat*math.sin(lon), math.sin(lat)]
verts = [lonlat_degrees_to_xyz(*[float(v) for v in line.split()])
for line in sys.stdin.readlines()]
#print "verts = "+`verts`
moment = sphericalPolygonMoment(verts)
print "moment = "+`moment`
print "centroid unit direction = "+`normalized(moment)`
For the example polygon, this gives the answer (unit vector):
[-0.7644875430808217, 0.579935445918147, -0.2814847687566214]
This is roughly the same as, but more accurate than, the answer computed by #KobeJohn's code, which uses rough tolerances and planar approximations to the sub-centroids:
[0.7628095787179151, -0.5977153368303585, 0.24669398601094406]
The directions of the two answers are roughly opposite (so I guess KobeJohn's code
decided to take the region to the right of the path in this case).
I think a good approximation would be to compute the center of mass using weighted cartesian coordinates and projecting the result onto the sphere (supposing the origin of coordinates is (0, 0, 0)^T).
Let be (p[0], p[1], ... p[n-1]) the n points of the polygon. The approximative (cartesian) centroid can be computed by:
c = 1 / w * (sum of w[i] * p[i])
whereas w is the sum of all weights and whereas p[i] is a polygon point and w[i] is a weight for that point, e.g.
w[i] = |p[i] - p[(i - 1 + n) % n]| / 2 + |p[i] - p[(i + 1) % n]| / 2
whereas |x| is the length of a vector x.
I.e. a point is weighted with half the length to the previous and half the length to the next polygon point.
This centroid c can now projected onto the sphere by:
c' = r * c / |c|
whereas r is the radius of the sphere.
To consider orientation of polygon (ccw, cw) the result may be
c' = - r * c / |c|.
Sorry I (as a newly registered user) had to write a new post instead of just voting/commenting on the above answer by Don Hatch. Don's answer, I think, is the best and most elegant. It is mathematically rigorous in computing the center of mass (first moment of mass) in a simple way when applying to the spherical polygon.
Kobe John's answer is a good approximation but only satisfactory for smaller areas. I also noticed a few glitches in the code. Firstly, the reference point should be projected to the spherical surface to compute the actual spherical area. Secondly, function points_are_equivalent() might need to be refined to avoid divided-by-zero.
The approximation error in Kobe's method lies in the calculation of the centroid of spherical triangles. The sub-centroid is NOT the center of mass of the spherical triangle but the planar one. This is not an issue if one is to determine that single triangle (sign may flip, see below). It is also not an issue if triangles are small (e.g. a dense triangulation of the polygon).
A few simple tests could illustrate the approximation error. For example if we use just four points:
10 -20
10 20
-10 20
-10 -20
The exact answer is (1,0,0) and both methods are good. But if you throw in a few more points along one edge (e.g. add {10,-15},{10,-10}... to the first edge), you'll see the results from Kobe's method start to shift. Further more, if you increase the longitude from [10,-10] to [100,-100], you'll see Kobe's result flips the direction. A possible improvement might be to add another level(s) for sub-centroid calculation (basically refine/reduce sizes of triangles).
For our application, the spherical area boundary is composed of multiple arcs and thus not polygon (i.e. the arc is not part of great circle). But this will just be a little more work to find the n-vector in the curve integration.
EDIT: Replacing the subcentroid calculation with the one given in Brock's paper should fix Kobe's method. But I did not try though.
I am trying to use circle fitting code for 3D data set. I have modified it for 3D points just adding z-coordinate where necessary. My modification works fine for one set of points and works bad for another. Please look at the code, if it has some errors.
import trig_items
import numpy as np
from trig_items import *
from numpy import *
from matplotlib import pyplot as p
from scipy import optimize
# Coordinates of the 3D points
##x = r_[36, 36, 19, 18, 33, 26]
##y = r_[14, 10, 28, 31, 18, 26]
##z = r_[0, 1, 2, 3, 4, 5]
x = r_[ 2144.18908574, 2144.26880854, 2144.05552972, 2143.90303742, 2143.62520676,
2143.43628579, 2143.14005775, 2142.79919654, 2142.51436023, 2142.11240866,
2141.68564346, 2141.29333828, 2140.92596405, 2140.3475612, 2139.90848046,
2139.24661021, 2138.67384709, 2138.03313547, 2137.40301734, 2137.40908256,
2137.06611224, 2136.50943781, 2136.0553113, 2135.50313189, 2135.07049922,
2134.62098139, 2134.10459535, 2133.50838433, 2130.6600465, 2130.03537342,
2130.04047644, 2128.83522468, 2127.79827542, 2126.43513385, 2125.36700593,
2124.00350543, 2122.68564431, 2121.20709478, 2119.79047011, 2118.38417647,
2116.90063343, 2115.52685778, 2113.82246629, 2112.21159431, 2110.63180117,
2109.00713198, 2108.94434529, 2106.82777156, 2100.62343757, 2098.5090226,
2096.28787738, 2093.91550703, 2091.66075061, 2089.15316429, 2086.69753869,
2084.3002414, 2081.87590579, 2079.19141866, 2076.5394574, 2073.89128676,
2071.18786213]
y = r_[ 725.74913818, 724.43874065, 723.15226506, 720.45950581, 717.77827954,
715.07048092, 712.39633862, 709.73267688, 707.06039438, 704.43405908,
701.80074596, 699.15371526, 696.5309022, 693.96109921, 691.35585501,
688.83496327, 686.32148661, 683.80286662, 681.30705568, 681.30530975,
679.66483676, 678.01922321, 676.32721779, 674.6667554, 672.9658024,
671.23686095, 669.52021535, 667.84999077, 659.19757984, 657.46179949,
657.45700508, 654.46901086, 651.38177517, 648.41739432, 645.32356976,
642.39034578, 639.42628453, 636.51107198, 633.57732055, 630.63825133,
627.75308356, 624.80162215, 622.01980232, 619.18814892, 616.37688894,
613.57400131, 613.61535723, 610.4724493, 600.98277781, 597.84782844,
594.75983001, 591.77946964, 588.74874068, 585.84525834, 582.92311166,
579.99564481, 577.06666417, 574.30782762, 571.54115037, 568.79760614,
566.08551098]
z = r_[ 339.77146775, 339.60021095, 339.47645894, 339.47130963, 339.37216218,
339.4126132, 339.67942046, 339.40917728, 339.39500353, 339.15041461,
339.38959195, 339.3358209, 339.47764895, 339.17854867, 339.14624071,
339.16403926, 339.02308811, 339.27011082, 338.97684183, 338.95087698,
338.97321177, 339.02175448, 339.02543922, 338.88725411, 339.06942374,
339.0557553, 339.04414618, 338.89234303, 338.95572249, 339.00880416,
339.00413073, 338.91080374, 338.98214758, 339.01135789, 338.96393537,
338.73446188, 338.62784913, 338.72443217, 338.74880562, 338.69090173,
338.50765186, 338.49056867, 338.57353355, 338.6196255, 338.43754399,
338.27218569, 338.10587265, 338.43880881, 338.28962141, 338.14338705,
338.25784154, 338.49792568, 338.15572139, 338.52967693, 338.4594245,
338.1511823, 338.03711207, 338.19144663, 338.22022045, 338.29032321,
337.8623197 ]
# coordinates of the barycenter
xm = mean(x)
ym = mean(y)
zm = mean(z)
### Basic usage of optimize.leastsq
def calc_R(xc, yc, zc):
""" calculate the distance of each 3D points from the center (xc, yc, zc) """
return sqrt((x - xc) ** 2 + (y - yc) ** 2 + (z - zc) ** 2)
def func(c):
""" calculate the algebraic distance between the 3D points and the mean circle centered at c=(xc, yc, zc) """
Ri = calc_R(*c)
return Ri - Ri.mean()
center_estimate = xm, ym, zm
center, ier = optimize.leastsq(func, center_estimate)
##print center
xc, yc, zc = center
Ri = calc_R(xc, yc, zc)
R = Ri.mean()
residu = sum((Ri - R)**2)
print 'R =', R
So, for the first set of x, y, z (commented in the code) it works well: the output is R = 39.0097846735. If I run the code with the second set of points (uncommented) the resulting radius is R = 108576.859834, which is almost straight line. I plotted the last one.
The blue points is a given data set, the red ones is the arc of the resulting radius R = 108576.859834. It is obvious that the given data set has much smaller radius than the result.
Here is another set of points.
It is clear that the least squares does not work correctly.
Please help me solving this issue.
UPDATE
Here is my solution:
### fit 3D arc into a set of 3D points ###
### output is the centre and the radius of the arc ###
def fitArc3d(arr, eps = 0.0001):
# Coordinates of the 3D points
x = numpy.array([arr[k][0] for k in range(len(arr))])
y = numpy.array([arr[k][4] for k in range(len(arr))])
z = numpy.array([arr[k][5] for k in range(len(arr))])
# coordinates of the barycenter
xm = mean(x)
ym = mean(y)
zm = mean(z)
### gradient descent minimisation method ###
pnts = [[x[k], y[k], z[k]] for k in range(len(x))]
meanP = Point(xm, ym, zm) # mean point
Ri = [Point(*meanP).distance(Point(*pnts[k])) for k in range(len(pnts))] # radii to the points
Rm = math.fsum(Ri) / len(Ri) # mean radius
dR = Rm + 10 # difference between mean radii
alpha = 0.1
c = meanP
cArr = []
while dR > eps:
cArr.append(c)
Jx = math.fsum([2 * (x[k] - c[0]) * (Ri[k] - Rm) / Ri[k] for k in range(len(Ri))])
Jy = math.fsum([2 * (y[k] - c[1]) * (Ri[k] - Rm) / Ri[k] for k in range(len(Ri))])
Jz = math.fsum([2 * (z[k] - c[2]) * (Ri[k] - Rm) / Ri[k] for k in range(len(Ri))])
gradJ = [Jx, Jy, Jz] # find gradient
c = [c[k] + alpha * gradJ[k] for k in range(len(c)) if len(c) == len(gradJ)] # find new centre point
Ri = [Point(*c).distance(Point(*pnts[k])) for k in range(len(pnts))] # calculate new radii
RmOld = Rm
Rm = math.fsum(Ri) / len(Ri) # calculate new mean radius
dR = abs(Rm - RmOld) # new difference between mean radii
return Point(*c), Rm
It is not very optimal code (I do not have time to fine tune it) but it works.
I guess the problem is the data and the corresponding algorithm. The least square method works fine if it produces a local parabolic minimum, such that a simple gradient method goes approximately direction minimum. Unfortunately, this is not necessarily the case for your data. You can check this by keeping some rough estimates for xc and yc fixed and plotting the sum of the squared residuals as a function of zc and R. I get a boomerang shaped minimum. Depending on your starting parameters you might end in one of the branches going away from the real minimum. Once in the valley this can be very flat such that you exceed the number of max iterations or get something that is accepted within the tolerance of the algorithm. As always, thinks are better the better your starting parameters. Unfortunately you have only a small arc of the circle, so that it is difficult to get better. I am not a specialist in Python, but I think that leastsq allows you to play with the Jacobian and Gradient Methods. Try to play with the tolerance as well.
In short: the code looks basically fine to me, but your data is pathological and you have to adapt the code to that kind of data.
There is a non-iterative solution in 2D from Karimäki, maybe you can adapt
this method to 3D. You can also look at this. Sure you will find more literature.
I just checked the data using a Simplex-Algorithm. The minimum is, as I said, not well behaved. See here some cuts of the residual function. Only in the xy-plane you get some reasonable behavior. The properties of the zr- and xr- plane make the finding process very difficult.
So in the beginning the simplex algorithm finds several almost stable solutions. You can see them as flat steps in the graph below (blue x, purple y, yellow z, green R). At the end the algorithm has to walk down the almost flat but very stretched out valley, resulting in the final conversion of z and R. Nevertheless, I expect many regions that look like a solution if the tolerance is insufficient. With the standard tolerance of 10^-5 the algoritm stopped after approx 350 iterations. I had to set it to 10^-10 to get this solution, i.e. [1899.32, 741.874, 298.696, 248.956], which seems quite ok.
Update
As mentioned earlier, the solution depends on the working precision and requested accuracy. So your hand made gradient method works probably better as these values are different compared to the build-in least square fit. Nevertheless, this is my version making a two step fit. First I fit a plane to the data. In a next step I fit a circle within this plane. Both steps use the least square method. This time it works, as each step avoids critically shaped minima. (Naturally, the plane fit runs into problems if the arc segment becomes small and the data lies virtually on a straight line. But this will happen for all algorithms)
from math import *
from matplotlib import pyplot as plt
from scipy import optimize
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
import pprint as pp
dataTupel=zip(xs,ys,zs) #your data from above
# Fitting a plane first
# let the affine plane be defined by two vectors,
# the zero point P0 and the plane normal n0
# a point p is member of the plane if (p-p0).n0 = 0
def distanceToPlane(p0,n0,p):
return np.dot(np.array(n0),np.array(p)-np.array(p0))
def residualsPlane(parameters,dataPoint):
px,py,pz,theta,phi = parameters
nx,ny,nz =sin(theta)*cos(phi),sin(theta)*sin(phi),cos(theta)
distances = [distanceToPlane([px,py,pz],[nx,ny,nz],[x,y,z]) for x,y,z in dataPoint]
return distances
estimate = [1900, 700, 335,0,0] # px,py,pz and zeta, phi
#you may automize this by using the center of mass data
# note that the normal vector is given in polar coordinates
bestFitValues, ier = optimize.leastsq(residualsPlane, estimate, args=(dataTupel))
xF,yF,zF,tF,pF = bestFitValues
point = [xF,yF,zF]
normal = [sin(tF)*cos(pF),sin(tF)*sin(pF),cos(tF)]
# Fitting a circle inside the plane
#creating two inplane vectors
sArr=np.cross(np.array([1,0,0]),np.array(normal))#assuming that normal not parallel x!
sArr=sArr/np.linalg.norm(sArr)
rArr=np.cross(sArr,np.array(normal))
rArr=rArr/np.linalg.norm(rArr)#should be normalized already, but anyhow
def residualsCircle(parameters,dataPoint):
r,s,Ri = parameters
planePointArr = s*sArr + r*rArr + np.array(point)
distance = [ np.linalg.norm( planePointArr-np.array([x,y,z])) for x,y,z in dataPoint]
res = [(Ri-dist) for dist in distance]
return res
estimateCircle = [0, 0, 335] # px,py,pz and zeta, phi
bestCircleFitValues, ier = optimize.leastsq(residualsCircle, estimateCircle,args=(dataTupel))
rF,sF,RiF = bestCircleFitValues
print bestCircleFitValues
# Synthetic Data
centerPointArr=sF*sArr + rF*rArr + np.array(point)
synthetic=[list(centerPointArr+ RiF*cos(phi)*rArr+RiF*sin(phi)*sArr) for phi in np.linspace(0, 2*pi,50)]
[cxTupel,cyTupel,czTupel]=[ x for x in zip(*synthetic)]
### Plotting
d = -np.dot(np.array(point),np.array(normal))# dot product
# create x,y mesh
xx, yy = np.meshgrid(np.linspace(2000,2200,10), np.linspace(540,740,10))
# calculate corresponding z
# Note: does not work if normal vector is without z-component
z = (-normal[0]*xx - normal[1]*yy - d)/normal[2]
# plot the surface, data, and synthetic circle
fig = plt.figure()
ax = fig.add_subplot(211, projection='3d')
ax.scatter(xs, ys, zs, c='b', marker='o')
ax.plot_wireframe(xx,yy,z)
ax.set_xlabel('X Label')
ax.set_ylabel('Y Label')
ax.set_zlabel('Z Label')
bx = fig.add_subplot(212, projection='3d')
bx.scatter(xs, ys, zs, c='b', marker='o')
bx.scatter(cxTupel,cyTupel,czTupel, c='r', marker='o')
bx.set_xlabel('X Label')
bx.set_ylabel('Y Label')
bx.set_zlabel('Z Label')
plt.show()
which give a radius of 245. This is close to what the other approach gave (249). So within error margins I get the same.
The plotted result looks reasonable.
Hope this helps.
Feel like you missed some constraints in your 1st version code. The implementation could be explained as fitting a sphere to 3d points. So that's why the 2nd radius for 2nd data list is almost straight line. It's thinking like you are giving it a small circle on a large sphere.