prtools 1.2.1__tar.gz → 1.3.0__tar.gz
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.
- {prtools-1.2.1/prtools.egg-info → prtools-1.3.0}/PKG-INFO +6 -5
- prtools-1.3.0/prtools/__init__.py +66 -0
- prtools-1.3.0/prtools/_backend.py +73 -0
- {prtools-1.2.1 → prtools-1.3.0}/prtools/array.py +161 -81
- prtools-1.3.0/prtools/cost.py +114 -0
- {prtools-1.2.1 → prtools-1.3.0}/prtools/fourier.py +26 -20
- {prtools-1.2.1 → prtools-1.3.0}/prtools/misc.py +21 -76
- {prtools-1.2.1 → prtools-1.3.0}/prtools/segmented.py +1 -2
- {prtools-1.2.1 → prtools-1.3.0}/prtools/shape.py +35 -31
- {prtools-1.2.1 → prtools-1.3.0}/prtools/sparse.py +32 -31
- {prtools-1.2.1 → prtools-1.3.0}/prtools/stats.py +79 -30
- {prtools-1.2.1 → prtools-1.3.0}/prtools/zernike.py +15 -11
- {prtools-1.2.1 → prtools-1.3.0/prtools.egg-info}/PKG-INFO +6 -5
- {prtools-1.2.1 → prtools-1.3.0}/prtools.egg-info/SOURCES.txt +2 -0
- prtools-1.3.0/prtools.egg-info/requires.txt +6 -0
- {prtools-1.2.1 → prtools-1.3.0}/pyproject.toml +5 -5
- prtools-1.2.1/prtools/__init__.py +0 -31
- prtools-1.2.1/prtools.egg-info/requires.txt +0 -2
- {prtools-1.2.1 → prtools-1.3.0}/LICENSE +0 -0
- {prtools-1.2.1 → prtools-1.3.0}/NOTICE +0 -0
- {prtools-1.2.1 → prtools-1.3.0}/README.rst +0 -0
- {prtools-1.2.1 → prtools-1.3.0}/prtools.egg-info/dependency_links.txt +0 -0
- {prtools-1.2.1 → prtools-1.3.0}/prtools.egg-info/top_level.txt +0 -0
- {prtools-1.2.1 → prtools-1.3.0}/setup.cfg +0 -0
- {prtools-1.2.1 → prtools-1.3.0}/tests/test_array.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: prtools
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Utility functions for image-based phase retrieval
|
|
5
5
|
Author: Andy Kee
|
|
6
6
|
License: MIT License
|
|
@@ -36,15 +36,16 @@ Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
|
36
36
|
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
37
37
|
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
38
38
|
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
39
|
-
Classifier: Topic :: Software Development
|
|
40
|
-
Classifier: Topic :: Software Development :: Libraries
|
|
41
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
42
39
|
Requires-Python: >=3.8
|
|
43
40
|
Description-Content-Type: text/x-rst
|
|
44
41
|
License-File: LICENSE
|
|
45
42
|
License-File: NOTICE
|
|
46
43
|
Requires-Dist: numpy
|
|
47
44
|
Requires-Dist: scipy
|
|
45
|
+
Provides-Extra: jax
|
|
46
|
+
Requires-Dist: jax; extra == "jax"
|
|
47
|
+
Requires-Dist: optax; extra == "jax"
|
|
48
|
+
Dynamic: license-file
|
|
48
49
|
|
|
49
50
|
prtools
|
|
50
51
|
=======
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
__version__ = '1.3.0'
|
|
2
|
+
|
|
3
|
+
from prtools._backend import __backend__, use
|
|
4
|
+
|
|
5
|
+
from prtools.array import (
|
|
6
|
+
centroid,
|
|
7
|
+
pad,
|
|
8
|
+
subarray,
|
|
9
|
+
boundary,
|
|
10
|
+
rebin,
|
|
11
|
+
rescale,
|
|
12
|
+
normpow,
|
|
13
|
+
shift,
|
|
14
|
+
register,
|
|
15
|
+
medfix
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from prtools.cost import sserror
|
|
19
|
+
|
|
20
|
+
from prtools.fourier import dft2, idft2
|
|
21
|
+
|
|
22
|
+
from prtools.misc import (
|
|
23
|
+
calcpsf,
|
|
24
|
+
translation_defocus,
|
|
25
|
+
fft_shape,
|
|
26
|
+
min_sampling,
|
|
27
|
+
pixelscale_nyquist,
|
|
28
|
+
find_wrapped
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from prtools.segmented import hex_segments
|
|
32
|
+
|
|
33
|
+
from prtools.shape import (
|
|
34
|
+
mesh,
|
|
35
|
+
circle,
|
|
36
|
+
hexagon,
|
|
37
|
+
rectangle,
|
|
38
|
+
spider,
|
|
39
|
+
gauss,
|
|
40
|
+
sin,
|
|
41
|
+
waffle
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
from prtools.sparse import (
|
|
45
|
+
index,
|
|
46
|
+
dense,
|
|
47
|
+
sparse,
|
|
48
|
+
index_from_mask,
|
|
49
|
+
mask_from_index
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
from prtools.stats import (
|
|
53
|
+
ee,
|
|
54
|
+
pv,
|
|
55
|
+
rms,
|
|
56
|
+
radial_avg
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
from prtools.zernike import (
|
|
60
|
+
zernike,
|
|
61
|
+
zernike_compose,
|
|
62
|
+
zernike_basis,
|
|
63
|
+
zernike_fit,
|
|
64
|
+
zernike_remove,
|
|
65
|
+
zernike_coordinates
|
|
66
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import types
|
|
3
|
+
|
|
4
|
+
from prtools.backend import registry
|
|
5
|
+
from prtools.backend._base import _BackendLibrary
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _BackendName:
|
|
9
|
+
"""Helper class for storing the backend name - needed since strings are
|
|
10
|
+
immutable.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, name):
|
|
14
|
+
self.name = name
|
|
15
|
+
|
|
16
|
+
def __repr__(self):
|
|
17
|
+
return self.name
|
|
18
|
+
|
|
19
|
+
def __str__(self):
|
|
20
|
+
return self.name
|
|
21
|
+
|
|
22
|
+
def __eq__(self, other):
|
|
23
|
+
return self.name == other
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _Backend(types.ModuleType):
|
|
27
|
+
__backend__ = _BackendName(None)
|
|
28
|
+
numpy = _BackendLibrary(None)
|
|
29
|
+
scipy = _BackendLibrary(None)
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def use(cls, backend):
|
|
33
|
+
"""Select the backend used for N-dimensional array operations.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
backend : str
|
|
38
|
+
The backend to use. This can be any of the following backends,
|
|
39
|
+
which are case-insensitive:
|
|
40
|
+
|
|
41
|
+
* NumPy
|
|
42
|
+
* JAX
|
|
43
|
+
"""
|
|
44
|
+
#if name in registry:
|
|
45
|
+
backend = backend.lower()
|
|
46
|
+
cls.__backend__.name = backend
|
|
47
|
+
cls.numpy.module = registry.load_numpy(backend)
|
|
48
|
+
cls.scipy.module = registry.load_scipy(backend)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def set_backend(cls, name, numpy_module, scipy_module):
|
|
53
|
+
"""Change the backend
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
name : str
|
|
58
|
+
Name of the backend.
|
|
59
|
+
numpy_module : module
|
|
60
|
+
Library providing numpy-like functionality
|
|
61
|
+
scipy_module : module
|
|
62
|
+
Library providing scipy-like functionality
|
|
63
|
+
"""
|
|
64
|
+
cls.__backend__.name = name
|
|
65
|
+
cls.numpy.module = numpy_module
|
|
66
|
+
cls.scipy.module = scipy_module
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# use NumPy by default
|
|
70
|
+
_Backend.use('numpy')
|
|
71
|
+
|
|
72
|
+
# https://stackoverflow.com/a/72911884
|
|
73
|
+
sys.modules[__name__].__class__ = _Backend
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import warnings
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
from
|
|
3
|
+
from prtools import __backend__
|
|
4
|
+
from prtools._backend import numpy as np
|
|
5
|
+
from prtools._backend import scipy
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def centroid(a, where=None, kind='absolute', indexing='ij'):
|
|
@@ -16,9 +16,9 @@ def centroid(a, where=None, kind='absolute', indexing='ij'):
|
|
|
16
16
|
Elements to include in the centroid calculation. If None (default),
|
|
17
17
|
all finite and non-NaN values are used.
|
|
18
18
|
kind : {'absolute', 'center'}, optional
|
|
19
|
-
Specifies the kind of centroid as a string. If 'absolute' (default),
|
|
20
|
-
absolute centroid within the input is returned. If 'center', the
|
|
21
|
-
relative to the center of the input is returned.
|
|
19
|
+
Specifies the kind of centroid as a string. If 'absolute' (default),
|
|
20
|
+
the absolute centroid within the input is returned. If 'center', the
|
|
21
|
+
centroid relative to the center of the input is returned.
|
|
22
22
|
indexing : {'ij', 'xy'}, optional
|
|
23
23
|
Matrix ('ij', default) or cartesian ('xy') indexing of mesh.
|
|
24
24
|
|
|
@@ -36,7 +36,7 @@ def centroid(a, where=None, kind='absolute', indexing='ij'):
|
|
|
36
36
|
|
|
37
37
|
>>> circ = prtools.circle(shape=(256, 256), radius=25, shift=(-80, 50))
|
|
38
38
|
>>> plt.imshow(circ, cmap='gray')
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
.. code:: pycon
|
|
41
41
|
|
|
42
42
|
>>> prtools.centroid(circ)
|
|
@@ -49,7 +49,7 @@ def centroid(a, where=None, kind='absolute', indexing='ij'):
|
|
|
49
49
|
"""
|
|
50
50
|
if kind not in ('absolute', 'center'):
|
|
51
51
|
raise ValueError(f'Unknown kind {kind}')
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
if indexing not in ('ij', 'xy'):
|
|
54
54
|
raise ValueError("Valid values for indexing are 'xy' and 'ij'.")
|
|
55
55
|
|
|
@@ -63,7 +63,7 @@ def centroid(a, where=None, kind='absolute', indexing='ij'):
|
|
|
63
63
|
if np.isnan(a[where]).any():
|
|
64
64
|
warnings.warn('Unmasked NaN in input', RuntimeWarning,
|
|
65
65
|
stacklevel=2)
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
anorm = a[where]/np.sum(a[where])
|
|
68
68
|
|
|
69
69
|
nr, nc = a.shape
|
|
@@ -96,7 +96,7 @@ def pad(a, shape, fill=0):
|
|
|
96
96
|
Shape of output array in ``(nrows, ncols)``.
|
|
97
97
|
fill : scalar
|
|
98
98
|
Fill vlue used when pad operation increases array size.
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
Returns
|
|
101
101
|
-------
|
|
102
102
|
padded_array : ndarray
|
|
@@ -117,7 +117,7 @@ def pad(a, shape, fill=0):
|
|
|
117
117
|
>>> ax[0].set_title('Original array')
|
|
118
118
|
>>> ax[1].imshow(circ_pad, cmap='gray')
|
|
119
119
|
>>> ax[1].set_title('Padded array')
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
.. plot::
|
|
122
122
|
:include-source:
|
|
123
123
|
:context: reset
|
|
@@ -133,14 +133,14 @@ def pad(a, shape, fill=0):
|
|
|
133
133
|
|
|
134
134
|
"""
|
|
135
135
|
a = np.atleast_2d(a)
|
|
136
|
-
shape = np.broadcast_to(np.asarray(shape, dtype=int), (2,))
|
|
136
|
+
shape = np.broadcast_to(np.asarray(shape, dtype=int), (2,))
|
|
137
137
|
|
|
138
138
|
if a.ndim == 2:
|
|
139
139
|
shape = np.append(1, shape)
|
|
140
|
-
a = a[np.newaxis
|
|
140
|
+
a = a[np.newaxis, :]
|
|
141
141
|
else: # a.ndim == 3
|
|
142
142
|
shape = np.append(a.shape[0], shape)
|
|
143
|
-
|
|
143
|
+
|
|
144
144
|
out = np.ones(shape, dtype=a.dtype) * fill
|
|
145
145
|
|
|
146
146
|
# The row and col indices here are 1 and 2 respectively since we've
|
|
@@ -153,14 +153,14 @@ def pad(a, shape, fill=0):
|
|
|
153
153
|
a_slc_r = slice(a.shape[1]//2-rmin, a.shape[1]//2+rmax)
|
|
154
154
|
a_slc_c = slice(a.shape[2]//2-cmin, a.shape[2]//2+cmax)
|
|
155
155
|
|
|
156
|
-
out_slc_r = slice(out.shape[1]//2-rmin, out.shape[1]//2+rmax)
|
|
156
|
+
out_slc_r = slice(out.shape[1]//2-rmin, out.shape[1]//2+rmax)
|
|
157
157
|
out_slc_c = slice(out.shape[2]//2-cmin, out.shape[2]//2+cmax)
|
|
158
|
-
|
|
158
|
+
|
|
159
159
|
out[:, out_slc_r, out_slc_c] = a[:, a_slc_r, a_slc_c]
|
|
160
160
|
return np.squeeze(out)
|
|
161
161
|
|
|
162
162
|
|
|
163
|
-
def subarray(a, shape, shift=(0,0)):
|
|
163
|
+
def subarray(a, shape, shift=(0, 0)):
|
|
164
164
|
"""Extract a contiguous subarray from a larger array.
|
|
165
165
|
|
|
166
166
|
The subarray is extracted about the center of the source array unless
|
|
@@ -174,7 +174,7 @@ def subarray(a, shape, shift=(0,0)):
|
|
|
174
174
|
Shape of subarray array in ``(nrows, ncols)``.
|
|
175
175
|
shift : array_like of ints
|
|
176
176
|
Relative shift of the center of the subarray in ``(row, col)``.
|
|
177
|
-
|
|
177
|
+
|
|
178
178
|
Returns
|
|
179
179
|
-------
|
|
180
180
|
out : ndarray
|
|
@@ -194,7 +194,7 @@ def subarray(a, shape, shift=(0,0)):
|
|
|
194
194
|
>>> ax[0].set_title('Original array')
|
|
195
195
|
>>> ax[1].imshow(circ_subarray, cmap='gray')
|
|
196
196
|
>>> ax[1].set_title('Subarray')
|
|
197
|
-
|
|
197
|
+
|
|
198
198
|
.. plot::
|
|
199
199
|
:include-source:
|
|
200
200
|
:context: reset
|
|
@@ -213,7 +213,6 @@ def subarray(a, shape, shift=(0,0)):
|
|
|
213
213
|
a = np.asarray(a)
|
|
214
214
|
shape = np.asarray(shape)
|
|
215
215
|
|
|
216
|
-
|
|
217
216
|
rmin = a.shape[0]//2 - shape[0]//2 + shift[0]
|
|
218
217
|
cmin = a.shape[1]//2 - shape[1]//2 + shift[1]
|
|
219
218
|
rmax = rmin + shape[0]
|
|
@@ -258,7 +257,7 @@ def boundary(x, threshold=0):
|
|
|
258
257
|
|
|
259
258
|
>>> prtools.boundary(circ)
|
|
260
259
|
(50, 150, 50, 150)
|
|
261
|
-
|
|
260
|
+
|
|
262
261
|
"""
|
|
263
262
|
x = np.asarray(x)
|
|
264
263
|
x = (x > threshold)
|
|
@@ -278,8 +277,8 @@ def rebin(img, factor):
|
|
|
278
277
|
Parameters
|
|
279
278
|
----------
|
|
280
279
|
img : array_like
|
|
281
|
-
Array or cube of arrays to rebin. If a cube is provided, the first
|
|
282
|
-
should index the image slices.
|
|
280
|
+
Array or cube of arrays to rebin. If a cube is provided, the first
|
|
281
|
+
dimension should index the image slices.
|
|
283
282
|
|
|
284
283
|
factor : int
|
|
285
284
|
Rebinning factor
|
|
@@ -321,8 +320,10 @@ def rescale(img, scale, shape=None, mask=None, order=3, mode='nearest',
|
|
|
321
320
|
img : array_like
|
|
322
321
|
Image to rescale
|
|
323
322
|
|
|
324
|
-
scale : float
|
|
325
|
-
Scaling factor.
|
|
323
|
+
scale : float or tuple of floats
|
|
324
|
+
Scaling factor. If scale is a tuple, img is rescaled according to
|
|
325
|
+
``scale_row, scale_col)``. A single value rescales img equally in both
|
|
326
|
+
dimensions. Scale factors less than 1 will shrink the image. Scale
|
|
326
327
|
factors greater than 1 will grow the image.
|
|
327
328
|
|
|
328
329
|
shape : array_like or int, optional
|
|
@@ -340,7 +341,7 @@ def rescale(img, scale, shape=None, mask=None, order=3, mode='nearest',
|
|
|
340
341
|
|
|
341
342
|
mode : {'constant', 'nearest', 'reflect', 'wrap'}, optional
|
|
342
343
|
Points outside the boundaries of the input are filled according to the
|
|
343
|
-
given mode. Default is '
|
|
344
|
+
given mode. Default is 'nearest'.
|
|
344
345
|
|
|
345
346
|
unitary : bool, optional
|
|
346
347
|
Normalization flag. If True (default), a normalization is performed on
|
|
@@ -364,35 +365,33 @@ def rescale(img, scale, shape=None, mask=None, order=3, mode='nearest',
|
|
|
364
365
|
"""
|
|
365
366
|
|
|
366
367
|
img = np.asarray(img)
|
|
368
|
+
scale = np.broadcast_to(scale, (2,))
|
|
369
|
+
|
|
370
|
+
if shape is None:
|
|
371
|
+
shape = img.shape
|
|
372
|
+
shape = np.broadcast_to(shape, (2,))
|
|
373
|
+
shape = np.ceil(shape * scale).astype(int)
|
|
367
374
|
|
|
368
375
|
if mask is None:
|
|
369
|
-
# take the real portion to ensure that even if img is complex, mask
|
|
370
|
-
# be real
|
|
376
|
+
# take the real portion to ensure that even if img is complex, mask
|
|
377
|
+
# will be real
|
|
371
378
|
mask = np.zeros_like(img).real
|
|
372
379
|
mask[img != 0] = 1
|
|
373
380
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
else:
|
|
377
|
-
if np.isscalar(shape):
|
|
378
|
-
shape = np.ceil((shape*scale, shape*scale)).astype(int)
|
|
379
|
-
else:
|
|
380
|
-
shape = np.ceil((shape[0]*scale, shape[1]*scale)).astype(int)
|
|
381
|
-
|
|
382
|
-
x = (np.arange(shape[1], dtype=np.float64) - shape[1]/2.)/scale + img.shape[1]/2.
|
|
383
|
-
y = (np.arange(shape[0], dtype=np.float64) - shape[0]/2.)/scale + img.shape[0]/2.
|
|
381
|
+
r = (np.arange(shape[0], dtype=np.float64) - shape[0]/2.)/scale[0] + img.shape[0]/2.
|
|
382
|
+
c = (np.arange(shape[1], dtype=np.float64) - shape[1]/2.)/scale[1] + img.shape[1]/2.
|
|
384
383
|
|
|
385
|
-
|
|
384
|
+
rr, cc = np.meshgrid(r, c, indexing='ij')
|
|
386
385
|
|
|
387
|
-
mask = map_coordinates(mask, [
|
|
386
|
+
mask = scipy.ndimage.map_coordinates(mask, [rr, cc], order=1, mode='nearest')
|
|
388
387
|
mask[mask < np.finfo(mask.dtype).eps] = 0
|
|
389
388
|
|
|
390
389
|
if np.iscomplexobj(img):
|
|
391
390
|
out = np.zeros(shape, dtype=np.complex128)
|
|
392
|
-
out.real = map_coordinates(img.real, [
|
|
393
|
-
out.imag = map_coordinates(img.imag, [
|
|
391
|
+
out.real = scipy.ndimage.map_coordinates(img.real, [rr, cc], order=order, mode=mode)
|
|
392
|
+
out.imag = scipy.ndimage.map_coordinates(img.imag, [rr, cc], order=order, mode=mode)
|
|
394
393
|
else:
|
|
395
|
-
out = map_coordinates(img, [
|
|
394
|
+
out = scipy.ndimage.map_coordinates(img, [rr, cc], order=order, mode=mode)
|
|
396
395
|
|
|
397
396
|
if unitary:
|
|
398
397
|
out *= np.sum(img)/np.sum(out)
|
|
@@ -401,6 +400,7 @@ def rescale(img, scale, shape=None, mask=None, order=3, mode='nearest',
|
|
|
401
400
|
|
|
402
401
|
return out
|
|
403
402
|
|
|
403
|
+
|
|
404
404
|
def normpow(array, power=1):
|
|
405
405
|
r"""Normalizie the power in an array.
|
|
406
406
|
|
|
@@ -437,12 +437,11 @@ def normpow(array, power=1):
|
|
|
437
437
|
return array * np.sqrt(power/np.sum(np.abs(array)**2))
|
|
438
438
|
|
|
439
439
|
|
|
440
|
-
|
|
441
|
-
def shift(a, shift):
|
|
440
|
+
def shift(a, shift, mode='wrap', fill=0.0):
|
|
442
441
|
"""Shift an array via FFT.
|
|
443
442
|
|
|
444
|
-
Shift an array by (row, column). The shifts may be non-integer as the
|
|
445
|
-
shift operation is implemented by introducing a Fourier-domain tilt. If
|
|
443
|
+
Shift an array by (row, column). The shifts may be non-integer as the
|
|
444
|
+
shift operation is implemented by introducing a Fourier-domain tilt. If
|
|
446
445
|
``a`` is complex, the result will also be complex.
|
|
447
446
|
|
|
448
447
|
Parameters
|
|
@@ -451,6 +450,26 @@ def shift(a, shift):
|
|
|
451
450
|
The input array.
|
|
452
451
|
shift : (2,) sequence
|
|
453
452
|
The shift specified as (row, column).
|
|
453
|
+
mode : {'wrap', 'reflect', 'mirror', 'constant'}, optional
|
|
454
|
+
Determines how the input array is extended beyond its boundaries.
|
|
455
|
+
Default is 'wrap'.
|
|
456
|
+
|
|
457
|
+
* 'wrap' (a b c d | a b c d | a b c d)
|
|
458
|
+
The input is extended by wrapping around to the opposite edge.
|
|
459
|
+
* 'reflect' (d c b a | a b c d | d c b a)
|
|
460
|
+
The input is extended by reflecting about the edge of the last
|
|
461
|
+
pixel. This mode is also sometimes referred to as half-sample
|
|
462
|
+
symmetric.
|
|
463
|
+
* 'mirror' (d c b | a b c d | c b a)
|
|
464
|
+
The input is extended by reflecting about the center of the last
|
|
465
|
+
pixel. This mode is also sometimes referred to as whole-sample
|
|
466
|
+
symmetric.
|
|
467
|
+
* 'constant'
|
|
468
|
+
The input is extended by filling all values beyond the edge with
|
|
469
|
+
the same constant value, defined by the fill parameter.
|
|
470
|
+
|
|
471
|
+
fill : scalar, optional
|
|
472
|
+
Value to fill past edges if `mode` is 'constant'. Default is 0.0
|
|
454
473
|
Returns
|
|
455
474
|
-------
|
|
456
475
|
shifted : ndarray
|
|
@@ -473,8 +492,11 @@ def shift(a, shift):
|
|
|
473
492
|
[-3.12823642e-17, 2.22044605e-16, -4.18468327e-17]])
|
|
474
493
|
"""
|
|
475
494
|
a = np.asarray(a)
|
|
476
|
-
|
|
495
|
+
r, c = a.shape
|
|
477
496
|
dr, dc = shift
|
|
497
|
+
|
|
498
|
+
a = _extend(a, shift, mode, fill)
|
|
499
|
+
|
|
478
500
|
R = dr * np.fft.fftfreq(a.shape[0])
|
|
479
501
|
C = dc * np.fft.fftfreq(a.shape[1])
|
|
480
502
|
|
|
@@ -482,19 +504,58 @@ def shift(a, shift):
|
|
|
482
504
|
K = np.exp(-1j*2*np.pi*(RR+CC))
|
|
483
505
|
shifted = np.fft.ifft2(np.fft.fft2(a)*K)
|
|
484
506
|
|
|
507
|
+
shifted = pad(shifted, (r, c)) # crop back to original size
|
|
508
|
+
|
|
485
509
|
if np.any(np.iscomplex(a)):
|
|
486
510
|
return shifted
|
|
487
511
|
else:
|
|
488
512
|
return shifted.real
|
|
489
|
-
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _extend(a, shift, mode, fill):
|
|
516
|
+
r, c = a.shape
|
|
517
|
+
dr, dc = np.ceil(np.abs(shift)).astype(int)
|
|
518
|
+
|
|
519
|
+
if mode == 'wrap': # (a b c d | a b c d | a b c d)
|
|
520
|
+
# no need to do anything since the Fourier transform
|
|
521
|
+
# already does this
|
|
522
|
+
out = a
|
|
523
|
+
else:
|
|
524
|
+
# pad the array to accomodate for
|
|
525
|
+
out = pad(a, shape=(r+2*dr, c+2*dc), fill=fill)
|
|
526
|
+
|
|
527
|
+
if mode == 'constant': # (k k k k | a b c d | k k k k)
|
|
528
|
+
pass
|
|
529
|
+
elif mode == 'reflect': # (d c b a | a b c d | d c b a)
|
|
530
|
+
out[0:dr, 0:dc] = np.flip(a[0:dr, 0:dc]) # upper left
|
|
531
|
+
out[0:dr, dc:c+dc] = np.flip(a[0:dr, :], axis=0) # top
|
|
532
|
+
out[0:dr, c+dc:] = np.flip(a[0:dr, c-dc:]) # upper right
|
|
533
|
+
out[dr:r+dr, 0:dc] = np.flip(a[:, 0:dc], axis=1) # left
|
|
534
|
+
out[dr:r+dr, c+dc:] = np.flip(a[:, c-dc:], axis=1) # right
|
|
535
|
+
out[r+dr:, 0:dc] = np.flip(a[r-dr:r, 0:dc]) # lower left
|
|
536
|
+
out[r+dr:, dc:c+dc] = np.flip(a[r-dr:r, :], axis=0) # bottom
|
|
537
|
+
out[r+dr:, c+dc:] = np.flip(a[r-dr:r, c-dc:c]) # lower right
|
|
538
|
+
elif mode == 'mirror': # (d c b | a b c d | c b a)
|
|
539
|
+
out[0:dr, 0:dc] = np.flip(a[1:dr+1, 1:dc+1]) # upper left
|
|
540
|
+
out[0:dr, dc:c+dc] = np.flip(a[1:dr+1, :], axis=0) # top
|
|
541
|
+
out[0:dr, c+dc:] = np.flip(a[1:dr+1, c-dc-1:c-1]) # upper right
|
|
542
|
+
out[dr:r+dr, 0:dc] = np.flip(a[:, 1:dc+1], axis=1) # left
|
|
543
|
+
out[dr:r+dr, c+dc:] = np.flip(a[:, c-dc-1:c-1], axis=1) # right
|
|
544
|
+
out[r+dr:, 0:dc] = np.flip(a[r-dr-1:r-1, 1:dc+1]) # lower left
|
|
545
|
+
out[r+dr:, dc:c+dc] = np.flip(a[r-dr-1:r-1, :], axis=0) # bottom
|
|
546
|
+
out[r+dr:, c+dc:] = np.flip(a[r-dr-1:r-1, c-dc-1:c-1]) # lower right
|
|
547
|
+
else:
|
|
548
|
+
raise ValueError(f'Unknown mode {mode}')
|
|
549
|
+
return out
|
|
550
|
+
|
|
490
551
|
|
|
491
552
|
def register(arr, ref, oversample, return_error=False):
|
|
492
553
|
"""Compute the subpixel image translation to register the input array to a
|
|
493
554
|
reference array.
|
|
494
555
|
|
|
495
|
-
The registration shift is computed in two steps: first a coarse estimate
|
|
556
|
+
The registration shift is computed in two steps: first a coarse estimate
|
|
496
557
|
is computed from the FFT-based cross-correlation of the two input arrays.
|
|
497
|
-
This estimate is then refined to subpixel accuracy by computing the
|
|
558
|
+
This estimate is then refined to subpixel accuracy by computing the
|
|
498
559
|
upsampled DFT-based cross-correlation in a small neigborhood around the
|
|
499
560
|
initial estimate.
|
|
500
561
|
|
|
@@ -519,7 +580,7 @@ def register(arr, ref, oversample, return_error=False):
|
|
|
519
580
|
|
|
520
581
|
References
|
|
521
582
|
----------
|
|
522
|
-
Guizar-Sicairos, Thurman, and Fienup, "Efficient subpixel image
|
|
583
|
+
Guizar-Sicairos, Thurman, and Fienup, "Efficient subpixel image
|
|
523
584
|
registration algorithms". Optics Letters 33, 156-158 (2008)
|
|
524
585
|
|
|
525
586
|
See also
|
|
@@ -542,7 +603,7 @@ def register(arr, ref, oversample, return_error=False):
|
|
|
542
603
|
F = np.fft.fft2(arr)
|
|
543
604
|
G = np.fft.fft2(ref)
|
|
544
605
|
xcorr = np.fft.fftshift(np.fft.ifft2(G*np.conj(F)))
|
|
545
|
-
|
|
606
|
+
|
|
546
607
|
# find peak
|
|
547
608
|
maxima = np.unravel_index(np.argmax(np.abs(xcorr)), xcorr.shape)
|
|
548
609
|
peak = xcorr[maxima]
|
|
@@ -550,8 +611,8 @@ def register(arr, ref, oversample, return_error=False):
|
|
|
550
611
|
# compute shifts
|
|
551
612
|
center = np.array([np.fix(x/2) for x in arr.shape])
|
|
552
613
|
shift = maxima - center
|
|
553
|
-
if oversample !=1:
|
|
554
|
-
# now we can set up and perform the oversampled dft on an oversampled
|
|
614
|
+
if oversample != 1:
|
|
615
|
+
# now we can set up and perform the oversampled dft on an oversampled
|
|
555
616
|
# 1.5 x 1.5 pixel region about the peak
|
|
556
617
|
npix_dft = np.ceil(oversample*1.5)
|
|
557
618
|
dft_shift = np.fix(npix_dft/2)
|
|
@@ -563,15 +624,15 @@ def register(arr, ref, oversample, return_error=False):
|
|
|
563
624
|
Y = np.arange(arr.shape[0]) - np.floor(arr.shape[0]/2)
|
|
564
625
|
U = np.arange(npix_dft) - cs
|
|
565
626
|
V = np.arange(npix_dft) - rs
|
|
566
|
-
E1 = np.exp(-2*np.pi*1j/(arr.shape[0]*oversample)*np.outer(V,Y))
|
|
567
|
-
E2 = np.exp(-2*np.pi*1j/(arr.shape[1]*oversample)*np.outer(X,U))
|
|
568
|
-
xcorr = np.dot(np.dot(E1,np.conj(np.fft.ifftshift(G*np.conj(F)))),E2)
|
|
569
|
-
|
|
627
|
+
E1 = np.exp(-2*np.pi*1j/(arr.shape[0]*oversample)*np.outer(V, Y))
|
|
628
|
+
E2 = np.exp(-2*np.pi*1j/(arr.shape[1]*oversample)*np.outer(X, U))
|
|
629
|
+
xcorr = np.dot(np.dot(E1, np.conj(np.fft.ifftshift(G*np.conj(F)))), E2)
|
|
630
|
+
|
|
570
631
|
maxima_subpx = np.unravel_index(np.argmax(np.abs(xcorr)), xcorr.shape)
|
|
571
632
|
peak = xcorr[maxima_subpx]
|
|
572
633
|
|
|
573
634
|
# Combine subpixel peak coordinates with integer pixel peak coords
|
|
574
|
-
maxima_subpx -= dft_shift
|
|
635
|
+
maxima_subpx -= dft_shift
|
|
575
636
|
shift[0] += maxima_subpx[0]/oversample
|
|
576
637
|
shift[1] += maxima_subpx[1]/oversample
|
|
577
638
|
|
|
@@ -588,7 +649,7 @@ def register(arr, ref, oversample, return_error=False):
|
|
|
588
649
|
return shift
|
|
589
650
|
|
|
590
651
|
|
|
591
|
-
def medfix(input, mask, kernel=(3,3), nanwarn=False):
|
|
652
|
+
def medfix(input, mask, kernel=(3, 3), nanwarn=False):
|
|
592
653
|
"""Fix masked entries in a 2-dimensional array via median filtering.
|
|
593
654
|
|
|
594
655
|
Parameters
|
|
@@ -599,8 +660,8 @@ def medfix(input, mask, kernel=(3,3), nanwarn=False):
|
|
|
599
660
|
A 2-dimensional mask with the same shape as input. Entries which
|
|
600
661
|
evaluate to True are considered masked and will be repaired.
|
|
601
662
|
kernel : array_like, optional
|
|
602
|
-
A scalar or list of length 2 specifying the filter window in
|
|
603
|
-
each dimension. Elements of *kernel*
|
|
663
|
+
A scalar or list of length 2 specifying the filter window in
|
|
664
|
+
each dimension. Elements of *kernel* must be odd. If *kernel*
|
|
604
665
|
is a scalar, it is used for each dimension. Default is (3,3).
|
|
605
666
|
nanwarn : bool, optional
|
|
606
667
|
If True, a RuntimeWarning will be raised if NaNs are present
|
|
@@ -609,7 +670,7 @@ def medfix(input, mask, kernel=(3,3), nanwarn=False):
|
|
|
609
670
|
Returns
|
|
610
671
|
-------
|
|
611
672
|
ndarray
|
|
612
|
-
|
|
673
|
+
|
|
613
674
|
Notes
|
|
614
675
|
-----
|
|
615
676
|
Masked areas larger than the kernel size will introduce NaNs into
|
|
@@ -618,36 +679,55 @@ def medfix(input, mask, kernel=(3,3), nanwarn=False):
|
|
|
618
679
|
"""
|
|
619
680
|
# force a copy by calling array instead of asarray
|
|
620
681
|
input = np.array(input, dtype=float)
|
|
621
|
-
|
|
622
682
|
mask = np.asarray(mask, dtype=bool)
|
|
623
683
|
|
|
684
|
+
if not mask.any():
|
|
685
|
+
# nothing to do
|
|
686
|
+
return input
|
|
687
|
+
|
|
624
688
|
kernel = np.asarray(kernel)
|
|
625
689
|
if kernel.shape == ():
|
|
626
690
|
kernel = np.repeat(kernel, 2)
|
|
627
691
|
if np.any(kernel % 2 == 0):
|
|
628
|
-
raise ValueError("
|
|
692
|
+
raise ValueError("Kernel must be odd sized")
|
|
693
|
+
|
|
694
|
+
if __backend__ == 'jax':
|
|
695
|
+
input = input.at[mask].set(np.nan)
|
|
696
|
+
else:
|
|
697
|
+
input[mask] = np.nan
|
|
629
698
|
|
|
630
|
-
|
|
699
|
+
pw = (kernel - 1)//2
|
|
700
|
+
pad_width = ((pw[0], pw[0]), (pw[1], pw[1]))
|
|
631
701
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
input.shape[1] + pad_size[1] * 2), np.nan)
|
|
635
|
-
slc = (slice(pad_size[0], pad_size[0] + input.shape[0]),
|
|
636
|
-
slice(pad_size[1], pad_size[1] + input.shape[1]))
|
|
637
|
-
input_pad[slc] = input
|
|
702
|
+
input_pad = np.pad(input, pad_width=pad_width, mode='constant',
|
|
703
|
+
constant_values=np.nan)
|
|
638
704
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
705
|
+
i, j = np.nonzero(mask) # indices of bad pixels
|
|
706
|
+
i_pad, j_pad = i + pw[0], j + pw[1] # indices offset by padding width
|
|
707
|
+
|
|
708
|
+
# define neighborhood offsets
|
|
709
|
+
di = np.arange(kernel[0]) - pw[0]
|
|
710
|
+
dj = np.arange(kernel[1]) - pw[1]
|
|
711
|
+
window = np.stack(np.meshgrid(di, dj, indexing='ij'), axis=-1).reshape(-1, 2) # shape (prod(kernel), 2)
|
|
712
|
+
|
|
713
|
+
# compute all neighborhood coordinates
|
|
714
|
+
rows = i_pad[:, None] + window[:, 0] # shape (num_bad_px, prod(kernel))
|
|
715
|
+
cols = j_pad[:, None] + window[:, 1] # shape (num_bad_px, prod(kernel))
|
|
716
|
+
|
|
717
|
+
# extract neighborhoods using advanced indexing and compute replacement
|
|
718
|
+
# values
|
|
719
|
+
bad_px_kernel = input_pad[rows, cols]
|
|
643
720
|
|
|
644
|
-
mask_idx = np.where(mask)
|
|
645
|
-
|
|
646
721
|
with warnings.catch_warnings():
|
|
647
722
|
warnings.simplefilter('ignore', category=RuntimeWarning)
|
|
648
|
-
|
|
723
|
+
bad_px_vals = np.nanmedian(bad_px_kernel, axis=1)
|
|
724
|
+
|
|
725
|
+
if __backend__ == 'jax':
|
|
726
|
+
input = input.at[i, j].set(bad_px_vals)
|
|
727
|
+
else:
|
|
728
|
+
input[i, j] = bad_px_vals
|
|
649
729
|
|
|
650
|
-
if np.isnan(input).any():
|
|
730
|
+
if nanwarn and np.isnan(input).any():
|
|
651
731
|
warnings.warn('Result contains NaNs', RuntimeWarning,
|
|
652
732
|
stacklevel=2)
|
|
653
733
|
|