15

I would like to fit a 2D array by an elliptic function: (x / a)² + (y / b)² = 1 ----> (and so get the a and b)

And then, be able to replot it on my graph. I found many examples on internet, but no one with this simple Cartesian equation. I probably have searched badly ! I think a basic solution for this problem could help many people.

Here is an example of the data:

enter image description here

Sadly, I can not put the values... So let's assume that I have an X,Y arrays defining the coordinates of each of those points.

President James K. Polk
  • 40,516
  • 21
  • 95
  • 125
Agape Gal'lo
  • 687
  • 4
  • 9
  • 23

3 Answers3

19

This can be solved directly using least squares. You can frame this as minimizing the sum of squares of quantity (alpha * x_i^2 + beta * y_i^2 - 1) where alpha is 1/a^2 and beta is 1/b^2. You have all the x_i's in X and the y_i's in Y so you can find the minimizer of ||Ax - b||^2 where A is an Nx2 matrix (i.e. [X^2, Y^2]), x is the column vector [alpha; beta] and b is column vector of all ones.

The following code solves the more general problem for an ellipse of the form Ax^2 + Bxy + Cy^2 + Dx +Ey = 1 though the idea is exactly the same. The print statement gives 0.0776x^2 + 0.0315xy+0.125y^2+0.00457x+0.00314y = 1 and the image of the ellipse generated is also below

import numpy as np
import matplotlib.pyplot as plt
alpha = 5
beta = 3
N = 500
DIM = 2

np.random.seed(2)

# Generate random points on the unit circle by sampling uniform angles
theta = np.random.uniform(0, 2*np.pi, (N,1))
eps_noise = 0.2 * np.random.normal(size=[N,1])
circle = np.hstack([np.cos(theta), np.sin(theta)])

# Stretch and rotate circle to an ellipse with random linear tranformation
B = np.random.randint(-3, 3, (DIM, DIM))
noisy_ellipse = circle.dot(B) + eps_noise

# Extract x coords and y coords of the ellipse as column vectors
X = noisy_ellipse[:,0:1]
Y = noisy_ellipse[:,1:]

# Formulate and solve the least squares problem ||Ax - b ||^2
A = np.hstack([X**2, X * Y, Y**2, X, Y])
b = np.ones_like(X)
x = np.linalg.lstsq(A, b)[0].squeeze()

# Print the equation of the ellipse in standard form
print('The ellipse is given by {0:.3}x^2 + {1:.3}xy+{2:.3}y^2+{3:.3}x+{4:.3}y = 1'.format(x[0], x[1],x[2],x[3],x[4]))

# Plot the noisy data
plt.scatter(X, Y, label='Data Points')

# Plot the original ellipse from which the data was generated
phi = np.linspace(0, 2*np.pi, 1000).reshape((1000,1))
c = np.hstack([np.cos(phi), np.sin(phi)])
ground_truth_ellipse = c.dot(B)
plt.plot(ground_truth_ellipse[:,0], ground_truth_ellipse[:,1], 'k--', label='Generating Ellipse')

# Plot the least squares ellipse
x_coord = np.linspace(-5,5,300)
y_coord = np.linspace(-5,5,300)
X_coord, Y_coord = np.meshgrid(x_coord, y_coord)
Z_coord = x[0] * X_coord ** 2 + x[1] * X_coord * Y_coord + x[2] * Y_coord**2 + x[3] * X_coord + x[4] * Y_coord
plt.contour(X_coord, Y_coord, Z_coord, levels=[1], colors=('r'), linewidths=2)

plt.legend()
plt.xlabel('X')
plt.ylabel('Y')
plt.show()

enter image description here

Casey
  • 417
  • 3
  • 10
  • Perfect. So just to be sure to understand: If I have the case **(x / a)² + (y / b)² = 1**, then, in your equation **Ax^2 + Bxy + Cy^2 + Dx +Ey = 1** B = D = E = 0. I will validate you answer, but before can you add a replot of the ellipse using the A, B, C, D and E founded through minimization ? – Agape Gal'lo Dec 19 '17 at 09:57
  • and I would change **X = np.hstack([x**2, x * y, y**2, x, y ])** --- by --- **X = np.column_stack((x**2, x * y, y**2, x, y))** With pythonxy python2.7 it doens't work ... – Agape Gal'lo Dec 19 '17 at 10:33
  • Yes, just replace the code with X = np.hstack([x^2, y^2]) as you mentioned since B=D=E=0. This of course assumes you know the ellipse is centered at the origin and not rotated. What error are you getting with python2.7? I ran with 3.5 but I'm not steeped in the differences between the two. Sure, I can edit with the minimized coefficients in a bit – Casey Dec 19 '17 at 20:06
  • Can you still use this method to fit a circle? I.e. how do you avoid the trivial solution in that case? – darda Jun 18 '21 at 19:14
  • 1
    If you know a-priori that the data should be circular, then you want to fit $Ax^2 + Bx + Ay^2 + Cy = 1$. Just change the line of the code defining the matrix to be $A = np.hstack([X**2+Y**2, X, Y])$. Otherwise, I'd just leave it as an ellipse and let the optimisation figure out if the coefficients of x**2 and y**2 should be the same. – Casey Jun 19 '21 at 21:11
14

Following the suggestion by ErroriSalvo, here is the complete process of fitting an ellipse using the SVD. The arrays x, y are coordinates of the given points, let's say there are N points. Then U, S, V are obtained from the SVD of the centered coordinate array of shape (2, N). So, U is a 2 by 2 orthogonal matrix (rotation), S is a vector of length 2 (singular values), and V, which we do not need, is an N by N orthogonal matrix.

The linear map transforming the unit circle to the ellipse of best fit is

sqrt(2/N) * U * diag(S) 

where diag(S) is the diagonal matrix with singular values on the diagonal. To see why the factor of sqrt(2/N) is needed, imagine that the points x, y are taken uniformly from the unit circle. Then sum(x**2) + sum(y**2) is N, and so the coordinate matrix consists of two orthogonal rows of length sqrt(N/2), hence its norm (the largest singular value) is sqrt(N/2). We need to bring this down to 1 to have the unit circle.

N = 300
t = np.linspace(0, 2*np.pi, N)
x = 5*np.cos(t) + 0.2*np.random.normal(size=N) + 1
y = 4*np.sin(t+0.5) + 0.2*np.random.normal(size=N)
plt.plot(x, y, '.')     # given points

xmean, ymean = x.mean(), y.mean()
x -= xmean
y -= ymean
U, S, V = np.linalg.svd(np.stack((x, y)))

tt = np.linspace(0, 2*np.pi, 1000)
circle = np.stack((np.cos(tt), np.sin(tt)))    # unit circle
transform = np.sqrt(2/N) * U.dot(np.diag(S))   # transformation matrix
fit = transform.dot(circle) + np.array([[xmean], [ymean]])
plt.plot(fit[0, :], fit[1, :], 'r')
plt.show()

example

But if you assume that there is no rotation, then np.sqrt(2/N) * S is all you need; these are a and b in the equation of the ellipse.

  • "But if you assume that there is no rotation, then np.sqrt(2/N) * S is all you need; these are a and b in the equation of the ellipse." Thanks for answering but sorry, but what do you mean ? a = ? b = ? – Agape Gal'lo Dec 19 '17 at 09:39
  • Also, I am using python 2.7 which doesn't recognize np.stack – Agape Gal'lo Dec 19 '17 at 09:43
  • 1
    Actually, I used Python 2.7 when answering this. Your version of NumPy is very old, look into upgrading it. np.stack is not a Python method, it's a NumPy method. –  Dec 19 '17 at 14:39
  • 4
    Good solution. But you have a problem when the points are not equally distributed – Crandel Jul 11 '20 at 07:26
1

You could try a Singular Value Decomposition of the data matrix. https://docs.scipy.org/doc/numpy-1.13.0/reference/generated/numpy.linalg.svd.html

First center the data by subtracting mean values of X,Y from each column respectively.

X=X-np.mean(X)
Y=Y-np.mean(Y)

D=np.vstack(X,Y)

Then, apply SVD and extract -eigenvalues (members of s) -> axis length -eigenvectors(U) -> axis orientation

U, s, V = np.linalg.svd(D, full_matrices=True)

This should be a least-squares fit. Of course, things can get more complicated than this, please see https://www.emis.de/journals/BBMS/Bulletin/sup962/gander.pdf

00__00__00
  • 4,834
  • 9
  • 41
  • 89
  • 1
    Small correction: the eigenvalues are not exactly axes lengths, an adjustement is needed to account for the number of points we have. –  Dec 18 '17 at 21:14
  • 1
    Also centering by subtracting the means only works sufficiently if data points are more or less "evenly" placed on an ellipse. – RadaD Feb 08 '23 at 16:02
  • any better alternatives? – 00__00__00 Feb 27 '23 at 16:36