Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 30 additions & 45 deletions packages/scratch-core/src/conversion/leveling/core.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,46 @@
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.

This computation effectively acts as a high-pass filter on the image data.

: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,
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
)
return LevelingResult(leveled_map=leveled.data, fitted_surface=trend.data)
39 changes: 1 addition & 38 deletions packages/scratch-core/src/conversion/leveling/data_types.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from enum import Flag, auto
import numpy as np
from typing import Callable
from pydantic import BaseModel

from container_models.base import FloatArray1D, FloatArray2D
from container_models.base import FloatArray2D


class SurfaceTerms(Flag):
Expand All @@ -26,48 +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[[FloatArray1D, FloatArray1D], FloatArray1D]
] = {
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: FloatArray2D
parameters: dict[SurfaceTerms, float]
residual_rms: float
fitted_surface: FloatArray2D


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: FloatArray1D
ys: FloatArray1D
x_mean: float
y_mean: float
scale: float
17 changes: 0 additions & 17 deletions packages/scratch-core/src/conversion/leveling/solver/__init__.py

This file was deleted.

46 changes: 0 additions & 46 deletions packages/scratch-core/src/conversion/leveling/solver/core.py

This file was deleted.

26 changes: 0 additions & 26 deletions packages/scratch-core/src/conversion/leveling/solver/design.py

This file was deleted.

27 changes: 0 additions & 27 deletions packages/scratch-core/src/conversion/leveling/solver/grid.py

This file was deleted.

85 changes: 0 additions & 85 deletions packages/scratch-core/src/conversion/leveling/solver/transforms.py

This file was deleted.

16 changes: 0 additions & 16 deletions packages/scratch-core/src/conversion/leveling/solver/utils.py

This file was deleted.

Loading