xpunwrap 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xpunwrap/__init__.py +29 -0
- xpunwrap/_dtype_utils.py +54 -0
- xpunwrap/_ndarray_backend.py +96 -0
- xpunwrap/algorithms/__init__.py +4 -0
- xpunwrap/algorithms/_ls_common.py +92 -0
- xpunwrap/algorithms/_plane_utils.py +65 -0
- xpunwrap/algorithms/ls_poisson.py +68 -0
- xpunwrap/algorithms/ls_poisson_pg.py +117 -0
- xpunwrap/algorithms/ls_weighted.py +188 -0
- xpunwrap/algorithms/skimage_unwrap.py +106 -0
- xpunwrap/fourier/__init__.py +88 -0
- xpunwrap/fourier/base.py +48 -0
- xpunwrap/fourier/fft_cupy.py +21 -0
- xpunwrap/fourier/fft_numpy.py +16 -0
- xpunwrap/fourier/fft_pyfftw.py +106 -0
- xpunwrap-0.0.0.dist-info/METADATA +225 -0
- xpunwrap-0.0.0.dist-info/RECORD +20 -0
- xpunwrap-0.0.0.dist-info/WHEEL +5 -0
- xpunwrap-0.0.0.dist-info/licenses/LICENSE +19 -0
- xpunwrap-0.0.0.dist-info/top_level.txt +1 -0
xpunwrap/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from ._ndarray_backend import get_ndarray_backend, set_ndarray_backend
|
|
5
|
+
|
|
6
|
+
from .algorithms import (
|
|
7
|
+
algo_ls_poisson,
|
|
8
|
+
algo_ls_poisson_pg,
|
|
9
|
+
algo_ls_weighted,
|
|
10
|
+
algo_skimage_unwrap,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def algos_available() -> dict[str, Callable[..., Any]]:
|
|
15
|
+
"""Return all available phase unwrapping algorithms.
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
Mapping from algorithm name to callable, in the order:
|
|
20
|
+
``algo_ls_poisson``, ``algo_ls_poisson_pg``, ``algo_ls_weighted``,
|
|
21
|
+
``algo_skimage_unwrap``.
|
|
22
|
+
"""
|
|
23
|
+
algos = {}
|
|
24
|
+
for algo in [algo_ls_poisson,
|
|
25
|
+
algo_ls_poisson_pg,
|
|
26
|
+
algo_ls_weighted,
|
|
27
|
+
algo_skimage_unwrap]:
|
|
28
|
+
algos[algo.__name__] = algo
|
|
29
|
+
return algos
|
xpunwrap/_dtype_utils.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import types
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def real_pi(xp: types.ModuleType, dtype: Any) -> Any:
|
|
10
|
+
"""Return pi cast to the requested real dtype.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
xp : module
|
|
15
|
+
Active ndarray backend (``numpy`` or ``cupy``).
|
|
16
|
+
dtype : dtype-like
|
|
17
|
+
Target real floating-point dtype.
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
scalar
|
|
22
|
+
``pi`` as a scalar of ``dtype``.
|
|
23
|
+
"""
|
|
24
|
+
dt = xp.dtype(dtype)
|
|
25
|
+
return dt.type(xp.pi)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def complex_dtype_for_real(xp: types.ModuleType, dtype: Any) -> np.dtype:
|
|
29
|
+
"""Map a real dtype to the corresponding complex dtype.
|
|
30
|
+
|
|
31
|
+
``float16`` and ``float32`` map to ``complex64``; ``float64`` maps to
|
|
32
|
+
``complex128``. Complex inputs are returned unchanged. Uncommon real
|
|
33
|
+
dtypes fall back to ``complex128``.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
xp : module
|
|
38
|
+
Active ndarray backend (``numpy`` or ``cupy``).
|
|
39
|
+
dtype : dtype-like
|
|
40
|
+
Input dtype, real or complex.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
dtype
|
|
45
|
+
Corresponding complex dtype.
|
|
46
|
+
"""
|
|
47
|
+
dt = xp.dtype(dtype)
|
|
48
|
+
if dt.kind == "c":
|
|
49
|
+
return dt
|
|
50
|
+
if dt == xp.float16 or dt == xp.float32:
|
|
51
|
+
return xp.complex64
|
|
52
|
+
if dt == xp.float64:
|
|
53
|
+
return xp.complex128
|
|
54
|
+
return xp.complex128
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module that controls and exposes the active ndarray backend.
|
|
3
|
+
NumPy is used for CPU-only. CuPy is used for GPU.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import importlib
|
|
7
|
+
import types
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
_default_backend = "numpy"
|
|
11
|
+
_xp = importlib.import_module(_default_backend)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NDArrayBackend:
|
|
15
|
+
"""Proxy object exposing the current ndarray backend.
|
|
16
|
+
|
|
17
|
+
All attribute access is forwarded to the active backend module, so
|
|
18
|
+
``xp.array``, ``xp.zeros``, etc. resolve to the correct implementation
|
|
19
|
+
without any conditional imports in calling code.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._xp = _xp
|
|
24
|
+
|
|
25
|
+
def get(self) -> types.ModuleType:
|
|
26
|
+
"""Return the currently active backend module.
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
module
|
|
31
|
+
The active ndarray backend (``numpy`` or ``cupy``).
|
|
32
|
+
"""
|
|
33
|
+
return self._xp
|
|
34
|
+
|
|
35
|
+
def set(self, backend_name: str = "numpy") -> None:
|
|
36
|
+
"""Switch the active ndarray backend.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
backend_name : str
|
|
41
|
+
Name of the backend to activate. ``"numpy"`` selects CPU,
|
|
42
|
+
``"cupy"`` selects GPU. Default ``"numpy"``.
|
|
43
|
+
|
|
44
|
+
Raises
|
|
45
|
+
------
|
|
46
|
+
ImportError
|
|
47
|
+
If the requested backend is not installed.
|
|
48
|
+
"""
|
|
49
|
+
global _xp
|
|
50
|
+
try:
|
|
51
|
+
self._xp = importlib.import_module(backend_name)
|
|
52
|
+
_xp = self._xp # keep global in sync
|
|
53
|
+
except ModuleNotFoundError as err:
|
|
54
|
+
raise ImportError(f"The backend '{backend_name}' is not "
|
|
55
|
+
f"installed. Either install it or use the "
|
|
56
|
+
f"default backend: 'numpy'.") from err
|
|
57
|
+
|
|
58
|
+
def __getattr__(self, name: str) -> Any:
|
|
59
|
+
"""Delegate attribute lookup to the active backend module.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
name : str
|
|
64
|
+
Attribute name.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
object
|
|
69
|
+
The corresponding attribute from the active backend.
|
|
70
|
+
"""
|
|
71
|
+
return getattr(self._xp, name)
|
|
72
|
+
|
|
73
|
+
def is_numpy(self) -> bool:
|
|
74
|
+
"""Return ``True`` if the active backend is NumPy (CPU).
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
bool
|
|
79
|
+
"""
|
|
80
|
+
return self._xp.__name__.startswith("numpy")
|
|
81
|
+
|
|
82
|
+
def is_cupy(self) -> bool:
|
|
83
|
+
"""Return ``True`` if the active backend is CuPy (GPU).
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
bool
|
|
88
|
+
"""
|
|
89
|
+
return self._xp.__name__.startswith("cupy")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Export a single global proxy instance
|
|
93
|
+
xp = NDArrayBackend()
|
|
94
|
+
# This is what is imported by the user
|
|
95
|
+
get_ndarray_backend = xp.get
|
|
96
|
+
set_ndarray_backend = xp.set
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared building blocks for the least-squares Poisson unwrap algorithms.
|
|
3
|
+
|
|
4
|
+
These helpers are used by both ``ls_poisson`` and ``ls_poisson_pg``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .._dtype_utils import complex_dtype_for_real, real_pi
|
|
8
|
+
from .._ndarray_backend import xp
|
|
9
|
+
from ..fourier import get_fft_engine
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def wrap_phase(x: xp.ndarray) -> xp.ndarray:
|
|
13
|
+
"""
|
|
14
|
+
Wrap phase to [-pi, pi).
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
x : xp.ndarray
|
|
19
|
+
Input phase values.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
xp.ndarray
|
|
24
|
+
Wrapped values in [-pi, pi).
|
|
25
|
+
"""
|
|
26
|
+
dtype = x.dtype
|
|
27
|
+
pi = real_pi(xp, dtype)
|
|
28
|
+
two_pi = dtype.type(2) * pi
|
|
29
|
+
return (x + pi) % two_pi - pi
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def divergence_stack(gx: xp.ndarray, gy: xp.ndarray) -> xp.ndarray:
|
|
33
|
+
"""
|
|
34
|
+
Compute the periodic divergence of a wrapped-gradient stack.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
gx, gy : xp.ndarray
|
|
39
|
+
Gradient stacks, shape (N, H, W).
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
xp.ndarray
|
|
44
|
+
Divergence, shape (N, H, W).
|
|
45
|
+
"""
|
|
46
|
+
# Periodic backward-difference divergence used by the FFT Poisson solve.
|
|
47
|
+
# div g = (g_x - g_x shifted right) + (g_y - g_y shifted down)
|
|
48
|
+
return (
|
|
49
|
+
gx - xp.roll(gx, 1, axis=2)
|
|
50
|
+
+ gy - xp.roll(gy, 1, axis=1)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def poisson_solve_fft_stack(rhs: xp.ndarray) -> xp.ndarray:
|
|
55
|
+
"""
|
|
56
|
+
Solve the periodic Poisson equation in the Fourier domain.
|
|
57
|
+
|
|
58
|
+
Returns the positive-denominator solution; callers multiply by -1 to
|
|
59
|
+
match the discrete Laplacian sign convention.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
rhs : xp.ndarray
|
|
64
|
+
Right-hand side, shape (N, H, W).
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
xp.ndarray
|
|
69
|
+
Solution, shape (N, H, W).
|
|
70
|
+
"""
|
|
71
|
+
_, H, W = rhs.shape
|
|
72
|
+
dtype = rhs.dtype
|
|
73
|
+
pi = real_pi(xp, dtype)
|
|
74
|
+
two = dtype.type(2)
|
|
75
|
+
|
|
76
|
+
ky = xp.fft.fftfreq(H).astype(dtype, copy=False).reshape(1, H, 1)
|
|
77
|
+
kx = xp.fft.fftfreq(W).astype(dtype, copy=False).reshape(1, 1, W)
|
|
78
|
+
|
|
79
|
+
denom = (two - two * xp.cos(two * pi * kx)) + \
|
|
80
|
+
(two - two * xp.cos(two * pi * ky))
|
|
81
|
+
|
|
82
|
+
denom[:, 0, 0] = dtype.type(1)
|
|
83
|
+
|
|
84
|
+
complex_dtype = complex_dtype_for_real(xp, dtype)
|
|
85
|
+
complex_dt = xp.dtype(complex_dtype)
|
|
86
|
+
fft = get_fft_engine()
|
|
87
|
+
rhs_hat = fft.fft2(rhs.astype(complex_dtype, copy=False))
|
|
88
|
+
phi_hat = rhs_hat / denom
|
|
89
|
+
phi_hat[:, 0, 0] = complex_dt.type(0)
|
|
90
|
+
|
|
91
|
+
out = xp.real(fft.ifft2(phi_hat))
|
|
92
|
+
return out.astype(dtype, copy=False)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from .._ndarray_backend import xp
|
|
2
|
+
from ._ls_common import wrap_phase
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def restore_mean_plane(
|
|
6
|
+
phase_unwrapped: xp.ndarray,
|
|
7
|
+
phase_wrapped: xp.ndarray,
|
|
8
|
+
) -> xp.ndarray:
|
|
9
|
+
"""Reintroduce the mean wrapped gradient plane removed by Poisson solvers.
|
|
10
|
+
|
|
11
|
+
Least-squares Poisson solvers discard the null-space component (a linear
|
|
12
|
+
ramp). This function estimates that ramp from the mean wrapped gradients of
|
|
13
|
+
the input and adds it back, anchored so that the value at pixel ``(0, 0)``
|
|
14
|
+
matches the original wrapped phase.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
phase_unwrapped : xp.ndarray
|
|
19
|
+
Solver output, shape (H, W) or (N, H, W).
|
|
20
|
+
phase_wrapped : xp.ndarray
|
|
21
|
+
Original wrapped phase, same shape as ``phase_unwrapped``.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
xp.ndarray
|
|
26
|
+
Phase with the mean gradient plane restored, same shape as input.
|
|
27
|
+
|
|
28
|
+
Raises
|
|
29
|
+
------
|
|
30
|
+
ValueError
|
|
31
|
+
If ``phase_unwrapped`` is not 2-D or 3-D.
|
|
32
|
+
"""
|
|
33
|
+
input_2d = False
|
|
34
|
+
if phase_unwrapped.ndim == 2:
|
|
35
|
+
input_2d = True
|
|
36
|
+
phase_unwrapped = xp.expand_dims(phase_unwrapped, axis=0)
|
|
37
|
+
phase_wrapped = xp.expand_dims(phase_wrapped, axis=0)
|
|
38
|
+
elif phase_unwrapped.ndim != 3:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
"phase_unwrapped must have shape (H, W) or (N, H, W)."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
dtype = phase_unwrapped.dtype
|
|
44
|
+
|
|
45
|
+
gx = wrap_phase(xp.diff(phase_wrapped, axis=2,
|
|
46
|
+
append=phase_wrapped[:, :, -1:]))
|
|
47
|
+
gy = wrap_phase(xp.diff(phase_wrapped, axis=1,
|
|
48
|
+
append=phase_wrapped[:, -1:, :]))
|
|
49
|
+
|
|
50
|
+
gx_mean = gx.mean(axis=(1, 2), keepdims=True)
|
|
51
|
+
gy_mean = gy.mean(axis=(1, 2), keepdims=True)
|
|
52
|
+
|
|
53
|
+
_, H, W = phase_unwrapped.shape
|
|
54
|
+
x_idx = xp.arange(W, dtype=dtype).reshape(1, 1, W)
|
|
55
|
+
y_idx = xp.arange(H, dtype=dtype).reshape(1, H, 1)
|
|
56
|
+
plane = gx_mean * x_idx + gy_mean * y_idx
|
|
57
|
+
anchor = (
|
|
58
|
+
phase_wrapped[:, 0, 0]
|
|
59
|
+
- (phase_unwrapped[:, 0, 0] + plane[:, 0, 0])
|
|
60
|
+
)
|
|
61
|
+
out = phase_unwrapped + plane + anchor[:, None, None]
|
|
62
|
+
|
|
63
|
+
if input_2d:
|
|
64
|
+
out = out[0]
|
|
65
|
+
return out
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from .._ndarray_backend import xp
|
|
2
|
+
from ._ls_common import divergence_stack, poisson_solve_fft_stack, wrap_phase
|
|
3
|
+
from ._plane_utils import restore_mean_plane
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def algo_ls_poisson(
|
|
7
|
+
phase_wrapped: xp.ndarray,
|
|
8
|
+
restore_plane: bool = False,
|
|
9
|
+
) -> xp.ndarray:
|
|
10
|
+
"""
|
|
11
|
+
Batched 2D phase unwrapping using a least-squares Poisson solver.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
phase_wrapped : xp.ndarray
|
|
16
|
+
Wrapped phase, shape (H, W) or (N, H, W), values in [-pi, pi).
|
|
17
|
+
restore_plane : bool, optional
|
|
18
|
+
If True, add back the mean wrapped gradient plane. Default False.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
phase_unwrapped : xp.ndarray
|
|
23
|
+
Unwrapped phase, same shape as input.
|
|
24
|
+
|
|
25
|
+
Notes
|
|
26
|
+
-----
|
|
27
|
+
The FFT Poisson solver assumes periodic boundary conditions: the input
|
|
28
|
+
domain is treated as if its left/right and top/bottom edges connect. No
|
|
29
|
+
zero-padding is applied before the FFT. If the wrapped phase is not
|
|
30
|
+
periodic at the boundaries, the solver may produce artifacts (ringing or
|
|
31
|
+
slope errors) near the domain edges. For non-periodic data consider
|
|
32
|
+
cropping the region of interest away from the edges or using
|
|
33
|
+
:func:`algo_ls_weighted`, which can suppress discontinuous boundary regions
|
|
34
|
+
via its border-weight mask.
|
|
35
|
+
|
|
36
|
+
References
|
|
37
|
+
----------
|
|
38
|
+
.. [1] D. C. Ghiglia and M. D. Pritt, "Two-Dimensional Phase Unwrapping:
|
|
39
|
+
Theory, Algorithms, and Software," Wiley, 1998.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
input_2d = False
|
|
43
|
+
if phase_wrapped.ndim == 2:
|
|
44
|
+
input_2d = True
|
|
45
|
+
phase_wrapped = xp.expand_dims(phase_wrapped, axis=0)
|
|
46
|
+
elif phase_wrapped.ndim != 3:
|
|
47
|
+
raise ValueError("phase_wrapped must have shape (H, W) or (N, H, W).")
|
|
48
|
+
|
|
49
|
+
dtype = phase_wrapped.dtype
|
|
50
|
+
# Wrapped gradients with periodic forward differences.
|
|
51
|
+
# This is the discrete operator used by the FFT Poisson solver.
|
|
52
|
+
gx = wrap_phase(xp.roll(phase_wrapped, -1, axis=2) - phase_wrapped)
|
|
53
|
+
gy = wrap_phase(xp.roll(phase_wrapped, -1, axis=1) - phase_wrapped)
|
|
54
|
+
|
|
55
|
+
rhs = divergence_stack(gx, gy)
|
|
56
|
+
|
|
57
|
+
# Solve the discrete Poisson equation in the Fourier domain.
|
|
58
|
+
# The FFT helper returns the positive-denominator form, so apply -1 to
|
|
59
|
+
# match the Laplacian sign convention here.
|
|
60
|
+
phase_unwrapped = poisson_solve_fft_stack(rhs)
|
|
61
|
+
phase_unwrapped *= dtype.type(-1)
|
|
62
|
+
|
|
63
|
+
if restore_plane:
|
|
64
|
+
phase_unwrapped = restore_mean_plane(phase_unwrapped, phase_wrapped)
|
|
65
|
+
|
|
66
|
+
if input_2d:
|
|
67
|
+
phase_unwrapped = phase_unwrapped[0]
|
|
68
|
+
return phase_unwrapped
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from .._ndarray_backend import xp
|
|
2
|
+
from ._ls_common import divergence_stack, poisson_solve_fft_stack, wrap_phase
|
|
3
|
+
from ._plane_utils import restore_mean_plane
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def algo_ls_poisson_pg(
|
|
7
|
+
phase_wrapped: xp.ndarray,
|
|
8
|
+
restore_plane: bool = False,
|
|
9
|
+
) -> xp.ndarray:
|
|
10
|
+
"""
|
|
11
|
+
Least-squares unwrapping with periodic gradient enforcement.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
phase_wrapped : xp.ndarray
|
|
16
|
+
Wrapped phase, shape (H, W) or (N, H, W), values in [-pi, pi).
|
|
17
|
+
restore_plane : bool, optional
|
|
18
|
+
If True, add back the mean wrapped gradient plane. Default False.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
phase_unwrapped : xp.ndarray
|
|
23
|
+
Unwrapped phase, same shape as input.
|
|
24
|
+
|
|
25
|
+
Notes
|
|
26
|
+
-----
|
|
27
|
+
The FFT Poisson solver assumes periodic boundary conditions: the input
|
|
28
|
+
domain is treated as if its left/right and top/bottom edges connect. No
|
|
29
|
+
zero-padding is applied before the FFT. This algorithm explicitly enforces
|
|
30
|
+
periodicity on the wrapped gradients (via
|
|
31
|
+
:func:`enforce_periodic_gradients_stack`) before solving, which reduces
|
|
32
|
+
gradient discontinuities at domain edges compared to
|
|
33
|
+
:func:`algo_ls_poisson`. If the wrapped phase is not periodic at the
|
|
34
|
+
boundaries, artifacts near the edges may still occur.
|
|
35
|
+
|
|
36
|
+
References
|
|
37
|
+
----------
|
|
38
|
+
.. [1] D. C. Ghiglia and M. D. Pritt, "Two-Dimensional Phase Unwrapping:
|
|
39
|
+
Theory, Algorithms, and Software," Wiley, 1998.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
input_2d = False
|
|
43
|
+
if phase_wrapped.ndim == 2:
|
|
44
|
+
input_2d = True
|
|
45
|
+
phase_wrapped = xp.expand_dims(phase_wrapped, axis=0)
|
|
46
|
+
elif phase_wrapped.ndim != 3:
|
|
47
|
+
raise ValueError("phase_wrapped must have shape (H, W) or (N, H, W).")
|
|
48
|
+
|
|
49
|
+
# Periodic gradients.
|
|
50
|
+
gx, gy = wrapped_gradients_stack(phase_wrapped)
|
|
51
|
+
gx, gy = enforce_periodic_gradients_stack(gx, gy)
|
|
52
|
+
rhs = divergence_stack(gx, gy)
|
|
53
|
+
|
|
54
|
+
# The FFT helper returns the positive-denominator form, so apply -1 to
|
|
55
|
+
# match the Laplacian sign convention here.
|
|
56
|
+
phi = poisson_solve_fft_stack(rhs)
|
|
57
|
+
phi *= phi.dtype.type(-1)
|
|
58
|
+
|
|
59
|
+
if restore_plane:
|
|
60
|
+
phi = restore_mean_plane(phi, phase_wrapped)
|
|
61
|
+
if input_2d:
|
|
62
|
+
phi = phi[0]
|
|
63
|
+
return phi
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def wrapped_gradients_stack(
|
|
67
|
+
phi: xp.ndarray,
|
|
68
|
+
) -> tuple[xp.ndarray, xp.ndarray]:
|
|
69
|
+
"""
|
|
70
|
+
Wrapped forward gradients for a stack of phases.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
phi : xp.ndarray
|
|
75
|
+
Wrapped phase, shape (N, H, W).
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
gx, gy : xp.ndarray
|
|
80
|
+
Wrapped gradients with shape (N, H, W).
|
|
81
|
+
"""
|
|
82
|
+
gx = wrap_phase(phi[:, :, 1:] - phi[:, :, :-1])
|
|
83
|
+
gy = wrap_phase(phi[:, 1:, :] - phi[:, :-1, :])
|
|
84
|
+
|
|
85
|
+
# Pad with zeros to restore the full (N, H, W) shape lost by diff.
|
|
86
|
+
# The padded edge values are immediately overwritten with the correct
|
|
87
|
+
# periodic boundary values in enforce_periodic_gradients_stack.
|
|
88
|
+
gx = xp.pad(gx, ((0, 0), (0, 0), (0, 1)))
|
|
89
|
+
gy = xp.pad(gy, ((0, 0), (0, 1), (0, 0)))
|
|
90
|
+
|
|
91
|
+
return gx, gy
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def enforce_periodic_gradients_stack(
|
|
95
|
+
gx: xp.ndarray,
|
|
96
|
+
gy: xp.ndarray,
|
|
97
|
+
) -> tuple[xp.ndarray, xp.ndarray]:
|
|
98
|
+
"""
|
|
99
|
+
Enforce periodic boundary conditions on wrapped gradients.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
gx, gy : xp.ndarray
|
|
104
|
+
Gradient stacks, shape (N, H, W).
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
gx, gy : xp.ndarray
|
|
109
|
+
Periodic gradients, shape (N, H, W).
|
|
110
|
+
"""
|
|
111
|
+
# x-direction periodicity
|
|
112
|
+
gx[:, :, -1] = gx[:, :, 0]
|
|
113
|
+
|
|
114
|
+
# y-direction periodicity
|
|
115
|
+
gy[:, -1, :] = gy[:, 0, :]
|
|
116
|
+
|
|
117
|
+
return gx, gy
|