From 11e293aa95527ee009231bc9ce3932b9acf5bd77 Mon Sep 17 00:00:00 2001 From: cfs-data <145435153+cfs-data@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:34:11 +0100 Subject: [PATCH 1/4] First commit --- .../src/conversion/leveling/core.py | 68 ++++------- .../src/conversion/leveling/data_types.py | 34 ------ .../conversion/leveling/solver/__init__.py | 17 --- .../src/conversion/leveling/solver/core.py | 45 ------- .../src/conversion/leveling/solver/design.py | 25 ---- .../src/conversion/leveling/solver/grid.py | 26 ----- .../conversion/leveling/solver/transforms.py | 86 -------------- .../src/conversion/leveling/solver/utils.py | 17 --- .../preprocess_impression/impression.py | 18 +-- .../resources/baseline_design_matrix.npy | Bin 4928 -> 0 bytes .../conversion/leveling/solver/test_core.py | 110 ------------------ .../conversion/leveling/solver/test_design.py | 23 ---- .../conversion/leveling/solver/test_grid.py | 62 ---------- .../leveling/solver/test_transforms.py | 73 ------------ .../conversion/leveling/solver/test_utils.py | 45 ------- .../conversion/leveling/test_level_map.py | 45 ------- 16 files changed, 28 insertions(+), 666 deletions(-) delete mode 100644 packages/scratch-core/src/conversion/leveling/solver/__init__.py delete mode 100644 packages/scratch-core/src/conversion/leveling/solver/core.py delete mode 100644 packages/scratch-core/src/conversion/leveling/solver/design.py delete mode 100644 packages/scratch-core/src/conversion/leveling/solver/grid.py delete mode 100644 packages/scratch-core/src/conversion/leveling/solver/transforms.py delete mode 100644 packages/scratch-core/src/conversion/leveling/solver/utils.py delete mode 100644 packages/scratch-core/tests/conversion/leveling/resources/baseline_design_matrix.npy delete mode 100644 packages/scratch-core/tests/conversion/leveling/solver/test_core.py delete mode 100644 packages/scratch-core/tests/conversion/leveling/solver/test_design.py delete mode 100644 packages/scratch-core/tests/conversion/leveling/solver/test_grid.py delete mode 100644 packages/scratch-core/tests/conversion/leveling/solver/test_transforms.py delete mode 100644 packages/scratch-core/tests/conversion/leveling/solver/test_utils.py diff --git a/packages/scratch-core/src/conversion/leveling/core.py b/packages/scratch-core/src/conversion/leveling/core.py index 4f6b5ec3..b15f5111 100644 --- a/packages/scratch-core/src/conversion/leveling/core.py +++ b/packages/scratch-core/src/conversion/leveling/core.py @@ -1,19 +1,12 @@ import numpy as np from conversion.leveling import SurfaceTerms, LevelingResult -from conversion.leveling.solver import ( - fit_surface, - get_2d_grid, - compute_root_mean_square, -) -from conversion.leveling.solver.utils import compute_image_center + from container_models.scan_image import ScanImage +from surfalize import Surface + -def level_map( - scan_image: ScanImage, - terms: SurfaceTerms, - reference_point: tuple[float, float] | None = None, -) -> LevelingResult: +def level_map(scan_image: ScanImage, terms: SurfaceTerms) -> LevelingResult: """ Compute the leveled map by fitting polynomial terms and subtracting them from the image data. @@ -21,41 +14,26 @@ def level_map( :param scan_image: The scan image containing the image data to level. :param terms: The surface terms to use in the fitting. Note: terms can be combined using bit-operators. - :param reference_point: A tuple representing a reference point (X, Y) in physical coordinate space. - If provided, then the coordinates will be translated such that (X, Y) lies in the origin after translation. - If `None`, then the coordinates will be translated such that the center of the image lies in the origin. :returns: An instance of `LevelingResult` containing the leveled scan data and estimated physical parameters. """ - if not reference_point: - reference_point = compute_image_center(scan_image) - - # Build the 2D grids and translate in the opposite direction of `reference_point` - x_grid, y_grid = get_2d_grid( - scan_image, offset=(-reference_point[0], -reference_point[1]) - ) - - # Get the point cloud (xs, ys, zs) for the numerical data - xs, ys, zs = ( - x_grid[scan_image.valid_mask], - y_grid[scan_image.valid_mask], - scan_image.valid_data, + if terms == SurfaceTerms.NONE: + return LevelingResult( + leveled_map=scan_image.data, + fitted_surface=np.full_like(scan_image.data, 0.0), + ) + + surface = Surface( + height_data=scan_image.data, + step_x=scan_image.scale_x, + step_y=scan_image.scale_y, ) - - # Fit surface by solving the least-squares solution to a linear matrix equation - fitted_surface, physical_params = fit_surface(xs, ys, zs, terms) - fitted_surface_2d = np.full_like(scan_image.data, np.nan) - fitted_surface_2d[scan_image.valid_mask] = fitted_surface - - # Compute the leveled map - leveled_map_2d = np.full_like(scan_image.data, np.nan) - leveled_map_2d[scan_image.valid_mask] = zs - fitted_surface - - # Calculate RMS of residuals - residual_rms = compute_root_mean_square(leveled_map_2d) - - return LevelingResult( - leveled_map=leveled_map_2d, - parameters=physical_params, - residual_rms=residual_rms, - fitted_surface=fitted_surface_2d, + if terms.name == "SPHERE": + degree = 2 + elif terms.name == "PLANE": + degree = 1 + else: + degree = 0 + leveled, trend = surface.detrend_polynomial( + degree=degree, inplace=False, return_trend=True ) + return LevelingResult(leveled_map=leveled.data, fitted_surface=trend.data) diff --git a/packages/scratch-core/src/conversion/leveling/data_types.py b/packages/scratch-core/src/conversion/leveling/data_types.py index c937c0a6..a87839b2 100644 --- a/packages/scratch-core/src/conversion/leveling/data_types.py +++ b/packages/scratch-core/src/conversion/leveling/data_types.py @@ -1,6 +1,5 @@ from enum import Flag, auto import numpy as np -from typing import Callable from numpy.typing import NDArray from pydantic import BaseModel @@ -25,46 +24,13 @@ class SurfaceTerms(Flag): SPHERE = OFFSET | TILT_X | TILT_Y | ASTIG_45 | DEFOCUS | ASTIG_0 -# Mapping mathematical term indices to lambda functions for design matrix generation -TERM_FUNCTIONS: dict[SurfaceTerms, Callable[[NDArray, NDArray], NDArray]] = { - SurfaceTerms.OFFSET: lambda xs, ys: np.ones_like(xs), - SurfaceTerms.TILT_X: lambda xs, ys: xs, - SurfaceTerms.TILT_Y: lambda xs, ys: ys, - SurfaceTerms.ASTIG_45: lambda xs, ys: xs * ys, - SurfaceTerms.DEFOCUS: lambda xs, ys: xs**2 + ys**2, - SurfaceTerms.ASTIG_0: lambda xs, ys: xs**2 - ys**2, -} - - class LevelingResult(BaseModel, arbitrary_types_allowed=True): """ Result of a leveling operation. :param leveled_map: 2D array with the leveled height data - :param parameters: Dictionary mapping SurfaceTerms to fitted coefficient values - :param residual_rms: Root mean square of residuals after leveling :param fitted_surface: 2D array of the fitted surface (same shape as `leveled_map`) """ leveled_map: NDArray[np.float64] - parameters: dict[SurfaceTerms, float] - residual_rms: float fitted_surface: NDArray[np.float64] - - -class NormalizedCoordinates(BaseModel, arbitrary_types_allowed=True): - """ - Container class for storing centered and rescaled coordinates. - - :param xs: The rescaled X-coordinates. - :param ys: The rescaled Y-coordinates. - :param x_mean: The mean of the X-coordinates before rescaling. - :param y_mean: The mean of the Y-coordinates before rescaling. - :param scale: The multiplier used for rescaling. - """ - - xs: NDArray[np.float64] - ys: NDArray[np.float64] - x_mean: float - y_mean: float - scale: float diff --git a/packages/scratch-core/src/conversion/leveling/solver/__init__.py b/packages/scratch-core/src/conversion/leveling/solver/__init__.py deleted file mode 100644 index f6cf3536..00000000 --- a/packages/scratch-core/src/conversion/leveling/solver/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from conversion.leveling.solver.grid import get_2d_grid -from conversion.leveling.solver.design import build_design_matrix -from conversion.leveling.solver.transforms import ( - denormalize_parameters, - normalize_coordinates, -) -from conversion.leveling.solver.utils import compute_root_mean_square -from conversion.leveling.solver.core import fit_surface - -__all__ = ( - "get_2d_grid", - "build_design_matrix", - "denormalize_parameters", - "fit_surface", - "normalize_coordinates", - "compute_root_mean_square", -) diff --git a/packages/scratch-core/src/conversion/leveling/solver/core.py b/packages/scratch-core/src/conversion/leveling/solver/core.py deleted file mode 100644 index 9e97404f..00000000 --- a/packages/scratch-core/src/conversion/leveling/solver/core.py +++ /dev/null @@ -1,45 +0,0 @@ -import numpy as np -from numpy.typing import NDArray -from conversion.leveling import SurfaceTerms -from conversion.leveling.solver import ( - normalize_coordinates, - build_design_matrix, - denormalize_parameters, -) - - -def fit_surface( - xs: NDArray, ys: NDArray, zs: NDArray, terms: SurfaceTerms -) -> tuple[NDArray[np.float64], dict[SurfaceTerms, float]]: - """ - Core solver: fits a surface to the point cloud (xs, ys, zs). - - :param xs: The X-coordinates. - :param ys: The Y-coordinates. - :param zs: The Z-values. - :param terms: The terms to use in the fitting - :return: A tuple containing the fitted surface (z̃s) and the estimated physical parameters. - """ - # 1. Normalize the grid coordinates by centering and rescaling them - normalized = normalize_coordinates(xs, ys) - - # 2. Build the design matrix for the least-squares solver - design_matrix = build_design_matrix(normalized.xs, normalized.ys, terms) - - # 3. Solve (Least Squares) - ( - coefficients, - *_, - ) = np.linalg.lstsq(design_matrix, zs, rcond=None) - - # 4. Compute the surface (z̃s-values) from the fitted coefficients - fitted_surface = design_matrix @ coefficients - - # 5. Recover physical parameters (optional usage, but part of original spec) - physical_params = denormalize_parameters( - dict(zip(terms, map(float, coefficients))), - normalized.x_mean, - normalized.y_mean, - normalized.scale, - ) - return fitted_surface, physical_params diff --git a/packages/scratch-core/src/conversion/leveling/solver/design.py b/packages/scratch-core/src/conversion/leveling/solver/design.py deleted file mode 100644 index ade6af87..00000000 --- a/packages/scratch-core/src/conversion/leveling/solver/design.py +++ /dev/null @@ -1,25 +0,0 @@ -import numpy as np -from numpy.typing import NDArray -from conversion.leveling import SurfaceTerms -from conversion.leveling.data_types import TERM_FUNCTIONS - - -def build_design_matrix( - xs: NDArray, ys: NDArray, terms: SurfaceTerms -) -> NDArray[np.float64]: - """ - Constructs the Least Squares design matrix based on grid coordinates (xs, ys) and requested terms. - - :param xs: The X-coordinates. - :param ys: The Y-coordinates. - :param terms: The surface terms to use in the design matrix. - :returns: The design matrix as a numpy array with shape [n_points, n_terms]. - """ - num_points = xs.size - matrix = np.zeros((num_points, len(terms)), dtype=np.float64) - - for column_index, term in enumerate(terms): - if func := TERM_FUNCTIONS.get(term): - matrix[:, column_index] = func(xs, ys) - - return matrix diff --git a/packages/scratch-core/src/conversion/leveling/solver/grid.py b/packages/scratch-core/src/conversion/leveling/solver/grid.py deleted file mode 100644 index 94d533dd..00000000 --- a/packages/scratch-core/src/conversion/leveling/solver/grid.py +++ /dev/null @@ -1,26 +0,0 @@ -import numpy as np -from numpy.typing import NDArray -from container_models.scan_image import ScanImage - - -def get_2d_grid( - scan_image: ScanImage, offset: tuple[float, float] = (0, 0) -) -> tuple[NDArray, NDArray]: - """ - Return a 2D grid containing the physical coordinates of the scan data. - - :param scan_image: An instance of `ScanImage` containing the recorded depth data. - :param offset: A tuple containing the physical coordinates of the image offset (in meters) - relative to the origin by which the grid coordinates need to be translated. The first element - corresponds to offset in the X-dimension, and the second element to the offset in the Y-dimension. - :returns: A tuple containing the grid coordinates for the X-direction and Y-direction. - """ - # Generate Grid (ij indexing to match matrix coordinates) - x_indices, y_indices = np.meshgrid( - np.arange(scan_image.width), np.arange(scan_image.height), indexing="xy" - ) - # Translate the grid by `offset` - x_grid = (x_indices * scan_image.scale_x) + offset[0] - y_grid = (y_indices * scan_image.scale_y) + offset[1] - - return x_grid, y_grid diff --git a/packages/scratch-core/src/conversion/leveling/solver/transforms.py b/packages/scratch-core/src/conversion/leveling/solver/transforms.py deleted file mode 100644 index a5e46e36..00000000 --- a/packages/scratch-core/src/conversion/leveling/solver/transforms.py +++ /dev/null @@ -1,86 +0,0 @@ -import numpy as np -from numpy.typing import NDArray -from collections.abc import Mapping -from conversion.leveling import SurfaceTerms -from conversion.leveling.data_types import NormalizedCoordinates - - -def normalize_coordinates( - xs: NDArray[np.float64], ys: NDArray[np.float64] -) -> NormalizedCoordinates: - """ - Normalize grid coordinates by centering and rescaling. - - This method is used to improve numerical stability during fitting. - - :param xs: The X-coordinates to normalize. - :param ys: The Y-coordinates to normalize. - :returns: An instance of `NormalizedCoordinatesResult` containing the rescaled grid coordinates. - """ - x_mean, y_mean = np.mean(xs), np.mean(ys) - vx_norm = xs - x_mean - vy_norm = ys - y_mean - - span_x = np.max(vx_norm) - np.min(vx_norm) - span_y = np.max(vy_norm) - np.min(vy_norm) - # Avoid division by zero - max_span = max(span_x, span_y) - scale = 1.0 if np.isclose(max_span, 0.0) else 1 / max_span - - return NormalizedCoordinates( - xs=vx_norm * scale, # rescale X-coordinates - ys=vy_norm * scale, # rescale Y-coordinates - x_mean=float(x_mean), - y_mean=float(y_mean), - scale=float(scale), - ) - - -def denormalize_parameters( - coefficients: Mapping[SurfaceTerms, float], - x_mean: float, - y_mean: float, - scale: float, -) -> dict[SurfaceTerms, float]: - """ - Converts normalized fit parameters back to real-world physical units. - - The computation matches the specific numerical corrections from the original MATLAB script. - - :param coefficients: A dictionary containing the normalized fit parameters. - :param x_mean: The mean of the x-coordinates. - :param y_mean: The mean of the y-coordinates. - :param scale: The scale factor. - :returns: A dictionary containing the denormalized fit parameters for all surface terms. - """ - params = np.array( - [coefficients.get(term, 0.0) for term in SurfaceTerms], dtype=np.float64 - ) - - # Un-normalize scaling - params[1:3] *= scale # Tilts - params[3:] *= scale**2 # Quadratic terms - - # Algebraic corrections for centering (x_mean, y_mean) - # Note: These formulas correspond exactly to the MATLAB implementation - # P[0] = Offset, P[1] = TiltX, P[2] = TiltY, etc. - - # Adjust Offset (p0) - params[0] = ( - params[0] - - params[1] * x_mean - - params[2] * y_mean - + params[3] * x_mean * y_mean - + params[4] * (x_mean**2 + y_mean**2) - + params[5] * (x_mean**2 - y_mean**2) - ) - # Adjust Tilt X (p1) - params[1] = ( - params[1] - params[3] * y_mean - 2 * params[4] * x_mean - 2 * params[5] * x_mean - ) - # Adjust Tilt Y (p2) - params[2] = ( - params[2] - params[3] * x_mean - 2 * params[4] * y_mean + 2 * params[5] * y_mean - ) - - return dict(zip(SurfaceTerms, map(float, params))) diff --git a/packages/scratch-core/src/conversion/leveling/solver/utils.py b/packages/scratch-core/src/conversion/leveling/solver/utils.py deleted file mode 100644 index 77925927..00000000 --- a/packages/scratch-core/src/conversion/leveling/solver/utils.py +++ /dev/null @@ -1,17 +0,0 @@ -import numpy as np -from typing import Any -from numpy.typing import NDArray - -from container_models.scan_image import ScanImage - - -def compute_root_mean_square(data: NDArray[Any]) -> float: - """Compute the root-mean-square from a data array and return as Python float.""" - return float(np.sqrt(np.nanmean(data**2))) - - -def compute_image_center(scan_image: ScanImage) -> tuple[float, float]: - """Compute the centerpoint (X, Y) of a scan image in physical coordinate space.""" - center_x = (scan_image.width - 1) * scan_image.scale_x * 0.5 - center_y = (scan_image.height - 1) * scan_image.scale_y * 0.5 - return center_x, center_y diff --git a/packages/scratch-core/src/conversion/preprocess_impression/impression.py b/packages/scratch-core/src/conversion/preprocess_impression/impression.py index 190cdf7b..61cdae72 100644 --- a/packages/scratch-core/src/conversion/preprocess_impression/impression.py +++ b/packages/scratch-core/src/conversion/preprocess_impression/impression.py @@ -21,7 +21,7 @@ needs_resampling, ) from conversion.preprocess_impression.tilt import apply_tilt_correction -from conversion.preprocess_impression.utils import update_mark_data, Point2D +from conversion.preprocess_impression.utils import update_mark_data from conversion.resample import get_scaling_factors, resample_image_array @@ -89,15 +89,11 @@ def preprocess_impression_mark( ) # Stage 8: Final leveling - mark_filtered, _ = _level_mark(mark_filtered, params.surface_terms, mark.center) + mark_filtered, _ = _level_mark(mark_filtered, params.surface_terms) # Prepare leveled-only output mark_leveled_final = _finalize_leveled_output( - mark_anti_aliased, - fitted_surface, - params.pixel_size, - params.surface_terms, - mark.center, + mark_anti_aliased, fitted_surface, params.pixel_size, params.surface_terms ) # Build output metadata @@ -109,11 +105,8 @@ def preprocess_impression_mark( def _level_mark( mark: Mark, terms: SurfaceTerms, - reference_point: Point2D | None = None, ) -> tuple[Mark, ScanMap2DArray]: - result = level_map( - mark.scan_image, terms=terms, reference_point=reference_point or mark.center - ) + result = level_map(mark.scan_image, terms=terms) leveled_mark = update_mark_data(mark, result.leveled_map) return leveled_mark, result.fitted_surface @@ -135,7 +128,6 @@ def _finalize_leveled_output( fitted_surface: ScanMap2DArray, target_scale: float | None, surface_terms: SurfaceTerms, - reference_point: Point2D, ) -> Mark: """ Prepare the leveled-only output. @@ -156,6 +148,6 @@ def _finalize_leveled_output( # Apply PLANE-only leveling (after resampling, like MATLAB) rigid_terms = surface_terms & SurfaceTerms.PLANE - leveled_mark, _ = _level_mark(mark_restored, rigid_terms, reference_point) + leveled_mark, _ = _level_mark(mark_restored, rigid_terms) return leveled_mark diff --git a/packages/scratch-core/tests/conversion/leveling/resources/baseline_design_matrix.npy b/packages/scratch-core/tests/conversion/leveling/resources/baseline_design_matrix.npy deleted file mode 100644 index 76fbf21baa2c0172b9ec96731dce45b1b6d93c4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4928 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 diff --git a/packages/scratch-core/tests/conversion/leveling/solver/test_core.py b/packages/scratch-core/tests/conversion/leveling/solver/test_core.py deleted file mode 100644 index 270f62f4..00000000 --- a/packages/scratch-core/tests/conversion/leveling/solver/test_core.py +++ /dev/null @@ -1,110 +0,0 @@ -from conversion.leveling import SurfaceTerms -from conversion.leveling.solver import fit_surface -import numpy as np -from numpy.typing import NDArray -import pytest - -from ..constants import SINGLE_AND_COMBINED_TERMS, SINGLE_TERMS - - -@pytest.mark.parametrize("terms", SINGLE_AND_COMBINED_TERMS) -def test_fit_surface_reduces_variance( - xs: NDArray[np.float64], - ys: NDArray[np.float64], - zs: NDArray[np.float64], - terms: SurfaceTerms, -): - fitted_surface, _ = fit_surface(xs=xs, ys=ys, zs=zs, terms=terms) - leveled_map = zs - fitted_surface - assert np.var(fitted_surface) < np.var(zs) - assert np.var(fitted_surface) < np.var(leveled_map) - - -@pytest.mark.parametrize("terms", list(SurfaceTerms.PLANE)) -def test_fit_surface_plane_reduces_variance( - xs: NDArray[np.float64], - ys: NDArray[np.float64], - zs: NDArray[np.float64], - terms: SurfaceTerms, -): - leveled_map_terms = zs - fit_surface(xs=xs, ys=ys, zs=zs, terms=terms)[0] - leveled_map_plane = ( - zs - fit_surface(xs=xs, ys=ys, zs=zs, terms=SurfaceTerms.PLANE)[0] - ) - - single_term_var = np.var(leveled_map_terms) - plane_var = np.var(leveled_map_plane) - - assert plane_var < single_term_var or np.isclose(plane_var, single_term_var) - - -@pytest.mark.parametrize("terms", SINGLE_TERMS + [SurfaceTerms.PLANE]) -def test_fit_surface_sphere_reduces_variance( - xs: NDArray[np.float64], - ys: NDArray[np.float64], - zs: NDArray[np.float64], - terms: SurfaceTerms, -): - leveled_map_terms = zs - fit_surface(xs=xs, ys=ys, zs=zs, terms=terms)[0] - leveled_map_sphere = ( - zs - fit_surface(xs=xs, ys=ys, zs=zs, terms=SurfaceTerms.SPHERE)[0] - ) - - assert np.var(leveled_map_sphere) < np.var(leveled_map_terms) - - -@pytest.mark.parametrize("terms", SINGLE_AND_COMBINED_TERMS) -def test_fit_surface_fits_terms( - xs: NDArray[np.float64], - ys: NDArray[np.float64], - zs: NDArray[np.float64], - terms: SurfaceTerms, -): - _, physical_params = fit_surface(xs=xs, ys=ys, zs=zs, terms=terms) - assert all(not np.isclose(v, 0.0) for p, v in physical_params.items() if p in terms) - assert all(np.isclose(v, 0.0) for p, v in physical_params.items() if p not in terms) - - -def test_fit_surface_offset_is_invariant_for_tilted_surfaces( - xs: NDArray[np.float64], ys: NDArray[np.float64], zs: NDArray[np.float64] -): - _, physical_params_tilt_x = fit_surface( - xs=xs, ys=ys, zs=zs, terms=SurfaceTerms.TILT_X - ) - _, physical_params_tilt_y = fit_surface( - xs=xs, ys=ys, zs=zs, terms=SurfaceTerms.TILT_Y - ) - assert np.isclose( - physical_params_tilt_x[SurfaceTerms.OFFSET], - physical_params_tilt_y[SurfaceTerms.OFFSET], - ) - - -def test_fit_surface_offset_equals_mean( - xs: NDArray[np.float64], ys: NDArray[np.float64], zs: NDArray[np.float64] -): - fitted_surface_offset, physical_params_offset = fit_surface( - xs=xs, ys=ys, zs=zs, terms=SurfaceTerms.OFFSET - ) - param = physical_params_offset[SurfaceTerms.OFFSET] - assert np.isclose(np.mean(zs), param) - assert np.allclose(fitted_surface_offset, param) - - -def test_fit_surface_defocus_is_positive( - xs: NDArray[np.float64], ys: NDArray[np.float64], zs: NDArray[np.float64] -): - fitted_surface_defocus, _ = fit_surface( - xs=xs, ys=ys, zs=zs, terms=SurfaceTerms.DEFOCUS - ) - assert np.all(fitted_surface_defocus > 0.0) - - -def test_fit_surface_none_has_no_effect( - xs: NDArray[np.float64], ys: NDArray[np.float64], zs: NDArray[np.float64] -): - fitted_surface_none, physical_params_none = fit_surface( - xs=xs, ys=ys, zs=zs, terms=SurfaceTerms.NONE - ) - assert np.allclose(fitted_surface_none, 0.0) - assert all(np.isclose(v, 0.0) for v in physical_params_none.values()) diff --git a/packages/scratch-core/tests/conversion/leveling/solver/test_design.py b/packages/scratch-core/tests/conversion/leveling/solver/test_design.py deleted file mode 100644 index ff04f1ea..00000000 --- a/packages/scratch-core/tests/conversion/leveling/solver/test_design.py +++ /dev/null @@ -1,23 +0,0 @@ -from conversion.leveling import SurfaceTerms -from conversion.leveling.solver import build_design_matrix -import numpy as np -from numpy.typing import NDArray -import pytest - -from ..constants import SINGLE_AND_COMBINED_TERMS, RESOURCES_DIR - - -@pytest.mark.parametrize("terms", SINGLE_AND_COMBINED_TERMS) -def test_design_matrix_shape_matches_number_of_terms( - xs: NDArray[np.float64], ys: NDArray[np.float64], terms: SurfaceTerms -): - design_matrix = build_design_matrix(xs=xs, ys=ys, terms=terms) - assert design_matrix.shape == (len(xs), len(terms)) - - -def test_design_matrix_matches_baseline_output( - xs: NDArray[np.float64], ys: NDArray[np.float64] -): - design_matrix = build_design_matrix(xs=xs, ys=ys, terms=SurfaceTerms.SPHERE) - verified = np.load(RESOURCES_DIR / "baseline_design_matrix.npy") - assert np.allclose(design_matrix, verified) diff --git a/packages/scratch-core/tests/conversion/leveling/solver/test_grid.py b/packages/scratch-core/tests/conversion/leveling/solver/test_grid.py deleted file mode 100644 index fbf63087..00000000 --- a/packages/scratch-core/tests/conversion/leveling/solver/test_grid.py +++ /dev/null @@ -1,62 +0,0 @@ -from conversion.leveling.solver import get_2d_grid -from conversion.leveling.solver.utils import compute_image_center -from container_models.scan_image import ScanImage -import numpy as np -import pytest - - -def test_grid_matches_scan_image_shape(scan_image_rectangular_with_nans: ScanImage): - x_grid, y_grid = get_2d_grid(scan_image_rectangular_with_nans) - assert x_grid.shape == ( - scan_image_rectangular_with_nans.height, - scan_image_rectangular_with_nans.width, - ) - assert y_grid.shape == ( - scan_image_rectangular_with_nans.height, - scan_image_rectangular_with_nans.width, - ) - - -def test_grid_is_a_meshgrid(scan_image_rectangular_with_nans: ScanImage): - x_grid, y_grid = get_2d_grid(scan_image_rectangular_with_nans) - - assert all( - np.array_equal(x_grid[i, :], x_grid[i + 1, :]) - for i in range(x_grid.shape[0] - 1) - ) - assert all( - np.array_equal(y_grid[:, i], y_grid[:, i + 1]) - for i in range(y_grid.shape[1] - 1) - ) - assert np.all(x_grid[0, :-1] < x_grid[0, 1:]) - assert np.all(y_grid[:-1, 0] < y_grid[1:, 0]) - - -def test_grid_is_translated_by_offset(scan_image_rectangular_with_nans: ScanImage): - si = scan_image_rectangular_with_nans - offset_x, offset_y = 0.1, 0.5 - x_grid, y_grid = get_2d_grid(si, offset=(offset_x, offset_y)) - - xs, ys = x_grid[0, :], y_grid[:, 0] - - assert np.isclose(xs[0], offset_x) - assert np.isclose(xs[-1], offset_x + (si.width - 1) * si.scale_x) - assert np.isclose(ys[0], offset_y) - assert np.isclose(ys[-1], offset_y + (si.height - 1) * si.scale_y) - - -@pytest.mark.integration -def test_grid_is_centered_around_origin( - scan_image_rectangular_with_nans: ScanImage, -): - center_x, center_y = compute_image_center(scan_image_rectangular_with_nans) - x_grid, y_grid = get_2d_grid( - scan_image_rectangular_with_nans, offset=(-center_x, -center_y) - ) - - # With xy indexing: x from row 0, y from column 0 - mid_x, mid_y = x_grid.shape[1] // 2, y_grid.shape[0] // 2 - xs, ys = x_grid[0, :], y_grid[:, 0] - - assert xs[mid_x - 1] < 0.0 < xs[mid_x] - assert ys[mid_y - 1] < 0.0 < ys[mid_y] diff --git a/packages/scratch-core/tests/conversion/leveling/solver/test_transforms.py b/packages/scratch-core/tests/conversion/leveling/solver/test_transforms.py deleted file mode 100644 index 7449a370..00000000 --- a/packages/scratch-core/tests/conversion/leveling/solver/test_transforms.py +++ /dev/null @@ -1,73 +0,0 @@ -import numpy as np -from numpy.typing import NDArray -import pytest -from conversion.leveling import SurfaceTerms -from conversion.leveling.solver import normalize_coordinates, denormalize_parameters -from ..constants import SINGLE_TERMS - - -class TestNormalizeCoordinates: - def test_normalized_coordinates_have_zero_mean( - self, xs: NDArray[np.float64], ys: NDArray[np.float64] - ): - normalized = normalize_coordinates(xs=xs, ys=ys) - assert np.isclose(np.mean(normalized.xs), 0.0) - assert np.isclose(np.mean(normalized.ys), 0.0) - assert normalized.xs[0] < 0 < normalized.xs[-1] - assert normalized.ys[0] < 0 < normalized.ys[-1] - - def test_normalized_coordinates_are_strictly_increasing( - self, xs: NDArray[np.float64], ys: NDArray[np.float64] - ): - normalized = normalize_coordinates(xs=xs, ys=ys) - assert np.all(normalized.xs[:-1] < normalized.xs[1:]) - assert np.all(normalized.ys[:-1] < normalized.ys[1:]) - - def test_normalize_coordinates_returns_means( - self, xs: NDArray[np.float64], ys: NDArray[np.float64] - ): - normalized = normalize_coordinates(xs=xs, ys=ys) - assert np.isclose(normalized.x_mean, np.mean(xs)) - assert np.isclose(normalized.y_mean, np.mean(ys)) - - def test_normalized_coordinates_are_bounded_by_unit_disk( - self, xs: NDArray[np.float64], ys: NDArray[np.float64] - ): - normalized = normalize_coordinates(xs=xs, ys=ys) - assert np.isclose(normalized.xs[-1] - normalized.xs[0], 1.0) - assert np.isclose(normalized.ys[-1] - normalized.ys[0], 0.5) - - -class TestDenormalizeParameters: - @pytest.mark.parametrize("terms", SINGLE_TERMS) - def test_denormalize_parameters_returns_all_terms(self, terms: SurfaceTerms): - params = denormalize_parameters( - coefficients={terms: 1.5}, x_mean=0.1, y_mean=0.5, scale=0.21 - ) - assert all(term in params for term in SurfaceTerms) - - @pytest.mark.parametrize("terms", SINGLE_TERMS) - def test_denormalize_parameters_adjusts_terms(self, terms: SurfaceTerms): - initial_value = 1.5 - params = denormalize_parameters( - coefficients={terms: initial_value}, x_mean=0.1, y_mean=0.5, scale=0.21 - ) - assert all( - not np.isclose(params[t], initial_value) - for t in SurfaceTerms - if t in terms and t != SurfaceTerms.OFFSET - ) - - def test_denormalize_parameters_matches_baseline_output(self): - params = denormalize_parameters( - coefficients={term: 1.0 for term in SurfaceTerms}, - x_mean=0.1, - y_mean=0.5, - scale=0.21, - ) - assert np.isclose(params[SurfaceTerms.OFFSET], 0.877087) - assert np.isclose(params[SurfaceTerms.TILT_X], 0.17031) - assert np.isclose(params[SurfaceTerms.TILT_Y], 0.20559) - assert np.isclose(params[SurfaceTerms.ASTIG_45], 0.0441) - assert np.isclose(params[SurfaceTerms.DEFOCUS], 0.0441) - assert np.isclose(params[SurfaceTerms.ASTIG_0], 0.0441) diff --git a/packages/scratch-core/tests/conversion/leveling/solver/test_utils.py b/packages/scratch-core/tests/conversion/leveling/solver/test_utils.py deleted file mode 100644 index 8e515259..00000000 --- a/packages/scratch-core/tests/conversion/leveling/solver/test_utils.py +++ /dev/null @@ -1,45 +0,0 @@ -import numpy as np -import pytest - -from conversion.leveling.solver import compute_root_mean_square -from conversion.leveling.solver.utils import compute_image_center -from container_models.scan_image import ScanImage - - -class TestRootMeanSquare: - @pytest.mark.parametrize("value", [0.0, 1.0, 2.0, -3.15, 40.123, -80, 100]) - def test_rms_is_constant_for_constant_input(self, value: float): - result = compute_root_mean_square(np.array([value] * 100)) - assert np.isclose(result, abs(value)) - - @pytest.mark.parametrize("value", list(range(-10, 10))) - def test_rms_is_non_negative(self, value: float): - result = compute_root_mean_square(np.array([value] * 100)) - assert result > 0 or np.isclose(result, 0.0) - - def test_rms_can_handle_nans(self): - array_with_nans = np.array( - [0, 1, 0.15, 2, np.nan, 3, 4, np.nan, -5], dtype=np.float64 - ) - array_without_nans = array_with_nans[~np.isnan(array_with_nans)] - - result_with_nans = compute_root_mean_square(array_with_nans) - result_without_nans = compute_root_mean_square(array_without_nans) - assert np.isclose(result_with_nans, result_without_nans) - assert np.isclose(result_with_nans, 2.8036328473709147) - - -class TestComputeImageCenter: - def test_compute_image_center_for_square_image( - self, scan_image_with_nans: ScanImage - ): - center_x, center_y = compute_image_center(scan_image_with_nans) - assert np.isclose(center_x, 0.0004037151235) - assert np.isclose(center_y, center_x) - - def test_compute_image_center_for_rectangular_image( - self, scan_image_rectangular_with_nans: ScanImage - ): - center_x, center_y = compute_image_center(scan_image_rectangular_with_nans) - assert np.isclose(center_x, 0.00030245829675) - assert np.isclose(center_y, 0.0004037151235) diff --git a/packages/scratch-core/tests/conversion/leveling/test_level_map.py b/packages/scratch-core/tests/conversion/leveling/test_level_map.py index d217c55d..f849bccc 100644 --- a/packages/scratch-core/tests/conversion/leveling/test_level_map.py +++ b/packages/scratch-core/tests/conversion/leveling/test_level_map.py @@ -11,12 +11,6 @@ def test_map_level_sphere(scan_image_with_nans: ScanImage): result = level_map(scan_image_with_nans, SurfaceTerms.SPHERE) assert result assert np.allclose(result.leveled_map, verified, equal_nan=True) - assert all( - np.isclose(result.parameters[p], 0.0) - for p in SurfaceTerms - if p not in SurfaceTerms.SPHERE - ) - assert all(not np.isclose(result.parameters[p], 0.0) for p in SurfaceTerms.SPHERE) @pytest.mark.integration @@ -25,12 +19,6 @@ def test_map_level_plane(scan_image_with_nans: ScanImage): result = level_map(scan_image_with_nans, SurfaceTerms.PLANE) assert result assert np.allclose(result.leveled_map, verified, equal_nan=True) - assert all( - np.isclose(result.parameters[p], 0.0) - for p in SurfaceTerms - if p not in SurfaceTerms.PLANE - ) - assert all(not np.isclose(result.parameters[p], 0.0) for p in SurfaceTerms.PLANE) @pytest.mark.integration @@ -38,7 +26,6 @@ def test_map_level_none(scan_image_with_nans: ScanImage): result = level_map(scan_image_with_nans, SurfaceTerms.NONE) assert result assert np.allclose(result.leveled_map, scan_image_with_nans.data, equal_nan=True) - assert all(np.isclose(result.parameters[p], 0.0) for p in SurfaceTerms) @pytest.mark.integration @@ -51,35 +38,3 @@ def test_map_level_offset(scan_image_with_nans: ScanImage): scan_image_with_nans.data, equal_nan=True, ) - assert result.parameters[SurfaceTerms.OFFSET] != 0 - assert all( - np.isclose(result.parameters[p], 0.0) - for p in SurfaceTerms - if p != SurfaceTerms.OFFSET - ) - - -@pytest.mark.integration -def test_map_level_reference_point_has_no_effect_with_none( - scan_image_with_nans: ScanImage, -): - result_centered = level_map(scan_image_with_nans, SurfaceTerms.NONE) - result_ref = level_map( - scan_image_with_nans, SurfaceTerms.NONE, reference_point=(10.5, -5.2) - ) - assert np.allclose( - result_centered.leveled_map, result_ref.leveled_map, equal_nan=True - ) - - -@pytest.mark.integration -def test_map_level_reference_point_has_effect_with_plane( - scan_image_with_nans: ScanImage, -): - result_centered = level_map(scan_image_with_nans, SurfaceTerms.PLANE) - result_ref = level_map( - scan_image_with_nans, SurfaceTerms.NONE, reference_point=(10.5, -5.2) - ) - assert not np.allclose( - result_centered.leveled_map, result_ref.leveled_map, equal_nan=True - ) From 736ff2559cd3d5b3867a223689e6e8a0538dfe9c Mon Sep 17 00:00:00 2001 From: cfs-data <145435153+cfs-data@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:37:32 +0100 Subject: [PATCH 2/4] Use match-case --- .../src/conversion/leveling/core.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/scratch-core/src/conversion/leveling/core.py b/packages/scratch-core/src/conversion/leveling/core.py index b15f5111..5cbc9c17 100644 --- a/packages/scratch-core/src/conversion/leveling/core.py +++ b/packages/scratch-core/src/conversion/leveling/core.py @@ -27,12 +27,19 @@ def level_map(scan_image: ScanImage, terms: SurfaceTerms) -> LevelingResult: step_x=scan_image.scale_x, step_y=scan_image.scale_y, ) - if terms.name == "SPHERE": - degree = 2 - elif terms.name == "PLANE": - degree = 1 - else: - degree = 0 + match terms: + case ( + SurfaceTerms.TILT_X + | SurfaceTerms.TILT_Y + | SurfaceTerms.ASTIG_45 + | SurfaceTerms.PLANE + ): + degree = 1 + case SurfaceTerms.DEFOCUS | SurfaceTerms.ASTIG_0 | SurfaceTerms.SPHERE: + degree = 2 + case _: + degree = 0 + leveled, trend = surface.detrend_polynomial( degree=degree, inplace=False, return_trend=True ) From 2857a203ba491eb58bcd597736517d68b7128211 Mon Sep 17 00:00:00 2001 From: cfs-data <145435153+cfs-data@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:12:57 +0100 Subject: [PATCH 3/4] update lock file --- uv.lock | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/uv.lock b/uv.lock index 0c840110..79953a3c 100644 --- a/uv.lock +++ b/uv.lock @@ -565,11 +565,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.0" +version = "3.20.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] @@ -678,11 +678,11 @@ wheels = [ [[package]] name = "identify" -version = "2.6.15" +version = "2.6.16" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, ] [[package]] @@ -1311,7 +1311,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.5.0" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -1320,9 +1320,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" }, + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, ] [[package]] @@ -2008,9 +2008,11 @@ source = { virtual = "." } dependencies = [ { name = "fastapi", extra = ["standard"] }, { name = "loguru" }, + { name = "numpy" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "returns" }, + { name = "scipy" }, { name = "scratch-core" }, { name = "uvicorn" }, ] @@ -2037,9 +2039,11 @@ dev = [ requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = ">=0.119.0" }, { name = "loguru", specifier = ">=0.7.3" }, + { name = "numpy", specifier = ">=2.3.5" }, { name = "pydantic", specifier = ">=2.12.4" }, { name = "pydantic-settings", specifier = ">=2.0.0" }, { name = "returns", specifier = ">=0.26.0" }, + { name = "scipy", specifier = ">=1.16.3" }, { name = "scratch-core", editable = "packages/scratch-core" }, { name = "uvicorn", specifier = ">=0.38.0" }, ] @@ -2362,16 +2366,16 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.35.4" +version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] [[package]] From 7a492e5e1d2d3fde8dccd1d7da6eea442f8667d7 Mon Sep 17 00:00:00 2001 From: cfs-data <145435153+cfs-data@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:41:26 +0100 Subject: [PATCH 4/4] remove ref in marks --- .../preprocess_impression/preprocess_impression.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/scratch-core/src/conversion/preprocess_impression/preprocess_impression.py b/packages/scratch-core/src/conversion/preprocess_impression/preprocess_impression.py index 43a10d1b..ecce2dd2 100644 --- a/packages/scratch-core/src/conversion/preprocess_impression/preprocess_impression.py +++ b/packages/scratch-core/src/conversion/preprocess_impression/preprocess_impression.py @@ -89,7 +89,7 @@ def preprocess_impression_mark( ) # Stage 8: Final leveling - mark_filtered, _ = _level_mark(mark_filtered, params.surface_terms, mark.center) + mark_filtered, _ = _level_mark(mark_filtered, params.surface_terms) # Prepare leveled-only output mark_leveled_final = _finalize_leveled_output( @@ -109,7 +109,6 @@ def preprocess_impression_mark( def _level_mark( mark: Mark, terms: SurfaceTerms, - reference_point: Point2D | None = None, ) -> tuple[Mark, DepthData]: result = level_map(mark.scan_image, terms=terms) leveled_mark = update_mark_data(mark, result.leveled_map) @@ -154,6 +153,5 @@ def _finalize_leveled_output( # Apply PLANE-only leveling (after resampling, like MATLAB) rigid_terms = surface_terms & SurfaceTerms.PLANE - leveled_mark, _ = _level_mark(mark_restored, rigid_terms, reference_point) - + leveled_mark, _ = _level_mark(mark_restored, rigid_terms) return leveled_mark