Approximating a polygon with a circle - python

Well, approximating a circle with a polygon and Pythagoras' story may be well known.
But what about the other way around?
I have some polygons, that should be in fact circles. However, due to measurement errors they are not. So, what I'm looking for is the circle that best "approximates" the given polygon.
In the following figure we can see two different examples.
My first Ansatz was to find the maximum distance of the points to the center as well as the minimum. The circle we are looking for is maybe somewhere in between.
Is there any algorithm out there for this problem?

I would use scipy to best-"fit" a circle onto my points. You can get a starting point for the center and radius by a simple center-of-mass calculation. This works well if the points are uniformly distributed over the circle. If they are not, as in the example below, it is still better than nothing!
The fitting function is simple because a circle is simple. You only need to find the radial distance from your fit circle to your points as the tangent (radial) surface will always be the best fit.
import numpy as np
from scipy.spatial.distance import cdist
from scipy.optimize import fmin
import scipy
# Draw a fuzzy circle to test
N = 15
THETA = np.random.random(15)*2*np.pi
R = 1.5 + (.1*np.random.random(15) - .05)
X = R*np.cos(THETA) + 5
Y = R*np.sin(THETA) - 2
# Choose the inital center of fit circle as the CM
xm = X.mean()
ym = Y.mean()
# Choose the inital radius as the average distance to the CM
cm = np.array([xm,ym]).reshape(1,2)
rm = cdist(cm, np.array([X,Y]).T).mean()
# Best fit a circle to these points
def err((w,v,r)):
pts = [np.linalg.norm([x-w,y-v])-r for x,y in zip(X,Y)]
return (np.array(pts)**2).sum()
xf,yf,rf = scipy.optimize.fmin(err,[xm,ym,rm])
# Viszualize the results
import pylab as plt
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
# Show the inital guess circle
circ = plt.Circle((xm, ym), radius=rm, color='y',lw=2,alpha=.5)
ax.add_patch(circ)
# Show the fit circle
circ = plt.Circle((xf, yf), radius=rf, color='b',lw=2,alpha=.5)
ax.add_patch(circ)
plt.axis('equal')
plt.scatter(X,Y)
plt.show()

Perhaps a simple algorithm would be firstly to calculate the centroid of the points (providing they are usually roughly regularly spaced). This is the circle centre. Once you have that you can calculate the mean radius of the points, giving the radius of the circle.
A more sophisticated answer might be to do a simple minimisation, where you minimise the sum of the distances of the points to the edge of the circle (or distance squared).

There are two different O(n) algorithms for determining the smallest circle you draw that encompasses a series of points on the wikipedia page smallest-circle problem. From here it should be fairly easy to draw the second circle, simply determine the center of the circle you found previously, and find the point closest to that point. The radius of the second circle is that.
This may not be exactly what you want, but this is how I would start.

That problem might be the same as the Smallest-circle problem.
But since you have measurement errors which could include outliers, then RANSAC is a good option instead. See http://cs.gmu.edu/~kosecka/cs482/lect-fitting.pdf for a overview of the method (as well other basic techniques), in http://www.asl.ethz.ch/education/master/info-process-rob/Hough-Ransac.pdf there is more information dedicated to circle fitting.

It's quite easy to find some approximation:
def find_circle_deterministically(x,y):
center = x.mean(), y.mean()
radius = np.sqrt((x-center[0])**2 + (y-center[1])**2).mean()
return center, radius
Explained: put the center of the circle to the mean x and mean y of your points. Then, for each point, determine the distance to the center and take the mean over all points. That's your radius.
This complete script:
import numpy as np
import matplotlib.pyplot as plt
n_points = 10
radius = 4
noise_std = 0.3
angles = np.linspace(0,2*np.pi,n_points,False)
x = np.cos(angles) * radius
y = np.sin(angles) * radius
x += np.random.normal(0,noise_std,x.shape)
y += np.random.normal(0,noise_std,y.shape)
plt.axes(aspect="equal")
plt.plot(x,y,"bx")
def find_circle_deterministically(x,y):
center = x.mean(), y.mean()
radius = np.sqrt((x-center[0])**2 + (y-center[1])**2).mean()
return center, radius
center, radius2 = find_circle_deterministically(x,y)
angles2 = np.linspace(0,2*np.pi,100,True)
x2 = center[0] + np.cos(angles2) * radius2
y2 = center[1] + np.sin(angles2) * radius2
plt.plot(x2,y2,"r-")
plt.show()
produces this plot:
This will work good as you have polygons with measurement errors. If your points are not approximately equally distributed over the angles [0,2pi[, it will perform poorly.
More generally, you could use optimization.

Related

Find new position for overlapping circles

I am trying to write a code that, for a given list of circles (list1), it is able to find the positions for new circles (list2). list1 and list2 have the same length, because for each circle in list1 there must be a circle from list2.
Each pair of circles (let's say circle1 from list1 and circle2 from list2), must be as close together as possible,
circles from list2 must not overlap with circles from list1, while circles of the single lists can overlap each other.
list1 is fixed, so now I have to find the right position for circles from list2.
I wrote this simple function to recognize if 2 circles overlap:
def overlap(x1, y1, x2, y2, r1, r2):
distSq = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)
radSumSq = (r1 + r2) * (r1 + r2)
if (distSq >= radSumSq):
return False # no overlap
else:
return True #overlap
and this is the list1:
with:
x=[14.11450195 14.14184093 14.15435028 14.16206741 14.16951752 14.17171097
14.18569565 14.19700241 14.23129082 14.24083233 14.24290752 14.24968338
14.2518959 14.26536751 14.27209759 14.27612877 14.2904377 14.29187012
14.29409599 14.29618549 14.30615044 14.31624985 14.3206892 14.3228569
14.36143875 14.36351967 14.36470699 14.36697292 14.37235737 14.41422081
14.42583466 14.43226814 14.43319225 14.4437027 14.4557848 14.46592999
14.47036076 14.47452068 14.47815609 14.52229309 14.53059006 14.53404236
14.5411644 ]
y=[-0.35319126 -0.44222349 -0.44763246 -0.35669261 -0.24366629 -0.3998799
-0.38940558 -0.57744932 -0.45223859 -0.21021004 -0.44250247 -0.45866323
-0.47203487 -0.51684451 -0.44884869 -0.2018993 -0.40296811 -0.23641759
-0.18019417 -0.33391538 -0.53565156 -0.45215255 -0.40939832 -0.26936951
-0.30894437 -0.55504167 -0.47177047 -0.45573688 -0.43100587 -0.5805912
-0.21770373 -0.199422 -0.17372169 -0.38522363 -0.56950212 -0.56947368
-0.48770753 -0.24940367 -0.31492445 -0.54263926 -0.53460872 -0.4053807
-0.43733299]
radius = 0.014
Copy and pasteable...
x = [14.11450195,14.14184093,14.15435028,14.16206741,14.16951752,
14.17171097,14.18569565,14.19700241,14.23129082,14.24083233,
14.24290752,14.24968338,14.2518959,14.26536751,14.27209759,
14.27612877,14.2904377,14.29187012,14.29409599,14.29618549,
14.30615044,14.31624985,14.3206892,14.3228569,14.36143875,
14.36351967,14.36470699,14.36697292,14.37235737,14.41422081,
14.42583466,14.43226814,14.43319225,14.4437027,14.4557848,
14.46592999,14.47036076,14.47452068,14.47815609,14.52229309,
14.53059006,14.53404236,14.5411644]
y = [-0.35319126,-0.44222349,-0.44763246,-0.35669261,-0.24366629,
-0.3998799,-0.38940558,-0.57744932,-0.45223859,-0.21021004,
-0.44250247,-0.45866323,-0.47203487,-0.51684451,-0.44884869,
-0.2018993,-0.40296811,-0.23641759,-0.18019417,-0.33391538,
-0.53565156,-0.45215255,-0.40939832,-0.26936951,-0.30894437,
-0.55504167,-0.47177047,-0.45573688,-0.43100587,-0.5805912,
-0.21770373,-0.199422,-0.17372169,-0.38522363,-0.56950212,
-0.56947368,-0.48770753,-0.24940367,-0.31492445,-0.54263926,
-0.53460872,-0.4053807,-0.43733299]
Now I am not sure about what I have to do, my first idea is to draw circles of list2 taking x and y from list one and do something like x+c and y+c, where c is a fixed value. Then I can call my overlapping function and, if there is overlap I can increase the c value.
In this way I have 2 for loops. Now, my questions are:
There is a way to avoid for loops?
Is there a smart solution to find a neighbor (circle from list2) for each circle from list1 (without overlaps with other circles from list2)?
Using numpy arrays, you can avoid for loops.
Setup from your example.
import numpy as np
#Using your x and y
c1 = np.array([x,y]).T
# random set of other centers within the same range as c1
c2 = np.random.random((10,2))
np.multiply(c2, c1.max(0)-c1.min(0),out = c2)
np.add(c2, c1.min(0), out=c2)
radius = 0.014
r = radius
min_d = (2*r)*(2*r)
plot_circles(c1,c2) # see function at end
An array of distances from each center in c1 to each center in c2
def dist(c1,c2):
dx = c1[:,0,None] - c2[:,0]
dy = c1[:,1,None] - c2[:,1]
return dx*dx + dy*dy
d = dist(c1,c2)
Or you could use scipy.spatial
from scipy.spatial import distance
d = distance.cdist(c1,c2,'sqeuclidean')
Create a 2d Boolean array for circles that intersect.
intersect = d <= min_d
Find the indices of overlapping circles from the two sets.
a,b = np.where(intersect)
plot_circles(c1[a],c2[b])
Using intersect or a and b to index c1,c2, and d you should be able to get groups of intersecting circles then figure out how to move the c2 centers - but I'll leave that for another question/answer. If a list2 circle intersects one list1 circle - find the line between the two and move along that line. If a list2 circle intersects more than one list1 circle - find the line between the two closestlist1circles and move thelitst2` circle along a line perpendicular to that. You didn't mention any constraints on moving the circles so maybe random movement then find the intersects again but that might be problematic. In the following image, it may be trivial to figure out how to move most of the red circles but the group circled in blue might require a different strategy.
Here are some examples for getting groups:
>>> for f,g,h in zip(c1[a],c2[b],d[a,b]):
print(f,g,h)
>>> c1[intersect.any(1)],c2[intersect.any(0)]
>>> for (f,g) in zip(c2,intersect.T):
if g.any():
print(f.tolist(),c1[g].tolist())
import matplotlib as mpl
from matplotlib import pyplot as plt
def plot_circles(c1,c2):
bounds = np.array([c1.min(0),c2.min(0),c1.max(0),c2.max(0)])
xmin, ymin = bounds.min(0)
xmax, ymax = bounds.max(0)
circles1 = [mpl.patches.Circle(xy,radius=r,fill=False,edgecolor='g') for xy in c1]
circles2 = [mpl.patches.Circle(xy,radius=r,fill=False,edgecolor='r') for xy in c2]
fig = plt.figure()
ax = fig.add_subplot(111)
for c in circles2:
ax.add_artist(c)
for c in circles1:
ax.add_artist(c)
ax.set_xlim(xmin-r,xmax+r)
ax.set_ylim(ymin-r,ymax+r)
plt.show()
plt.close()
This problem can very well be seen as an optimization problem. To be more precise, a nonlinear optimization problem with constraints.
Since optimization strategies are not always so easy to understand, I will define the problem as simply as possible and also choose an approach that is as general as possible (but less efficient) and does not involve a lot of mathematics. As a spoiler: We are going to formulate the problem and the minimization process in less than 10 lines of code using the scipy library.
However, I will still provide hints on where you can get your hands even dirtier.
Formulating the problem
As a guide for a formulation of an NLP-class problem (Nonlinear Programming), you can go directly to the two requirements in the original post.
Each pair of circles must be as close together as possible -> Hint for a cost-function
Circles must not overlap with other (moved) circles -> Hint for a constraint
Cost function
Let's start with the formulation of the cost function to be minimized.
Since the circles should be moved as little as possible (resulting in the closest possible neighborhood), a quadratic penalty term for the distances between the circles of the two lists can be chosen for the cost function:
import scipy.spatial.distance as sd
def cost_function(new_positions, old_positions):
new_positions = np.reshape(new_positions, (-1, 2))
return np.trace(sd.cdist(new_positions, old_positions, metric='sqeuclidean'))
Why quadratic? Partly because of differentiability and for stochastic reasons (think of the circles as normally distributed measurement errors -> least squares is then a maximum likelihood estimator). By exploiting the structure of the cost function, the efficiency of the optimization can be increased (elimination of sqrt). By the way, this problem is related to nonlinear regression, where (nonlinear) least squares are also used.
Now that we have a cost function at hand, we also have a good way to evaluate our optimization. To be able to compare solutions of different optimization strategies, we simply pass the newly calculated positions to the cost function.
Let's give it a try: For example, let us use the calculated positions from the Voronoi approach (by Paul Brodersen).
print(cost_function(new_positions, old_positions))
# prints 0.007999244511697411
That's a pretty good value if you ask me. Considering that the cost function spits out zero when there is no displacement at all, this cost is pretty close. We can now try to outperform this value by using classical optimization!
Non-linear constraint
We know that circles must not overlap with other circles in the new set. If we translate this into a constraint, we find that the lower bound for the distance is 2 times the radius and the upper bound is simply infinity.
import scipy.spatial.distance as sd
from scipy.optimize import NonlinearConstraint
def cons_f(x):
x = np.reshape(x, (-1, 2))
return sd.pdist(x)
nonlinear_constraint = NonlinearConstraint(cons_f, 2*radius, np.inf, jac='2-point')
Here we make life easy by approximating the Jacobi matrix via finite differences (see parameter jac='2-point'). At this point it should be said that we can increase the efficiency here, by formulating the derivatives of the first and second order ourselves instead of using approximations. But this is left to the interested reader. (It is not that hard, because we use quite simple mathematical expressions for distance calculation here.)
One additional note: You can also set a boundary constraint for the positions themselves not to exceed a specified region. This can then be used as another parameter. (See scipy.optimize.Bounds)
Minimizing the cost function under constraints
Now we have both ingredients, the cost function and the constraint, in place. Now let's minimize the whole thing!
from scipy.optimize import minimize
res = minimize(lambda x: cost_function(x, positions), positions.flatten(), method='SLSQP',
jac="2-point", constraints=[nonlinear_constraint])
As you can see, we approximate the first derivatives here as well. You can also go deeper here and set up the derivatives yourself (analytically).
Also note that we must always pass the parameters (an nx2 vector specifying the positions of the new layout for n circles) as a flat vector. For this reason, reshaping can be found several times in the code.
Evaluation, summary and visualization
Let's see how the optimization result performs in our cost function:
new_positions = np.reshape(res.x, (-1,2))
print(cost_function(new_positions, old_positions))
# prints 0.0010314079483565686
Starting from the Voronoi approach, we actually reduced the cost by another 87%! Thanks to the power of modern optimization strategies, we can solve a lot of problems in no time.
Of course, it would be interesting to see how the shifted circles look now:
Circles after Optimization
Performance: 77.1 ms ± 1.17 ms
The entire code:
from scipy.optimize import minimize
import scipy.spatial.distance as sd
from scipy.optimize import NonlinearConstraint
# Given by original post
positions = np.array([x, y]).T
def cost_function(new_positions, old_positions):
new_positions = np.reshape(new_positions, (-1, 2))
return np.trace(sd.cdist(new_positions, old_positions, metric='sqeuclidean'))
def cons_f(x):
x = np.reshape(x, (-1, 2))
return sd.pdist(x)
nonlinear_constraint = NonlinearConstraint(cons_f, 2*radius, np.inf, jac='2-point')
res = minimize(lambda x: cost_function(x, positions), positions.flatten(), method='SLSQP',
jac="2-point", constraints=[nonlinear_constraint])
One solution could be to follow the gradient of the unwanted spacing between each circle, though maybe there is a better way. This approach has a few parameters to tune and takes some time to run.
import matplotlib.pyplot as plt
from scipy.optimize import minimize as mini
import numpy as np
from scipy.optimize import approx_fprime
x = np.array([14.11450195,14.14184093,14.15435028,14.16206741,14.16951752,
14.17171097,14.18569565,14.19700241,14.23129082,14.24083233,
14.24290752,14.24968338,14.2518959,14.26536751,14.27209759,
14.27612877,14.2904377,14.29187012,14.29409599,14.29618549,
14.30615044,14.31624985,14.3206892,14.3228569,14.36143875,
14.36351967,14.36470699,14.36697292,14.37235737,14.41422081,
14.42583466,14.43226814,14.43319225,14.4437027,14.4557848,
14.46592999,14.47036076,14.47452068,14.47815609,14.52229309,
14.53059006,14.53404236,14.5411644])
y = np.array([-0.35319126,-0.44222349,-0.44763246,-0.35669261,-0.24366629,
-0.3998799,-0.38940558,-0.57744932,-0.45223859,-0.21021004,
-0.44250247,-0.45866323,-0.47203487,-0.51684451,-0.44884869,
-0.2018993,-0.40296811,-0.23641759,-0.18019417,-0.33391538,
-0.53565156,-0.45215255,-0.40939832,-0.26936951,-0.30894437,
-0.55504167,-0.47177047,-0.45573688,-0.43100587,-0.5805912,
-0.21770373,-0.199422,-0.17372169,-0.38522363,-0.56950212,
-0.56947368,-0.48770753,-0.24940367,-0.31492445,-0.54263926,
-0.53460872,-0.4053807,-0.43733299])
radius = 0.014
x0, y0 = (x, y)
def plot_circles(x, y, name='initial'):
fig, ax = plt.subplots()
for ii in range(x.size):
ax.add_patch(plt.Circle((x[ii], y[ii]), radius, color='b', fill=False))
ax.set_xlim(x.min() - radius, x.max() + radius)
ax.set_ylim(y.min() - radius, y.max() + radius)
fig.savefig(name)
plt.clf()
def spacing(s):
x, y = np.split(s, 2)
dX, dY = [np.subtract(*np.meshgrid(xy, xy, indexing='ij')).T
for xy in [x, y]]
dXY2 = dX**2 + dY**2
return np.minimum(dXY2[np.triu_indices(x.size, 1)] - (2 * radius) ** 2, 0).sum()
plot_circles(x, y)
def spacingJ(s):
return approx_fprime(s, spacing, 1e-8)
s = np.append(x, y)
for ii in range(50):
j = spacingJ(s)
if j.sum() == 0: break
s += .01 * j
x_new, y_new = np.split(s, 2)
plot_circles(x_new, y_new, 'new%i' % ii)
plot_circles(x_new, y_new, 'new%i' % ii)
https://giphy.com/gifs/x0lWDLZBz5O3gWTbLa
This answer implements a variation of the Lloyds algorithm. The basic idea is to compute the Voronoi diagram for your points / circles. This assigns each point a cell, which is a region that includes the point and which has a center that is maximally far away from all other points.
In the original algorithm, we would move each point towards the center of its Voronoi cell. Over time, this results in an even spread of points, as illustrated here.
In this variant, we only move points that overlap another point.
import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial import Voronoi
from scipy.spatial.distance import cdist
def remove_overlaps(positions, radii, tolerance=1e-6):
"""Use a variation of Lloyds algorithm to move circles apart from each other until none overlap.
Parameters
----------
positions : array
The (x, y) coordinates of the circle origins.
radii : array
The radii for each circle.
tolerance : float
If all circles overlap less than this threshold, the computation stops.
Higher values leads to faster convergence.
Returns
-------
new_positions : array
The (x, y) coordinates of the circle origins.
See also
--------
https://en.wikipedia.org/wiki/Lloyd%27s_algorithm
"""
positions = np.array(positions)
radii = np.array(radii)
minimum_distances = radii[np.newaxis, :] + radii[:, np.newaxis]
minimum_distances[np.diag_indices_from(minimum_distances)] = 0 # ignore distances to self
# Initialize the first loop.
distances = cdist(positions, positions)
displacements = np.max(np.clip(minimum_distances - distances, 0, None), axis=-1)
while np.any(displacements > tolerance):
centroids = _get_voronoi_centroids(positions)
# Compute the direction from each point towards its corresponding Voronoi centroid.
deltas = centroids - positions
magnitudes = np.linalg.norm(deltas, axis=-1)
directions = deltas / magnitudes[:, np.newaxis]
# Mask NaNs that arise if the magnitude is zero, i.e. the point is already center of the Voronoi cell.
directions[np.isnan(directions)] = 0
# Step into the direction of the centroid.
# Clipping prevents overshooting of the centroid when stepping into the direction of the centroid.
# We step by half the displacement as the other overlapping point will be moved in approximately the opposite direction.
positions = positions + np.clip(0.5 * displacements, None, magnitudes)[:, np.newaxis] * directions
# Initialize next loop.
distances = cdist(positions, positions)
displacements = np.max(np.clip(minimum_distances - distances, 0, None), axis=-1)
return positions
def _get_voronoi_centroids(positions):
"""Construct a Voronoi diagram from the given positions and determine the center of each cell."""
voronoi = Voronoi(positions)
centroids = np.zeros_like(positions)
for ii, idx in enumerate(voronoi.point_region):
region = [jj for jj in voronoi.regions[idx] if jj != -1] # i.e. ignore points at infinity; TODO: compute correctly clipped regions
centroids[ii] = np.mean(voronoi.vertices[region], axis=0)
return centroids
if __name__ == '__main__':
x = np.array([14.11450195,14.14184093,14.15435028,14.16206741,14.16951752,
14.17171097,14.18569565,14.19700241,14.23129082,14.24083233,
14.24290752,14.24968338,14.2518959,14.26536751,14.27209759,
14.27612877,14.2904377,14.29187012,14.29409599,14.29618549,
14.30615044,14.31624985,14.3206892,14.3228569,14.36143875,
14.36351967,14.36470699,14.36697292,14.37235737,14.41422081,
14.42583466,14.43226814,14.43319225,14.4437027,14.4557848,
14.46592999,14.47036076,14.47452068,14.47815609,14.52229309,
14.53059006,14.53404236,14.5411644])
y = np.array([-0.35319126,-0.44222349,-0.44763246,-0.35669261,-0.24366629,
-0.3998799,-0.38940558,-0.57744932,-0.45223859,-0.21021004,
-0.44250247,-0.45866323,-0.47203487,-0.51684451,-0.44884869,
-0.2018993,-0.40296811,-0.23641759,-0.18019417,-0.33391538,
-0.53565156,-0.45215255,-0.40939832,-0.26936951,-0.30894437,
-0.55504167,-0.47177047,-0.45573688,-0.43100587,-0.5805912,
-0.21770373,-0.199422,-0.17372169,-0.38522363,-0.56950212,
-0.56947368,-0.48770753,-0.24940367,-0.31492445,-0.54263926,
-0.53460872,-0.4053807,-0.43733299])
radius = 0.014
positions = np.c_[x, y]
radii = np.full(len(positions), radius)
fig, axes = plt.subplots(1, 2, sharex=True, sharey=True, figsize=(14, 7))
for position, radius in zip(positions, radii):
axes[0].add_patch(plt.Circle(position, radius, fill=False))
axes[0].set_xlim(x.min() - radius, x.max() + radius)
axes[0].set_ylim(y.min() - radius, y.max() + radius)
axes[0].set_aspect('equal')
new_positions = remove_overlaps(positions, radii)
for position, radius in zip(new_positions, radii):
axes[1].add_patch(plt.Circle(position, radius, fill=False))
for ax in axes.ravel():
ax.set_aspect('equal')
plt.show()

Masking a 3D numpy array with a tilted disc

I'm working in Python2.7 with 3D numpy arrays, and trying to retrieve only pixels who fall on a 2D tilted disc.
Here is my code to plot the border of the disc (= a circle) I am interested in
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
#creating a 3d numpy array (empty in this example, but will represent a binary 3D image in my application)
space=np.zeros((40,40,20))
r = 8 #radius of the circle
theta = np.pi / 4 # "tilt" of the circle
phirange = np.linspace(0, 2 * np.pi) #to make a full circle
#center of the circle
center=[20,20,10]
#computing the values of the circle in spherical coordinates and converting them
#back to cartesian
for phi in phirange:
x = r * np.cos(theta) * np.cos(phi) + center[0]
y= r*np.sin(phi) + center[1]
z= r*np.sin(theta)* np.cos(phi) + center[2]
space[int(round(x)),int(round(y)),int(round(z))]=1
x,y,z = space.nonzero()
#plotting
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.scatter(x, y, z, zdir='z', c= 'red')
plt.show()
The plot gives the following figure :
which is a good start, but now I want a way to retrieve only the values of the pixels of space which are located in the disc defined by the circle : the ones in the pink zone in the following image (in my application, space will be a 3D binary image, here it is numpy.zeros() just to be able to plot and show you the disc I want):
How should I procede ?
I guess there is some numpy masking involved, an I understand how you would do it in 2D (like this question) but I'm having trouble applying this to 3D.
One easy way would be to calculate the normal vector to your disc plane. You can use your spherical coordinates for that. Be sure not to add the centre, set phi at zero and swap cos and sin theta, also stick a minus sign to the sin.
lets call that vector v. The plane is given by v0*x0 + v1*x1 + v2*x2 == c you can calculate c by inserting a point from your circle for x.
Next you can make a 2d grid for x0 and x1 and solve for x2. this gives you the height x2 as a function of the x0, x1 mesh. for these points you can calculate the distance from your disc centre and discard the points that are too far off. This you would indeed do using a mask.
Finally, depending on how precisely you want to plot you could round the x2 values to grid units, but for example for a surface plot I wouldn't do that.
To get a 3d mask as you describe you would round x2 and then starting from an all zero space set the disc pixels using space[x0, x1, x2] = True. This assumes that you have masked x0, x1, x2 as described earlier.
Well that is a math problem, you should ask it in the Mathematics Stack Exchange site.
From my perspective, you should first find the surface your disc is in, and do the area calculation within that surface, by, for example, the method you mentioned in the linked question.
numpy or matplotlib here definitely do not responsible for the projection, you do.
Without clearly point out which (or which kind of) surface they are in, and the equation does not guarantee it is a plane, the area does not mean anything.

Check if points are inside ellipse faster than contains_point method

I use matplotlib 1.15.1 and I try to generate scattergrams like this:
The ellipses have fixes size and are drawn with center coordinates, width, height and angle (provided from outside): I have no idea what their equotions are.
g_ell_center = (0.8882, 0.8882)
g_ell_width = 0.36401857095483
g_ell_height = 0.16928136341606
g_ellipse = patches.Ellipse(g_ell_center, g_ell_width, g_ell_height, angle=angle, fill=False, edgecolor='green', linewidth=2)
This ellipses should mark normal and semi-normal data on my plot.
Then, I have an array of ~500 points which must be colored according to ellipse they belong to. So I tried to check each point with contains_point method:
colors_array = []
colors_scheme = ['green', 'yellow', 'black']
for point in points_array:
if g_ellipse.contains_point(point, radius=0):
colors_array.append(0)
elif y_ellipse.contains_point(point, radius=0):
colors_array.append(1)
else:
colors_array.append(2)
Finally, points are drawn:
plt.scatter(x_array, y_array, s=10, c=[colors_scheme[x] for x in colors_array], edgecolor="k", linewidths=0.3)
But contains_point is extremely slow! It worked for 5 minutes for 300-points scattergram, and I have to generate thousands of them in parallel. Maybe there's faster approach?
P.S. Whole project is bound to matplotlib, I can't use other libraries.
This approach should test if a point is within an ellipse, given the ellipse's centre, width, height and angle. You find the point's x and y coordinates relative to the ellipse centre, then transform those using the angle to be the coordinates along the major and minor axes. Finally, you find the normalised distance of the point from the cell centre, where a distance of 1 would be on the ellipse, less than 1 is inside, and more than 1 is outside.
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
fig,ax = plt.subplots(1)
ax.set_aspect('equal')
# Some test points
x = np.random.rand(500)*0.5+0.7
y = np.random.rand(500)*0.5+0.7
# The ellipse
g_ell_center = (0.8882, 0.8882)
g_ell_width = 0.36401857095483
g_ell_height = 0.16928136341606
angle = 30.
g_ellipse = patches.Ellipse(g_ell_center, g_ell_width, g_ell_height, angle=angle, fill=False, edgecolor='green', linewidth=2)
ax.add_patch(g_ellipse)
cos_angle = np.cos(np.radians(180.-angle))
sin_angle = np.sin(np.radians(180.-angle))
xc = x - g_ell_center[0]
yc = y - g_ell_center[1]
xct = xc * cos_angle - yc * sin_angle
yct = xc * sin_angle + yc * cos_angle
rad_cc = (xct**2/(g_ell_width/2.)**2) + (yct**2/(g_ell_height/2.)**2)
# Set the colors. Black if outside the ellipse, green if inside
colors_array = np.array(['black'] * len(rad_cc))
colors_array[np.where(rad_cc <= 1.)[0]] = 'green'
ax.scatter(x,y,c=colors_array,linewidths=0.3)
plt.show()
Note, this whole script takes 0.6 seconds to run and process 500 points. That includes creating and saving the figure, etc.
The process of setting the colors_array using the np.where method above takes 0.00007s for 500 points.
Note, in an older implementation shown below, setting the colors_array in a loop took 0.00016 s:
colors_array = []
for r in rad_cc:
if r <= 1.:
# point in ellipse
colors_array.append('green')
else:
# point not in ellipse
colors_array.append('black')
Your current implementation should only be calling contains_point 25,000 to 50,000 times, which isn't a lot. So, I'm guessing that the implementation of contains_point is targeted toward precision rather than speed.
Since you have a distribution of points where only a small percentage will be in any given ellipse, and therefore most will rarely be anywhere near any given ellipse, you can easily use rectangular coordinates as a short-cut to figure out whether the point is close enough to the ellipse to be worth calling contains_point.
Compute the left and right x coordinates and top and bottom y coordinates of the ellipse, possibly with a bit of padding to account for rendering differences, then check if the point is within those, such as the following pseudo-code:
if point.x >= ellipse_left and point.x <= ellipse_right and _
point.y >= ellipse_top and point.y <= ellipse_bottom:
if ellipse.contains_point(point, radius=0):
... use the contained point here
This approach eliminates expensive calculations for most of the points, allowing simple comparisons instead to rule out the obvious mismatches, while preserving the accuracy of the computations where the point is close enough that it might be in the ellipse. If e.g. only 1% of your points are anywhere near a given ellipse, this approach will eliminate 99% of your calls to contains_point and instead replace them with much faster comparisons.

Take data from a circle in python

I am looking into how the intensity of a ring changes depending on angle. Here is an example of an image:
What I would like to do is take a circle of values from within the center of that doughnut and plot them vs angle. What I'm currently doing is using scipy.ndimage.interpolation.rotate and taking slices radially through the ring, and extracting the maximum of the two peaks and plotting those vs angle.
crop = np.ones((width,width)) #this is my image
slices = np.arange(0,width,1)
stack = np.zeros((2*width,len(slices)))
angles = np.linspace(0,2*np.pi,len(crop2))
for j in range(len(slices2)): # take slices
stack[:,j] = rotate(crop,slices[j],reshape=False)[:,width]
However I don't think this is doing what I'm actually looking for. I'm mostly struggling with how to extract the data I want. I have also tried applying a mask which looks like this;
to the image, but then I don't know how to get the values within that mask in the correct order (ie. in order of increasing angle 0 - 2pi)
Any other ideas would be of great help!
I made a different input image to help verifying correctness:
import numpy as np
import scipy as sp
import scipy.interpolate
import matplotlib.pyplot as plt
# Mock up an image.
W = 100
x = np.arange(W)
y = np.arange(W)
xx,yy = np.meshgrid(x,y)
image = xx//5*5 + yy//5*5
image = image / np.max(image) # scale into [0,1]
plt.imshow(image, interpolation='nearest', cmap='gray')
plt.show()
To sample values from circular paths in the image, we first build an interpolator because we want to access arbitrary locations. We also vectorize it to be faster.
Then, we generate the coordinates of N points on the circle's circumference using the parametric definition of the circle x(t) = sin(t), y(t) = cos(t).
N should be at least twice the circumference (Nyquist–Shannon sampling theorem).
interp = sp.interpolate.interp2d(x, y, image)
vinterp = np.vectorize(interp)
for r in (15, 30, 45): # radii for circles around image's center
xcenter = len(x)/2
ycenter = len(y)/2
arclen = 2*np.pi*r
angle = np.linspace(0, 2*np.pi, arclen*2, endpoint=False)
value = vinterp(xcenter + r*np.sin(angle),
ycenter + r*np.cos(angle))
plt.plot(angle, value, label='r={}'.format(r))
plt.legend()
plt.show()

Extract coordinates enclosed by a matplotlib patch.

I have created an ellipse using matplotlib.patches.ellipse as shown below:
patch = mpatches.Ellipse(center, major_ax, minor_ax, angle_deg, fc='none', ls='solid', ec='g', lw='3.')
What I want is a list of all the integer coordinates enclosed inside this patch.
I.e. If I was to plot this ellipse along with every integer point on the same grid, how many of those points are enclosed in the ellipse?
I have tried seeing if I can extract the equation of the ellipse so I can loop through each point and see whether it falls within the line but I can't seem to find an obvious way to do this, it becomes more complicated as the major axis of the ellipse can be orientated at any angle. The information to do this must be stored in patches somewhere, but I can't seem to find it.
Any advice on this would be much appreciated.
Ellipse objects have a method contains_point which will return 1 if the point is in the ellipse, 0 other wise.
Stealing from #DrV 's answer:
import matplotlib.pyplot as plt
import matplotlib.patches
import numpy as np
# create an ellipse
el = matplotlib.patches.Ellipse((50,-23), 10, 13.7, 30, facecolor=(1,0,0,.2), edgecolor='none')
# calculate the x and y points possibly within the ellipse
y_int = np.arange(-30, -15)
x_int = np.arange(40, 60)
# create a list of possible coordinates
g = np.meshgrid(x_int, y_int)
coords = list(zip(*(c.flat for c in g)))
# create the list of valid coordinates (from untransformed)
ellipsepoints = np.vstack([p for p in coords if el.contains_point(p, radius=0)])
# just to see if this works
fig = plt.figure()
ax = fig.add_subplot(111)
ax.add_artist(el)
ep = np.array(ellipsepoints)
ax.plot(ellipsepoints[:,0], ellipsepoints[:,1], 'ko')
plt.show()
This will give you the result as below:
If you really want to use the methods offered by matplotlib, then:
import matplotlib.pyplot as plt
import matplotlib.patches
import numpy as np
# create an ellipse
el = matplotlib.patches.Ellipse((50,-23), 10, 13.7, 30, facecolor=(1,0,0,.2), edgecolor='none')
# find the bounding box of the ellipse
bb = el.get_window_extent()
# calculate the x and y points possibly within the ellipse
x_int = np.arange(np.ceil(bb.x0), np.floor(bb.x1) + 1, dtype='int')
y_int = np.arange(np.ceil(bb.y0), np.floor(bb.y1) + 1, dtype='int')
# create a list of possible coordinates
g = np.meshgrid(x_int, y_int)
coords = np.array(zip(*(c.flat for c in g)))
# create a list of transformed points (transformed so that the ellipse is a unit circle)
transcoords = el.get_transform().inverted().transform(coords)
# find the transformed coordinates which are within a unit circle
validcoords = transcoords[:,0]**2 + transcoords[:,1]**2 < 1.0
# create the list of valid coordinates (from untransformed)
ellipsepoints = coords[validcoords]
# just to see if this works
fig = plt.figure()
ax = fig.add_subplot(111)
ax.add_artist(el)
ep = np.array(ellipsepoints)
ax.plot(ellipsepoints[:,0], ellipsepoints[:,1], 'ko')
Seems to work:
(Zooming in reveals that even the points hanging on the edge are inside.)
The point here is that matplotlib handles ellipses as transformed circles (translate, rotate, scale, anything affine). If the transform is applied in reverse, the result is a unit circle at origin, and it is very simple to check if a point is within that.
Just a word of warning: The get_window_extent may not be extremely reliable, as it seems to use the spline approximation of a circle. Also, see tcaswell's comment on the renderer-dependency.
In order to find a more reliable bounding box, you may:
create a horizontal and vertical vector into the plot coordinates (their position is not important, ([0,0],[1,0]) and ([0,0], [0,1]) will do)
transform these vectors into the ellipse coordinates (the get_transform, etc.)
find in the ellipse coordinate system (i.e. the system where the ellipse is a unit circle around the origin) the four tangents of the circle which are parallel to these two vectors
find the intersection points of the vectors (4 intersections, but 2 diagonal will be enough)
transform the intersection points back to the plot coordinates
This will give an accurate (but of course limited by the numerical precision) square bounding box.
However, you may use a simple approximation:
all possible points are within a circle whose center is the same as that of the ellipse and whose diameter is the same as that of the major axis of the ellipse
In other words, all possible points are within a square bounding box which is between x0+-m/2, y0+-m/2, where (x0, y0) is the center of the ellipse and m the major axis.
I'd like to offer another solution that uses the Path object's contains_points() method instead of contains_point():
First get the coordinates of the ellipse and make it into a Path object:
elpath=Path(el.get_verts())
(NOTE that el.get_paths() won't work for some reason.)
Then call the path's contains_points():
validcoords=elpath.contains_points(coords)
Below I'm comparing #tacaswell's solution (method 1), #Drv's (method 2) and my own (method 3) (I've enlarged the ellipse by ~5 times):
import numpy
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
from matplotlib.path import Path
import time
#----------------Create an ellipse----------------
el=Ellipse((50,-23),50,70,30,facecolor=(1,0,0,.2), edgecolor='none')
#---------------------Method 1---------------------
t1=time.time()
for ii in range(50):
y=numpy.arange(-100,50)
x=numpy.arange(-30,130)
g=numpy.meshgrid(x,y)
coords=numpy.array(zip(*(c.flat for c in g)))
ellipsepoints = numpy.vstack([p for p in coords if el.contains_point(p, radius=0)])
t2=time.time()
print 'time of method 1',t2-t1
#---------------------Method 2---------------------
t2=time.time()
for ii in range(50):
y=numpy.arange(-100,50)
x=numpy.arange(-30,130)
g=numpy.meshgrid(x,y)
coords=numpy.array(zip(*(c.flat for c in g)))
invtrans=el.get_transform().inverted()
transcoords=invtrans.transform(coords)
validcoords=transcoords[:,0]**2+transcoords[:,1]**2<=1.0
ellipsepoints=coords[validcoords]
t3=time.time()
print 'time of method 2',t3-t2
#---------------------Method 3---------------------
t3=time.time()
for ii in range(50):
y=numpy.arange(-100,50)
x=numpy.arange(-30,130)
g=numpy.meshgrid(x,y)
coords=numpy.array(zip(*(c.flat for c in g)))
#------Create a path from ellipse's vertices------
elpath=Path(el.get_verts())
# call contains_points()
validcoords=elpath.contains_points(coords)
ellipsepoints=coords[validcoords]
t4=time.time()
print 'time of method 3',t4-t3
#---------------------Plot it ---------------------
fig,ax=plt.subplots()
ax.add_artist(el)
ep=numpy.array(ellipsepoints)
ax.plot(ellipsepoints[:,0],ellipsepoints[:,1],'ko')
plt.show(block=False)
I got these execution time:
time of method 1 62.2502269745
time of method 2 0.488734006882
time of method 3 0.588987112045
So the contains_point() approach is way slower. The coordinate-transformation method is faster than mine, but when you get irregular shaped contours/polygons, this method would still work.
Finally the result plot:

Categories