impy-array 2.4.10__tar.gz → 2.5.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.
Files changed (72) hide show
  1. {impy_array-2.4.10 → impy_array-2.5.0}/PKG-INFO +1 -1
  2. {impy_array-2.4.10 → impy_array-2.5.0}/impy/__init__.py +1 -1
  3. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/_utils/_misc.py +14 -0
  4. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/axesmixin.py +52 -51
  5. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/bases/metaarray.py +2 -5
  6. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/imgarray.py +49 -1
  7. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/labeledarray.py +3 -4
  8. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/lazy.py +1 -1
  9. {impy_array-2.4.10 → impy_array-2.5.0}/impy/core.py +5 -7
  10. {impy_array-2.4.10 → impy_array-2.5.0}/impy/io/_mrc.py +20 -18
  11. {impy_array-2.4.10 → impy_array-2.5.0}/impy/lazy/core.py +2 -1
  12. {impy_array-2.4.10 → impy_array-2.5.0}/.gitignore +0 -0
  13. {impy_array-2.4.10 → impy_array-2.5.0}/LICENSE +0 -0
  14. {impy_array-2.4.10 → impy_array-2.5.0}/README.md +0 -0
  15. {impy_array-2.4.10 → impy_array-2.5.0}/impy/__main__.py +0 -0
  16. {impy_array-2.4.10 → impy_array-2.5.0}/impy/_const.py +0 -0
  17. {impy_array-2.4.10 → impy_array-2.5.0}/impy/_types.py +0 -0
  18. {impy_array-2.4.10 → impy_array-2.5.0}/impy/array_api.py +0 -0
  19. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/__init__.py +0 -0
  20. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/_imshow.py +0 -0
  21. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/_utils/__init__.py +0 -0
  22. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/_utils/_corr.py +0 -0
  23. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/_utils/_deconv.py +0 -0
  24. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/_utils/_docs.py +0 -0
  25. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/_utils/_filters.py +0 -0
  26. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/_utils/_glcm.py +0 -0
  27. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/_utils/_linalg.py +0 -0
  28. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/_utils/_plot.py +0 -0
  29. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/_utils/_process_numba.py +0 -0
  30. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/_utils/_skimage.py +0 -0
  31. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/_utils/_structures.py +0 -0
  32. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/_utils/_transform.py +0 -0
  33. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/bases/__init__.py +0 -0
  34. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/bases/overload.py +0 -0
  35. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/big.py +0 -0
  36. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/label.py +0 -0
  37. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/phasearray.py +0 -0
  38. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/specials.py +0 -0
  39. {impy_array-2.4.10 → impy_array-2.5.0}/impy/arrays/tiled.py +0 -0
  40. {impy_array-2.4.10 → impy_array-2.5.0}/impy/axes/__init__.py +0 -0
  41. {impy_array-2.4.10 → impy_array-2.5.0}/impy/axes/_axes.py +0 -0
  42. {impy_array-2.4.10 → impy_array-2.5.0}/impy/axes/_axes_tuple.py +0 -0
  43. {impy_array-2.4.10 → impy_array-2.5.0}/impy/axes/_axis.py +0 -0
  44. {impy_array-2.4.10 → impy_array-2.5.0}/impy/axes/_slicer.py +0 -0
  45. {impy_array-2.4.10 → impy_array-2.5.0}/impy/binder.py +0 -0
  46. {impy_array-2.4.10 → impy_array-2.5.0}/impy/collections.py +0 -0
  47. {impy_array-2.4.10 → impy_array-2.5.0}/impy/correlation.py +0 -0
  48. {impy_array-2.4.10 → impy_array-2.5.0}/impy/frame/__init__.py +0 -0
  49. {impy_array-2.4.10 → impy_array-2.5.0}/impy/frame/frames.py +0 -0
  50. {impy_array-2.4.10 → impy_array-2.5.0}/impy/io/__init__.py +0 -0
  51. {impy_array-2.4.10 → impy_array-2.5.0}/impy/io/_czi.py +0 -0
  52. {impy_array-2.4.10 → impy_array-2.5.0}/impy/io/_nd2.py +0 -0
  53. {impy_array-2.4.10 → impy_array-2.5.0}/impy/io/_npy.py +0 -0
  54. {impy_array-2.4.10 → impy_array-2.5.0}/impy/io/_registry.py +0 -0
  55. {impy_array-2.4.10 → impy_array-2.5.0}/impy/io/_tiff.py +0 -0
  56. {impy_array-2.4.10 → impy_array-2.5.0}/impy/io/_utils.py +0 -0
  57. {impy_array-2.4.10 → impy_array-2.5.0}/impy/io/_zarr.py +0 -0
  58. {impy_array-2.4.10 → impy_array-2.5.0}/impy/lazy/__init__.py +0 -0
  59. {impy_array-2.4.10 → impy_array-2.5.0}/impy/lazy/random.py +0 -0
  60. {impy_array-2.4.10 → impy_array-2.5.0}/impy/random.py +0 -0
  61. {impy_array-2.4.10 → impy_array-2.5.0}/impy/roi.py +0 -0
  62. {impy_array-2.4.10 → impy_array-2.5.0}/impy/utils/__init__.py +0 -0
  63. {impy_array-2.4.10 → impy_array-2.5.0}/impy/utils/axesop.py +0 -0
  64. {impy_array-2.4.10 → impy_array-2.5.0}/impy/utils/deco.py +0 -0
  65. {impy_array-2.4.10 → impy_array-2.5.0}/impy/utils/gauss.py +0 -0
  66. {impy_array-2.4.10 → impy_array-2.5.0}/impy/utils/misc.py +0 -0
  67. {impy_array-2.4.10 → impy_array-2.5.0}/impy/utils/slicer.py +0 -0
  68. {impy_array-2.4.10 → impy_array-2.5.0}/impy/utils/utilcls.py +0 -0
  69. {impy_array-2.4.10 → impy_array-2.5.0}/impy/viewer/__init__.py +0 -0
  70. {impy_array-2.4.10 → impy_array-2.5.0}/impy/viewer/utils.py +0 -0
  71. {impy_array-2.4.10 → impy_array-2.5.0}/impy/viewer/viewer.py +0 -0
  72. {impy_array-2.4.10 → impy_array-2.5.0}/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.0
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.0"
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(
@@ -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
 
@@ -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):
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