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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: prtools
3
- Version: 1.2.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
- import numpy as np
4
- from numpy.lib.stride_tricks import as_strided
5
- from scipy.ndimage import map_coordinates
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), the
20
- absolute centroid within the input is returned. If 'center', the centroid
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 dimension
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. Scale factors less than 1 will shrink the image. Scale
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 'constant'.
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 will
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
- if shape is None:
375
- shape = np.ceil((img.shape[0]*scale, img.shape[1]*scale)).astype(int)
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
- xx, yy = np.meshgrid(x, y)
384
+ rr, cc = np.meshgrid(r, c, indexing='ij')
386
385
 
387
- mask = map_coordinates(mask, [yy, xx], order=1, mode='nearest')
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, [yy, xx], order=order, mode=mode)
393
- out.imag = map_coordinates(img.imag, [yy, xx], order=order, mode=mode)
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, [yy, xx], order=order, mode=mode)
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* should be odd. If *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("Each element of kernel must be odd")
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
- input[mask] = np.nan
699
+ pw = (kernel - 1)//2
700
+ pad_width = ((pw[0], pw[0]), (pw[1], pw[1]))
631
701
 
632
- pad_size = np.array((kernel-1)/2, dtype=int)
633
- input_pad = np.full((input.shape[0] + pad_size[0] * 2,
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
- input_pad = as_strided(input_pad,
640
- shape=(input_pad.shape[0], input_pad.shape[1], kernel[0], kernel[1]),
641
- strides=input_pad.strides + input_pad.strides)
642
- input_pad = input_pad.reshape((input_pad.shape[0], input_pad.shape[1], np.prod(kernel)))
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
- input[mask_idx] = np.nanmedian(input_pad[mask_idx[0], mask_idx[1], :], axis=1)
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