impy-array 2.4.10__tar.gz → 2.5.1__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.
Files changed (72) hide show
  1. {impy_array-2.4.10 → impy_array-2.5.1}/PKG-INFO +1 -1
  2. {impy_array-2.4.10 → impy_array-2.5.1}/impy/__init__.py +1 -1
  3. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/_utils/_misc.py +14 -0
  4. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/axesmixin.py +52 -51
  5. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/bases/metaarray.py +2 -5
  6. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/imgarray.py +50 -6
  7. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/labeledarray.py +3 -4
  8. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/lazy.py +6 -8
  9. {impy_array-2.4.10 → impy_array-2.5.1}/impy/core.py +5 -7
  10. {impy_array-2.4.10 → impy_array-2.5.1}/impy/io/_mrc.py +20 -18
  11. {impy_array-2.4.10 → impy_array-2.5.1}/impy/lazy/core.py +3 -2
  12. {impy_array-2.4.10 → impy_array-2.5.1}/.gitignore +0 -0
  13. {impy_array-2.4.10 → impy_array-2.5.1}/LICENSE +0 -0
  14. {impy_array-2.4.10 → impy_array-2.5.1}/README.md +0 -0
  15. {impy_array-2.4.10 → impy_array-2.5.1}/impy/__main__.py +0 -0
  16. {impy_array-2.4.10 → impy_array-2.5.1}/impy/_const.py +0 -0
  17. {impy_array-2.4.10 → impy_array-2.5.1}/impy/_types.py +0 -0
  18. {impy_array-2.4.10 → impy_array-2.5.1}/impy/array_api.py +0 -0
  19. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/__init__.py +0 -0
  20. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/_imshow.py +0 -0
  21. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/_utils/__init__.py +0 -0
  22. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/_utils/_corr.py +0 -0
  23. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/_utils/_deconv.py +0 -0
  24. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/_utils/_docs.py +0 -0
  25. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/_utils/_filters.py +0 -0
  26. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/_utils/_glcm.py +0 -0
  27. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/_utils/_linalg.py +0 -0
  28. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/_utils/_plot.py +0 -0
  29. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/_utils/_process_numba.py +0 -0
  30. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/_utils/_skimage.py +0 -0
  31. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/_utils/_structures.py +0 -0
  32. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/_utils/_transform.py +0 -0
  33. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/bases/__init__.py +0 -0
  34. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/bases/overload.py +0 -0
  35. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/big.py +0 -0
  36. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/label.py +0 -0
  37. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/phasearray.py +0 -0
  38. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/specials.py +0 -0
  39. {impy_array-2.4.10 → impy_array-2.5.1}/impy/arrays/tiled.py +0 -0
  40. {impy_array-2.4.10 → impy_array-2.5.1}/impy/axes/__init__.py +0 -0
  41. {impy_array-2.4.10 → impy_array-2.5.1}/impy/axes/_axes.py +0 -0
  42. {impy_array-2.4.10 → impy_array-2.5.1}/impy/axes/_axes_tuple.py +0 -0
  43. {impy_array-2.4.10 → impy_array-2.5.1}/impy/axes/_axis.py +0 -0
  44. {impy_array-2.4.10 → impy_array-2.5.1}/impy/axes/_slicer.py +0 -0
  45. {impy_array-2.4.10 → impy_array-2.5.1}/impy/binder.py +0 -0
  46. {impy_array-2.4.10 → impy_array-2.5.1}/impy/collections.py +0 -0
  47. {impy_array-2.4.10 → impy_array-2.5.1}/impy/correlation.py +0 -0
  48. {impy_array-2.4.10 → impy_array-2.5.1}/impy/frame/__init__.py +0 -0
  49. {impy_array-2.4.10 → impy_array-2.5.1}/impy/frame/frames.py +0 -0
  50. {impy_array-2.4.10 → impy_array-2.5.1}/impy/io/__init__.py +0 -0
  51. {impy_array-2.4.10 → impy_array-2.5.1}/impy/io/_czi.py +0 -0
  52. {impy_array-2.4.10 → impy_array-2.5.1}/impy/io/_nd2.py +0 -0
  53. {impy_array-2.4.10 → impy_array-2.5.1}/impy/io/_npy.py +0 -0
  54. {impy_array-2.4.10 → impy_array-2.5.1}/impy/io/_registry.py +0 -0
  55. {impy_array-2.4.10 → impy_array-2.5.1}/impy/io/_tiff.py +0 -0
  56. {impy_array-2.4.10 → impy_array-2.5.1}/impy/io/_utils.py +0 -0
  57. {impy_array-2.4.10 → impy_array-2.5.1}/impy/io/_zarr.py +0 -0
  58. {impy_array-2.4.10 → impy_array-2.5.1}/impy/lazy/__init__.py +0 -0
  59. {impy_array-2.4.10 → impy_array-2.5.1}/impy/lazy/random.py +0 -0
  60. {impy_array-2.4.10 → impy_array-2.5.1}/impy/random.py +0 -0
  61. {impy_array-2.4.10 → impy_array-2.5.1}/impy/roi.py +0 -0
  62. {impy_array-2.4.10 → impy_array-2.5.1}/impy/utils/__init__.py +0 -0
  63. {impy_array-2.4.10 → impy_array-2.5.1}/impy/utils/axesop.py +0 -0
  64. {impy_array-2.4.10 → impy_array-2.5.1}/impy/utils/deco.py +0 -0
  65. {impy_array-2.4.10 → impy_array-2.5.1}/impy/utils/gauss.py +0 -0
  66. {impy_array-2.4.10 → impy_array-2.5.1}/impy/utils/misc.py +0 -0
  67. {impy_array-2.4.10 → impy_array-2.5.1}/impy/utils/slicer.py +0 -0
  68. {impy_array-2.4.10 → impy_array-2.5.1}/impy/utils/utilcls.py +0 -0
  69. {impy_array-2.4.10 → impy_array-2.5.1}/impy/viewer/__init__.py +0 -0
  70. {impy_array-2.4.10 → impy_array-2.5.1}/impy/viewer/utils.py +0 -0
  71. {impy_array-2.4.10 → impy_array-2.5.1}/impy/viewer/viewer.py +0 -0
  72. {impy_array-2.4.10 → impy_array-2.5.1}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: impy-array
3
- Version: 2.4.10
3
+ Version: 2.5.1
4
4
  Summary: Speed up coding/extending image analysis in Python.
5
5
  Project-URL: Download, https://github.com/hanjinliu/impy
6
6
  Author-email: Hanjin Liu <liuhanjin-sc@g.ecc.u-tokyo.ac.jp>
@@ -1,4 +1,4 @@
1
- __version__ = "2.4.10"
1
+ __version__ = "2.5.1"
2
2
  __author__ = "Hanjin Liu"
3
3
  __email__ = "liuhanjin-sc@g.ecc.u-tokyo.ac.jp"
4
4
 
@@ -93,3 +93,17 @@ def inpaint_mean(img: np.ndarray, mask: np.ndarray):
93
93
  border = expanded_i ^ mask_i
94
94
  out[mask_i] = xp.mean(img[border])
95
95
  return out
96
+
97
+
98
+ def fft_crop(freq: np.ndarray, new_shape: tuple[int, ...]) -> np.ndarray:
99
+ """Crop the Fourier transformed array to the new_shape."""
100
+ cur_freq = freq
101
+ for i, (n, s) in enumerate(zip(new_shape, freq.shape)):
102
+ if i != len(new_shape) - 1:
103
+ # rfftn non last axis
104
+ left = n // 2
105
+ right = s - left
106
+ cur_freq = np.concatenate([cur_freq[:left], cur_freq[right:]], axis=i)
107
+ else:
108
+ cur_freq = cur_freq[..., :n]
109
+ return cur_freq
@@ -24,28 +24,28 @@ if TYPE_CHECKING:
24
24
 
25
25
  class AxesMixin:
26
26
  """Abstract class with shape, ndim and axes are defined."""
27
-
27
+
28
28
  _INHERIT = object()
29
29
  _axes: Axes
30
30
  ndim: int
31
31
  shape: tuple[int, ...]
32
32
  dtype: np.dtype
33
33
  value: Any
34
-
34
+
35
35
  @property
36
36
  def shape_info(self) -> str:
37
37
  shape_info = ", ".join([f"{s}({o})" for s, o in zip(self.shape, self.axes)])
38
38
  return shape_info
39
-
39
+
40
40
  @property
41
41
  def spatial_shape(self) -> tuple[int, ...]:
42
42
  return tuple(self.sizeof(a) for a in "zyx" if a in self.axes)
43
-
43
+
44
44
  @property
45
45
  def axes(self) -> Axes:
46
46
  """Axes of the array."""
47
47
  return self._axes
48
-
48
+
49
49
  @axes.setter
50
50
  def axes(self, value: Iterable[AxisLike] | None):
51
51
  if value is None:
@@ -58,7 +58,7 @@ class AxesMixin:
58
58
  f"array (ndim={self.ndim}) and axes ({value})"
59
59
  )
60
60
  self._axes = axes
61
-
61
+
62
62
  @property
63
63
  def metadata(self) -> dict[str, Any]:
64
64
  raise NotImplementedError()
@@ -66,16 +66,16 @@ class AxesMixin:
66
66
  @property
67
67
  def scale(self) -> ScaleView:
68
68
  return self.axes.scale
69
-
69
+
70
70
  @scale.setter
71
71
  def scale(self, value: dict):
72
72
  if not isinstance(value, dict):
73
73
  raise TypeError(f"Cannot set scale using {type(value)}.")
74
74
  return self.set_scale(value)
75
-
75
+
76
76
  @property
77
77
  def scale_unit(self) -> str:
78
- units = set(a.unit for a in self.axes if str(a) in "zyx")
78
+ units = {a.unit for a in self.axes if str(a) in "zyx"}
79
79
  if len(units) == 0:
80
80
  return self.axes[-1].unit
81
81
  elif len(units) == 1:
@@ -83,21 +83,21 @@ class AxesMixin:
83
83
  else:
84
84
  warn(f"Inconsistent spatial unit: {units}.")
85
85
  return list(units)[-1]
86
-
86
+
87
87
  @scale_unit.setter
88
88
  def scale_unit(self, unit) -> None:
89
89
  unit = str(unit)
90
90
  for a in self.axes:
91
91
  if str(a) in ["z", "y", "x"]:
92
92
  a.unit = unit
93
-
93
+
94
94
  def _repr_dict_(self) -> dict[str, Any]:
95
95
  raise NotImplementedError()
96
-
96
+
97
97
  def __repr__(self) -> str:
98
98
  info = "\n".join(f"{k:^16}: {v}" for k, v in self._repr_dict_().items())
99
99
  return f"{self.__class__.__name__} of\n{info}\n"
100
-
100
+
101
101
  def _repr_html_(self) -> str:
102
102
  strs = []
103
103
  for k, v in self._repr_dict_().items():
@@ -128,23 +128,22 @@ class AxesMixin:
128
128
  return symbol
129
129
  else:
130
130
  return self.axes.find(symbol)
131
-
132
-
131
+
132
+
133
133
  def sizeof(self, axis: AxisLike) -> int:
134
134
  return getattr(self.shape, str(axis))
135
-
135
+
136
136
  def sizesof(self, axes: str) -> tuple[int, ...]:
137
137
  return tuple(self.sizeof(a) for a in axes)
138
-
138
+
139
139
  def set_axes(self, axes) -> Self:
140
- """
141
- Set axes of the array and return the updated one.
140
+ """Set axes of the array and return the updated one.
142
141
 
143
142
  Parameters
144
143
  ----------
145
144
  axes : AxesLike
146
145
  New axes.
147
-
146
+
148
147
  Returns
149
148
  -------
150
149
  Self
@@ -164,10 +163,10 @@ class AxesMixin:
164
163
  scales of common axes are copied.
165
164
  unit : str, optional
166
165
  Scale unit if needs update.
167
- kwargs :
166
+ kwargs :
168
167
  This enables function call like set_scale(x=0.1, y=0.1).
169
- """
170
-
168
+ """
169
+
171
170
  if isinstance(other, MutableMapping):
172
171
  # voxel-scale can be set with one keyword.
173
172
  if "zyx" in other:
@@ -186,21 +185,23 @@ class AxesMixin:
186
185
  # check if all the keys are contained in axes.
187
186
  for a, val in other.items():
188
187
  if a not in self.axes:
189
- raise ImageAxesError(f"Image does not have axis {a}.")
188
+ raise ImageAxesError(f"Image does not have axis {a}.")
190
189
  elif not np.isscalar(val):
191
190
  raise TypeError(f"Cannot set non-numeric value as scales.")
192
191
 
193
192
  for k, v in other.items():
194
193
  self.axes[k].scale = v
195
-
194
+
196
195
  elif kwargs:
197
196
  self.set_scale(dict(kwargs))
198
-
197
+
199
198
  elif hasattr(other, "scale"):
200
199
  self.set_scale(
201
200
  {a: s for a, s in other.scale.items() if a in self.axes} # type: ignore
202
201
  )
203
-
202
+ if unit is None and hasattr(other, "scale_unit"):
203
+ unit = other.scale_unit
204
+
204
205
  elif isinstance(other, (list, tuple)):
205
206
  for a, s in zip(self.axes, other):
206
207
  a.scale = s
@@ -208,27 +209,26 @@ class AxesMixin:
208
209
  raise TypeError(
209
210
  f"'other' must be str or axes supported object, but got {type(other)}"
210
211
  )
211
-
212
+
212
213
  if unit is not None:
213
214
  self.scale_unit = unit
214
-
215
+
215
216
  return self
216
-
217
+
217
218
  def set_axis_label(
218
219
  self,
219
220
  _dict: MutableMapping[str, Iterable[Any]] = None,
220
221
  /,
221
222
  **kwargs
222
223
  ) -> Self:
223
- """
224
- Set labels to an axis (axes).
224
+ """Set labels to an axis (axes).
225
225
 
226
226
  Parameters
227
227
  ----------
228
228
  _dict : mapping, optional
229
229
  Mapping from axis to its labels. Labels must have the same length as its
230
230
  dimensionality.
231
-
231
+
232
232
  Examples
233
233
  --------
234
234
  >>> img.set_axis_label(c=["Blue", "Green", "Red"])
@@ -242,31 +242,32 @@ class AxesMixin:
242
242
  for k, v in _dict.items():
243
243
  self.axes[self.axes.find(k)].labels = v
244
244
  return self
245
-
245
+
246
246
  @overload
247
247
  def iter(
248
248
  self,
249
249
  axes: AxesLike,
250
- israw: Literal[False] = False,
250
+ israw: Literal[False] = False,
251
251
  exclude: Dims = "",
252
252
  ) -> Iterator[tuple[Slices, np.ndarray]]:
253
253
  ...
254
-
254
+
255
255
  @overload
256
256
  def iter(
257
257
  self,
258
258
  axes: AxesLike,
259
- israw: Literal[True] = False,
259
+ israw: Literal[True] = False,
260
260
  exclude: Dims = "",
261
261
  ) -> Iterator[tuple[Slices, Self]]:
262
262
  ...
263
-
263
+
264
264
  def iter(self, axes, israw = False, exclude = ""):
265
- """
266
- Iteration along axes. If axes="tzc", then equivalent to following pseudo code:
267
-
265
+ """Iteration along axes.
266
+
267
+ If axes="tzc", then equivalent to following pseudo code:
268
+
268
269
  .. code-block::
269
-
270
+
270
271
  for t in all_t:
271
272
  for z in all_z:
272
273
  for c in all_c:
@@ -279,19 +280,19 @@ class AxesMixin:
279
280
  israw : bool, default is False
280
281
  If True, MetaArray will be returned. If False, np.ndarray will be returned.
281
282
  exclude : str, optional
282
- Which axes will be excluded in output. For example, self.axes="tcyx" and
283
- exclude="c" then the axes of output will be "tyx" and slice is also correctly
283
+ Which axes will be excluded in output. For example, self.axes="tcyx" and
284
+ exclude="c" then the axes of output will be "tyx" and slice is also correctly
284
285
  arranged.
285
-
286
+
286
287
  Yields
287
288
  -------
288
289
  slice and (np.ndarray or AxesMixin)
289
290
  slice and a subimage=self[sl]
290
- """
291
+ """
291
292
  iterlist = switch_slice(
292
- axes=axes,
293
+ axes=axes,
293
294
  all_axes=self.axes,
294
- ifin=[range(s) for s in self.shape],
295
+ ifin=[range(s) for s in self.shape],
295
296
  ifnot=[(slice(None),)] * self.ndim
296
297
  )
297
298
  exclude = list(exclude)
@@ -302,11 +303,11 @@ class AxesMixin:
302
303
  if len(exclude) == 0:
303
304
  outsl = sl
304
305
  else:
305
- outsl = tuple(s for i, s in enumerate(sl)
306
+ outsl = tuple(s for i, s in enumerate(sl)
306
307
  if self.axes[i] not in exclude)
307
308
  yield outsl, selfview[sl]
308
309
  c += 1
309
-
310
+
310
311
  # if iterlist = []
311
312
  if c == 0:
312
313
  outsl = (slice(None),) * (self.ndim - len(exclude))
@@ -342,4 +343,4 @@ def _getitem(self: NamedTuple, key: slice) -> tuple[int, ...]:
342
343
  def _getitem(self: NamedTuple, key, /):
343
344
  if isinstance(key, (str, Axis)):
344
345
  return self._asdict()[key]
345
- return tuple.__getitem__(self, key)
346
+ return tuple.__getitem__(self, key)
@@ -170,8 +170,7 @@ class MetaArray(AxesMixin, np.ndarray):
170
170
  return self[tuple(slices)]
171
171
 
172
172
  def isel(self, indexer=None, /, **kwargs: dict[str, Any]) -> Self:
173
- """
174
- A index based indexing method, mimicking ``xarray.isel``.
173
+ """A index based indexing method, mimicking ``xarray.isel``.
175
174
 
176
175
  Example
177
176
  -------
@@ -259,9 +258,7 @@ class MetaArray(AxesMixin, np.ndarray):
259
258
 
260
259
  @classmethod
261
260
  def implements(cls, numpy_function):
262
- """
263
- Add functions to NP_DISPATCH so that numpy functions can be overloaded.
264
- """
261
+ """Add functions to NP_DISPATCH so that numpy functions can be overloaded."""
265
262
  def decorator(func):
266
263
  cls.NP_DISPATCH[numpy_function] = func
267
264
  func.__name__ = numpy_function.__name__
@@ -340,7 +340,7 @@ class ImgArray(LabeledArray):
340
340
  def binning(
341
341
  self,
342
342
  binsize: int = 2,
343
- method = "mean",
343
+ method: str | Callable[[np.ndarray], Any] = "mean",
344
344
  *,
345
345
  check_edges: bool = True,
346
346
  dims: Dims = None
@@ -390,6 +390,54 @@ class ImgArray(LabeledArray):
390
390
  )
391
391
  return out
392
392
 
393
+ @_docs.write_docs
394
+ @dims_to_spatial_axes
395
+ @same_dtype
396
+ def fourier_crop(
397
+ self,
398
+ binsize: float = 2,
399
+ dims: Dims = None,
400
+ ) -> ImgArray:
401
+ """Crop image in Fourier space.
402
+
403
+ Parameters
404
+ ----------
405
+ binsize : float, default is 2
406
+ Crop factor. If 2, the output image will have half the shape of the input
407
+ image.
408
+ {dims}
409
+
410
+ Returns
411
+ -------
412
+ ImgArray
413
+ Cropped image.
414
+ """
415
+ if binsize == 1:
416
+ return self
417
+ if binsize < 1:
418
+ raise ValueError("`binsize` must be 1 or larger.")
419
+ new_shape = list(self.shape)
420
+ new_scale = dict(self.scale)
421
+ for i, a in enumerate(self.axes):
422
+ if a in dims:
423
+ new_shape[i] = int(self.shape[i] / binsize)
424
+ new_scale[a] = self.scale[a] * binsize
425
+ spatial_axes = [self.axisof(a) for a in dims]
426
+ input = xp.asarray(self.value)
427
+
428
+ out = xp.fft.irfftn(
429
+ _misc.fft_crop(xp.fft.rfftn(input, axes=spatial_axes), new_shape),
430
+ s=new_shape,
431
+ axes=spatial_axes,
432
+ )
433
+ out = xp.asnumpy(out)
434
+
435
+ out_img = out.view(self.__class__)
436
+ out_img._set_info(self)
437
+ out_img.axes = self.axes.copy() # _set_info does not pass copy so new axes must be defined here.
438
+ out_img.set_scale(new_scale)
439
+ return out_img
440
+
393
441
  @_docs.write_docs
394
442
  @dims_to_spatial_axes
395
443
  def radial_profile(
@@ -4063,8 +4111,6 @@ class ImgArray(LabeledArray):
4063
4111
 
4064
4112
  if along is None:
4065
4113
  along = find_first_appeared("tpzcia", include=self.axes)
4066
- elif len(along) != 1:
4067
- raise ValueError("`along` must be single character.")
4068
4114
  if not isinstance(upsample_factor, int):
4069
4115
  raise TypeError(f"upsample-factor must be integer but got {type(upsample_factor)}")
4070
4116
  if max_shift is not None:
@@ -4074,7 +4120,7 @@ class ImgArray(LabeledArray):
4074
4120
  c_axes = complement_axes(along, self.axes)
4075
4121
  last_img = None
4076
4122
  img_fft = self.fft(shift=False, dims=c_axes)
4077
- for i, (_, img) in enumerate(img_fft.iter(along)):
4123
+ for i, (_, img) in enumerate(img_fft.iter([along])):
4078
4124
  img = xp.asarray(img)
4079
4125
  if last_img is not None:
4080
4126
  result[i] = xp.asnumpy(
@@ -4150,8 +4196,6 @@ class ImgArray(LabeledArray):
4150
4196
 
4151
4197
  if along is None:
4152
4198
  along = find_first_appeared("tpzcia", include=self.axes, exclude=dims)
4153
- elif len(along) != 1:
4154
- raise ValueError("`along` must be single character.")
4155
4199
 
4156
4200
  if shift is None:
4157
4201
  # determine 'ref'
@@ -273,8 +273,7 @@ class LabeledArray(MetaArray):
273
273
  dtype: DTypeLike = None,
274
274
  overwrite: bool = True,
275
275
  ) -> None:
276
- """
277
- Save image at the same directory as the original image by default.
276
+ """Save image at the same directory as the original image by default.
278
277
 
279
278
  For tif file format, if the image contains wrong axes for ImageJ (= except for tzcyx),
280
279
  then it will converted automatically if possible. For mrc file format, only zyx and yx is
@@ -290,7 +289,7 @@ class LabeledArray(MetaArray):
290
289
  Whether to overwrite the file if it already exists.
291
290
 
292
291
  """
293
- save_path = Path(save_path)
292
+ save_path = Path(save_path).expanduser()
294
293
  if self.ndim < 2:
295
294
  raise ValueError("Cannot save <2D array as an image.")
296
295
 
@@ -310,7 +309,7 @@ class LabeledArray(MetaArray):
310
309
  " >>> img.source = \"...\"\n"
311
310
  "or specify absolute path like\n"
312
311
  " >>> img.imsave(\"/path/to/XXX.tif\")"
313
- )
312
+ )
314
313
  save_path = self.source.parent / save_path
315
314
 
316
315
  if not overwrite and save_path.exists():
@@ -339,7 +339,7 @@ class LazyImgArray(AxesMixin):
339
339
 
340
340
  @_docs.copy_docs(LabeledArray.imsave)
341
341
  def imsave(self, save_path: str | Path, *, dtype = None, overwrite: bool = True):
342
- save_path = Path(save_path)
342
+ save_path = Path(save_path).expanduser()
343
343
  if self.ndim < 2:
344
344
  raise ValueError("Cannot save <2D array as an image.")
345
345
 
@@ -776,12 +776,13 @@ class LazyImgArray(AxesMixin):
776
776
  sigma: nDFloat = 1.0,
777
777
  *,
778
778
  dims: Dims = None,
779
+ fourier: bool = False,
779
780
  update: bool = False
780
781
  ) -> LazyImgArray:
781
782
  c_axes = complement_axes(dims, self.axes)
782
783
  depth = _ceilint(sigma*4)
783
784
  return self._apply_map_overlap(
784
- xp.ndi.gaussian_filter,
785
+ _filters.gaussian_filter_fourier if fourier else _filters.gaussian_filter,
785
786
  c_axes=c_axes,
786
787
  depth=depth,
787
788
  kwargs=dict(sigma=sigma),
@@ -795,13 +796,14 @@ class LazyImgArray(AxesMixin):
795
796
  low_sigma: nDFloat = 1.0,
796
797
  high_sigma: nDFloat = 2.0,
797
798
  *,
799
+ fourier: bool = False,
798
800
  dims: Dims = None,
799
801
  update: bool = False
800
802
  ) -> LazyImgArray:
801
803
  c_axes = complement_axes(dims, self.axes)
802
804
  depth = _ceilint(high_sigma*4)
803
805
  return self._apply_map_overlap(
804
- _filters.dog_filter,
806
+ _filters.dog_filter_fourier if fourier else _filters.dog_filter,
805
807
  c_axes=c_axes,
806
808
  depth=depth,
807
809
  kwargs=dict(low_sigma=low_sigma, high_sigma=high_sigma),
@@ -1317,8 +1319,6 @@ class LazyImgArray(AxesMixin):
1317
1319
  def track_drift(self, along: str = None, upsample_factor: int = 10) -> DaskArray:
1318
1320
  if along is None:
1319
1321
  along = find_first_appeared("tpzc<i", include=self.axes)
1320
- elif len(along) != 1:
1321
- raise ValueError("`along` must be single character.")
1322
1322
 
1323
1323
  dims = complement_axes(along, self.axes)
1324
1324
  chunks = switch_slice(dims, self.axes, ifin=self.shape, ifnot=1)
@@ -1370,8 +1370,6 @@ class LazyImgArray(AxesMixin):
1370
1370
  ) -> LazyImgArray:
1371
1371
  if along is None:
1372
1372
  along = find_first_appeared("tpzcia", include=self.axes, exclude=dims)
1373
- elif len(along) != 1:
1374
- raise ValueError("`along` must be single character.")
1375
1373
 
1376
1374
  from impy.frame import MarkerFrame
1377
1375
 
@@ -1387,7 +1385,7 @@ class LazyImgArray(AxesMixin):
1387
1385
  dims = _dims
1388
1386
  elif not isinstance(ref, self.__class__):
1389
1387
  ref = self[ref]
1390
- if ref.axes != along + dims:
1388
+ if ref.axes != [along] + dims:
1391
1389
  raise ValueError(f"Arguments `along`({along}) + `dims`({dims}) do not match "
1392
1390
  f"axes of `ref`({ref.axes})")
1393
1391
 
@@ -501,9 +501,8 @@ def imread(
501
501
  name: str | None = None,
502
502
  squeeze: bool = False,
503
503
  ) -> ImgArray:
504
- """
505
- Load image(s) from a path. You can read list of images from directories with
506
- wildcards or ``"$"`` in ``path``.
504
+ """Load image(s) from a path.
505
+ You can read list of images from directories with wildcards or ``"$"`` in ``path``.
507
506
 
508
507
  Parameters
509
508
  ----------
@@ -536,7 +535,7 @@ def imread(
536
535
  Wall time: 3.01 ms
537
536
 
538
537
  """
539
- path = str(path)
538
+ path = str(Path(path).expanduser())
540
539
  is_memmap = (key is not None)
541
540
 
542
541
  if "$" in path:
@@ -593,11 +592,10 @@ def imread(
593
592
  if k in "zyx":
594
593
  self.axes[k].unit = scale_unit[k]
595
594
 
596
- return self.sort_axes().as_img_type(dtype) # arrange in tzcyx-order
595
+ return self.as_img_type(dtype) # arrange in tzcyx-order
597
596
 
598
597
  def _imread_glob(path: str, squeeze: bool = False, **kwargs) -> ImgArray:
599
- """
600
- Read images recursively from a directory, and stack them into one ImgArray.
598
+ """Read images recursively from a directory, and stack them into one ImgArray.
601
599
 
602
600
  Parameters
603
601
  ----------
@@ -7,12 +7,10 @@ import numpy as np
7
7
  from impy.io._registry import IO
8
8
  from impy.io._utils import rechunk_to_ones, MemmapArrayWriter, ImpyArray, ImageData, ImageMetadata
9
9
 
10
- from impy.axes import ImageAxesError
11
-
12
10
  if TYPE_CHECKING:
13
11
  from mrcfile.mrcobject import MrcObject
14
12
 
15
- @IO.mark_reader(".mrc", ".rec", ".st", ".map", ".mrc.gz", ".map.gz")
13
+ @IO.mark_reader(".mrc", ".rec", ".st", ".map", ".mrc.gz", ".map.gz", ".mrcs")
16
14
  def _(path: str, memmap: bool = False) -> ImageData:
17
15
  """The MRC format reader"""
18
16
  import mrcfile
@@ -29,7 +27,7 @@ def _(path: str, memmap: bool = False) -> ImageData:
29
27
  return ImageData.from_metadata(image, meta)
30
28
 
31
29
 
32
- @IO.mark_header_reader(".mrc", ".rec", ".st", ".map", ".mrc.gz", ".map.gz")
30
+ @IO.mark_header_reader(".mrc", ".rec", ".st", ".map", ".mrc.gz", ".map.gz", ".mrcs")
33
31
  def _(path: str) -> ImageData:
34
32
  """The MRC header reader"""
35
33
  import mrcfile
@@ -39,12 +37,14 @@ def _(path: str) -> ImageData:
39
37
 
40
38
  return meta
41
39
 
42
- @IO.mark_writer(".mrc", ".rec", ".st", ".map")
40
+ @IO.mark_writer(".mrc", ".rec", ".st", ".map", ".mrcs")
43
41
  def _(path: str, img: ImpyArray, lazy: bool = False):
44
42
  """The MRC writer."""
45
43
 
46
44
  import mrcfile
47
45
 
46
+ input_axes = [str(a) for a in img.axes]
47
+
48
48
  if img.scale_unit == "nm":
49
49
  voxel_size = tuple(np.array(img.scale)[::-1] * 10)
50
50
  elif img.scale_unit in ("ang", "Å", "angstrom"):
@@ -56,6 +56,8 @@ def _(path: str, img: ImpyArray, lazy: bool = False):
56
56
  voxel_size = (1.0, 1.0, 1.0)
57
57
  if len(voxel_size) == 2:
58
58
  voxel_size = (1.0,) + voxel_size
59
+ elif len(voxel_size) > 3:
60
+ voxel_size = voxel_size[:3]
59
61
 
60
62
  if img.dtype == "bool":
61
63
  img = img.astype(np.int8)
@@ -79,20 +81,16 @@ def _(path: str, img: ImpyArray, lazy: bool = False):
79
81
  return None
80
82
 
81
83
  # get voxel_size
82
- if img.axes not in ("zyx", "yx"):
83
- raise ImageAxesError(
84
- f"Can only save zyx- or yx- image as a mrc file, but image has {img.axes} "
85
- "axes."
86
- )
87
84
  if Path(path).exists():
88
- with mrcfile.open(path, mode="r+") as mrc:
89
- mrc.set_data(img.value)
90
- mrc.voxel_size = voxel_size
91
-
85
+ ctx_mgr = mrcfile.open(path, mode="r+")
92
86
  else:
93
- with mrcfile.new(path) as mrc:
94
- mrc.set_data(img.value)
95
- mrc.voxel_size = voxel_size
87
+ ctx_mgr = mrcfile.new(path)
88
+ with ctx_mgr as mrc:
89
+ mrc.set_data(img.value)
90
+ mrc.voxel_size = voxel_size
91
+ if input_axes == ["t", "y", "x"]:
92
+ mrc.set_image_stack()
93
+
96
94
  return None
97
95
 
98
96
  _MRC_MODE = {
@@ -115,7 +113,11 @@ def _parse_mrcfile(mrc: MrcObject) -> ImageMetadata:
115
113
  metadata = {}
116
114
  scale = dict.fromkeys(axes, 1.0)
117
115
  for a in axes:
118
- scale[a] = mrc.voxel_size[a] / 10
116
+ if a in "zyx":
117
+ this_scale = mrc.voxel_size[a] / 10
118
+ if this_scale == 0:
119
+ this_scale = 1.0
120
+ scale[a] = this_scale
119
121
  try:
120
122
  orig_x, orig_y, orig_z = mrc.header["origin"].item()
121
123
  except Exception:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ from pathlib import Path
4
5
  import re
5
6
  import glob
6
7
  from typing import TYPE_CHECKING, Any, Callable, Sequence
@@ -244,7 +245,7 @@ def imread(
244
245
  -------
245
246
  LazyImgArray
246
247
  """
247
- path = str(path)
248
+ path = str(Path(path).expanduser())
248
249
  if "*" in path:
249
250
  return _imread_glob(path, chunks=chunks, squeeze=squeeze)
250
251
  if not os.path.exists(path):
@@ -277,7 +278,7 @@ def imread(
277
278
  if k in "zyx":
278
279
  self.axes[k].unit = spatial_scale_unit.get(k)
279
280
 
280
- return self.sort_axes()
281
+ return self
281
282
 
282
283
 
283
284
  def _imread_glob(path: str, squeeze: bool = False, **kwargs) -> LazyImgArray:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes