impy-array 2.2.0__py3-none-any.whl → 2.2.2__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.
impy/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "2.2.0"
1
+ __version__ = "2.2.2"
2
2
  __author__ = "Hanjin Liu"
3
3
  __email__ = "liuhanjin-sc@g.ecc.u-tokyo.ac.jp"
4
4
 
@@ -6,7 +6,7 @@ import logging
6
6
 
7
7
  from ._const import Const, SetConst, use
8
8
 
9
- from .collections import *
9
+ from .collections import DataList, DataDict
10
10
  from .core import *
11
11
  from .binder import bind
12
12
  from .viewer import gui
@@ -15,19 +15,17 @@ from .arrays import ImgArray, LazyImgArray, BigImgArray, Label # for typing
15
15
  from . import random, io
16
16
  from .axes import slicer
17
17
 
18
- r"""
19
- Inheritance
20
- -----------
18
+ # Inheritance
19
+ # -----------
20
+ #
21
+ # AxesMixin -----> LazyImgArray ------> BigImgArray
22
+ # |
23
+ # +-----> MetaArray --+--> LabeledArray --+--> ImgArray
24
+ # | | |
25
+ # np.ndarray +--> Label +--> PhaseArray
26
+ # |
27
+ # +--> PropArray
21
28
 
22
- AxesMixin -----> LazyImgArray ------> BigImgArray
23
- |
24
- +-----> MetaArray --+--> LabeledArray --+--> ImgArray
25
- | | |
26
- np.ndarray +--> Label +--> PhaseArray
27
- |
28
- +--> PropArray
29
-
30
- """
31
29
 
32
30
  logging.getLogger("skimage").setLevel(logging.ERROR)
33
31
  logging.getLogger("tifffile").setLevel(logging.ERROR)
impy/array_api.py CHANGED
@@ -72,11 +72,13 @@ class XP:
72
72
  self.round = np.round
73
73
  self.gradient = np.gradient
74
74
  self.tensordot = np.tensordot
75
+ self.concatenate = np.concatenate
75
76
  self.stack = np.stack
76
77
  self.unravel_index = np.unravel_index
77
78
  self.argmax = np.argmax
78
79
  self.argmin = np.argmin
79
80
  self.pad = np.pad
81
+ self.isnan = np.isnan
80
82
 
81
83
  self.state = "numpy"
82
84
  from ._const import Const
@@ -141,11 +143,13 @@ class XP:
141
143
  except AttributeError:
142
144
  self.gradient = _gradient
143
145
  self.tensordot = cp.tensordot
146
+ self.concatenate = cp.concatenate
144
147
  self.stack = cp.stack
145
148
  self.unravel_index = cp.unravel_index
146
149
  self.argmax = cp.argmax
147
150
  self.argmin = cp.argmin
148
151
  self.pad = cp.pad
152
+ self.isnan = cp.isnan
149
153
  self.state = "cupy"
150
154
 
151
155
  from ._const import Const
impy/arrays/__init__.py CHANGED
@@ -16,3 +16,13 @@ try:
16
16
 
17
17
  except Exception:
18
18
  pass
19
+
20
+ __all__ = [
21
+ "ImgArray",
22
+ "LabeledArray",
23
+ "Label",
24
+ "PhaseArray",
25
+ "PropArray",
26
+ "LazyImgArray",
27
+ "BigImgArray",
28
+ ]
@@ -1,7 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
1
4
  from functools import partial
2
5
  import numpy as np
3
- from ...array_api import xp
6
+ from impy.array_api import xp
4
7
 
8
+ if TYPE_CHECKING:
9
+ from impy.arrays import ImgArray
10
+
5
11
  try:
6
12
  gradient = xp.gradient
7
13
  except AttributeError:
@@ -11,11 +17,7 @@ except AttributeError:
11
17
  out = numpy.gradient(a.get(), axis=axis)
12
18
  return xp.asarray(out)
13
19
 
14
- __all__ = ["wiener",
15
- "lucy",
16
- "lucy_tv",
17
- "check_psf",
18
- ]
20
+ __all__ = ["wiener", "lucy", "lucy_tv", "check_psf"]
19
21
 
20
22
  def wiener(obs, psf_ft, psf_ft_conj, lmd):
21
23
  obs = xp.asarray(obs)
@@ -67,20 +69,23 @@ def richardson_lucy_tv(obs, psf_ft, psf_ft_conj, max_iter, lmd, tol, eps):
67
69
  return xp.fft.fftshift(est_new)
68
70
 
69
71
 
70
- def _safe_div(a, b, eps=1e-8):
72
+ def _safe_div(a: np.ndarray, b: np.ndarray, eps: float = 1e-8):
71
73
  out = xp.zeros(a.shape, dtype=np.float32)
72
74
  mask = b > eps
73
- out[mask] = a[mask]/b[mask]
75
+ out[mask] = a[mask] / b[mask]
74
76
  return out
75
77
 
76
78
 
77
- def check_psf(img, psf, dims):
79
+ def check_psf(img_shape: tuple[int, ...], img_scale: tuple[float, ...], psf):
80
+ if callable(psf):
81
+ psf = psf(img_shape, img_scale)
78
82
  psf = xp.asarray(psf, dtype=np.float32)
79
83
  psf /= xp.sum(psf)
80
84
 
81
- if img.sizesof(dims) != psf.shape:
82
- raise ValueError("observation and PSF have different shape: "
83
- f"{img.sizesof(dims)} and {psf.shape}")
85
+ if img_shape != psf.shape:
86
+ raise ValueError(
87
+ f"observation and PSF have different shape: {img_shape} and {psf.shape}"
88
+ )
84
89
  psf_ft = xp.fft.rfftn(psf)
85
90
  psf_ft_conj = xp.conjugate(psf_ft)
86
- return psf_ft, psf_ft_conj
91
+ return psf_ft, psf_ft_conj
@@ -3,11 +3,11 @@ import numpy as np
3
3
  from typing import TYPE_CHECKING
4
4
  from skimage.feature.corner import _symmetric_image
5
5
  from ._skimage import skfeat
6
- from ...array_api import xp
6
+ from impy.array_api import xp
7
7
 
8
8
  if TYPE_CHECKING:
9
- from ..imgarray import ImgArray
10
- from ...axes import Axes
9
+ from impy.arrays.imgarray import ImgArray
10
+ from impy.axes import Axes
11
11
 
12
12
  def eigh(a: np.ndarray):
13
13
  a = xp.asarray(a, dtype=a.dtype)
@@ -20,33 +20,35 @@ def eigvalsh(a: np.ndarray):
20
20
  return val
21
21
 
22
22
  def structure_tensor_eigval(img: np.ndarray, sigma: float, pxsize: float):
23
- tensor_elements = skfeat.structure_tensor(img, sigma, order="xy",
24
- mode="reflect")
23
+ tensor_elements = skfeat.structure_tensor(
24
+ img, sigma, mode="reflect"
25
+ )
25
26
  return _solve_hermitian_eigval(tensor_elements, pxsize)
26
27
 
27
28
  def structure_tensor_eigh(img: np.ndarray, sigma: float, pxsize: float):
28
- tensor_elements = skfeat.structure_tensor(img, sigma, order="xy",
29
- mode="reflect")
29
+ tensor_elements = skfeat.structure_tensor(
30
+ img, sigma, mode="reflect"
31
+ )
30
32
  return _solve_hermitian_eigs(tensor_elements, pxsize)
31
33
 
32
34
  def hessian_eigval(img: np.ndarray, sigma: float, pxsize: float):
33
35
  hessian_elements = skfeat.hessian_matrix(
34
- img, sigma=sigma, order="xy", mode="reflect"
36
+ img, sigma=sigma, mode="reflect", use_gaussian_derivatives=False,
35
37
  )
36
38
  return _solve_hermitian_eigval(hessian_elements, pxsize)
37
39
 
38
40
  def hessian_eigh(img: np.ndarray, sigma: float, pxsize: float):
39
41
  hessian_elements = skfeat.hessian_matrix(
40
- img, sigma=sigma, order="xy", mode="reflect"
42
+ img, sigma=sigma, mode="reflect", use_gaussian_derivatives=False,
41
43
  )
42
44
  return _solve_hermitian_eigs(hessian_elements, pxsize)
43
45
 
44
46
 
45
- def _solve_hermitian_eigval(elems, pxsize):
47
+ def _solve_hermitian_eigval(elems, pxsize) -> np.ndarray:
46
48
  # Correct for scale
47
49
  pxsize = np.asarray(pxsize)
48
50
  hessian = _symmetric_image(elems)
49
- hessian *= (pxsize.reshape(-1,1) * pxsize.reshape(1,-1))
51
+ hessian *= (pxsize.reshape(-1, 1) * pxsize.reshape(1, -1))
50
52
  eigval = eigvalsh(hessian)
51
53
  return eigval
52
54
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from typing import Iterable, NamedTuple, Sequence, TYPE_CHECKING
4
+ import math
4
5
  import numpy as np
5
6
 
6
7
  from ._skimage import sktrans
@@ -149,7 +150,7 @@ def check_matrix(matrices: list[np.ndarray | float]):
149
150
  def polar2d(
150
151
  img: xp.ndarray,
151
152
  rmax: int,
152
- dtheta: float = 0.1,
153
+ dtheta: float = 0.1, # radian
153
154
  order: int = 1,
154
155
  mode: str = "constant",
155
156
  cval: float = 0,
@@ -307,8 +308,8 @@ def iradon(
307
308
  # Reconstruct image by interpolation
308
309
  reconstructed = np.zeros(output_shape, dtype=dtype)
309
310
  xpr, ypr = np.indices(output_shape)
310
- xpr -= output_shape[0] // 2
311
- ypr -= output_shape[1] // 2
311
+ xpr = xpr - output_shape[0] // 2
312
+ ypr = ypr - output_shape[1] // 2
312
313
 
313
314
  from scipy.interpolate import interp1d
314
315
  x = np.arange(img_shape) - img_shape // 2 # NOTE: use CPU!
@@ -322,12 +323,13 @@ def iradon(
322
323
  # This function is almost ported from `skimage.transform`.
323
324
  def get_fourier_filter(size: int, filter_name: str):
324
325
  n = xp.concatenate(
325
- [xp.arange(1, size / 2 + 1, 2, dtype=int),
326
- xp.arange(size / 2 - 1, 0, -2, dtype=int)]
326
+ [xp.arange(1, size // 2 + 1, 2, dtype=np.float32),
327
+ xp.arange(size // 2 - 1, 0, -2, dtype=np.float32)]
327
328
  )
328
329
  f = xp.zeros(size)
329
330
  f[0] = 0.25
330
331
  f[1::2] = -1 / (np.pi * n[:len(f[1::2])]) ** 2
332
+
331
333
  fourier_filter = 2 * xp.real(xp.fft.fft(f)) # ramp filter
332
334
  if filter_name == "ramp":
333
335
  pass
impy/arrays/axesmixin.py CHANGED
@@ -200,6 +200,9 @@ class AxesMixin:
200
200
  {a: s for a, s in other.scale.items() if a in self.axes} # type: ignore
201
201
  )
202
202
 
203
+ elif isinstance(other, (list, tuple)):
204
+ for a, s in zip(self.axes, other):
205
+ a.scale = s
203
206
  else:
204
207
  raise TypeError(
205
208
  f"'other' must be str or axes supported object, but got {type(other)}"
@@ -27,7 +27,7 @@ SupportSlicing = Union[
27
27
  Mapping[str, SupportOneSlicing],
28
28
  ]
29
29
 
30
- class MetaArray(AxesMixin, np.ndarray):
30
+ class MetaArray(AxesMixin, np.ndarray[Any, np.dtype[np.number]]):
31
31
  additional_props = ["_source", "_metadata", "_name"]
32
32
  NP_DISPATCH = {}
33
33
  _name: str
@@ -747,7 +747,7 @@ class MetaArray(AxesMixin, np.ndarray):
747
747
  except ImageAxesError:
748
748
  self.axes = None
749
749
 
750
- return None
750
+ return self
751
751
 
752
752
  def _process_output(self, func, args, kwargs):
753
753
  # find the largest MetaArray. Largest because of broadcasting.
@@ -181,6 +181,29 @@ def _(img: MetaArray, axis1: int | AxisLike, axis2: int | AxisLike):
181
181
  out._set_info(img, new_axes=axes_list)
182
182
  return out
183
183
 
184
+ # argmax does not support axis as tuple
185
+ @MetaArray.implements(np.argmax)
186
+ def _(img: MetaArray, axis: int | AxisLike | None = None):
187
+ if isinstance(axis, (str, Axis)):
188
+ axis = img.axisof(axis)
189
+ new_axes = img.axes.drop(axis)
190
+ out = np.argmax(img.value, axis=axis)
191
+ if isinstance(out, np.ndarray):
192
+ out = out.view(img.__class__)
193
+ out._set_info(img, new_axes=new_axes)
194
+ return out
195
+
196
+ @MetaArray.implements(np.argmin)
197
+ def _(img: MetaArray, axis: int | AxisLike | None = None):
198
+ if isinstance(axis, (str, Axis)):
199
+ axis = img.axisof(axis)
200
+ new_axes = img.axes.drop(axis)
201
+ out = np.argmin(img.value, axis=axis)
202
+ if isinstance(out, np.ndarray):
203
+ out = out.view(img.__class__)
204
+ out._set_info(img, new_axes=new_axes)
205
+ return out
206
+
184
207
  @MetaArray.implements(np.cross)
185
208
  def _(
186
209
  img: MetaArray,
@@ -206,6 +229,41 @@ def _(
206
229
  out._set_info(img, new_axes=img.axes)
207
230
  return out
208
231
 
232
+ @MetaArray.implements(np.diff)
233
+ def _(
234
+ img: MetaArray,
235
+ n: int = 1,
236
+ axis: int | AxisLike = -1,
237
+ prepend=None,
238
+ append=None,
239
+ ):
240
+ if isinstance(axis, (str, Axis)):
241
+ axis = img.axisof(axis)
242
+ kwargs = dict(n=n, axis=axis)
243
+ if prepend is not None:
244
+ kwargs["prepend"] = prepend
245
+ if append is not None:
246
+ kwargs["append"] = append
247
+ out = np.diff(img.value, **kwargs)
248
+ out = out.view(img.__class__)
249
+ out._set_info(img, new_axes=img.axes)
250
+ return out
251
+
252
+ @MetaArray.implements(np.gradient)
253
+ def _(img: MetaArray, *varargs, axis=None, edge_order=1):
254
+ if isinstance(axis, (str, Axis)):
255
+ axis = img.axisof(axis)
256
+ elif isinstance(axis, Axes):
257
+ axis = tuple(img.axisof(ax) for ax in axis)
258
+ out = np.gradient(img.value, *varargs, axis=axis, edge_order=edge_order)
259
+ if isinstance(out, np.ndarray):
260
+ out = out.view(img.__class__)._set_info(img, new_axes=img.axes)
261
+ else:
262
+ out = [arr.view(img.__class__)._set_info(img, new_axes=img.axes) for arr in out]
263
+ out = DataList(out)
264
+ return out
265
+
266
+
209
267
  # This function is ported from numpy.core.numeric.normalize_axis_tuple
210
268
  def np_normalize_axis_tuple(axis, ndim, argname=None, allow_duplicate=False):
211
269
  # Optimization to speed-up the most common cases.
impy/arrays/imgarray.py CHANGED
@@ -13,25 +13,24 @@ from .specials import PropArray
13
13
  from ._utils._skimage import skexp, skfeat, skfil, skimage, skmes, skres, skseg, sktrans
14
14
  from ._utils import _filters, _linalg, _deconv, _misc, _glcm, _docs, _transform, _structures, _corr
15
15
 
16
- from ..utils.axesop import add_axes, switch_slice, complement_axes, find_first_appeared
17
- from ..utils.deco import check_input_and_output, dims_to_spatial_axes, same_dtype
18
- from ..utils.gauss import GaussianBackground, GaussianParticle
19
- from ..utils.misc import check_nd, largest_zeros
20
- from ..utils.slicer import solve_slicer
21
-
22
- from ..collections import DataDict
23
- from ..axes import AxisLike, slicer, Axes, Axis
24
- from .._types import nDInt, nDFloat, Dims, Coords, AxesTargetedSlicer, PaddingMode
25
- from .._const import Const
26
- from ..array_api import xp, cupy_dispatcher
16
+ from impy.utils.axesop import add_axes, switch_slice, complement_axes, find_first_appeared
17
+ from impy.utils.deco import check_input_and_output, dims_to_spatial_axes, same_dtype
18
+ from impy.utils.gauss import GaussianBackground, GaussianParticle
19
+ from impy.utils.misc import check_nd, largest_zeros
20
+ from impy.utils.slicer import solve_slicer
21
+
22
+ from impy.collections import DataDict
23
+ from impy.axes import AxisLike, slicer, Axes, Axis
24
+ from impy._types import nDInt, nDFloat, Dims, Coords, AxesTargetedSlicer, PaddingMode
25
+ from impy._const import Const
26
+ from impy.array_api import xp, cupy_dispatcher
27
27
 
28
28
  if TYPE_CHECKING:
29
29
  from ..frame import MarkerFrame
30
30
  from typing import Literal, Union
31
- ThreasholdMethod = Union[
32
- Literal["isodata"], Literal["li"], Literal["local"], Literal["mean"], Literal["min"],
33
- Literal["minimum"], Literal["niblack"], Literal["otsu"], Literal["sauvola"],
34
- Literal["triangle"], Literal["yen"]
31
+ ThreasholdMethod = Literal[
32
+ "isodata", "li", "local", "mean", "min", "minimum", "niblack", "otsu", "sauvola",
33
+ "triangle", "yen"
35
34
  ]
36
35
  FftShape = Literal["same", "square"]
37
36
 
@@ -220,7 +219,7 @@ class ImgArray(LabeledArray):
220
219
  @same_dtype(asfloat=True)
221
220
  def rescale(
222
221
  self,
223
- scale: float = 1/16,
222
+ scale: nDFloat = 1/16,
224
223
  *,
225
224
  order: int = None,
226
225
  mode: PaddingMode = "reflect",
@@ -228,31 +227,45 @@ class ImgArray(LabeledArray):
228
227
  dims: Dims = None,
229
228
  ) -> ImgArray:
230
229
  """
231
- Rescale image.
230
+ Rescale image by applying the diagonal component of Affine matrix.
232
231
 
233
232
  Parameters
234
233
  ----------
235
234
  scale : float, optional
236
- scale of the new image.
235
+ Relative scale of the new image. scale 1/2 means that the shape of
236
+ the output image will be (N/2, ...).
237
237
  {order}{mode}{cval}{dims}
238
238
 
239
239
  Returns
240
240
  -------
241
241
  ImgArray
242
242
  Rescaled image.
243
- """
244
- outshape = switch_slice(
245
- dims, self.axes, ifin=np.array(self.shape)*scale, ifnot=self.shape
246
- )
247
- gb = np.prod(outshape) * 4/1e9
243
+ """
244
+ scale_is_seq = hasattr(scale, "__iter__")
245
+
246
+ # Check if output is too large.
247
+ gb = -1
248
+ if not scale_is_seq and scale > 1:
249
+ gb = np.prod(self.shape) * (scale ** len(dims)) / 1e9
250
+ elif scale_is_seq and np.prod(list(scale)) > 1:
251
+ gb = np.prod(self.shape) * np.prod(list(scale)) / 1e9
248
252
  if gb > Const["MAX_GB"]:
249
- raise MemoryError(f"Too large: {gb} GB")
250
- scale_ = [scale if a in dims else 1 for a in self.axes]
253
+ raise MemoryError(f"Output image is too large: {gb} GB")
254
+
255
+ if scale_is_seq:
256
+ scale = list(scale)
257
+ if len(scale) != len(dims):
258
+ raise ValueError(
259
+ "scale must have the same length as the spatial dimensions."
260
+ )
261
+ it = iter(scale)
262
+ scale_ = [next(it) if a in dims else 1 for a in self.axes]
263
+ else:
264
+ scale_ = [scale if a in dims else 1 for a in self.axes]
251
265
  out = sktrans.rescale(
252
266
  self.value, scale_, order=order, mode=mode, cval=cval, anti_aliasing=False
253
267
  )
254
- out: ImgArray = out.view(self.__class__)
255
- out._set_info(self)
268
+ out = out.view(self.__class__)._set_info(self)
256
269
  out.axes = self.axes.copy()
257
270
  out.set_scale({a: self.scale[a]/scale for a, scale in zip(self.axes, scale_)})
258
271
  return out
@@ -303,11 +316,11 @@ class ImgArray(LabeledArray):
303
316
  )
304
317
 
305
318
  reshaped_img = img_to_reshape.reshape(shape)
306
- axes_to_reduce = tuple(i*2+1 for i in range(self.ndim))
319
+ axes_to_reduce = tuple(i * 2 + 1 for i in range(self.ndim))
307
320
  out: np.ndarray = binfunc(reshaped_img, axis=axes_to_reduce)
308
321
  out: ImgArray = out.view(self.__class__)
309
322
  out._set_info(self)
310
- out.axes = str(self.axes) # _set_info does not pass copy so new axes must be defined here.
323
+ out.axes = self.axes.copy() # _set_info does not pass copy so new axes must be defined here.
311
324
  out.set_scale(
312
325
  {a: self.scale[a]/scale for a, scale in zip(self.axes, scale_)}
313
326
  )
@@ -318,7 +331,7 @@ class ImgArray(LabeledArray):
318
331
  def radial_profile(
319
332
  self,
320
333
  nbin: int = 32,
321
- center: Iterable[float] = None,
334
+ center: Iterable[float] | None = None,
322
335
  r_max: float = None,
323
336
  *,
324
337
  method: str = "mean",
@@ -3111,8 +3124,7 @@ class ImgArray(LabeledArray):
3111
3124
  if out.ndim == 3:
3112
3125
  out = np.moveaxis(out, 0, 1)
3113
3126
  out = out[::-1]
3114
- out._set_info(self, new_axes)
3115
- return out
3127
+ return out._set_info(self, new_axes)
3116
3128
 
3117
3129
  @check_input_and_output
3118
3130
  def threshold(
@@ -4195,7 +4207,7 @@ class ImgArray(LabeledArray):
4195
4207
  @check_input_and_output
4196
4208
  def wiener(
4197
4209
  self,
4198
- psf: np.ndarray,
4210
+ psf: np.ndarray | Callable[[tuple[int, ...]], np.ndarray],
4199
4211
  lmd: float = 0.1,
4200
4212
  *,
4201
4213
  dims: Dims = None,
@@ -4213,8 +4225,9 @@ class ImgArray(LabeledArray):
4213
4225
 
4214
4226
  Parameters
4215
4227
  ----------
4216
- psf : np.ndarray
4217
- Point spread function.
4228
+ psf : ndarray or callable
4229
+ Point spread function. If a function is given, `psf(shape)` will be
4230
+ called to generate the PSF.
4218
4231
  lmd : float, default is 0.1
4219
4232
  Constant value used in the deconvolution. See Formulation below.
4220
4233
  {dims}{update}
@@ -4233,22 +4246,23 @@ class ImgArray(LabeledArray):
4233
4246
  if lmd <= 0:
4234
4247
  raise ValueError(f"lmd must be positive, but got: {lmd}")
4235
4248
 
4236
- psf_ft, psf_ft_conj = _deconv.check_psf(self, psf, dims)
4249
+ psf_ft, psf_ft_conj = _deconv.check_psf(
4250
+ self.sizesof(dims), tuple(self.scale[axis] for axis in dims), psf,
4251
+ )
4237
4252
 
4238
4253
  return self._apply_dask(
4239
4254
  _deconv.wiener,
4240
4255
  c_axes=complement_axes(dims, self.axes),
4241
4256
  args=(psf_ft, psf_ft_conj, lmd)
4242
- )
4243
-
4244
-
4257
+ )
4258
+
4245
4259
  @_docs.write_docs
4246
4260
  @dims_to_spatial_axes
4247
4261
  @same_dtype(asfloat=True)
4248
4262
  @check_input_and_output
4249
4263
  def lucy(
4250
4264
  self,
4251
- psf: np.ndarray,
4265
+ psf: np.ndarray | Callable[[tuple[int, ...]], np.ndarray],
4252
4266
  niter: int = 50,
4253
4267
  eps: float = 1e-5,
4254
4268
  *,
@@ -4260,14 +4274,15 @@ class ImgArray(LabeledArray):
4260
4274
 
4261
4275
  Parameters
4262
4276
  ----------
4263
- psf : np.ndarray
4264
- Point spread function.
4277
+ psf : ndarray or callable
4278
+ Point spread function. If a function is given, `psf(shape)` will be
4279
+ called to generate the PSF.
4265
4280
  niter : int, default is 50.
4266
4281
  Number of iterations.
4267
4282
  eps : float, default is 1e-5
4268
- During deconvolution, division by small values in the convolve image of estimation and
4269
- PSF may cause divergence. Therefore, division by values under `eps` is substituted
4270
- to zero.
4283
+ During deconvolution, division by small values in the convolve image
4284
+ of estimation and PSF may cause divergence. Therefore, division by
4285
+ values under `eps` is substituted to zero.
4271
4286
  {dims}{update}
4272
4287
 
4273
4288
  Returns
@@ -4281,7 +4296,9 @@ class ImgArray(LabeledArray):
4281
4296
  wiener
4282
4297
  """
4283
4298
 
4284
- psf_ft, psf_ft_conj = _deconv.check_psf(self, psf, dims)
4299
+ psf_ft, psf_ft_conj = _deconv.check_psf(
4300
+ self.sizesof(dims), tuple(self.scale[axis] for axis in dims), psf,
4301
+ )
4285
4302
 
4286
4303
  return self._apply_dask(
4287
4304
  _deconv.richardson_lucy,
@@ -4295,7 +4312,7 @@ class ImgArray(LabeledArray):
4295
4312
  @check_input_and_output
4296
4313
  def lucy_tv(
4297
4314
  self,
4298
- psf: np.ndarray,
4315
+ psf: np.ndarray | Callable[[tuple[int, ...]], np.ndarray],
4299
4316
  max_iter: int = 50,
4300
4317
  lmd: float = 1e-3,
4301
4318
  tol: float = 1e-3,
@@ -4319,8 +4336,9 @@ class ImgArray(LabeledArray):
4319
4336
 
4320
4337
  Parameters
4321
4338
  ----------
4322
- psf : np.ndarray
4323
- Point spread function.
4339
+ psf : ndarray or callable
4340
+ Point spread function. If a function is given, `psf(shape)` will be
4341
+ called to generate the PSF.
4324
4342
  max_iter : int, default is 50.
4325
4343
  Maximum number of iterations.
4326
4344
  lmd : float, default is 1e-3
@@ -4361,7 +4379,9 @@ class ImgArray(LabeledArray):
4361
4379
  "In Richadson-Lucy with total-variance-regularization, "
4362
4380
  "parameter `lmd` must be positive."
4363
4381
  )
4364
- psf_ft, psf_ft_conj = _deconv.check_psf(self, psf, dims)
4382
+ psf_ft, psf_ft_conj = _deconv.check_psf(
4383
+ self.sizesof(dims), tuple(self.scale[axis] for axis in dims), psf
4384
+ )
4365
4385
 
4366
4386
  return self._apply_dask(
4367
4387
  _deconv.richardson_lucy_tv,
@@ -4369,6 +4389,39 @@ class ImgArray(LabeledArray):
4369
4389
  args=(psf_ft, psf_ft_conj, max_iter, lmd, tol, eps)
4370
4390
  )
4371
4391
 
4392
+ @_docs.write_docs
4393
+ @dims_to_spatial_axes
4394
+ @same_dtype(asfloat=True)
4395
+ @check_input_and_output
4396
+ def tiled_lucy(
4397
+ self,
4398
+ psf: np.ndarray | Callable[[tuple[int, ...]], np.ndarray],
4399
+ niter: int = 50,
4400
+ eps: float = 1e-5,
4401
+ chunks = "auto",
4402
+ overlap: int = 32,
4403
+ *,
4404
+ dims: Dims = None,
4405
+ update: bool = False
4406
+ ) -> ImgArray:
4407
+ from dask import array as da
4408
+
4409
+ input = da.from_array(self.value, chunks=chunks)
4410
+ depth = switch_slice(dims, self.axes, overlap, 0)
4411
+ scale = tuple(self.scale[axis] for axis in dims)
4412
+
4413
+ def func(arr: np.ndarray):
4414
+ psf_ft, psf_ft_conj = _deconv.check_psf(arr.shape, scale, psf)
4415
+ return _deconv.richardson_lucy(arr, psf_ft, psf_ft_conj, niter, eps)
4416
+
4417
+ out = da.map_overlap(
4418
+ func,
4419
+ input,
4420
+ depth=depth,
4421
+ boundary="reflect",
4422
+ dtype=self.dtype,
4423
+ ).compute()
4424
+ return out
4372
4425
 
4373
4426
  def _check_coordinates(coords, img: ImgArray, dims: Dims = None):
4374
4427
  from impy.frame import MarkerFrame