prtools 2.0.1__tar.gz → 2.1.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-2.0.1/prtools.egg-info → prtools-2.1.0}/PKG-INFO +2 -2
- {prtools-2.0.1 → prtools-2.1.0}/prtools/__init__.py +6 -2
- {prtools-2.0.1 → prtools-2.1.0}/prtools/array.py +78 -2
- {prtools-2.0.1 → prtools-2.1.0}/prtools/convolve.py +26 -10
- prtools-2.1.0/prtools/cost.py +120 -0
- {prtools-2.0.1 → prtools-2.1.0}/prtools/jax.py +1 -1
- prtools-2.1.0/prtools/mesh.py +40 -0
- {prtools-2.0.1 → prtools-2.1.0}/prtools/segmented.py +18 -13
- {prtools-2.0.1 → prtools-2.1.0}/prtools/shape.py +91 -97
- {prtools-2.0.1 → prtools-2.1.0}/prtools/zernike.py +259 -43
- {prtools-2.0.1 → prtools-2.1.0/prtools.egg-info}/PKG-INFO +2 -2
- {prtools-2.0.1 → prtools-2.1.0}/prtools.egg-info/SOURCES.txt +5 -1
- {prtools-2.0.1 → prtools-2.1.0}/prtools.egg-info/requires.txt +1 -1
- {prtools-2.0.1 → prtools-2.1.0}/pyproject.toml +1 -1
- prtools-2.1.0/tests/test_array.py +207 -0
- prtools-2.1.0/tests/test_cost.py +103 -0
- prtools-2.1.0/tests/test_shape.py +109 -0
- prtools-2.1.0/tests/test_zernike.py +246 -0
- prtools-2.0.1/prtools/cost.py +0 -115
- prtools-2.0.1/tests/test_array.py +0 -112
- {prtools-2.0.1 → prtools-2.1.0}/LICENSE +0 -0
- {prtools-2.0.1 → prtools-2.1.0}/NOTICE +0 -0
- {prtools-2.0.1 → prtools-2.1.0}/README.rst +0 -0
- {prtools-2.0.1 → prtools-2.1.0}/prtools/_array_api.py +0 -0
- {prtools-2.0.1 → prtools-2.1.0}/prtools/fourier.py +0 -0
- {prtools-2.0.1 → prtools-2.1.0}/prtools/misc.py +0 -0
- {prtools-2.0.1 → prtools-2.1.0}/prtools/morphology.py +0 -0
- {prtools-2.0.1 → prtools-2.1.0}/prtools/sparse.py +0 -0
- {prtools-2.0.1 → prtools-2.1.0}/prtools/stats.py +0 -0
- {prtools-2.0.1 → prtools-2.1.0}/prtools.egg-info/dependency_links.txt +0 -0
- {prtools-2.0.1 → prtools-2.1.0}/prtools.egg-info/top_level.txt +0 -0
- {prtools-2.0.1 → prtools-2.1.0}/setup.cfg +0 -0
- {prtools-2.0.1 → prtools-2.1.0}/tests/test_convolve.py +0 -0
- {prtools-2.0.1 → prtools-2.1.0}/tests/test_fourier.py +0 -0
- {prtools-2.0.1 → prtools-2.1.0}/tests/test_jax.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: prtools
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Utility functions for image-based phase retrieval
|
|
5
5
|
Author: Andy Kee
|
|
6
6
|
License: MIT License
|
|
@@ -45,7 +45,7 @@ Requires-Dist: scipy
|
|
|
45
45
|
Requires-Dist: array_api_compat
|
|
46
46
|
Provides-Extra: jax
|
|
47
47
|
Requires-Dist: jax; extra == "jax"
|
|
48
|
-
Requires-Dist: optax; extra == "jax"
|
|
48
|
+
Requires-Dist: optax>=0.2.5; extra == "jax"
|
|
49
49
|
Dynamic: license-file
|
|
50
50
|
|
|
51
51
|
prtools
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = '2.0
|
|
1
|
+
__version__ = '2.1.0'
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
from prtools._array_api import use
|
|
@@ -8,6 +8,7 @@ from prtools.array import (
|
|
|
8
8
|
pad,
|
|
9
9
|
crop,
|
|
10
10
|
boundary,
|
|
11
|
+
sample,
|
|
11
12
|
rebin,
|
|
12
13
|
rescale,
|
|
13
14
|
normpow,
|
|
@@ -32,6 +33,8 @@ from prtools.fourier import dft2, idft2
|
|
|
32
33
|
|
|
33
34
|
import prtools.jax
|
|
34
35
|
|
|
36
|
+
from prtools.mesh import mesh
|
|
37
|
+
|
|
35
38
|
from prtools.misc import (
|
|
36
39
|
calcpsf,
|
|
37
40
|
translation_defocus,
|
|
@@ -85,5 +88,6 @@ from prtools.zernike import (
|
|
|
85
88
|
zernike_basis,
|
|
86
89
|
zernike_fit,
|
|
87
90
|
zernike_remove,
|
|
88
|
-
zernike_coordinates
|
|
91
|
+
zernike_coordinates,
|
|
92
|
+
zernike_coeff_legacy
|
|
89
93
|
)
|
|
@@ -75,7 +75,7 @@ def centroid(x, where=None, mode='absolute', indexing='ij'):
|
|
|
75
75
|
c = xp.dot(cc[where].ravel(), anorm.ravel())
|
|
76
76
|
|
|
77
77
|
if mode == 'center':
|
|
78
|
-
rc, cc = np.array(x.shape)/2
|
|
78
|
+
rc, cc = np.floor(np.array(x.shape)/2)
|
|
79
79
|
r = r - rc
|
|
80
80
|
c = c - cc
|
|
81
81
|
|
|
@@ -261,7 +261,7 @@ def crop(x, shape, shift=None, mode='absolute', indexing='ij'):
|
|
|
261
261
|
if shift is not None:
|
|
262
262
|
shift = np.asarray(shift, dtype=int)
|
|
263
263
|
if indexing == 'xy':
|
|
264
|
-
shift = (
|
|
264
|
+
shift = (shift[1], shift[0])
|
|
265
265
|
if mode == 'absolute':
|
|
266
266
|
shift = np.array(shift - np.array(x.shape)//2)
|
|
267
267
|
else:
|
|
@@ -326,6 +326,82 @@ def boundary(x, threshold=0):
|
|
|
326
326
|
return rmin, rmax, cmin, cmax
|
|
327
327
|
|
|
328
328
|
|
|
329
|
+
def sample(x, oversample, offset=0, axes=(-2, -1), indexing='ij'):
|
|
330
|
+
"""Extract samples from an oversampled array.
|
|
331
|
+
|
|
332
|
+
The sampled axes are tiled into contiguous blocks of ``oversample``
|
|
333
|
+
samples, with one sample being taken from each block. No averaging or
|
|
334
|
+
filtering is performed.
|
|
335
|
+
|
|
336
|
+
Parameters
|
|
337
|
+
----------
|
|
338
|
+
x : array_like
|
|
339
|
+
Array to sample
|
|
340
|
+
oversample : int or tuple of ints
|
|
341
|
+
Decimation factor. Providing two values defines asymmetric
|
|
342
|
+
oversampling with ordering interpreted according to ``indexing``.
|
|
343
|
+
offset : int or tuple of ints, optional
|
|
344
|
+
Position of the sample with each block, measured from
|
|
345
|
+
``oversample//2``. The sub-sampled position reachable by ``offset`` is
|
|
346
|
+
quantized to ``1/oversample``. Default is 0. If two values are
|
|
347
|
+
provided, their ordering is interpreted according to ``indexing``.
|
|
348
|
+
axes : (2,) sequence of ints, optional
|
|
349
|
+
Axes over which to sample. If not provided, the last two axes are
|
|
350
|
+
used.
|
|
351
|
+
indexing : {'ij', 'xy'}, optional
|
|
352
|
+
Matrix (‘ij’, default) or cartesian (‘xy’) indexing of ``x`` grid.
|
|
353
|
+
|
|
354
|
+
Returns
|
|
355
|
+
-------
|
|
356
|
+
x : ndarray
|
|
357
|
+
Each sampled axis has length ``x.shape[axis]//oversample``. If axis
|
|
358
|
+
length is not a multiple of ``oversample``, samples taken beyond the
|
|
359
|
+
last whole block are ignored.
|
|
360
|
+
|
|
361
|
+
Examples
|
|
362
|
+
--------
|
|
363
|
+
.. code-block:: python
|
|
364
|
+
|
|
365
|
+
>>> psf = prtools.calcpsf(amp, opd, wave, sampling, (32, 32),
|
|
366
|
+
... oversample=8)
|
|
367
|
+
>>> k = prtools.pixel_kernel(psf.shape, oversample=8)
|
|
368
|
+
>>> native = prtools.sample(prtools.fftconv(psf, k), 8)
|
|
369
|
+
>>> native.shape
|
|
370
|
+
(32, 32)
|
|
371
|
+
|
|
372
|
+
"""
|
|
373
|
+
xp = array_namespace(x)
|
|
374
|
+
x = xp.asarray(x)
|
|
375
|
+
|
|
376
|
+
if indexing not in ('ij', 'xy'):
|
|
377
|
+
raise ValueError("Valid values for indexing are 'xy' and 'ij'.")
|
|
378
|
+
|
|
379
|
+
oversample = tuple(int(v) for v in np.broadcast_to(oversample, (2,)))
|
|
380
|
+
offset = tuple(int(v) for v in np.broadcast_to(offset, (2,)))
|
|
381
|
+
axes = tuple(int(a) % x.ndim for a in axes)
|
|
382
|
+
|
|
383
|
+
if indexing == 'xy':
|
|
384
|
+
offset = (offset[1], offset[0])
|
|
385
|
+
oversample = (oversample[1], oversample[0])
|
|
386
|
+
|
|
387
|
+
idx = [slice(None)] * x.ndim
|
|
388
|
+
|
|
389
|
+
for ax, m, o in zip(axes, oversample, offset):
|
|
390
|
+
|
|
391
|
+
lo, hi = -(m // 2), m - 1 - m // 2
|
|
392
|
+
if not lo <= o <= hi:
|
|
393
|
+
raise ValueError(f'offset {o} lies outside one pixel for oversample '
|
|
394
|
+
f'{m} on axis {ax}; require {lo} <= offset <= {hi}')
|
|
395
|
+
|
|
396
|
+
n = x.shape[ax] // m
|
|
397
|
+
start = m // 2 + o
|
|
398
|
+
stop = start + m * n
|
|
399
|
+
|
|
400
|
+
idx[ax] = slice(start, stop, m)
|
|
401
|
+
|
|
402
|
+
return x[tuple(idx)]
|
|
403
|
+
|
|
404
|
+
|
|
329
405
|
def rebin(x, factor):
|
|
330
406
|
"""Rebin an array by an integer factor.
|
|
331
407
|
|
|
@@ -7,7 +7,7 @@ def fftconv(array, kernel, axes=(-2, -1), normalize_kernel=True,
|
|
|
7
7
|
fft_array=True, fft_kernel=False, fftshift_kernel=False):
|
|
8
8
|
r"""Convolve an array with a kernel using the FFT.
|
|
9
9
|
|
|
10
|
-
The
|
|
10
|
+
The convolution is computed as
|
|
11
11
|
|
|
12
12
|
.. math::
|
|
13
13
|
|
|
@@ -18,12 +18,13 @@ def fftconv(array, kernel, axes=(-2, -1), normalize_kernel=True,
|
|
|
18
18
|
array : array_like
|
|
19
19
|
Array to be convolved with ``kernel``.
|
|
20
20
|
kernel : array_like
|
|
21
|
-
Convolution kernel. Should have the same shape as ``array``.
|
|
21
|
+
Convolution kernel. Should have the same shape as ``array``. May be
|
|
22
|
+
given in either the frequency or spatial domain, as indicated by
|
|
23
|
+
``fft_kernel``. The kernel is applied exactly as supplied. See Notes
|
|
24
|
+
for normalization details.
|
|
22
25
|
axes : sequence of ints, optional
|
|
23
26
|
Axes over which to apply the convolution. If not given, the last two
|
|
24
27
|
axes are used.
|
|
25
|
-
normalize_kernel : bool, optional
|
|
26
|
-
If True (default), kernel is normalized so that ``sum(kernel) == 1``.
|
|
27
28
|
fft_array : bool, optional
|
|
28
29
|
If True (default), the array is assumed to be provided in the spatial
|
|
29
30
|
domain and its FFT will be computed by this function. If False, the
|
|
@@ -46,6 +47,24 @@ def fftconv(array, kernel, axes=(-2, -1), normalize_kernel=True,
|
|
|
46
47
|
--------
|
|
47
48
|
:func:`~prtools.gauss_blur`
|
|
48
49
|
:func:`~prtools.pixelate`
|
|
50
|
+
|
|
51
|
+
Notes
|
|
52
|
+
-----
|
|
53
|
+
**Kernel normalization** This function does not normalize or scale the
|
|
54
|
+
supplied kernel in any way.
|
|
55
|
+
|
|
56
|
+
The sum of ``array`` is conserved when the kernel's transfer function is
|
|
57
|
+
unity at zero frequency. Depending on which domain the kernel is supplied
|
|
58
|
+
in, the kernel normaliztion approach is different:
|
|
59
|
+
|
|
60
|
+
* A frequency-domain kernel (``fft_kernel=False``) should be scaled such
|
|
61
|
+
that ``kernel[0,0] == 1``. Note that ``kernel[0,0]`` is the
|
|
62
|
+
zero-frequency term only for a kernel in standard (unshifted) FFT layout.
|
|
63
|
+
If the kernel origin lies at the center of the array
|
|
64
|
+
(``fftshift_kernel=True``), the DC term is at ``kernel[nr//2, nc//2]``.
|
|
65
|
+
|
|
66
|
+
* A spatial-domain kernal(``fft_kernel=True``) should have
|
|
67
|
+
``sum(kernel) == 1``.
|
|
49
68
|
"""
|
|
50
69
|
xp = array_namespace(array)
|
|
51
70
|
|
|
@@ -53,9 +72,6 @@ def fftconv(array, kernel, axes=(-2, -1), normalize_kernel=True,
|
|
|
53
72
|
k = xp.fft.fftshift(kernel) if fftshift_kernel else kernel
|
|
54
73
|
k = xp.fft.fft2(k) if fft_kernel else k
|
|
55
74
|
|
|
56
|
-
if normalize_kernel:
|
|
57
|
-
k = k / xp.sum(k)
|
|
58
|
-
|
|
59
75
|
return xp.fft.ifft2(a*k, axes=axes).real
|
|
60
76
|
|
|
61
77
|
|
|
@@ -312,10 +328,10 @@ def gauss_kernel(shape, sigma, oversample=1, pixelscale=1.0, fftshift=False,
|
|
|
312
328
|
return k
|
|
313
329
|
|
|
314
330
|
|
|
315
|
-
def pixel_kernel(shape, oversample=1, pixelscale=1.0, fftshift=False,
|
|
331
|
+
def pixel_kernel(shape, oversample=1, pixelscale=1.0, fftshift=False,
|
|
316
332
|
xp=None):
|
|
317
333
|
r"""2D pixel MTF filter kernel
|
|
318
|
-
|
|
334
|
+
|
|
319
335
|
This function returns a normalized 2D sinc function sized to represent
|
|
320
336
|
the transfer function of an idealized square pixel.
|
|
321
337
|
|
|
@@ -361,7 +377,7 @@ def pixel_kernel(shape, oversample=1, pixelscale=1.0, fftshift=False,
|
|
|
361
377
|
x1 = xp.fft.fftfreq(shape[0], d=pixelscale[0]) * oversample
|
|
362
378
|
x2 = xp.fft.fftfreq(shape[1], d=pixelscale[1]) * oversample
|
|
363
379
|
|
|
364
|
-
k =
|
|
380
|
+
k = sinc(x1, x2, indexing='ij', xp=xp)
|
|
365
381
|
|
|
366
382
|
if fftshift:
|
|
367
383
|
return xp.fft.fftshift(k)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from prtools._array_api import array_namespace
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def sserror(data, est, mask=None, gain_bias_invariant=False, ghat=None):
|
|
5
|
+
r"""Compute the normalized sum squared error between two arrays.
|
|
6
|
+
|
|
7
|
+
The error between measured data :math:`\tilde{s}` and estimated data
|
|
8
|
+
:math:`\hat{s}` is computed as the gain invariant error metric
|
|
9
|
+
|
|
10
|
+
.. math::
|
|
11
|
+
|
|
12
|
+
\Phi = \frac{1}{K}\sum_k
|
|
13
|
+
\frac{\sum_{x,y} w(x,y,k)\left[\alpha_k\hat{s}(x,y,k) -
|
|
14
|
+
\tilde{s}(x,y,k)\right]^2}{\sum_{x,y} w(x,y,k)\tilde{s}^2(x,y,k)}
|
|
15
|
+
|
|
16
|
+
for :math:`k = 1, 2, ... K` independent measurements, where
|
|
17
|
+
:math:`w` is a weighting function given by `mask` and each
|
|
18
|
+
:math:`\alpha_k` is chosen to minimize the error. The result is
|
|
19
|
+
independent of any relative scaling (gain) difference between the inputs.
|
|
20
|
+
|
|
21
|
+
If `gain_bias_invariant` is ``True``, the gain and bias invariant error
|
|
22
|
+
metric is computed instead
|
|
23
|
+
|
|
24
|
+
.. math::
|
|
25
|
+
|
|
26
|
+
\Phi = \frac{1}{K}\sum_k
|
|
27
|
+
\frac{\sum_{x,y} w(x,y,k)\left[\alpha_k\hat{g}(x,y,k) -
|
|
28
|
+
\tilde{g}(x,y,k)\right]^2}{\sum_{x,y} w(x,y,k)\tilde{s}^2(x,y,k)}
|
|
29
|
+
|
|
30
|
+
where :math:`\hat{g}` and :math:`\tilde{g}` are :math:`\hat{s}` and
|
|
31
|
+
:math:`\tilde{s}` with their weighted means subtracted and each
|
|
32
|
+
:math:`\alpha_k` is chosen to minimize the error. The result is
|
|
33
|
+
independent of any relative scaling (gain) and offset (bias) differences
|
|
34
|
+
between the inputs.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
data : array_like
|
|
39
|
+
Ideal or measured data with shape (M, N) or (K, M, N)
|
|
40
|
+
est : array_like
|
|
41
|
+
Estimated or modeled data with the same shape as `data`
|
|
42
|
+
mask : array_like, optional
|
|
43
|
+
Weighting function :math:`w` applied to the inputs. Entries equal to
|
|
44
|
+
1 (or True) are fully included in the error computation and entries
|
|
45
|
+
equal to 0 (or False) are ignored. Intermediate values partially
|
|
46
|
+
weight the corresponding elements. Mask must either have the same
|
|
47
|
+
shape as the inputs or be broadcastable to the same shape. If None
|
|
48
|
+
(default), all elements are equally weighted.
|
|
49
|
+
gain_bias_invariant : bool, optional
|
|
50
|
+
If True, the error is computed to be independent of any gain or bias
|
|
51
|
+
differences between the inputs. If False (default), the error is
|
|
52
|
+
computed to be independent of any gain differences between the inputs.
|
|
53
|
+
ghat : array_like, optional
|
|
54
|
+
Precomputed bias-subtracted estimate :math:`\hat{g}`. Only used if
|
|
55
|
+
`gain_bias_invariant` is True. If None (default), :math:`\hat{g}`
|
|
56
|
+
is computed from `est`.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
error : float
|
|
61
|
+
Normalized sum squared error
|
|
62
|
+
|
|
63
|
+
References
|
|
64
|
+
----------
|
|
65
|
+
[1] S. Thurman and J. Fienup, "Phase retrieval with signal bias", J. Opt. Soc. Am. A/Vol. 26, No. 4 (2009)
|
|
66
|
+
|
|
67
|
+
[2] A. Jurling and J. Fienup, "Applications of algorithmic differentiation to phase retrieval algorithms", J. Opt. Soc. Am. A/Vol. 31, No. 7 (2014)
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
xp = array_namespace(data, est)
|
|
71
|
+
|
|
72
|
+
if gain_bias_invariant:
|
|
73
|
+
if ghat is None:
|
|
74
|
+
ghat = g(est, mask, xp) # Eq. 12
|
|
75
|
+
mtil = g(data, mask, xp) # Eq. 13
|
|
76
|
+
mhat = ghat
|
|
77
|
+
else:
|
|
78
|
+
mtil = data
|
|
79
|
+
mhat = est
|
|
80
|
+
|
|
81
|
+
alpha = _a(mtil, mhat, mask, xp) # Eq. 4 or Eq. 14
|
|
82
|
+
alpha = alpha[..., xp.newaxis, xp.newaxis]
|
|
83
|
+
|
|
84
|
+
num = _wsum(xp.square(alpha * mhat - mtil), mask, xp)
|
|
85
|
+
den = _wsum(xp.square(data), mask, xp)
|
|
86
|
+
|
|
87
|
+
# Eq. 3 or Eq. 11
|
|
88
|
+
return xp.mean(num/den)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _wsum(x, mask, xp):
|
|
92
|
+
# Weighted sum over the last two axes
|
|
93
|
+
if mask is not None:
|
|
94
|
+
x = mask * x
|
|
95
|
+
return xp.sum(x, axis=(-2, -1))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def g(data, mask, xp):
|
|
99
|
+
# This function implements Eqs. 12 and 13 in [1]
|
|
100
|
+
if mask is None:
|
|
101
|
+
numer = xp.sum(data, axis=(-2, -1))
|
|
102
|
+
denom = xp.prod(xp.asarray(data.shape[-2:]))
|
|
103
|
+
else:
|
|
104
|
+
numer = xp.sum(mask * data, axis=(-2, -1))
|
|
105
|
+
denom = xp.sum(mask, axis=(-2, -1))
|
|
106
|
+
x = numer/denom
|
|
107
|
+
if data.ndim == 3:
|
|
108
|
+
x = x[:, xp.newaxis, xp.newaxis]
|
|
109
|
+
return data - x
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _a(gtil, ghat, mask, xp):
|
|
113
|
+
# Eq. 4 or Eq. 14 in [1]
|
|
114
|
+
try:
|
|
115
|
+
return _wsum(gtil * ghat, mask, xp)/_wsum(xp.square(ghat), mask, xp)
|
|
116
|
+
except FloatingPointError as e:
|
|
117
|
+
if xp.all(ghat == 0):
|
|
118
|
+
raise ZeroDivisionError('ghat must be nonzero')
|
|
119
|
+
else:
|
|
120
|
+
raise e
|
|
@@ -87,7 +87,7 @@ def lbfgs(fn, x0, gtol=None, maxiter=None, callback=None, fn_kwargs=None):
|
|
|
87
87
|
_, state = carry
|
|
88
88
|
iter_num = otu.tree_get(state, 'count')
|
|
89
89
|
grad = otu.tree_get(state, 'grad')
|
|
90
|
-
grad_norm = otu.
|
|
90
|
+
grad_norm = otu.tree_norm(grad)
|
|
91
91
|
return (iter_num == 0) | ((iter_num < maxiter) & (grad_norm >= gtol))
|
|
92
92
|
|
|
93
93
|
init_carry = (x0, opt.init(x0))
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def mesh(shape, shift=(0, 0), angle=0, indexing='ij'):
|
|
5
|
+
"""Generate a standard mesh.
|
|
6
|
+
|
|
7
|
+
Parameters
|
|
8
|
+
----------
|
|
9
|
+
shape : array_like
|
|
10
|
+
Size of output in pixels (nrows, ncols)
|
|
11
|
+
shift : (2,) array_like, optional
|
|
12
|
+
Coordinate translation from center of containing array.
|
|
13
|
+
Default is (0, 0). Shift is applied according to the specified
|
|
14
|
+
``indexing`` scheme.
|
|
15
|
+
angle : float, optional
|
|
16
|
+
Mesh rotation in degrees. When ``indexing = 'xy'`` the rotation is
|
|
17
|
+
measured from the +x axis toward the +y axis. When ``indexing = 'ij'``
|
|
18
|
+
the rotation is measured from the +i axis toward the +j axis. Note
|
|
19
|
+
that rotations occur about the shifted center if ``shift`` is nonzero.
|
|
20
|
+
Default is 0.
|
|
21
|
+
indexing : {'ij', 'xy'}, optional
|
|
22
|
+
Matrix ('ij', default) or Cartesian ('xy') indexing of mesh.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
list of ndarrays
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
shape = np.broadcast_to(np.asarray(shape), (2,))
|
|
30
|
+
if indexing == 'xy':
|
|
31
|
+
shape = np.flip(shape)
|
|
32
|
+
angle = np.radians(angle)
|
|
33
|
+
|
|
34
|
+
x1, x2 = np.meshgrid(np.arange(shape[0]) - np.floor(shape[0]/2) - shift[0],
|
|
35
|
+
np.arange(shape[1]) - np.floor(shape[1]/2) - shift[1],
|
|
36
|
+
indexing=indexing)
|
|
37
|
+
|
|
38
|
+
X1 = x1 * np.cos(angle) + x2 * np.sin(angle)
|
|
39
|
+
X2 = x1 * -np.sin(angle) + x2 * np.cos(angle)
|
|
40
|
+
return X1, X2
|
|
@@ -5,7 +5,7 @@ import numpy as np
|
|
|
5
5
|
import prtools
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def hex_segments(rings, seg_radius, seg_gap, rotate=
|
|
8
|
+
def hex_segments(rings, seg_radius, seg_gap, rotate=True, antialias=True,
|
|
9
9
|
flatten=False, pad=2, drop=(0,)):
|
|
10
10
|
"""Draw a segmented aperture made up of hexagonal segments
|
|
11
11
|
|
|
@@ -18,9 +18,8 @@ def hex_segments(rings, seg_radius, seg_gap, rotate=False, antialias=True,
|
|
|
18
18
|
seg_gap : float
|
|
19
19
|
Spacing between adjacent segments in pixels
|
|
20
20
|
rotate : bool, optional
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
direction.
|
|
21
|
+
If True (default), the segment mask is rotated so that flat sides are
|
|
22
|
+
aligned with the X direction instead of the Y direction.
|
|
24
23
|
antialias : bool, optional
|
|
25
24
|
If True (default), the segment edges are antialiased.
|
|
26
25
|
flatten : bool, optional
|
|
@@ -29,8 +28,8 @@ def hex_segments(rings, seg_radius, seg_gap, rotate=False, antialias=True,
|
|
|
29
28
|
pad : int, optional
|
|
30
29
|
Number of additional pixels to zero-pad output by. Default is 2.
|
|
31
30
|
drop : list_like, optional
|
|
32
|
-
Segments to exclude from output. 0 represents the central segment.
|
|
33
|
-
|
|
31
|
+
Segments to exclude from output. 0 represents the central segment.
|
|
32
|
+
|
|
34
33
|
Returns
|
|
35
34
|
-------
|
|
36
35
|
ndarray
|
|
@@ -63,20 +62,24 @@ def hex_segments(rings, seg_radius, seg_gap, rotate=False, antialias=True,
|
|
|
63
62
|
mask = []
|
|
64
63
|
|
|
65
64
|
if 0 not in drop:
|
|
66
|
-
mask.append(prtools.hexagon(shape, seg_radius, shift=(0, 0),
|
|
65
|
+
mask.append(prtools.hexagon(shape, seg_radius, shift=(0, 0),
|
|
66
|
+
antialias=antialias, rotate=rotate,
|
|
67
|
+
indexing='ij'))
|
|
67
68
|
|
|
68
69
|
seg = 1
|
|
69
70
|
for ring in range(1, rings+1):
|
|
70
71
|
for h in hex_ring(ring):
|
|
71
72
|
r, c = hex_to_rc(h, seg_radius + seg_gap/2, rotate)
|
|
72
73
|
if seg not in drop:
|
|
73
|
-
mask.append(prtools.hexagon(shape, seg_radius, shift=(r, c),
|
|
74
|
+
mask.append(prtools.hexagon(shape, seg_radius, shift=(r, c),
|
|
75
|
+
antialias=antialias, rotate=rotate,
|
|
76
|
+
indexing='ij'))
|
|
74
77
|
seg += 1
|
|
75
|
-
|
|
78
|
+
|
|
76
79
|
mask = np.asarray(mask)
|
|
77
80
|
if flatten:
|
|
78
81
|
mask = np.sum(mask, axis=0)
|
|
79
|
-
|
|
82
|
+
|
|
80
83
|
return mask
|
|
81
84
|
|
|
82
85
|
|
|
@@ -84,6 +87,8 @@ def hex_segments(rings, seg_radius, seg_gap, rotate=False, antialias=True,
|
|
|
84
87
|
# exactly from http://www.redblobgames.com/grids/hexagons/
|
|
85
88
|
|
|
86
89
|
_Hex = collections.namedtuple("Hex", ["q", "r", "s"])
|
|
90
|
+
|
|
91
|
+
|
|
87
92
|
def Hex(q, r, s):
|
|
88
93
|
assert not (round(q + r + s) != 0), "q + r + s must be 0"
|
|
89
94
|
return _Hex(q, r, s)
|
|
@@ -93,7 +98,7 @@ def hex_add(a, b):
|
|
|
93
98
|
return Hex(a.q + b.q, a.r + b.r, a.s + b.s)
|
|
94
99
|
|
|
95
100
|
|
|
96
|
-
hex_directions = [Hex(1, 0, -1), Hex(1, -1, 0), Hex(0, -1, 1),
|
|
101
|
+
hex_directions = [Hex(1, 0, -1), Hex(1, -1, 0), Hex(0, -1, 1),
|
|
97
102
|
Hex(-1, 0, 1), Hex(-1, 1, 0), Hex(0, 1, -1)]
|
|
98
103
|
|
|
99
104
|
|
|
@@ -117,13 +122,13 @@ def hex_ring(radius):
|
|
|
117
122
|
|
|
118
123
|
|
|
119
124
|
def hex_to_xy(hex, radius, rotate=False):
|
|
120
|
-
if rotate:
|
|
125
|
+
if not rotate:
|
|
121
126
|
x = radius * (np.sqrt(3) * hex.q + np.sqrt(3)/2 * hex.r)
|
|
122
127
|
y = radius * (3/2 * hex.r)
|
|
123
128
|
else:
|
|
124
129
|
x = radius * (3/2 * hex.q)
|
|
125
130
|
y = radius * (np.sqrt(3)/2 * hex.q + np.sqrt(3) * hex.r)
|
|
126
|
-
|
|
131
|
+
|
|
127
132
|
return x, y
|
|
128
133
|
|
|
129
134
|
|