API reference

Sampling moire analysis for displacement and strain measurement.

class moirestrain.AnalysisResult(reference_phase: ndarray, deformed_phase: ndarray, phase_difference: ndarray, displacement: ndarray)

Bases: object

Result returned by analyze().

deformed_phase: ndarray
displacement: ndarray
phase_difference: ndarray
reference_phase: ndarray
class moirestrain.GratingROI(image_points: ndarray, bounds: tuple[int, int, int, int], mask: ndarray, energy: ndarray)

Bases: object

Detected grating region in a larger image.

bounds and mask are the primary detection results. image_points are optional rectification hints estimated from the mask; they are not required when the downstream analysis works on an axis-aligned crop.

bounds: tuple[int, int, int, int]
energy: ndarray
image_points: ndarray
mask: ndarray
class moirestrain.GridAnalysisResult(x: AnalysisResult, y: AnalysisResult, strain: StrainResult, reference_x_component: ndarray, reference_y_component: ndarray, deformed_x_component: ndarray, deformed_y_component: ndarray)

Bases: object

Two-direction grid analysis result.

deformed_x_component: ndarray
deformed_y_component: ndarray
reference_x_component: ndarray
reference_y_component: ndarray
strain: StrainResult
x: AnalysisResult
y: AnalysisResult
class moirestrain.PerspectiveCalibration(image_points: ndarray, world_points: ndarray, output_shape: tuple[int, int], homography: ndarray, pixel_spacing: tuple[float, float])

Bases: object

Planar perspective calibration for a grating region.

image_points and world_points are four (x, y) corner points in corresponding order. The homography maps image coordinates to rectified pixel coordinates.

classmethod from_points(image_points: ndarray, world_points: ndarray, *, output_shape: tuple[int, int]) PerspectiveCalibration
homography: ndarray
image_points: ndarray
output_shape: tuple[int, int]
pixel_spacing: tuple[float, float]
world_points: ndarray
class moirestrain.StrainResult(exx: ndarray, eyy: ndarray | None, gamma_xy: ndarray | None)

Bases: object

Small-strain components calculated from displacement fields.

exx: ndarray
eyy: ndarray | None
gamma_xy: ndarray | None
class moirestrain.SyntheticExperiment(reference_image: ndarray, deformed_image: ndarray, true_u: ndarray, true_exx: ndarray, period: int, strain_xx: float, rigid_shift: float, noise_std: float)

Bases: object

Synthetic grating image pair with ground-truth displacement.

deformed_image: ndarray
noise_std: float
period: int
reference_image: ndarray
rigid_shift: float
strain_xx: float
true_exx: ndarray
true_u: ndarray
class moirestrain.SyntheticSquareGridExperiment(reference: ndarray, deformed: ndarray, true_u: ndarray, true_v: ndarray, true_exx: ndarray, true_eyy: ndarray, true_gamma_xy: ndarray, period: int, marker_size: int, strain_xx: float, strain_yy: float, shear_xy: float, rigid_shift: tuple[float, float], noise_std: float)

Bases: object

Synthetic square-marker grid pair with ground-truth strain.

deformed: ndarray
marker_size: int
noise_std: float
period: int
reference: ndarray
rigid_shift: tuple[float, float]
shear_xy: float
strain_xx: float
strain_yy: float
true_exx: ndarray
true_eyy: ndarray
true_gamma_xy: ndarray
true_u: ndarray
true_v: ndarray
class moirestrain.SyntheticStrainExperiment(reference_x: ndarray, deformed_x: ndarray, reference_y: ndarray, deformed_y: ndarray, true_u: ndarray, true_v: ndarray, true_exx: ndarray, true_eyy: ndarray, true_gamma_xy: ndarray, period: int, strain_xx: float, strain_yy: float, shear_xy: float, rigid_shift: tuple[float, float], noise_std: float)

Bases: object

Synthetic two-direction grating data with ground-truth strain.

deformed_x: ndarray
deformed_y: ndarray
noise_std: float
period: int
reference_x: ndarray
reference_y: ndarray
rigid_shift: tuple[float, float]
shear_xy: float
strain_xx: float
strain_yy: float
true_exx: ndarray
true_eyy: ndarray
true_gamma_xy: ndarray
true_u: ndarray
true_v: ndarray
moirestrain.analyze(reference_image: ndarray, deformed_image: ndarray, period: int, *, axis: Literal['x', 'y', 0, 1] = 'x', grating_pitch: float | None = None, unwrap_axis: Literal['x', 'y', 0, 1] | None = None) AnalysisResult

Run the sampling moire pipeline for a reference/deformed image pair.

moirestrain.analyze_grid(reference_image: ndarray, deformed_image: ndarray, period: int, *, grating_pitch: float | None = None, strain_spacing: float | tuple[float, float] = 1.0, strain_smooth_window: int | tuple[int, int] = 1) GridAnalysisResult

Analyze a two-direction square-marker grid image pair.

The x/y grating components are first separated by directional smoothing. Each component is then analyzed with the phase-shifted sampling moire method, and small-strain components are calculated from the resulting displacement fields.

moirestrain.apply_homography(points: ndarray, homography: ndarray) ndarray

Apply a homography to (..., 2) points.

moirestrain.apply_valid_mask(array: ndarray, valid_mask: ndarray, fill_value: float = nan) ndarray

Return a copy of array with invalid pixels replaced by fill_value.

moirestrain.crop_grating_roi(image: ndarray, roi: GratingROI, *, margin: int = 0) ndarray

Crop the axis-aligned bounding box of a detected grating ROI.

moirestrain.crop_roi(image: ndarray, bounds: tuple[int, int, int, int]) ndarray

Crop a rectangular ROI as (y0, x0, y1, x1).

moirestrain.crop_to_mask(array: ndarray, valid_mask: ndarray) ndarray

Crop an array to the bounding box of valid_mask.

moirestrain.detect_grating_roi(image: ndarray, *, period: int, threshold: float | None = None, min_area: int | None = None, corner_method: str = 'oriented_box') GratingROI

Detect the dominant grating patch by thresholding grating energy.

moirestrain.displacement(reference_phase: ndarray, deformed_phase: ndarray, grating_pitch: float, *, unwrap: bool = True, unwrap_axis: Literal['x', 'y', 0, 1] | None = None) ndarray

Calculate displacement from phase difference.

The sign convention is u = (phase_deformed - phase_reference) * pitch / 2pi. Use a negative grating_pitch if your imaging setup uses the opposite phase-to-displacement convention.

moirestrain.grating_energy(image: ndarray, *, period: int, window: int | None = None) ndarray

Calculate local high-frequency energy for grating ROI detection.

moirestrain.homography_from_points(source_points: ndarray, destination_points: ndarray) ndarray

Estimate a projective transform from four point correspondences.

Points are (x, y) pairs. The returned matrix maps homogeneous source coordinates to destination coordinates.

moirestrain.inner_valid_mask(shape: tuple[int, int], margin: int) ndarray

Return a mask that excludes a fixed margin from image borders.

moirestrain.load_grayscale_image(path: str | Path, *, npz_key: str | None = None) ndarray

Load a grayscale image from .npy, .npz, or common image files.

moirestrain.make_microstrain_experiment(*, shape: tuple[int, int] = (128, 160), period: int = 8, strain_xx: float = 0.0008, rigid_shift: float = 0.6, noise_std: float = 0.01, contrast: float = 0.42, background: float = 0.5, illumination_gradient: float = 0.15, seed: int | None = 0) SyntheticExperiment

Create an experimental-looking grating image pair.

The deformed image contains a known x-direction displacement field u = rigid_shift + strain_xx * (x - x_center). Images are normalized to roughly [0, 1] and include smooth illumination variation plus Gaussian sensor noise.

moirestrain.make_microstrain_square_grid(*, shape: tuple[int, int] = (160, 192), period: int = 8, marker_size: int | None = None, strain_xx: float = 0.0005, strain_yy: float = -0.0002, shear_xy: float = 0.00015, rigid_shift: tuple[float, float] = (0.6, -0.35), supersample: int = 8, blur_window: int = 3, noise_std: float = 0.0, seed: int | None = 0) SyntheticSquareGridExperiment

Create a fronto-parallel square-marker grid with known microstrain.

The target geometry is white background with black square markers on a regular grid. By default, marker_size = period / 2, so the black square width equals the white gap width. The captured image is simulated by supersampling each pixel, then applying an optional box blur.

moirestrain.make_strain_distribution_experiment(*, shape: tuple[int, int] = (160, 192), period: int = 8, strain_xx: float = 0.0008, strain_yy: float = -0.00025, shear_xy: float = 0.00035, rigid_shift: tuple[float, float] = (0.6, -0.35), noise_std: float = 0.008, contrast: float = 0.42, background: float = 0.5, illumination_gradient: float = 0.12, seed: int | None = 1) SyntheticStrainExperiment

Create x/y grating images for full small-strain recovery.

The displacement field is linear:

u = u0 + exx * x + 0.5 * gamma_xy * y v = v0 + eyy * y + 0.5 * gamma_xy * x

Coordinates are centered, so the rigid shift is easy to interpret.

moirestrain.mask_bounds(valid_mask: ndarray) tuple[int, int, int, int]

Return (y0, x0, y1, x1) bounds for true pixels in a mask.

moirestrain.phase_shifted_sampling_moire(reference_image: ndarray, deformed_image: ndarray, period: int, *, axis: Literal['x', 'y', 0, 1] = 'x', grating_pitch: float | None = None, unwrap_axis: Literal['x', 'y', 0, 1] | None = None) AnalysisResult

Analyze displacement with the phase-shifted sampling moire method.

This is an explicit alias for analyze(). Internally it generates period phase-shifted moire images by shifting the sampling offset and estimates the wrapped phase using the phase-shifting/DFT formula in wrapped_phase().

moirestrain.phase_shifted_stack(image: ndarray, period: int, axis: Literal['x', 'y', 0, 1] = 'x') ndarray

Generate phase-shifted moire images from one grating image.

The image is thinned every period pixels at each possible offset, then linearly interpolated back to the original size. The returned array has shape (period, height, width).

moirestrain.pixel_spacing_from_world_points(world_points: ndarray, output_shape: tuple[int, int]) tuple[float, float]

Estimate (dy, dx) spacing from rectangular world corner points.

moirestrain.recommended_strain_smoothing_window(period: int, *, cycles: float = 8.0, minimum: int = 9) int

Return an odd smoothing window for strain estimation.

Square-marker targets contain strong harmonics, and displacement fields can retain pixel-periodic ripple after phase analysis. Strain calculation differentiates displacement, so this ripple should be smoothed before gradients are taken. cycles controls the smoothing length in grating periods. Larger values reduce ripple but lower spatial resolution.

moirestrain.rectangle_points(shape: tuple[int, int]) ndarray

Return rectangle corner points for shape in (x, y) order.

moirestrain.rectify_image(image: ndarray, calibration_or_points: PerspectiveCalibration | ndarray, *, output_shape: tuple[int, int] | None = None, world_points: ndarray | None = None, fill_value: float = nan) ndarray

Rectify a planar grating ROI to a fronto-parallel image.

Pass either a PerspectiveCalibration or four image corner points. When passing points directly, output_shape is required. The point order should be top-left, top-right, bottom-right, bottom-left.

moirestrain.rectify_image_pair(reference_image: ndarray, deformed_image: ndarray, calibration_or_points: PerspectiveCalibration | ndarray, *, output_shape: tuple[int, int] | None = None, world_points: ndarray | None = None, fill_value: float = nan) tuple[ndarray, ndarray]

Rectify reference/deformed images with the same planar transform.

moirestrain.resample_oblique_grid(image: ndarray, *, origin: tuple[float, float], x_vector: tuple[float, float], y_vector: tuple[float, float], output_shape: tuple[int, int], fill_value: float = nan) ndarray

Sample an image on an oblique grid defined by two image-space vectors.

This is useful when the grating is rotated or sheared but a full perspective model is unnecessary. origin, x_vector, and y_vector are image-space (x, y) coordinates. The output x/y axes follow those vectors.

moirestrain.robust_limits(array: ndarray, *, percentiles: tuple[float, float] = (2.0, 98.0)) tuple[float, float]

Return percentile color limits while ignoring NaNs.

moirestrain.sample_bilinear(image: ndarray, x: ndarray, y: ndarray, fill_value: float = nan) ndarray

Sample a 2D image at floating-point x/y coordinates.

moirestrain.save_analysis_npz(path: str | Path, *, u: ndarray, v: ndarray, exx: ndarray, eyy: ndarray, gamma_xy: ndarray, valid_mask: ndarray, reference: ndarray | None = None, deformed: ndarray | None = None, extra_arrays: Mapping[str, Any] | None = None) None

Save grid-analysis arrays to a compressed .npz file.

moirestrain.save_experiment_npz(path: str | Path, experiment: SyntheticExperiment) None

Save a synthetic experiment to a compressed .npz file.

moirestrain.save_grayscale_image(path: str | Path, image: ndarray) None

Save a grayscale array to .npy, .npz, or a common image file.

moirestrain.save_grid_analysis_figure(path: str | Path, *, reference: ndarray, deformed: ndarray, u: ndarray, exx: ndarray, eyy: ndarray, gamma_xy: ndarray, valid_mask: ndarray) None

Save a compact PNG summary for grid analysis.

moirestrain.save_grid_truth_comparison_figure(path: str | Path, *, measured: dict[str, ndarray], truth: dict[str, ndarray], valid_mask: ndarray) None

Save measured/truth/error comparison panels for grid analysis.

moirestrain.save_square_grid_experiment_npz(path: str | Path, experiment: SyntheticSquareGridExperiment) None

Save a square-marker grid experiment to .npz.

moirestrain.save_strain_experiment_npz(path: str | Path, experiment: SyntheticStrainExperiment) None

Save a two-direction synthetic strain experiment to .npz.

moirestrain.separate_grid_components(image: ndarray, *, period: int) tuple[ndarray, ndarray]

Separate a square 2D grid target into x/y grating components.

Smoothing along y suppresses row-wise markers and leaves the x-periodic component. Smoothing along x suppresses column-wise markers and leaves the y-periodic component.

moirestrain.smooth_axis(image: ndarray, window: int, axis: Literal['x', 'y', 0, 1]) ndarray

Smooth a 2D image along one axis using a box filter.

moirestrain.strain_field(u: ndarray, v: ndarray | None = None, *, spacing: float | tuple[float, float] = 1.0, smooth_window: int | tuple[int, int] = 1) StrainResult

Calculate small-strain components from displacement fields.

Parameters:
  • u – x-direction displacement field.

  • v – Optional y-direction displacement field. When omitted, only exx is returned and eyy / gamma_xy are None.

  • spacing – Pixel spacing as a scalar or (dy, dx) tuple. The returned strain is displacement unit divided by spacing unit.

  • smooth_window – Optional box smoothing window applied before differentiation. This is useful because strain estimation differentiates displacement noise.

moirestrain.unwrap_phase(phase: ndarray, axis: Literal['x', 'y', 0, 1] | None = None) ndarray

Unwrap phase along one axis or both image axes.

moirestrain.warp_perspective(image: ndarray, homography: ndarray, output_shape: tuple[int, int], *, fill_value: float = nan) ndarray

Warp an image with bilinear interpolation.

homography maps input image coordinates to output image coordinates. The implementation samples the input image by applying the inverse homography to each output pixel.

moirestrain.wrapped_phase(images: ndarray) ndarray

Estimate wrapped phase from equally phase-shifted images.

images must have shape (n_shifts, height, width). For synthetic data I_k = a + b * cos(phi + 2*pi*k/n_shifts), this returns phi wrapped to [-pi, pi].

Core module

class moirestrain.core.AnalysisResult(reference_phase: ndarray, deformed_phase: ndarray, phase_difference: ndarray, displacement: ndarray)

Bases: object

Result returned by analyze().

deformed_phase: ndarray
displacement: ndarray
phase_difference: ndarray
reference_phase: ndarray
class moirestrain.core.GridAnalysisResult(x: AnalysisResult, y: AnalysisResult, strain: StrainResult, reference_x_component: ndarray, reference_y_component: ndarray, deformed_x_component: ndarray, deformed_y_component: ndarray)

Bases: object

Two-direction grid analysis result.

deformed_x_component: ndarray
deformed_y_component: ndarray
reference_x_component: ndarray
reference_y_component: ndarray
strain: StrainResult
x: AnalysisResult
y: AnalysisResult
class moirestrain.core.StrainResult(exx: ndarray, eyy: ndarray | None, gamma_xy: ndarray | None)

Bases: object

Small-strain components calculated from displacement fields.

exx: ndarray
eyy: ndarray | None
gamma_xy: ndarray | None
moirestrain.core.analyze(reference_image: ndarray, deformed_image: ndarray, period: int, *, axis: Literal['x', 'y', 0, 1] = 'x', grating_pitch: float | None = None, unwrap_axis: Literal['x', 'y', 0, 1] | None = None) AnalysisResult

Run the sampling moire pipeline for a reference/deformed image pair.

moirestrain.core.analyze_grid(reference_image: ndarray, deformed_image: ndarray, period: int, *, grating_pitch: float | None = None, strain_spacing: float | tuple[float, float] = 1.0, strain_smooth_window: int | tuple[int, int] = 1) GridAnalysisResult

Analyze a two-direction square-marker grid image pair.

The x/y grating components are first separated by directional smoothing. Each component is then analyzed with the phase-shifted sampling moire method, and small-strain components are calculated from the resulting displacement fields.

moirestrain.core.displacement(reference_phase: ndarray, deformed_phase: ndarray, grating_pitch: float, *, unwrap: bool = True, unwrap_axis: Literal['x', 'y', 0, 1] | None = None) ndarray

Calculate displacement from phase difference.

The sign convention is u = (phase_deformed - phase_reference) * pitch / 2pi. Use a negative grating_pitch if your imaging setup uses the opposite phase-to-displacement convention.

moirestrain.core.phase_shifted_sampling_moire(reference_image: ndarray, deformed_image: ndarray, period: int, *, axis: Literal['x', 'y', 0, 1] = 'x', grating_pitch: float | None = None, unwrap_axis: Literal['x', 'y', 0, 1] | None = None) AnalysisResult

Analyze displacement with the phase-shifted sampling moire method.

This is an explicit alias for analyze(). Internally it generates period phase-shifted moire images by shifting the sampling offset and estimates the wrapped phase using the phase-shifting/DFT formula in wrapped_phase().

moirestrain.core.phase_shifted_stack(image: ndarray, period: int, axis: Literal['x', 'y', 0, 1] = 'x') ndarray

Generate phase-shifted moire images from one grating image.

The image is thinned every period pixels at each possible offset, then linearly interpolated back to the original size. The returned array has shape (period, height, width).

moirestrain.core.recommended_strain_smoothing_window(period: int, *, cycles: float = 8.0, minimum: int = 9) int

Return an odd smoothing window for strain estimation.

Square-marker targets contain strong harmonics, and displacement fields can retain pixel-periodic ripple after phase analysis. Strain calculation differentiates displacement, so this ripple should be smoothed before gradients are taken. cycles controls the smoothing length in grating periods. Larger values reduce ripple but lower spatial resolution.

moirestrain.core.separate_grid_components(image: ndarray, *, period: int) tuple[ndarray, ndarray]

Separate a square 2D grid target into x/y grating components.

Smoothing along y suppresses row-wise markers and leaves the x-periodic component. Smoothing along x suppresses column-wise markers and leaves the y-periodic component.

moirestrain.core.smooth_axis(image: ndarray, window: int, axis: Literal['x', 'y', 0, 1]) ndarray

Smooth a 2D image along one axis using a box filter.

moirestrain.core.strain_field(u: ndarray, v: ndarray | None = None, *, spacing: float | tuple[float, float] = 1.0, smooth_window: int | tuple[int, int] = 1) StrainResult

Calculate small-strain components from displacement fields.

Parameters:
  • u – x-direction displacement field.

  • v – Optional y-direction displacement field. When omitted, only exx is returned and eyy / gamma_xy are None.

  • spacing – Pixel spacing as a scalar or (dy, dx) tuple. The returned strain is displacement unit divided by spacing unit.

  • smooth_window – Optional box smoothing window applied before differentiation. This is useful because strain estimation differentiates displacement noise.

moirestrain.core.unwrap_phase(phase: ndarray, axis: Literal['x', 'y', 0, 1] | None = None) ndarray

Unwrap phase along one axis or both image axes.

moirestrain.core.wrapped_phase(images: ndarray) ndarray

Estimate wrapped phase from equally phase-shifted images.

images must have shape (n_shifts, height, width). For synthetic data I_k = a + b * cos(phi + 2*pi*k/n_shifts), this returns phi wrapped to [-pi, pi].

Synthetic data

class moirestrain.synthetic.SyntheticExperiment(reference_image: ndarray, deformed_image: ndarray, true_u: ndarray, true_exx: ndarray, period: int, strain_xx: float, rigid_shift: float, noise_std: float)

Bases: object

Synthetic grating image pair with ground-truth displacement.

deformed_image: ndarray
noise_std: float
period: int
reference_image: ndarray
rigid_shift: float
strain_xx: float
true_exx: ndarray
true_u: ndarray
class moirestrain.synthetic.SyntheticSquareGridExperiment(reference: ndarray, deformed: ndarray, true_u: ndarray, true_v: ndarray, true_exx: ndarray, true_eyy: ndarray, true_gamma_xy: ndarray, period: int, marker_size: int, strain_xx: float, strain_yy: float, shear_xy: float, rigid_shift: tuple[float, float], noise_std: float)

Bases: object

Synthetic square-marker grid pair with ground-truth strain.

deformed: ndarray
marker_size: int
noise_std: float
period: int
reference: ndarray
rigid_shift: tuple[float, float]
shear_xy: float
strain_xx: float
strain_yy: float
true_exx: ndarray
true_eyy: ndarray
true_gamma_xy: ndarray
true_u: ndarray
true_v: ndarray
class moirestrain.synthetic.SyntheticStrainExperiment(reference_x: ndarray, deformed_x: ndarray, reference_y: ndarray, deformed_y: ndarray, true_u: ndarray, true_v: ndarray, true_exx: ndarray, true_eyy: ndarray, true_gamma_xy: ndarray, period: int, strain_xx: float, strain_yy: float, shear_xy: float, rigid_shift: tuple[float, float], noise_std: float)

Bases: object

Synthetic two-direction grating data with ground-truth strain.

deformed_x: ndarray
deformed_y: ndarray
noise_std: float
period: int
reference_x: ndarray
reference_y: ndarray
rigid_shift: tuple[float, float]
shear_xy: float
strain_xx: float
strain_yy: float
true_exx: ndarray
true_eyy: ndarray
true_gamma_xy: ndarray
true_u: ndarray
true_v: ndarray
moirestrain.synthetic.make_microstrain_experiment(*, shape: tuple[int, int] = (128, 160), period: int = 8, strain_xx: float = 0.0008, rigid_shift: float = 0.6, noise_std: float = 0.01, contrast: float = 0.42, background: float = 0.5, illumination_gradient: float = 0.15, seed: int | None = 0) SyntheticExperiment

Create an experimental-looking grating image pair.

The deformed image contains a known x-direction displacement field u = rigid_shift + strain_xx * (x - x_center). Images are normalized to roughly [0, 1] and include smooth illumination variation plus Gaussian sensor noise.

moirestrain.synthetic.make_microstrain_square_grid(*, shape: tuple[int, int] = (160, 192), period: int = 8, marker_size: int | None = None, strain_xx: float = 0.0005, strain_yy: float = -0.0002, shear_xy: float = 0.00015, rigid_shift: tuple[float, float] = (0.6, -0.35), supersample: int = 8, blur_window: int = 3, noise_std: float = 0.0, seed: int | None = 0) SyntheticSquareGridExperiment

Create a fronto-parallel square-marker grid with known microstrain.

The target geometry is white background with black square markers on a regular grid. By default, marker_size = period / 2, so the black square width equals the white gap width. The captured image is simulated by supersampling each pixel, then applying an optional box blur.

moirestrain.synthetic.make_strain_distribution_experiment(*, shape: tuple[int, int] = (160, 192), period: int = 8, strain_xx: float = 0.0008, strain_yy: float = -0.00025, shear_xy: float = 0.00035, rigid_shift: tuple[float, float] = (0.6, -0.35), noise_std: float = 0.008, contrast: float = 0.42, background: float = 0.5, illumination_gradient: float = 0.12, seed: int | None = 1) SyntheticStrainExperiment

Create x/y grating images for full small-strain recovery.

The displacement field is linear:

u = u0 + exx * x + 0.5 * gamma_xy * y v = v0 + eyy * y + 0.5 * gamma_xy * x

Coordinates are centered, so the rigid shift is easy to interpret.

moirestrain.synthetic.save_experiment_npz(path: str | Path, experiment: SyntheticExperiment) None

Save a synthetic experiment to a compressed .npz file.

moirestrain.synthetic.save_square_grid_experiment_npz(path: str | Path, experiment: SyntheticSquareGridExperiment) None

Save a square-marker grid experiment to .npz.

moirestrain.synthetic.save_strain_experiment_npz(path: str | Path, experiment: SyntheticStrainExperiment) None

Save a two-direction synthetic strain experiment to .npz.

Geometry

class moirestrain.geometry.PerspectiveCalibration(image_points: ndarray, world_points: ndarray, output_shape: tuple[int, int], homography: ndarray, pixel_spacing: tuple[float, float])

Bases: object

Planar perspective calibration for a grating region.

image_points and world_points are four (x, y) corner points in corresponding order. The homography maps image coordinates to rectified pixel coordinates.

classmethod from_points(image_points: ndarray, world_points: ndarray, *, output_shape: tuple[int, int]) PerspectiveCalibration
homography: ndarray
image_points: ndarray
output_shape: tuple[int, int]
pixel_spacing: tuple[float, float]
world_points: ndarray
moirestrain.geometry.apply_homography(points: ndarray, homography: ndarray) ndarray

Apply a homography to (..., 2) points.

moirestrain.geometry.crop_roi(image: ndarray, bounds: tuple[int, int, int, int]) ndarray

Crop a rectangular ROI as (y0, x0, y1, x1).

moirestrain.geometry.homography_from_points(source_points: ndarray, destination_points: ndarray) ndarray

Estimate a projective transform from four point correspondences.

Points are (x, y) pairs. The returned matrix maps homogeneous source coordinates to destination coordinates.

moirestrain.geometry.pixel_spacing_from_world_points(world_points: ndarray, output_shape: tuple[int, int]) tuple[float, float]

Estimate (dy, dx) spacing from rectangular world corner points.

moirestrain.geometry.rectangle_points(shape: tuple[int, int]) ndarray

Return rectangle corner points for shape in (x, y) order.

moirestrain.geometry.rectify_image(image: ndarray, calibration_or_points: PerspectiveCalibration | ndarray, *, output_shape: tuple[int, int] | None = None, world_points: ndarray | None = None, fill_value: float = nan) ndarray

Rectify a planar grating ROI to a fronto-parallel image.

Pass either a PerspectiveCalibration or four image corner points. When passing points directly, output_shape is required. The point order should be top-left, top-right, bottom-right, bottom-left.

moirestrain.geometry.rectify_image_pair(reference_image: ndarray, deformed_image: ndarray, calibration_or_points: PerspectiveCalibration | ndarray, *, output_shape: tuple[int, int] | None = None, world_points: ndarray | None = None, fill_value: float = nan) tuple[ndarray, ndarray]

Rectify reference/deformed images with the same planar transform.

moirestrain.geometry.resample_oblique_grid(image: ndarray, *, origin: tuple[float, float], x_vector: tuple[float, float], y_vector: tuple[float, float], output_shape: tuple[int, int], fill_value: float = nan) ndarray

Sample an image on an oblique grid defined by two image-space vectors.

This is useful when the grating is rotated or sheared but a full perspective model is unnecessary. origin, x_vector, and y_vector are image-space (x, y) coordinates. The output x/y axes follow those vectors.

moirestrain.geometry.sample_bilinear(image: ndarray, x: ndarray, y: ndarray, fill_value: float = nan) ndarray

Sample a 2D image at floating-point x/y coordinates.

moirestrain.geometry.warp_perspective(image: ndarray, homography: ndarray, output_shape: tuple[int, int], *, fill_value: float = nan) ndarray

Warp an image with bilinear interpolation.

homography maps input image coordinates to output image coordinates. The implementation samples the input image by applying the inverse homography to each output pixel.

ROI detection

class moirestrain.roi.GratingROI(image_points: ndarray, bounds: tuple[int, int, int, int], mask: ndarray, energy: ndarray)

Bases: object

Detected grating region in a larger image.

bounds and mask are the primary detection results. image_points are optional rectification hints estimated from the mask; they are not required when the downstream analysis works on an axis-aligned crop.

bounds: tuple[int, int, int, int]
energy: ndarray
image_points: ndarray
mask: ndarray
moirestrain.roi.crop_grating_roi(image: ndarray, roi: GratingROI, *, margin: int = 0) ndarray

Crop the axis-aligned bounding box of a detected grating ROI.

moirestrain.roi.detect_grating_roi(image: ndarray, *, period: int, threshold: float | None = None, min_area: int | None = None, corner_method: str = 'oriented_box') GratingROI

Detect the dominant grating patch by thresholding grating energy.

moirestrain.roi.grating_energy(image: ndarray, *, period: int, window: int | None = None) ndarray

Calculate local high-frequency energy for grating ROI detection.

Masking and display helpers

moirestrain.masking.apply_valid_mask(array: ndarray, valid_mask: ndarray, fill_value: float = nan) ndarray

Return a copy of array with invalid pixels replaced by fill_value.

moirestrain.masking.crop_to_mask(array: ndarray, valid_mask: ndarray) ndarray

Crop an array to the bounding box of valid_mask.

moirestrain.masking.inner_valid_mask(shape: tuple[int, int], margin: int) ndarray

Return a mask that excludes a fixed margin from image borders.

moirestrain.masking.mask_bounds(valid_mask: ndarray) tuple[int, int, int, int]

Return (y0, x0, y1, x1) bounds for true pixels in a mask.

moirestrain.masking.robust_limits(array: ndarray, *, percentiles: tuple[float, float] = (2.0, 98.0)) tuple[float, float]

Return percentile color limits while ignoring NaNs.