diff --git a/sigima/tests/image/preprocessing_tools_unit_test.py b/sigima/tests/image/preprocessing_tools_unit_test.py index 220adf0..c3d43d6 100644 --- a/sigima/tests/image/preprocessing_tools_unit_test.py +++ b/sigima/tests/image/preprocessing_tools_unit_test.py @@ -11,6 +11,7 @@ from sigima.enums import BinningOperation from sigima.tools.image.preprocessing import ( + _USE_NEW_SHAPE_API, binning, distance_matrix, fit_circle_model, @@ -84,6 +85,53 @@ def test_fit_ellipse_model_failure() -> None: assert result is None or isinstance(result, tuple) +def test_fit_circle_model_ground_truth() -> None: + """Verify that ``fit_circle_model`` recovers **all** circle parameters + (xc, yc, radius) from a noise-free contour. + + Note: the functions swap x/y because scikit-image models interpret the + contour columns as (row, col), so the returned ``xc`` corresponds to the + contour's second column and ``yc`` to the first. + """ + if not _USE_NEW_SHAPE_API: + pytest.skip() + xc_in, yc_in, r_in = 7.0, -5.0, 12.0 + contour = _circle_contour(xc_in, yc_in, r_in, n=256) + result = fit_circle_model(contour) + assert result is not None + # xc/yc are swapped by the row/col → x/y conversion + yc, xc, r = result + assert xc == pytest.approx(xc_in, abs=1e-6) + assert yc == pytest.approx(yc_in, abs=1e-6) + assert r == pytest.approx(r_in, abs=1e-6) + + +def test_fit_ellipse_model_ground_truth() -> None: + """Verify that ``fit_ellipse_model`` recovers **all** ellipse parameters + (xc, yc, a, b, theta) from a noise-free contour. + + Note: the functions swap x/y and a/b because scikit-image models interpret + the contour columns as (row, col), so centre and semi-axes are transposed. + """ + if not _USE_NEW_SHAPE_API: + pytest.skip() + xc_in, yc_in = -3.0, 5.0 + a_in, b_in = 4.0, 8.0 + theta_in = np.pi / 6 + contour = _ellipse_contour(xc_in, yc_in, a_in, b_in, theta0=theta_in, n=256) + result = fit_ellipse_model(contour) + assert result is not None + # xc/yc are swapped by the row/col → x/y conversion + yc, xc, a, b, theta = result + assert xc == pytest.approx(xc_in, abs=1e-4) + assert yc == pytest.approx(yc_in, abs=1e-4) + assert a == pytest.approx(a_in, abs=1e-4) + assert b == pytest.approx(b_in, abs=1e-4) + # The fitted angle is expected to differ by π/2, + # theta along y axis instead of x axis + assert theta == pytest.approx(theta_in + np.pi / 2, abs=1e-4) + + # =========================================================================== # get_absolute_level # =========================================================================== diff --git a/sigima/tools/coordinates.py b/sigima/tools/coordinates.py index 1e1d160..17886e1 100644 --- a/sigima/tools/coordinates.py +++ b/sigima/tools/coordinates.py @@ -98,7 +98,7 @@ def ellipse_to_diameters( Ellipse X/Y diameters (major/minor axes) coordinates """ dxa, dya = a * np.cos(theta), a * np.sin(theta) - dxb, dyb = -b * np.sin(theta), b * np.cos(theta) + dxb, dyb = b * np.sin(theta), b * np.cos(theta) x0, y0, x1, y1 = xc - dxa, yc - dya, xc + dxa, yc + dya x2, y2, x3, y3 = xc - dxb, yc - dyb, xc + dxb, yc + dyb return x0, y0, x1, y1, x2, y2, x3, y3 @@ -117,7 +117,7 @@ def array_ellipse_to_diameters(data: np.ndarray) -> np.ndarray: """ xc, yc, a, b, theta = data[:, 0], data[:, 1], data[:, 2], data[:, 3], data[:, 4] dxa, dya = a * np.cos(theta), a * np.sin(theta) - dxb, dyb = -b * np.sin(theta), b * np.cos(theta) + dxb, dyb = b * np.sin(theta), b * np.cos(theta) x0, y0, x1, y1 = xc - dxa, yc - dya, xc + dxa, yc + dya x2, y2, x3, y3 = xc - dxb, yc - dyb, xc + dxb, yc + dyb result = np.column_stack((x0, y0, x1, y1, x2, y2, x3, y3)).astype(float)