Mon. May 18th, 2026

I have been building moirestrain, a small NumPy-first Python package for sampling moire analysis of periodic grating images.

The original scope was planar measurement: estimate phase from reference and deformed grating images, convert the phase difference to in-plane displacement, and calculate small strain fields.

Recently I added an experimental extension for known 3D surfaces. This is not a full 3D sampling moire system. It does not reconstruct an unknown surface from images. Instead, it answers a narrower question:

If the 3D surface geometry is already known, can the 2D sampling moire displacement field be lifted to 3D tangent displacement and surface strain?

The answer, at least for synthetic tests, is yes.

Repository

Package:

moirestrain

Main ideas already implemented:

  • phase-shifted sampling moire image generation,
  • wrapped phase estimation,
  • reference/deformed phase difference,
  • displacement and strain estimation,
  • square-marker grid analysis,
  • planar rectification,
  • and now a known-surface post-processing layer.

Reminder: What Sampling Moire Does Here

For each analysis direction, the implementation creates a stack of phase-shifted moire images from one grating image:

  1. choose a sampling offset,
  2. sample pixels every period pixels,
  3. linearly interpolate the sampled image back to the original image grid.

The stack is then treated as a set of equally phase-shifted images:

I_k = a + b \cos\left(\phi + \frac{2\pi k}{N}\right)

and the wrapped phase is estimated from the first Fourier component:

\phi =
\mathrm{atan2}\left(
-\sum_{k=0}^{N-1} I_k \sin\frac{2\pi k}{N},
\sum_{k=0}^{N-1} I_k \cos\frac{2\pi k}{N}
\right).

The displacement component is then calculated from the phase difference:

u = \frac{\Delta\phi}{2\pi}p

where p is the grating pitch.

What Changed

The new layer introduces a sampled surface geometry:

surface_points[y, x] = (X, Y, Z)

The existing 2D pipeline still estimates displacement in surface-parameter coordinates:

u: displacement along the x-parameter direction
v: displacement along the y-parameter direction

The new surface layer converts those fields to 3D tangent displacement:

from moirestrain import SurfaceGeometry, tangent_displacement, surface_strain

surface = SurfaceGeometry(surface_points)

disp3d = tangent_displacement(
    surface,
    result.x.displacement,
    result.y.displacement,
)

strain = surface_strain(surface, disp3d, smooth_window=17)

Conceptually:

reference/deformed grating images
  -> sampling moire phase analysis
  -> u, v in surface-parameter coordinates
  -> 3D tangent displacement on known surface
  -> surface strain

Important Scope Limit

This is not the same as a full 3D sampling moire method from the literature.

Many reported 3D sampling moire methods include stereo vision, camera calibration, phase matching, or phase-to-depth calibration. They estimate 3D shape and 3D displacement from images.

This implementation currently assumes that the 3D surface grid is already known or separately measured.

So the safe description is:

a known-surface extension layer for a 2D sampling moire pipeline

not:

a complete unknown free-surface reconstruction method.

Synthetic Experiments

I added three experiments.

1. Ideal Sinusoidal Gratings

Two separated sinusoidal grating pairs are generated, one for the x direction and one for the y direction. There is no blur and no noise.

Result:

metricvalue
mean u error5.252e-06 px
mean v error1.422e-06 px
mean 3D displacement error5.517e-06
mean surface exx error2.176e-08
mean surface shear error1.016e-08

This confirms that the known-surface lifting step itself is working.

2. Camera-Like Sinusoidal Gratings

The same sinusoidal setup is used, but with:

  • 4x supersampling,
  • box blur window 3,
  • Gaussian noise standard deviation 0.002.

Result:

metricvalue
mean u error3.559e-03 px
mean v error3.761e-03 px
mean 3D displacement error5.885e-03
mean surface exx error7.605e-05
mean surface shear error2.316e-04

The displacement remains fairly accurate, but strain is more sensitive because it differentiates the displacement field. Smoothing before surface-strain differentiation is important.

3. Square-Marker Grid on a Known Surface

This is closer to the original target of moirestrain: a white grid with black square markers. The grid is synthesized in surface-parameter coordinates and then analyzed with analyze_grid.

Setup:

  • shape: 128 x 160,
  • period: 8 px,
  • known surface amplitude: 7,
  • 32x supersampling,
  • box blur window 3,
  • no added noise.

Result:

metricvalue
mean u error5.590e-03 px
mean v error1.044e-02 px
mean 3D displacement error1.308e-02
mean surface exx error8.143e-05
mean surface shear error1.497e-04

The square-marker case is harder than separated sinusoidal gratings, but the known-surface conversion remains stable. Most of the error appears to come from phase/displacement extraction from the square-marker image, not from the 3D surface conversion.

Small Sweep

I also ran a small sweep over surface amplitude, blur, and noise.

surface ampblurnoise3D disp MAEgamma_xy MAE
0107.595e-063.211e-09
010.0024.422e-032.503e-04
0301.540e-041.082e-05
030.0025.493e-033.098e-04
7107.820e-063.588e-08
710.0024.622e-032.489e-04
7301.565e-041.091e-05
730.0025.741e-033.078e-04

In this synthetic setup, noise and blur dominate more than the surface amplitude itself.

Literature Position

There are already research papers on 3D sampling moire methods. Examples include:

methodology.

  • binocular-vision-based 3D sampling moire for complex shape measurement,
  • 3D sampling moire for shape and deformation using stereo vision,
  • rotating tire shape and strain measurement by sampling moire,
  • single-camera calibrated 3D displacement measurement based on moire

Compared with those methods, this implementation is much simpler. It does not yet solve:

  • stereo correspondence,
  • camera calibration,
  • unknown surface reconstruction,
  • phase matching between camera views,
  • out-of-plane displacement estimation from image data alone.

That is intentional for now. The purpose of this branch is to create a clean computational layer:

2D sampling moire displacement
  + known surface geometry
  -> 3D tangent displacement and surface strain

Reproduction

Example commands:

PYTHONPATH=src python examples/freeform_surface_experiment.py \
  --output-dir /tmp/moirestrain-result/sinusoidal_ideal \
  --shape 96,112 \
  --period 8 \
  --supersample 1 \
  --blur-window 1 \
  --noise-std 0 \
  --strain-smooth-window 17 \
  --no-figure
PYTHONPATH=src python examples/freeform_surface_experiment.py \
  --output-dir /tmp/moirestrain-result/sinusoidal_camera \
  --shape 96,112 \
  --period 8 \
  --supersample 4 \
  --blur-window 3 \
  --noise-std 0.002 \
  --strain-smooth-window 17 \
  --no-figure
PYTHONPATH=src python examples/freeform_square_grid_experiment.py \
  --output-dir /tmp/moirestrain-result/square_grid \
  --shape 128,160 \
  --period 8 \
  --surface-amplitude 7 \
  --supersample 32 \
  --blur-window 3 \
  --noise-std 0 \
  --strain-cycles 8 \
  --no-figure

What Comes Next

The next natural step is to move closer to the literature:

  1. calibrated stereo or multi-view input,
  2. phase extraction in each camera view,
  3. phase-based correspondence,
  4. triangulation of the surface,
  5. deformation tracking between reference and deformed states.

For now, the known-surface layer is useful as a controlled stepping stone. It separates the geometry problem from the phase-estimation problem and gives a simple way to validate surface displacement and strain calculations.