astroimred 0.4.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.
Files changed (52) hide show
  1. astroimred/__init__.py +18 -0
  2. astroimred/_core/__init__.py +9 -0
  3. astroimred/_core/astropy_helpers.py +299 -0
  4. astroimred/_core/geometry.py +268 -0
  5. astroimred/_core/numeric.py +531 -0
  6. astroimred/_core/scales.py +61 -0
  7. astroimred/_core/system.py +36 -0
  8. astroimred/_core/types.py +30 -0
  9. astroimred/_core/wcs.py +122 -0
  10. astroimred/cli.py +44 -0
  11. astroimred/external/__init__.py +7 -0
  12. astroimred/external/sep.py +838 -0
  13. astroimred/fitsmgmt/__init__.py +9 -0
  14. astroimred/fitsmgmt/airmass.py +464 -0
  15. astroimred/fitsmgmt/header.py +778 -0
  16. astroimred/fitsmgmt/io.py +1029 -0
  17. astroimred/fitsmgmt/naming.py +276 -0
  18. astroimred/fitsmgmt/table.py +904 -0
  19. astroimred/fitsmgmt/wcs.py +683 -0
  20. astroimred/imutil/__init__.py +6 -0
  21. astroimred/imutil/_docstrings.py +360 -0
  22. astroimred/imutil/_util_fits.py +1372 -0
  23. astroimred/imutil/ccdops.py +865 -0
  24. astroimred/imutil/imarith.py +420 -0
  25. astroimred/imutil/imcombine.py +1057 -0
  26. astroimred/imutil/imcopy.py +160 -0
  27. astroimred/imutil/imsmooth.py +45 -0
  28. astroimred/imutil/imstat.py +322 -0
  29. astroimred/imutil/pixels.py +456 -0
  30. astroimred/logging.py +35 -0
  31. astroimred/phot/__init__.py +14 -0
  32. astroimred/phot/aperture.py +159 -0
  33. astroimred/phot/apphot.py +549 -0
  34. astroimred/phot/background.py +565 -0
  35. astroimred/phot/center.py +1277 -0
  36. astroimred/phot/daopsf.py +479 -0
  37. astroimred/phot/growth.py +164 -0
  38. astroimred/phot/polarimetry.py +1006 -0
  39. astroimred/phot/radprof.py +480 -0
  40. astroimred/reduction/__init__.py +3 -0
  41. astroimred/reduction/_presets.py +74 -0
  42. astroimred/reduction/cli.py +744 -0
  43. astroimred/reduction/crrej.py +898 -0
  44. astroimred/reduction/preproc.py +1011 -0
  45. astroimred/viz/__init__.py +4 -0
  46. astroimred/viz/imshow.py +251 -0
  47. astroimred/viz/ticks.py +77 -0
  48. astroimred-0.4.0.dist-info/METADATA +63 -0
  49. astroimred-0.4.0.dist-info/RECORD +52 -0
  50. astroimred-0.4.0.dist-info/WHEEL +4 -0
  51. astroimred-0.4.0.dist-info/entry_points.txt +3 -0
  52. astroimred-0.4.0.dist-info/licenses/LICENSE +28 -0
astroimred/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """astroimred: astronomical image reduction and FITS utilities."""
2
+
3
+ from . import _core, external, fitsmgmt, imutil, phot, viz
4
+ from ._core.astropy_helpers import *
5
+ from ._core.geometry import *
6
+ from ._core.numeric import *
7
+ from ._core.scales import *
8
+ from ._core.system import *
9
+ from .fitsmgmt.airmass import *
10
+ from .fitsmgmt.header import *
11
+ from .fitsmgmt.io import *
12
+ from .fitsmgmt.naming import *
13
+ from .fitsmgmt.table import *
14
+ from .fitsmgmt.wcs import *
15
+ from .imutil.ccdops import *
16
+ from .imutil.imstat import *
17
+ from .imutil.pixels import *
18
+ from .logging import enable_console_logging, logger, set_log_level
@@ -0,0 +1,9 @@
1
+ """Low-level helpers shared by astroimred subpackages."""
2
+
3
+ from .astropy_helpers import *
4
+ from .geometry import *
5
+ from .numeric import *
6
+ from .scales import *
7
+ from .system import *
8
+ from .types import *
9
+ from .wcs import *
@@ -0,0 +1,299 @@
1
+ """Small helpers built around Astropy units, modeling, and visualization."""
2
+
3
+ import copy
4
+
5
+ import numpy as np
6
+ from astropy import units as u
7
+ from astropy.modeling.fitting import LevMarLSQFitter
8
+ from astropy.modeling.functional_models import Gaussian2D
9
+ from astropy.time import Time
10
+ from astropy.visualization import (
11
+ AsinhStretch,
12
+ AsymmetricPercentileInterval,
13
+ BaseInterval,
14
+ BaseStretch,
15
+ LinearStretch,
16
+ LogStretch,
17
+ PercentileInterval,
18
+ PowerStretch,
19
+ SinhStretch,
20
+ SqrtStretch,
21
+ SquaredStretch,
22
+ ZScaleInterval,
23
+ )
24
+
25
+ from .numeric import normalize
26
+
27
+ __all__ = [
28
+ "as_quantity",
29
+ "str_now",
30
+ "parse_stretch",
31
+ "parse_interval",
32
+ "Gaussian2D_correct",
33
+ "fit_astropy_model",
34
+ "fit_Gaussian2D",
35
+ "gaussian_kernel",
36
+ ]
37
+
38
+ _STRETCH_CLASS_MAP: dict[str, type[BaseStretch]] = {
39
+ "linear": LinearStretch,
40
+ "sqrt": SqrtStretch,
41
+ "asinh": AsinhStretch,
42
+ "log": LogStretch,
43
+ "power": PowerStretch,
44
+ "sinh": SinhStretch,
45
+ "square": SquaredStretch,
46
+ "squared": SquaredStretch,
47
+ }
48
+
49
+ _STRETCH_DEFAULTS: dict[str, dict[str, float]] = {
50
+ "asinh": {"a": 0.1},
51
+ "log": {"a": 1000.0},
52
+ "power": {"a": 1.0},
53
+ "sinh": {"a": 0.3},
54
+ }
55
+
56
+
57
+ def as_quantity(
58
+ value: object,
59
+ unit: str | u.Unit = "",
60
+ to_value: bool = False,
61
+ ) -> u.Quantity | object:
62
+ """Convert an object to an Astropy Quantity or scalar in the requested unit.
63
+
64
+ Non-Quantity inputs are multiplied by ``unit`` unless ``to_value=True``.
65
+ Objects that cannot be multiplied by a unit are copied and returned as-is.
66
+ """
67
+ if value is None:
68
+ return None
69
+
70
+ try:
71
+ return value.to(unit).value if to_value else value.to(unit)
72
+ except AttributeError:
73
+ if to_value:
74
+ return _copy(value)
75
+ if isinstance(unit, str):
76
+ unit = u.Unit(unit)
77
+ try:
78
+ return value * unit
79
+ except TypeError:
80
+ return _copy(value)
81
+ except TypeError:
82
+ return _copy(value)
83
+ except u.UnitConversionError as err:
84
+ raise ValueError(
85
+ "If you use astropy.Quantity, you should use unit convertible to `unit`."
86
+ + f'\nYou gave "{value.unit}", unconvertible with "{unit}".'
87
+ ) from err
88
+
89
+
90
+ def _copy(x: object) -> object:
91
+ try:
92
+ return x.copy()
93
+ except AttributeError:
94
+ return copy.deepcopy(x)
95
+
96
+
97
+ def str_now(
98
+ precision: int = 3,
99
+ fmt: str = "{:.>72s}",
100
+ t_ref: Time | None = None,
101
+ dt_fmt: str = "(dt = {:.3f} s)",
102
+ return_time: bool = False,
103
+ ) -> str | tuple:
104
+ """Get stringified time now in UT ISOT format.
105
+
106
+ Parameters
107
+ ----------
108
+ precision : `int`, optional.
109
+ The precision of the isot format time.
110
+ Default: ``3``.
111
+
112
+ fmt : `str`, optional.
113
+ The Python 3 format string to format the time. Examples::
114
+
115
+ * ``"{:s}"``: plain time ``2020-01-01T01:01:01.23``
116
+ * ``"({:s})"``: plain time in parentheses ``(2020-01-01T01:01:01.23)``
117
+ * ``"{:_^72s}"``: center align, filling with _.
118
+ Default: ``'{:.>72s}'``.
119
+
120
+ t_ref : `~astropy.time.Time`, optional.
121
+ The reference time. If not `None`, delta time is calculated.
122
+ Default: `None`.
123
+
124
+ dt_fmt : `str`, optional.
125
+ The Python 3 format string to format the delta time.
126
+ Default: ``'(dt = {:.3f} s)'``.
127
+
128
+ return_time : `bool`, optional.
129
+ Whether to return the time at the start of this function and the delta
130
+ time (`dt`), as well as the time information string. If `t_ref` is
131
+ `None`, `dt` is automatically set to `None`.
132
+ Default: `False`.
133
+ """
134
+ now = Time(Time.now(), precision=precision)
135
+ timestr = now.isot
136
+ if t_ref is not None:
137
+ dt = (now - Time(t_ref)).sec
138
+ timestr = dt_fmt.format(dt) + " " + timestr
139
+ else:
140
+ dt = None
141
+
142
+ if return_time:
143
+ return fmt.format(timestr), now, dt
144
+ return fmt.format(timestr)
145
+
146
+
147
+ def parse_stretch(
148
+ stretch: str | BaseStretch,
149
+ *,
150
+ asinh_a: float | None = None,
151
+ log_a: float | None = None,
152
+ power: float | None = None,
153
+ sinh_a: float | None = None,
154
+ **kwargs: object,
155
+ ) -> BaseStretch:
156
+ """Resolve a stretch specification to an Astropy stretch instance.
157
+
158
+ Accepts a `~astropy.visualization.BaseStretch` instance, bare names such as
159
+ `"sqrt"`, and full class-style names such as `"SqrtStretch"`. Extra
160
+ keyword arguments are forwarded to the stretch class constructor.
161
+ Named tuning parameters (`asinh_a`, `log_a`, `power`, `sinh_a`) override
162
+ the package defaults for the corresponding stretches.
163
+ """
164
+ if isinstance(stretch, BaseStretch):
165
+ return stretch
166
+
167
+ key = stretch.strip().lower()
168
+ if key.endswith("stretch"):
169
+ key = key[: -len("stretch")]
170
+ if key not in _STRETCH_CLASS_MAP:
171
+ supported = ", ".join(sorted(_STRETCH_CLASS_MAP))
172
+ raise ValueError(f"Unknown stretch {stretch!r}. Supported names: {supported}")
173
+
174
+ params = {**_STRETCH_DEFAULTS.get(key, {})}
175
+ if key == "asinh" and asinh_a is not None:
176
+ params["a"] = asinh_a
177
+ elif key == "log" and log_a is not None:
178
+ params["a"] = log_a
179
+ elif key == "power" and power is not None:
180
+ params["a"] = power
181
+ elif key == "sinh" and sinh_a is not None:
182
+ params["a"] = sinh_a
183
+ params.update(kwargs)
184
+ return _STRETCH_CLASS_MAP[key](**params)
185
+
186
+
187
+ def parse_interval(
188
+ interval: str | BaseInterval | None = None,
189
+ *,
190
+ percent: float | None = None,
191
+ min_percent: float | None = None,
192
+ max_percent: float | None = None,
193
+ ) -> BaseInterval | None:
194
+ """Resolve an interval specification to an Astropy interval instance.
195
+
196
+ Accepts a `~astropy.visualization.BaseInterval` instance, `"zscale"`,
197
+ `percent`, or asymmetric `min_percent`/`max_percent` bounds. Returns
198
+ `None` when no interval shortcut is requested.
199
+ """
200
+ if isinstance(interval, str):
201
+ if interval.lower() == "zscale":
202
+ return ZScaleInterval()
203
+ raise ValueError(
204
+ f"Unknown interval string {interval!r}. "
205
+ "Use 'zscale' or a BaseInterval instance."
206
+ )
207
+
208
+ if interval is not None:
209
+ return interval
210
+
211
+ if percent is not None:
212
+ return PercentileInterval(percent)
213
+
214
+ if min_percent is not None or max_percent is not None:
215
+ lo = min_percent if min_percent is not None else 0.0
216
+ hi = max_percent if max_percent is not None else 100.0
217
+ return AsymmetricPercentileInterval(lo, hi)
218
+
219
+ return None
220
+
221
+
222
+ def Gaussian2D_correct(
223
+ model, theta_lower: float = -np.pi / 2, theta_upper: float = np.pi / 2
224
+ ):
225
+ """Return a Gaussian2D-like model with x as semimajor axis.
226
+
227
+ The returned copy has positive ``x_stddev``/``y_stddev``, ``x_stddev`` as
228
+ the semimajor-axis sigma, and ``theta`` normalized into the requested
229
+ interval.
230
+ """
231
+ new_model = model.copy()
232
+ sig_x = np.abs(model.x_stddev.value)
233
+ sig_y = np.abs(model.y_stddev.value)
234
+ theta = model.theta.value
235
+
236
+ if sig_x > sig_y:
237
+ theta_norm = normalize(theta, theta_lower, theta_upper)
238
+ new_model.x_stddev.value = sig_x
239
+ new_model.y_stddev.value = sig_y
240
+ new_model.theta.value = theta_norm
241
+ else:
242
+ theta_norm = normalize(theta + np.pi / 2, theta_lower, theta_upper)
243
+ new_model.x_stddev.value = sig_y
244
+ new_model.y_stddev.value = sig_x
245
+ new_model.theta.value = theta_norm
246
+
247
+ return new_model
248
+
249
+
250
+ def fit_astropy_model(data, model_init, sigma=None, fitter=None, **kwargs):
251
+ """Fit an Astropy 2D model to image data.
252
+
253
+ ``sigma`` is interpreted as per-pixel 1-sigma uncertainty and passed to
254
+ Astropy as ``weights=1/sigma``.
255
+ """
256
+ yy, xx = np.mgrid[: data.shape[0], : data.shape[1]]
257
+ weights = 1 / sigma if sigma is not None else None
258
+ if fitter is None:
259
+ fitter = LevMarLSQFitter()
260
+ fitted = fitter(model_init, xx, yy, data, weights=weights, **kwargs)
261
+ return fitted, fitter
262
+
263
+
264
+ def fit_Gaussian2D(data, model_init, correct=True, sigma=None, fitter=None, **kwargs):
265
+ """Fit a Gaussian2D-like Astropy model and optionally normalize orientation."""
266
+ fitted, fitter = fit_astropy_model(
267
+ data=data, model_init=model_init, sigma=sigma, fitter=fitter, **kwargs
268
+ )
269
+ if correct:
270
+ fitted = Gaussian2D_correct(fitted)
271
+ return fitted, fitter
272
+
273
+
274
+ def gaussian_kernel(fwhm=None, sigma=None, theta=0, nsigma=5, normalize_area=False):
275
+ """Generate a 2D Gaussian kernel array.
276
+
277
+ Exactly one of ``fwhm`` and ``sigma`` must be supplied. Scalar widths are
278
+ applied to both axes; two-element widths are interpreted as ``(x, y)``.
279
+ """
280
+ if ((fwhm is None) + (sigma is None)) != 1:
281
+ raise ValueError("One and only one of `fwhm` and `sigma` should be given.")
282
+ elif fwhm is not None:
283
+ sigma = fwhm / (2 * np.sqrt(2 * np.log(2)))
284
+ sigma = np.atleast_1d(sigma)
285
+ if len(sigma) == 1:
286
+ sigma = np.repeat(sigma, 2)
287
+ amp = 1 / (2 * np.pi * sigma[0] * sigma[1]) if normalize_area else 1
288
+ gauss = Gaussian2D(
289
+ amplitude=amp,
290
+ x_mean=0,
291
+ y_mean=0,
292
+ x_stddev=sigma[0],
293
+ y_stddev=sigma[1],
294
+ theta=theta,
295
+ )
296
+ shape = np.ceil(nsigma * sigma[::-1]).astype(int)
297
+ return gauss(
298
+ *np.ogrid[-shape[0] / 2 : shape[0] / 2 + 1, -shape[1] / 2 : shape[1] / 2 + 1]
299
+ )
@@ -0,0 +1,268 @@
1
+ """
2
+ Objects that are
3
+ (1) too fundamental, so used in various places,
4
+ (2) completely INDEPENDENT of all other modules of this package.
5
+ """
6
+
7
+ import numpy as np
8
+ from astro_ndslice import listify as listify # noqa: F401
9
+
10
+ _numba_available = False
11
+ try:
12
+ import importlib.util as _ilu
13
+
14
+ _numba_available = _ilu.find_spec("numba") is not None
15
+ except Exception:
16
+ pass
17
+
18
+ __all__ = [
19
+ "sigclip_dataerr",
20
+ "circular_mask",
21
+ "enclosing_circle_radius",
22
+ "mask_enclosing_circle_radius",
23
+ "bezel_mask",
24
+ ]
25
+
26
+
27
+ # !FIXME: not finished
28
+ # TODO: add err_lower, err_upper, sigma_lower, sigma_upper
29
+ def sigclip_dataerr(
30
+ val: np.ndarray,
31
+ err: np.ndarray,
32
+ cenfunc: str = "wvg",
33
+ sigma: float = 3,
34
+ maxiters: int = 3,
35
+ ) -> tuple[np.ma.MaskedArray, np.ndarray]:
36
+ """Sigma-clip values using per-point error estimates.
37
+
38
+ Parameters
39
+ ----------
40
+ val, err : array-like
41
+ Values and corresponding 1-sigma errors.
42
+
43
+ cenfunc : {"wvg", "avg", "average", "mean"}, optional
44
+ Center estimator. ``"wvg"`` uses weighted average.
45
+
46
+ sigma : float, optional
47
+ Rejection threshold in units of `err`.
48
+
49
+ maxiters : int, optional
50
+ Maximum clipping iterations.
51
+ """
52
+ if cenfunc == "wvg":
53
+ from .._core.numeric import wvg
54
+
55
+ def cenfunc(val, err):
56
+ return wvg(val, err=err)
57
+ elif cenfunc in ["avg", "average", "mean"]:
58
+
59
+ def cenfunc(val, err):
60
+ return np.mean(val)[0] # err is dummy
61
+ else:
62
+ raise ValueError(f"{cenfunc=} is not implemented yet.")
63
+
64
+ val = np.ma.array(val)
65
+ val_clipped = val.compressed()
66
+ err_clipped = err[val.mask]
67
+ cen = cenfunc(val_clipped, err_clipped)
68
+
69
+ for _i in range(maxiters):
70
+ # calculate deviation for all (even masked) elements:
71
+ deviation = np.abs(val.data - cen)
72
+ mask = deviation > sigma * err
73
+
74
+ return val, mask
75
+
76
+
77
+ def circular_mask(
78
+ shape: tuple[int, ...],
79
+ center: tuple | None = None,
80
+ radius: float | None = None,
81
+ center_xyz: bool = True,
82
+ ) -> np.ndarray:
83
+ """Creates an N-D circular (circular, spherical, ...) mask.
84
+
85
+ Parameters
86
+ ----------
87
+ shape : `tuple`
88
+ The pythonic shape, i.e., `arr.shape` (not xyz order).
89
+
90
+ center : `tuple`, `None`, optional.
91
+ The center of the circular mask. If `None` (default), the central
92
+ position is used.
93
+ Default: `None`.
94
+
95
+ radius : `float`, `None`, optional.
96
+ The radius of the mask. If `None`, the distance to the closest edge of
97
+ the image is used.
98
+ Default: `None`.
99
+
100
+ center_xyz : `bool`, optional.
101
+ Whether the center is in xyz order.
102
+ Default: `True`.
103
+
104
+ Notes
105
+ -----
106
+ Idea copied from
107
+ https://stackoverflow.com/questions/44865023/how-can-i-create-a-circular-mask-for-a-numpy-array
108
+
109
+ Note that this is slow due to the "general" N-D nature of the mask.
110
+ If you need an aperture-style 2-D mask, use ``astroapers.CircAp``.
111
+ """
112
+ if center is None: # use the middle of the image
113
+ center = [npix / 2 for npix in shape[::-1]]
114
+
115
+ if center_xyz:
116
+ center = center[::-1]
117
+
118
+ shape = np.array(shape)
119
+ center = np.array(center)
120
+
121
+ if radius is None: # use the smallest distance between the center and image walls
122
+ radius = np.min([center, shape - center])
123
+
124
+ slices = tuple([slice(None, npix, None) for npix in shape])
125
+
126
+ zyx = np.ogrid[slices]
127
+ dist_sq = [((zyx[i] - center[i]) ** 2) for i in range(len(shape))]
128
+ dist_from_center = np.sqrt(np.sum(np.array(dist_sq, dtype=object)))
129
+
130
+ mask = dist_from_center <= radius
131
+ return mask
132
+
133
+
134
+ def _enclosing_circle_radius_numpy(segm, center, segm_id, output):
135
+ for i in range(len(segm_id)):
136
+ _segm_id = segm_id[i]
137
+ mask = segm == _segm_id
138
+ y, x = np.nonzero(mask)
139
+ rsq_max = np.max((x - center[i][0]) ** 2 + (y - center[i][1]) ** 2)
140
+ output[i] = np.sqrt(rsq_max)
141
+
142
+
143
+ _enclosing_circle_radius_nb = None
144
+
145
+
146
+ def _get_enclosing_circle_radius_nb():
147
+ global _enclosing_circle_radius_nb
148
+ if _enclosing_circle_radius_nb is None:
149
+ import numba as nb
150
+
151
+ @nb.njit(fastmath=False, parallel=True)
152
+ def _kernel(segm, center, segm_id, output):
153
+ for i in nb.prange(len(segm_id)):
154
+ _segm_id = segm_id[i]
155
+ mask = segm == _segm_id
156
+ y, x = np.nonzero(mask)
157
+ rsq_max = np.max((x - center[i][0]) ** 2 + (y - center[i][1]) ** 2)
158
+ output[i] = np.sqrt(rsq_max)
159
+
160
+ _enclosing_circle_radius_nb = _kernel
161
+ return _enclosing_circle_radius_nb
162
+
163
+
164
+ def enclosing_circle_radius(
165
+ segm: np.ndarray,
166
+ center: np.ndarray,
167
+ segm_id: np.ndarray | None = None,
168
+ ) -> np.ndarray:
169
+ """
170
+ Calculate the radius of the smallest enclosing circle for a given mask.
171
+
172
+ Parameters
173
+ ----------
174
+ segm : 2D array-like
175
+ The input segmentation map (binary image) where non-zero values are
176
+ considered as the region of interest.
177
+
178
+ center : 2-D array, optional
179
+ The (x, y) coordinates of the center of the circles. If not provided,
180
+ the center will be calculated as the centroid of the masked region.
181
+
182
+ segm_id : `list` of `int`, optional
183
+ The `list` of segmentation IDs to calculate the radius for. If not provided,
184
+ it defaults to `[1]`, which is equivalent to `True` for binary masks.
185
+ Default: `None`.
186
+
187
+ Returns
188
+ -------
189
+ `~numpy.ndarray`
190
+ The radius of the smallest enclosing circle.
191
+
192
+ Notes
193
+ -----
194
+ Since it calculates distances from the center to the pixel center, one may
195
+ want to add ~0.5 (or sqrt(2)*0.5) to enclose the full pixel area.
196
+
197
+ By using numba, single segmentation radius finding is ~5 times faster than
198
+ pure numpy, and it is boosted further if `parallel=True` is used.
199
+ """
200
+
201
+ if segm_id is None:
202
+ segm_id = np.array([1], dtype=segm.dtype) # same as `True`
203
+
204
+ center = np.atleast_2d(center)
205
+ if center.shape[1] != 2:
206
+ raise ValueError("Center must be a 2D array with shape (N, 2)")
207
+
208
+ radii = np.empty(len(segm_id), dtype=np.float64)
209
+
210
+ if _numba_available:
211
+ _get_enclosing_circle_radius_nb()(segm, center, segm_id, radii)
212
+ else:
213
+ _enclosing_circle_radius_numpy(segm, center, segm_id, radii)
214
+
215
+ return radii
216
+
217
+
218
+ def mask_enclosing_circle_radius(
219
+ mask: np.ndarray,
220
+ center: tuple[float, float] | None = None,
221
+ ) -> float:
222
+ """Return the radius enclosing all non-zero pixels in a binary mask."""
223
+ y, x = np.nonzero(mask)
224
+
225
+ if len(x) == 0 or len(y) == 0:
226
+ return 0.0
227
+
228
+ if center is None:
229
+ center = (float(np.mean(x)), float(np.mean(y)))
230
+
231
+ distances = np.sqrt((x - center[0]) ** 2 + (y - center[1]) ** 2)
232
+ return float(np.max(distances))
233
+
234
+
235
+ def bezel_mask(
236
+ xvals,
237
+ yvals,
238
+ nx,
239
+ ny,
240
+ bezel=(0, 0),
241
+ bezel_x=None,
242
+ bezel_y=None,
243
+ ) -> np.ndarray:
244
+ """Return mask for positions inside the image border bezel."""
245
+ bezel = np.array(bezel)
246
+ if len(bezel) == 1:
247
+ bezel = np.repeat(bezel, 2)
248
+
249
+ if bezel_x is None:
250
+ bezel_x = bezel.copy()
251
+ else:
252
+ bezel_x = np.atleast_1d(bezel_x)
253
+ if len(bezel_x) == 1:
254
+ bezel_x = np.repeat(bezel_x, 2)
255
+
256
+ if bezel_y is None:
257
+ bezel_y = bezel.copy()
258
+ else:
259
+ bezel_y = np.atleast_1d(bezel_y)
260
+ if len(bezel_y) == 1:
261
+ bezel_y = np.repeat(bezel_y, 2)
262
+
263
+ return (
264
+ (xvals < bezel_x[0] + 0.5)
265
+ | (yvals < bezel_y[0] + 0.5)
266
+ | (xvals > (nx - bezel_x[1]) - 0.5)
267
+ | (yvals > (ny - bezel_y[1]) - 0.5)
268
+ )