Skip to content

Ellipse convention is inconsistent across fitting, coordinates, shapes and consumers #47

Description

@PierreRaybaut

Summary

An audit carried out before the 1.1.6 release showed that the ellipse convention is inconsistent across the whole chain — fitting, coordinate conversion, shape objects, result headers and downstream consumers. Two independent defects partially compensate each other, which is why the existing tests pass and why the problem has survived a fix-then-revert cycle.

This issue deliberately covers the entire ellipse chain at once, rather than patching a single function, so that the convention is settled once and for all.

Defect 1 — fit_ellipse_model violates its documented contract

sigima/tools/image/preprocessing.py::fit_ellipse_model documents its return value as (xc, yc, a, b, theta) "where a and b are semi-major and semi-minor axes".

Measured on synthetic ellipses with true semi-axes of 30 (major) and 10 (minor):

true θ a returned b returned θ returned 90° − θ_returned
9.934 30.108 90.00° 0.00°
20° 10.020 29.686 70.25° 19.75°
30° 10.026 30.067 59.93° 30.07°
45° 10.060 29.718 45.00° 45.00°
70° 10.019 29.686 19.74° 70.26°

So a is always the semi-minor axis, b the semi-major axis, and θ is mirrored (90° − θ_true).

Both scikit-image branches swap the axes:

  • new API: a, b = model.axis_lengths[1], model.axis_lengths[0], with a comment claiming axis_lengths is (semi_row, semi_col) — it is in fact (semi-major, semi-minor), so the swap is unjustified;
  • legacy API: yc, xc, b, a, theta = model.params — same inversion.

The TypeError fallback _estimate_ellipse_params is inconsistent with both: it swaps x/y and width/height but returns phi unmirrored.

Defect 2 — the minor-axis diameter is not perpendicular to the major axis

sigima/tools/coordinates.py, lines 101 and 120:

dxa, dya = a * np.cos(theta), a * np.sin(theta)
dxb, dyb = b * np.sin(theta), b * np.cos(theta)   # missing minus sign

The dot product of the two returned axes is a·b·sin(2θ), which is non-zero. Measured angle between the two axes returned by ellipse_to_diameters:

θ 15° 30° 45° 135°
angle between axes 90° 60° 30° 180°

At θ = 45° the two "axes" are collinear. The correct form is dxb, dyb = -b * np.sin(theta), b * np.cos(theta).

Why this was not caught

  • ellipse_to_center_axes_angle (and its array variant) derives θ and both axis lengths from the major-axis endpoints only. The round-trip test sigima/tests/common/tools_coordinates_geom_unit_test.py::test_ellipse_roundtrip_scalar therefore passes while completely ignoring the faulty quantity.
  • sigima/tests/image/preprocessing_tools_unit_test.py::test_fit_ellipse_model_success unpacks yc, xc, a, b, theta — i.e. it was written to match the implementation, not the documented contract.

History

  • d47c6e4 (2026-05-28), "fix(ellipse): correct sign for ellipse minor axis diameter calculation", applied the correct -b*sin form and shipped in v1.1.3.
  • dfe62a3 (2026-06-04), "fix sign after regression", reverted it, with no accompanying test and no release-note change. It shipped in v1.1.4 and v1.1.5.
  • doc/release_notes/release_1.01.md still claims, in its v1.1.3 section, that this bug is fixed.

Defect 1 is almost certainly the reason defect 2 was reintroduced: because fit_ellipse_model returns a mirrored angle, the wrong sign in coordinates.py makes the major axis look right on screen, which was mistaken for a regression when the correct sign was applied.

Blocking prerequisite

The _USE_NEW_SHAPE_API branch of fit_ellipse_model is never exercised in the current development environment (installed scikit-image is < 0.26, so EllipseModel.from_estimate raises AttributeError). Fixing the axis convention there would be done blind — which is exactly how the previous fix-then-revert cycle happened.

A CI matrix covering both scikit-image < 0.26 and >= 0.26 is a prerequisite for this issue.

Scope

  • Add a CI matrix covering scikit-image < 0.26 and >= 0.26
  • sigima/tools/image/preprocessing.py::fit_ellipse_model — stop swapping a/b in both branches, mirror the angle into the (x, y) convention, enforce a >= b, fix the misleading comments, document that the input contour is (row, col)
  • sigima/tools/image/preprocessing.py::_estimate_ellipse_params — make the fallback consistent with the library branches
  • sigima/tools/coordinates.py lines 101 and 120 — restore the perpendicular minor axis
  • sigima/tools/coordinates.py::ellipse_to_center_axes_angle (and array variant) — stop deriving the minor axis from the major-axis endpoints
  • sigima/objects/shape.py::EllipseCoordinates — docstring and behaviour
  • sigima/objects/scalar/geometry.py — user-visible ["x", "y", "a", "b", "θ"] result headers
  • Consumers: sigima/viz/viz_plotpy.py, sigima/tools/image/detection.py, sigima/tools/image/geometry.py
  • Replace the round-trip tests with contract tests: a is the semi-major axis, θ is not mirrored, the two returned axes are orthogonal
  • Re-baseline sigima/tests/image/contour_unit_test.py
  • Correct the v1.1.3 release-note claim
  • Open mirror issues in DataLab and DataLab-Web (ellipse result columns and overlays)

Impact

User-visible: the a and b columns of ellipse geometry results are swapped, the θ column is mirrored, and the minor-axis overlay is drawn non-perpendicular. Present since v1.1.4.

Fixing this is a behaviour change for every consumer of ellipse results and must be announced as such.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions