Tutorial

Basic displacement analysis

Given a reference grating image and a deformed grating image:

import numpy as np
from moirestrain import phase_shifted_sampling_moire

result = phase_shifted_sampling_moire(
    reference_image,
    deformed_image,
    period=8,
    axis="x",
    grating_pitch=8.0,
    unwrap_axis="x",
)

displacement = result.displacement
phase_ref = result.reference_phase
phase_def = result.deformed_phase

The sign convention is:

\[u = \frac{(\phi_\mathrm{def} - \phi_\mathrm{ref}) p}{2\pi}\]

where p is grating_pitch.

Synthetic example

import numpy as np
from moirestrain import analyze

height, width = 80, 120
period = 8
y, x = np.mgrid[:height, :width]

reference = 128.0 + 80.0 * np.cos(2.0 * np.pi * x / period)
true_u = 0.6 + 0.002 * x
deformed = 128.0 + 80.0 * np.cos(2.0 * np.pi * (x + true_u) / period)

result = analyze(reference, deformed, period=period, axis="x", unwrap_axis="x")
print(np.mean(result.displacement))

Synthetic experimental data

moirestrain includes a small generator for experimental-looking grating image pairs. It adds illumination variation and Gaussian sensor noise while keeping ground-truth displacement and strain fields.

from moirestrain import (
    analyze,
    make_microstrain_experiment,
    save_experiment_npz,
    strain_field,
)

experiment = make_microstrain_experiment(
    shape=(128, 160),
    period=8,
    strain_xx=800e-6,
    rigid_shift=0.6,
    noise_std=0.01,
    seed=2,
)
save_experiment_npz("data/synthetic_microstrain_x.npz", experiment)

result = analyze(
    experiment.reference_image,
    experiment.deformed_image,
    period=experiment.period,
    axis="x",
    unwrap_axis="x",
)
strain = strain_field(result.displacement, smooth_window=17)

The same workflow is available as a script:

PYTHONPATH=src python examples/generate_experiment.py

Front-facing square-marker microstrain target

Before testing ROI detection or camera tilt, use a front-facing square-marker grid with known microstrain. The target has black square markers on a white background, with marker width equal to the white gap width. The captured image is simulated by pixel supersampling, so the analysis input is grayscale rather than a perfectly binary image.

The synthetic reference image is generated by evaluating the square-marker target at pixel sample positions. The deformed image is generated from the same target after shifting the sample coordinates by prescribed displacement fields u(x, y) and v(x, y). Ground-truth strain is therefore not estimated from the image; it is calculated directly from the displacement gradients used to make the deformed image:

\[\varepsilon_{xx} = \frac{\partial u}{\partial x}, \quad \varepsilon_{yy} = \frac{\partial v}{\partial y}, \quad \gamma_{xy} = \frac{\partial u}{\partial y} + \frac{\partial v}{\partial x}.\]

This gives a controlled image pair where the measured strain fields can be compared against known truth.

from moirestrain import (
    analyze_grid,
    make_microstrain_square_grid,
    recommended_strain_smoothing_window,
)

experiment = make_microstrain_square_grid(
    period=8,
    strain_xx=500e-6,
    strain_yy=-200e-6,
    shear_xy=150e-6,
    supersample=64,
)
strain_window = recommended_strain_smoothing_window(experiment.period, cycles=8)
result = analyze_grid(
    experiment.reference,
    experiment.deformed,
    period=8,
    strain_smooth_window=strain_window,
)
exx = result.strain.exx

Square-marker targets contain strong harmonics. Their displacement fields can include pixel-periodic ripple, which is amplified by differentiation. Use a larger strain_smooth_window when evaluating microstrain from these targets.

The same workflow is available as a script:

PYTHONPATH=src python examples/microstrain_square_grid.py

For repeatable checks, run the benchmark script. It writes the synthetic input, the analysis result, a JSON metrics file, and a PNG summary. The command exits with a non-zero status when the mean absolute errors exceed the configured limits.

PYTHONPATH=src python examples/benchmark_microstrain.py

To compare measurement conditions, run the sweep script. It varies grating period, blur, noise level, and strain preset, then writes CSV, JSON, a Markdown summary, and a compact period-performance plot.

PYTHONPATH=src python examples/benchmark_sweep.py
Microstrain benchmark sweep period plot

The presets serve different purposes. micro is a small-strain accuracy check, demo makes strain fields easier to see, and stress probes where the pipeline begins to lose accuracy. In these synthetic square-marker cases, larger camera-space periods generally reduce phase ripple and improve strain estimation. Treat the sweep as a design tool for choosing target pitch, imaging magnification, smoothing length, and valid ROI margins before running real experiments.

Command line analysis

For real image files, start with a manually specified ROI and known period:

moirestrain analyze-grid ref.png def.png \
    --period 8 \
    --roi 100,120,260,320 \
    --out result.npz \
    --figure result.png

The .npz output stores full analysis arrays. The PNG summary uses a valid inner ROI for display.

When the grating occupies only part of the image, use automatic ROI detection:

moirestrain analyze-grid ref.png def.png \
    --period 8 \
    --auto-roi \
    --roi-margin 8 \
    --out result.npz \
    --figure result.png

For batch jobs that only need arrays, add --no-figure. The output .npz stores u, v, exx, eyy, gamma_xy, valid_mask, and the analysis settings such as period and strain_window.

If four corners of a tilted planar grating are known, rectify the image pair before analysis:

moirestrain make-calibration \
    --image-points x0,y0,x1,y1,x2,y2,x3,y3 \
    --output-shape 240,320 \
    --period 8 \
    --out calibration.json

moirestrain rectify-pair ref.png def.png \
    --calibration calibration.json \
    --reference-out rectified_ref.npy \
    --deformed-out rectified_def.npy \
    --metadata-out rectification_metadata.json

moirestrain analyze-grid ref.png def.png \
    --period 8 \
    --calibration calibration.json \
    --out result.npz \
    --figure result.png

The corner order is top-left, top-right, bottom-right, bottom-left in image-space x,y coordinates. analyze-grid still accepts inline --rectify-corners for quick experiments, but --calibration is the recommended format for repeatable work.

When physical dimensions are known, add --world-points and --unit to make-calibration. The calibration JSON then records the rectified pixel spacing, which is useful for reporting displacement in physical units. With a calibration that contains world_points, analyze-grid saves u_physical, v_physical, pixel_spacing, unit, and physical-coordinate strain fields in addition to the pixel-unit arrays.

Strain distribution example

For a two-direction grating setup, analyze x and y grating images separately and pass the resulting displacement components to strain_field.

from moirestrain import analyze_grid, make_strain_distribution_experiment

experiment = make_strain_distribution_experiment()

reference_grid = 0.5 * (experiment.reference_x + experiment.reference_y)
deformed_grid = 0.5 * (experiment.deformed_x + experiment.deformed_y)
result = analyze_grid(reference_grid, deformed_grid, experiment.period)
strain = result.strain
exx = strain.exx
eyy = strain.eyy
gamma_xy = strain.gamma_xy

The script below writes input data, result arrays, and a PNG visualization:

PYTHONPATH=src python examples/strain_distribution.py

Perspective rectification and partial grating ROI

When the grating occupies only part of the camera image, first detect or crop the grating ROI. Four corner points are not mandatory for ROI detection; they are only needed when perspective rectification is required.

from moirestrain import analyze, rectify_image

image_points = [
    [34.0, 18.0],
    [164.0, 30.0],
    [150.0, 128.0],
    [20.0, 118.0],
]

reference = rectify_image(reference_camera, image_points, output_shape=(96, 128))
deformed = rectify_image(deformed_camera, image_points, output_shape=(96, 128))
result = analyze(reference, deformed, period=8, axis="x", unwrap_axis="x")

This handles a planar grating region under perspective projection. For an axis-aligned target, an ROI crop from detect_grating_roi is enough. For a tilted target, corner points or a reliable geometric fit are needed to rectify the image before strain analysis.

The same workflow is available as a script:

PYTHONPATH=src python examples/perspective_rectification.py

When a target is strongly tilted, use one of two routes:

  • Rectify the grating ROI with rectify_image or rectify_image_pair when four planar corner points are available.

  • Use resample_oblique_grid when the local grating direction vectors are known and an affine/oblique sampling model is sufficient.

The analysis arrays are kept at full size. Error metrics should use a valid inner ROI near image boundaries; inner_valid_mask and apply_valid_mask provide this workflow without changing the displayed array shape.

Partial-grid detection and oblique-grid rectification

For an axis-aligned square-grid patch inside a larger camera image, use the partial-grid detection example. It detects the grating ROI from local grating energy, analyzes the detected ROI, and writes a README-oriented figure with the full image, grating-energy map, detected mask, and measured/true/error exx fields.

PYTHONPATH=src python examples/partial_grid_detection_analysis.py
Partial square-grid ROI detection and measured/true strain comparison

The example below demonstrates oblique-grid rectification. It places a tilted square-marker grid patch into a camera image, detects the patch, rectifies it from four image-space corner points, and visualizes the rectified reference/deformed grids plus the separated x/y grating components. The black square width and the white gap width are equal.

PYTHONPATH=src python examples/skimage_natural_grating_strain.py
Oblique square-grid target detection, rectification, and component separation

Choosing the axis

Use axis="x" when phase shifts are generated by shifting sampling positions along the image x direction. Use axis="y" for the y direction.

For two-dimensional grating measurement, analyze the x and y grating components separately and combine the resulting displacement components.