From 6d01022079f61b0d361c911ad1921658ebcc38ae Mon Sep 17 00:00:00 2001 From: xixihaha-ha <182837830+xixihaha-ha@users.noreply.github.com> Date: Fri, 6 Nov 2026 11:15:04 +0800 Subject: [PATCH] Normalize ellipse polynomial coefficients using the constant term --- spatialmath/geom2d.py | 9 +++++---- tests/test_geom2d.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/spatialmath/geom2d.py b/spatialmath/geom2d.py index ee42c08f..2bf391c1 100755 --- a/spatialmath/geom2d.py +++ b/spatialmath/geom2d.py @@ -765,10 +765,8 @@ def Polynomial(cls, e: ArrayLike, p: Optional[ArrayLike2] = None) -> Self: x^2 + \epsilon_1 y^2 + \epsilon_2 xy + \epsilon_3 x + \epsilon_4 y + \epsilon_5 = 0 - In this latter case, position, orientation and aspect ratio of the - ellipse will be correct, but the overall scale of the ellipse is not - determined. To correct this, we can pass in a single point ``p`` that - we know lies on the perimeter of the ellipse. + Both forms determine the position, orientation and size of the ellipse. + An optional perimeter point ``p`` can be used to override the size. Example: @@ -802,6 +800,9 @@ def Polynomial(cls, e: ArrayLike, p: Optional[ArrayLike2] = None) -> Self: p = smb.getvector(p, 2) - centre s = p @ E @ p E /= s + else: + # Complete the square: (x-centre).T E (x-centre) = s. + E /= centre @ E @ centre - e[5] return cls(E=E, centre=centre) diff --git a/tests/test_geom2d.py b/tests/test_geom2d.py index 49aa1d8b..a1b80a01 100755 --- a/tests/test_geom2d.py +++ b/tests/test_geom2d.py @@ -142,6 +142,42 @@ def test_intersect_segment(self): class EllipseTest(unittest.TestCase): + def test_polynomial_circle_size(self): + for coefficients, centre in ( + ([1, 1, 0, 0, 0, -4], (0, 0)), + ([1, 1, 0, -6, -8, 21], (3, 4)), + ): + with self.subTest(centre=centre): + circle = Ellipse.Polynomial(coefficients) + nt.assert_allclose(circle.centre, centre) + nt.assert_allclose(circle.radii, [2, 2]) + self.assertAlmostEqual(circle.area, 4 * np.pi) + + def test_polynomial_scale(self): + for theta, centre, scale, implicit in ( + (0, (0, 0), 1, False), + (0, (0, 0), 4, False), + (0, (3, 4), 1, False), + (0.4, (3, -2), 3, False), + (0.4, (3, -2), 1, True), + ): + with self.subTest( + theta=theta, centre=centre, scale=scale, implicit=implicit + ): + expected = Ellipse(radii=(2, 3), theta=theta, centre=centre) + E = expected.E + c = np.array(centre) + linear = -2 * E @ c + coefficients = ( + scale * np.r_[E[0, 0], E[1, 1], 2 * E[0, 1], linear, c @ E @ c - 1] + ) + if implicit: + coefficients = (coefficients / coefficients[0])[1:] + actual = Ellipse.Polynomial(coefficients) + nt.assert_allclose(actual.centre, c, atol=1e-12) + nt.assert_allclose(actual.E, expected.E, atol=1e-12) + self.assertAlmostEqual(actual.area, expected.area) + def test_constructor(self): E = np.array([[1, 1], [1, 3]]) e = Ellipse(E=E)