0

I have been working with lsq-ellipse package where I get the coordinates of ellipse with the following code below:

from ellipse import LsqEllipse
from matplotlib.patches import Ellipse
coords_D0 = np.array(coords_D0)
reg = LsqEllipse().fit(coords_D0)
center_D0, width_D0, height_D0, phi_D0 = reg.as_parameters()
print(f'center: {center_D0[0]:.3f}, {center_D0[1]:.3f}')
print(f'width: {width_D0:.3f}')
print(f'height: {height_D0:.3f}')
print(f'phi: {phi_D0:.3f}')

However, my coords_D0 variable consists of three coordinates which caused the following error:

ValueError: Received too few samplesGot 3 features, 5 or more required.

But, after looking into some packages and online, I found that sympy also can do Ellipse and I understand that you can extract the centre, vradius and hradius from sympy. But, I would like to know how to get the width, height and phi from sympy and will it be the same as the lsq-ellipse package to be used in Ellipse of matplotlib? I use the values from lsq-ellipse package in matplotlib to form the ellipse part and it can be found in the following code line:

Code:

ellipse_D0 = Ellipse(xy=center_D0, width=2*width_D0, height=2*height_D0, angle=np.rad2deg(phi_D0),edgecolor='b', fc='None', lw=2, label='Fit', zorder=2)

My coordinates are the following:

coords_D0 =
-1.98976     -1.91574
-0.0157721    2.5438
2.00553      -0.628061

# another points
coords_D1 =
-0.195518   0.0273673
-0.655686   -1.45848
-0.447061   -0.168108

# another points
coords_D2 =
-2.28529    0.91896
-2.43207    0.446211
-2.23044    0.200087

Side Question:

Is there a way to fit an ellipse to these coordinates (in general, 3 coordinates or more)?

WDpad159
  • 359
  • 3
  • 15
  • You use `fit`. Two crossing ellipses can have 4 points in common, so there is no unique solution giving less than 5. Did you read the docs? – mikuszefski Nov 09 '21 at 07:53
  • @mikuszefski which package are you hinting? Do you mean fit in the Lsq package I get the error for requiring more features more than 5. – WDpad159 Nov 10 '21 at 08:15
  • Hi, so if I got it right , the `LsqEllipse().fit()` method takes a list of `(x, y)` to fit an ellipse. To give a proper unique result, you need 5 or more values. So naturally you get an error. – mikuszefski Nov 10 '21 at 16:19
  • @mikuszefski I understand. I have been looking online and found these links (https://stackoverflow.com/questions/39693869/fitting-an-ellipse-to-a-set-of-data-points-in-python/48002645) (https://stackoverflow.com/questions/47873759/how-to-fit-a-2d-ellipse-to-given-points) (https://stackoverflow.com/questions/52818206/fitting-an-ellipse-to-a-set-of-2-d-points) (https://stackoverflow.com/questions/39693869/fitting-an-ellipse-to-a-set-of-data-points-in-python/48002645), however, I can’t get proper centre point, width, height and phi using my coordinates. – WDpad159 Nov 11 '21 at 18:58
  • So we are clear that you cannot fit an ellipse providing 3 points. Which coordinates are we talking about then? I think I put an easy to follow code [here](https://stackoverflow.com/questions/61593411/fit-ellipse-over-2d-data-using-python-and-matplotlib/61607790#61607790) – mikuszefski Nov 12 '21 at 06:21
  • @mikuszefski using the coordinates which can be found in this question post. I tried using your answer from the other question and I still can’t get the centre point, width, height and phi. – WDpad159 Nov 12 '21 at 06:51
  • Sorry, but I am not sure if we are talking the same language. You CANNOT get an ellipse from the three coordinates given e.g. in `coords_D0`. Are we clear about this? Three points define a circle. – mikuszefski Nov 12 '21 at 07:23
  • @mikuszefski I do understand you and I feel that it might not be possible to plot, but, that’s why I’m asking in this post if there is a way to plot ellipse using these coordinates and extract required parameters to use for matplotlib ellipse part. – WDpad159 Nov 12 '21 at 08:39
  • Well, no it is not possible to get a unique ellipse. You can get a unique circle. – mikuszefski Nov 12 '21 at 09:46
  • @mikuszefski Well, speaking of circle, I tried a package called ‘circle_fit’ (https://github.com/marian42/circle-fit), but I get a circle passing through the points and centre far away from the points. Is there a way to get a shape that fits with the datapoints? – WDpad159 Nov 12 '21 at 11:40
  • I am a bit lost. Can you please clarify your post. What are the three points in your `coord_D0` etc. and what do you expect to get? – mikuszefski Nov 15 '21 at 06:25
  • @mikuszefski if you copied the coordinates you’ll get a cluster. I would like to get a shape of cluster from these datapoints which is coord_D0, coord_D1 and coord_D2. I would like to get an ellipse shape or circle where I can get the centre of the cluster and shape, radius and angle of rotation. – WDpad159 Nov 15 '21 at 10:22
  • ...oh man...it is really hard to figure out what you actually want, let alone providing the answer. So, is it this: for each set of three points you want the smallest encapsulating ellipse? Basically as a very specific type of convex hull. – mikuszefski Nov 15 '21 at 10:58
  • ...then maybe [this](https://stackoverflow.com/questions/1768197/bounding-ellipse) helps – mikuszefski Nov 15 '21 at 11:05
  • @mikuszefski yes, these coordinates are clusters of certain label and I would like to fit the shape (ellipse/circle/.etc) to fit these datapoints . – WDpad159 Nov 15 '21 at 11:40
  • Once again: You are not clear in what you want. 1) Dou you want one fit for every cluster? 2) Do you want an ellipse through the points ( as said that is not possible) or an enclosing ellipse? Can you update your post to clarify this? A sketch would be very helpful. – mikuszefski Nov 16 '21 at 06:36

1 Answers1

2

Assuming that the OP is about the Minimum Volume Enclosing Ellipse, I'd suggest the following solution.

#! /usr/bin/python3
# coding=utf-8
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse

import numpy as np

from mymodules3.mvee import mvee

coords= list()
coords.append( np.array([
    -1.98976,     -1.91574,
    -0.0157721,    2.5438,
    2.00553,      -0.628061
]).reshape(3,-1) )

coords.append(  np.array([
    -0.195518,   0.0273673,
    -0.655686,   -1.4584,8
    -0.447061,   -0.168108,
]).reshape(3,-1)
)

coords.append( np.array([
    -2.28529,    0.91896,
    -2.43207,    0.446211,
    -2.23044,    0.200087
]).reshape(3,-1)
)


fig = plt.figure()
ax = fig.add_subplot( 1, 1, 1 )

for i, col in enumerate( ['k', 'g', 'm'] ):
    sol = mvee( coords[i] )

    e =Ellipse(
        sol[0],
        width=2 * sol[1][0],
        height=2 * sol[1][1],
        angle=sol[2] *  180/3.1415926,
        color=col, alpha=0.5,
        zorder = -1000 + i
    )
    ax.scatter( coords[i][:,0], coords[i][:,1], c=col, zorder=10 * i )
    ax.add_artist( e )
plt.show()

providing

MVEE Solution

The mvee is is based on an SE answer on a similar question.

"""
NMI : 2021-11-11 
Minimum Volume Enclosing Ellipsoids, see e.g.
NIMA MOSHTAGH : MINIMUM VOLUME ENCLOSING ELLIPSOIDS
or 
Linus Källberg : Minimum_Enclosing_Balls_and_Ellipsoids (Thesis)
"""
from warnings import warn

from numpy import pi
from numpy import sqrt
from numpy import arccos
from numpy import dot, outer
from numpy import diag, transpose
from numpy import append
from numpy import asarray
from numpy import ones
from numpy import argmax

from numpy.linalg import inv
from numpy.linalg import norm
from numpy.linalg import eig


def mvee( data, tolerance=1e-4, maxcnt=1000 ):
    """
    param data: list of xy data points
    param tolerance: termination condition for iterative approximation
    param maxcnt: maximum number of iterations
    type data: iterable of float
    type tolerance: float
    return: (offset, semiaxis, angle)
    return type: ( (float, float), (float, float), float )
    """
    locdata = asarray( data )
    N = len( locdata )
    if not locdata.shape == ( N, 2):
        raise ValueError ( " data must be of shape( n, 2 )" )
    if tolerance >= 1 or tolerance <= 0:
        raise ValueError (" 0 < tolerance < 1 required")
    if not isinstance( maxcnt, int ):
        raise TypeError
    if not maxcnt > 0:
        raise ValueError
    count = 1
    err = 1
    d = 2
    d1 = d + 1
    u = ones( N ) / N
    P = transpose( locdata )
    Q = append( P, ones( N ) ).reshape( 3, -1 )
    while ( err > tolerance):
        X = dot( Q, dot( diag( u ), transpose( Q ) ) )
        M = diag( 
            dot( 
                transpose( Q ),
                dot(
                    inv( X ),
                    Q
                )
            )
        )
        maximum = max( M )
        j = argmax( M )
        step_size = ( maximum - d1 ) / ( d1 * ( maximum - 1 ) )
        new_u = ( 1 - step_size ) * u
        new_u[ j ] += step_size
        err = norm( new_u - u )
        count = count + 1
        u = new_u
        if count > maxcnt:
            warn(
                "Process did not converge in {} steps".format(
                    count - 1
                ),
                UserWarning
            )
            break
    U = diag( u )
    c = dot( P,  u )
    A = inv(
        dot(
            P,
            dot( U, transpose( P ) )
        ) - outer( c, c )
    ) / d
    E, V = eig( A )
    phiopt = arccos( V[ 0, 0 ] )
    if V[ 0, 1 ] < 0: 
        phiopt = 2 * pi - phiopt
    ### cw vs ccw and periodicity of pi
    phiopt = -phiopt % pi
    sol =  (  c, sqrt( 1.0 / E ), phiopt)
    return sol
mikuszefski
  • 3,943
  • 1
  • 25
  • 38
  • Thank you for the answer. I looked into the code, link and I managed to get it to work. I just have a question, about the tolerance and maxcnt. What parameters I could use to fit the ellipse to the data points? – WDpad159 Nov 24 '21 at 12:53
  • @WDpad159 As you can see, my image is using standard settings. `maxcnt` shouldn't be the problem as you expect convergence and the loop should terminate before this is reached. I did not go into fully understanding the theory so I do not have a good feeling for what this tolerance means in details. Sure, it is the change of length in the `u` vector. which somehow encodes the eigenvalues of `A`. As this is a gradient method, it is testing how much the gradient is changing. If you have a good assumption about the shape of the extremum that gives you a hint how far you are away... – mikuszefski Nov 24 '21 at 13:36
  • ... if you have now clue, it might be a very shallow extremum and even with a very small gradient you can be miles off. tldr; I don't know. Play with it. – mikuszefski Nov 24 '21 at 13:38