0

I would like to utilize the following code to fit some 2D data to an ellipse, which I got from this post.

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 ||Mx - b ||^2
M = np.hstack([X**2, X * Y, Y**2, X, Y])
b = np.ones_like(X)
x = np.linalg.lstsq(M, 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()

However, I need to use the full general equation: Ax^2 + Bxy + Cy^2 + Dx + Ey + F = 1, which I got from here and includes tilt and translation of the ellipse.

In the above code, the M matrix:

M = np.hstack([X**2, X * Y, Y**2, X, Y])

assumes I'm fitting Ax^2 + Bxy + Cy^2 + Dx + Ey = 1.

Question: How do I "add" in the F coefficient, or is that not possible using this method?

D.Gordon
  • 35
  • 5
  • 1
    Have you tried adding `np.ones_like(X)` to `M` (after `Y`)? – jared Jun 14 '23 at 17:38
  • 1
    But you can't. That would make no sense. Because if what you are trying to solve is AX²+BXY+CY²+DX+EY+F=1, I can already give your the perfect answer the least square will come up with: `A=0, B=0, C=0, D=0, E=0, F=1`. So 1=1. Can't beat that error-wise. Your F is already included in the left part of the equation. The regression you are already doing is, in fact αX²+βXY+γY²+δX+ζY+φ=0. And since this equation is true to a multiplicative factor, it is the same as -α/φX²-β/φXY-γ/φY²-δ/φX-ζ/φY=1, with A=-α/φ, B=-β/φ, C=-γ/φ, D=-δ/φ, E=-ζ/φ – chrslg Jun 14 '23 at 21:21
  • 1
    To simplify to an equivalent but simpler problem, it is as if you were trying to solve 2x=3 (solution x=3/2, obviously), and were trying to "add more precision" by solving "2x=y" instead (solution: an infinite bunch of them, including x=y=0). It does not add precision. It just add non-definition to your problem – chrslg Jun 14 '23 at 21:23
  • 1
    Another (more positive maybe) way to tell it is, you already have F. It is 0. By definition of least square, any other F than 0 would worsen the result. Or, said yet otherwise, any F can fit, but then you have to change A,B,C,D,E. If I call A,B,C,D,E the solution of your current regression, then, f'or any F, the best solution to A'X²+B'XY+C'Y²+D'X+E'Y+F'=1 is A*(1-F)X²+B*(1-F)XY+C*(1-F)Y²+D*(1-F)X+E*(1-F)Y+F=1. (So A'=A(1-F), B'=B(1-F) and so on). WIth F=0, you get your current solution. With F=1, the perfect solution 1=1. And for other F a linear combination of both. – chrslg Jun 14 '23 at 21:33
  • If it is not for academic reason that you need to implement this yourself, you could/should use a library which already does what you need, e.g. https://scikit-image.org/docs/stable/api/skimage.measure.html#skimage.measure.EllipseModel – Carlos Horn Jun 15 '23 at 20:29

1 Answers1

1

Indeed it is possible with a little knowledge in optimization. We know that the trivial solution for F is certainly F=1 and other parameters equal to 0, but we don't want that and there is probably another solution other than the trivial solution. So let's get into some theory.

In the standard least squares method we have

Standard least squares
standardlstsq method

But there are several ways to extend this method so that it can meet the demands of each problem. In your case, what we know is that the trivial solution resets the parameters A, B, C, D and E, that is, the simplest solution is to think that these for meters cannot be Zero

So, a solution is to add a regularizer that adds to our objective function, a term that adds an error smaller and smaller if our parameters distance from zero. One of the ways is: to obtain the inverse of the sum of the parameters we have (the higher the sum of the parameters, the smaller the penalty from the regularizer will be.), in the way that Regularizer = alpha/sum(|x|). And sum(|x|) is L1 norm in Linear Algebra. Increase alpha to increase the strength of the regularizer

So our LSTSQ is

Our modified least squares
enter image description here

but be careful, because the norm of x cannot be zero, we will deal with this in code.

So, let's code

Step 1

Import minimize from scipy

from scipy.optimize import minimize

Step 2

Create your own minimize function

(the adjusted variable always comes before the other parameters, that's why x comes before)

def target_func(x, M, b):
    norm = np.linalg.norm(x, ord=1)     # get L1 norm of x
    norm = norm if norm != 0 else 1e-6  # Verify Zero, in case Zero, place a tolerance (1e-6 in this example)
    reg = 100/(norm)                    # Evaluate regularizer (using alpha= 100 for this exemple)
    return np.linalg.norm(M@x - b, ord=2) + reg # Write de minimize function based on lstsq

Step 3

Create parameters and apply on minimize function

M = np.hstack([X**2, X * Y, Y**2, X, Y, np.ones_like(X)]) # Add ones for param F
b = np.ones_like(X).flatten()                             # Create b (as flatten for prevent problems)
x0 = np.random.rand(6)                                    # Create Initial solution for our problem

res_log = minimize(target_func,                           # Our minimize target
                   x0,                                    # Initial solution
                   args=(M, b),                           # Args for problem (M and b)
)
x = res_log.x # Get the x 

Step 4

Execute the same code for plot, now with the x[5] (F param).

# 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 + {5:.3} = 1'.format(*x))

# 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 + x[5] # Add x[5]
plt.contour(X_coord, Y_coord, Z_coord, levels=[1], colors=('r'), linewidths=2)

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

Result

The ellipse is given by -0.337x^2 + -0.143xy + -0.543y^2 + -0.0325x + -0.0331y + 5.33 = 1

enter image description here

Minimize of Scipy

Basically the "minimize" function works by iterations, where it calculates where the cost function has the lowest value, so it adjusts the parameters at each iteration until it finds the best ones given a tolerance and maximum number of iterations allowed.

See more on docs: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html

  • Hi William, thank you for your answer, I'm having a bit of trouble with the bit of code in Step 3. What is "target_func"? I know it refers to the function I'm trying to minimize, but I thought you were attempting to minimize the function in Step 2. Is that correct? – D.Gordon Jun 19 '23 at 13:58
  • Ops, my bad, you are correct, 'target_func' is 'minimize', i will edit the answer – William Hideki Nakata Jun 19 '23 at 17:26