@@ -278,6 +278,9 @@ def __str__(self) -> str:
278278 """
279279 return f"Polygon2 with { len (self .path )} vertices"
280280
281+ def __repr__ (self ) -> str :
282+ return str (self )
283+
281284 def __len__ (self ) -> int :
282285 """
283286 Number of vertices in polygon
@@ -671,7 +674,7 @@ def __init__(
671674 radii : Optional [ArrayLike2 ] = None ,
672675 E : Optional [NDArray ] = None ,
673676 centre : ArrayLike2 = (0 , 0 ),
674- theta : float = None ,
677+ theta : Optional [ float ] = None ,
675678 ):
676679 r"""
677680 Create an ellipse
@@ -686,22 +689,23 @@ def __init__(
686689 :type theta: float, optional
687690 :raises ValueError: bad parameters
688691
689- The ellipse shape can be specified by ``radii`` and ``theta`` or by a 2x2
690- matrix ``E``.
692+ The ellipse shape can be specified by ``radii`` and ``theta`` or by a
693+ symmetric 2x2 matrix ``E``.
691694
692- Internally the ellipse is represented by a 2x2 matrix and the centre coordinate
693- such that
695+ Internally the ellipse is represented by a symmetric matrix :math:`\mat{E} \in \mathbb{R}^{2\times 2}`
696+ and its centre coordinate :math:`\vec{x}_0 \in \mathbb{R}^2` such that
694697
695698 .. math::
696699
697- (\vec{x} - \vec{x}_0)^T \mat{E} (\vec{x} - \vec{x}_0) = 1
700+ (\vec{x} - \vec{x}_0)^{\top} \mat{E} \, (\vec{x} - \vec{x}_0) = 1
698701
699702 Example:
700703
701704 .. runblock:: pycon
702705
703706 >>> from spatialmath import Ellipse
704- >>> Ellipse(radii=(1,2), theta=0
707+ >>> import numpy as np
708+ >>> Ellipse(radii=(1,2), theta=0)
705709 >>> Ellipse(E=np.array([[1, 1], [1, 2]]))
706710
707711 """
@@ -724,30 +728,43 @@ def __init__(
724728 self ._centre = centre
725729
726730 @classmethod
727- def Polynomial (cls , e : ArrayLike ) -> Self :
731+ def Polynomial (cls , e : ArrayLike , p : Optional [ ArrayLike2 ] = None ) -> Self :
728732 r"""
729733 Create an ellipse from polynomial
730734
731735 :param e: polynomial coeffients :math:`e` or :math:`\eta`
732736 :type e: arraylike(4) or arraylike(5)
737+ :param p: point to set scale
738+ :type p: array_like(2), optional
733739 :return: an ellipse instance
734740 :rtype: Ellipse
735741
736- An ellipse can be specified by a polynomial
742+ An ellipse can be specified by a polynomial :math:`\vec{e} \in \mathbb{R}^6`
737743
738744 .. math::
739745
740746 e_0 x^2 + e_1 y^2 + e_2 xy + e_3 x + e_4 y + e_5 = 0
741747
742- or
748+ or :math:`\vec{\epsilon} \in \mathbb{R}^5` where the leading coefficient is
749+ implicitly one
743750
744751 .. math::
745752
746- x^2 + \eta_1 y^2 + \eta_2 xy + \eta_3 x + \eta_4 y + \eta_5 = 0
753+ x^2 + \epsilon_1 y^2 + \epsilon_2 xy + \epsilon_3 x + \epsilon_4 y + \epsilon_5 = 0
754+
755+ In this latter case, position, orientation and aspect ratio of the
756+ ellipse will be correct, but the overall scale of the ellipse is not
757+ determined. To correct this, we can pass in a single point ``p`` that
758+ we know lies on the perimeter of the ellipse.
747759
748- The ellipse matrix and centre coordinate are determined from the polynomial
749- coefficients.
760+ Example:
761+
762+ .. runblock:: pycon
750763
764+ >>> from spatialmath import Ellipse
765+ >>> Ellipse.Polynomial([0.625, 0.625, 0.75, -6.75, -7.25, 24.625])
766+
767+ :seealso: :meth:`polynomial`
751768 """
752769 e = np .array (e )
753770 if len (e ) == 5 :
@@ -765,15 +782,13 @@ def Polynomial(cls, e: ArrayLike) -> Self:
765782 # fmt: on
766783
767784 # solve for the centre
768- # fmt: off
769- M = - 2 * np .array ([
770- [a , c ],
771- [c , b ],
772- ])
773- # fmt: on
774- centre = np .linalg .lstsq (M , e [3 :5 ], rcond = None )[0 ]
785+ centre = np .linalg .lstsq (- 2 * E , e [3 :5 ], rcond = None )[0 ]
775786
776- z = e [5 ] - a * centre [0 ] ** 2 - b * centre [1 ] ** 2 - 2 * c * np .prod (centre )
787+ if p is not None :
788+ # point was passed in, use this to set the scale
789+ p = smb .getvector (p , 2 ) - centre
790+ s = p @ E @ p
791+ E /= s
777792
778793 return cls (E = E , centre = centre )
779794
@@ -819,6 +834,19 @@ def FromPerimeter(cls, p: Points2) -> Self:
819834 :type p: ndarray(2,N)
820835 :return: an ellipse instance
821836 :rtype: Ellipse
837+
838+ Example:
839+
840+ .. runblock:: pycon
841+
842+ >>> from spatialmath import Ellipse
843+ >>> import numpy as np
844+ >>> eref = Ellipse(radii=(1, 2), theta=np.pi / 4, centre=[3, 4])
845+ >>> perim = eref.points()
846+ >>> print(perim.shape)
847+ >>> Ellipse.FromPerimeter(perim)
848+
849+ :seealso: :meth:`points`
822850 """
823851 A = []
824852 b = []
@@ -829,8 +857,8 @@ def FromPerimeter(cls, p: Points2) -> Self:
829857 # x^2 + eta[0] y^2 + eta[1] xy + eta[2] x + eta[3] y + eta[4] = 0
830858 e = np .linalg .lstsq (A , b , rcond = None )[0 ]
831859
832- # solve for the quadratic term
833- return cls .Polynomial (e )
860+ # create ellipse from the polynomial, using one point to set scale
861+ return cls .Polynomial (e , p [:, 0 ] )
834862
835863 def __str__ (self ) -> str :
836864 return f"Ellipse(radii={ self .radii } , centre={ self .centre } , theta={ self .theta } )"
@@ -846,13 +874,22 @@ def E(self):
846874 :return: ellipse matrix
847875 :rtype: ndarray(2,2)
848876
849- The matrix ``E`` describes the shape of the ellipse
877+ The symmetric matrix :math:`\mat{E} \in \mathbb{R}^{2\times 2}` determines the radii and
878+ the orientation of the ellipse
850879
851880 .. math::
852881
853- (\vec{x} - \vec{x}_0)^T \mat{E} (\vec{x} - \vec{x}_0) = 1
882+ (\vec{x} - \vec{x}_0)^{\top} \mat{E} \, (\vec{x} - \vec{x}_0) = 1
854883
855884 :seealso: :meth:`centre` :meth:`theta` :meth:`radii`
885+
886+ Example:
887+
888+ .. runblock:: pycon
889+
890+ >>> from spatialmath import Ellipse
891+ >>> e = Ellipse(radii=(1,2), centre=(3,4), theta=0.5)
892+ >>> e.E
856893 """
857894 # return 2x2 ellipse matrix
858895 return self ._E
@@ -865,6 +902,14 @@ def centre(self) -> R2:
865902 :return: centre of the ellipse
866903 :rtype: ndarray(2)
867904
905+ Example:
906+
907+ .. runblock:: pycon
908+
909+ >>> from spatialmath import Ellipse
910+ >>> e = Ellipse(radii=(1,2), centre=(3,4), theta=0.5)
911+ >>> e.centre
912+
868913 :seealso: :meth:`radii` :meth:`theta` :meth:`E`
869914 """
870915 # return centre
@@ -878,6 +923,14 @@ def radii(self) -> R2:
878923 :return: radii of the ellipse
879924 :rtype: ndarray(2)
880925
926+ Example:
927+
928+ .. runblock:: pycon
929+
930+ >>> from spatialmath import Ellipse
931+ >>> e = Ellipse(radii=(1,2), centre=(3,4), theta=0.5)
932+ >>> e.radii
933+
881934 :seealso: :meth:`centre` :meth:`theta` :meth:`E`
882935 """
883936 return np .linalg .eigvals (self .E ) ** (- 0.5 )
@@ -890,6 +943,14 @@ def theta(self) -> float:
890943 :return: orientation in radians, in the interval [-pi, pi)
891944 :rtype: float
892945
946+ Example:
947+
948+ .. runblock:: pycon
949+
950+ >>> from spatialmath import Ellipse
951+ >>> e = Ellipse(radii=(1,2), centre=(3,4), theta=0.5)
952+ >>> e.theta
953+
893954 :seealso: :meth:`centre` :meth:`radii` :meth:`E`
894955 """
895956 e , x = np .linalg .eigh (self .E )
@@ -903,20 +964,41 @@ def area(self) -> float:
903964
904965 :return: area
905966 :rtype: float
967+
968+ Example:
969+
970+ .. runblock:: pycon
971+
972+ >>> from spatialmath import Ellipse
973+ >>> e = Ellipse(radii=(1,2), centre=(3,4), theta=0.5)
974+ >>> e.area
906975 """
907976 return np .pi / np .sqrt (np .linalg .det (self .E ))
908977
909978 @property
910979 def polynomial (self ):
911- """
980+ r """
912981 Return ellipse as a polynomial
913982
914983 :return: polynomial
915984 :rtype: ndarray(6)
916985
986+ An ellipse can be described by :math:`\vec{e} \in \mathbb{R}^6` which are the
987+ coefficents of a quadratic in :math:`x` and :math:`y`
988+
917989 .. math::
918990
919991 e_0 x^2 + e_1 y^2 + e_2 xy + e_3 x + e_4 y + e_5 = 0
992+
993+ Example:
994+
995+ .. runblock:: pycon
996+
997+ >>> from spatialmath import Ellipse
998+ >>> e = Ellipse(radii=(1,2), centre=(3,4), theta=0.5)
999+ >>> e.polynomial
1000+
1001+ :seealso: :meth:`Polynomial`
9201002 """
9211003 a = self ._E [0 , 0 ]
9221004 b = self ._E [1 , 1 ]
@@ -930,15 +1012,15 @@ def polynomial(self):
9301012 2 * c ,
9311013 - 2 * a * x_0 - 2 * c * y_0 ,
9321014 - 2 * b * y_0 - 2 * c * x_0 ,
933- a * x_0 ** 2 + b * y_0 ** 2 ,
1015+ a * x_0 ** 2 + b * y_0 ** 2 + 2 * c * x_0 * y_0 ,
9341016 ]
9351017 )
9361018
9371019 def plot (self , ** kwargs ) -> None :
9381020 """
9391021 Plot ellipse
9401022
941- :param kwargs: arguments passed to :func:`~spatialmath.base.graphics.plot_ellipse
1023+ :param kwargs: arguments passed to :func:`~spatialmath.base.graphics.plot_ellipse`
9421024 :return: list of artists
9431025 :rtype: _type_
9441026
@@ -956,17 +1038,19 @@ def plot(self, **kwargs) -> None:
9561038
9571039 from spatialmath import Ellipse
9581040 from spatialmath.base import plotvol2
959- plotvol2(5)
1041+ ax = plotvol2(5)
9601042 e = Ellipse(E=np.array([[1, 1], [1, 2]]))
9611043 e.plot()
1044+ ax.grid()
9621045
9631046 .. plot::
9641047
9651048 from spatialmath import Ellipse
9661049 from spatialmath.base import plotvol2
967- plotvol2(5)
1050+ ax = plotvol2(5)
9681051 e = Ellipse(E=np.array([[1, 1], [1, 2]]))
9691052 e.plot(filled=True, color='r')
1053+ ax.grid()
9701054
9711055 :seealso: :func:`~spatialmath.base.graphics.plot_ellipse`
9721056 """
@@ -980,6 +1064,16 @@ def contains(self, p):
9801064 :type p: arraylike(2), ndarray(2,N)
9811065 :return: true if point is contained within ellipse
9821066 :rtype: bool or list(bool)
1067+
1068+ Example:
1069+
1070+ .. runblock:: pycon
1071+
1072+ >>> from spatialmath import Ellipse
1073+ >>> e = Ellipse(radii=(1,2), centre=(3,4), theta=0.5)
1074+ >>> e.contains((3,4))
1075+ >>> e.contains((0,0))
1076+
9831077 """
9841078 inside = []
9851079 p = smb .getmatrix (p , (2 , None ))
@@ -1001,24 +1095,46 @@ def points(self, resolution=20) -> Points2:
10011095 :return: set of perimeter points
10021096 :rtype: Points2
10031097
1004- Return a set of `resolution` points on the perimeter of the ellipse. The perimeter
1098+ Return a set of `` resolution` ` points on the perimeter of the ellipse. The perimeter
10051099 set is not closed, that is, last point != first point.
10061100
1007- :seealso: :func:`~spatialmath.base.graphics.ellipse`
1101+
1102+ Example:
1103+
1104+ .. runblock:: pycon
1105+
1106+ >>> from spatialmath import Ellipse
1107+ >>> e = Ellipse(radii=(1,2), centre=(3,4), theta=0.5)
1108+ >>> e.points()[:,:5] # first 5 points
1109+
1110+ :seealso: :meth:`polygon` :func:`~spatialmath.base.graphics.ellipse`
10081111 """
10091112 return smb .ellipse (self .E , self .centre , resolution = resolution )
10101113
1114+ def polygon (self , resolution = 10 ) -> Polygon2 :
1115+ """
1116+ Approximate with a polygon
1117+
1118+ :param resolution: number of polygon vertices, defaults to 10
1119+ :type resolution: int, optional
1120+ :return: a polygon approximating the ellipse
1121+ :rtype: :class:`Polygon2` instance
1122+
1123+ Return a polygon instance with ``resolution`` vertices. A :class:`Polygon2`` can be
1124+ used for intersection testing with lines or other polygons.
1125+
1126+ Example:
1127+
1128+ .. runblock:: pycon
1129+
1130+ >>> from spatialmath import Ellipse
1131+ >>> e = Ellipse(radii=(1,2), centre=(3,4), theta=0.5)
1132+ >>> e.polygon()
1133+
1134+ :seealso: :meth:`points`
1135+ """
1136+ return Polygon2 (smb .ellipse (self .E , self .centre , resolution = resolution - 1 ))
10111137
1012- # alpha, beta, gamma, xc, yc, e0, e1, e2, e3, e4, e5 = symbols("alpha, beta, gamma, xc, yc, e0, e1, e2, e3, e4, e5")
1013- # solve(eq, [alpha, beta, gamma, xc, yc])
1014- # eq = [
1015- # alpha - e0,
1016- # beta- e1,
1017- # 2 * gamma - e2,
1018- # -2 * (alpha * xc + gamma * yc) - e3,
1019- # -2 * (beta * yc + gamma * xc) - e4,
1020- # alpha * xc**2 + beta * yc**2 + 2 * gamma * xc * yc - 1 - e5
1021- # ]
10221138
10231139if __name__ == "__main__" :
10241140 pass
0 commit comments