swcgeom 0.16.0__py3-none-any.whl → 0.17.1__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.

Potentially problematic release.


This version of swcgeom might be problematic. Click here for more details.

Files changed (50) hide show
  1. swcgeom/_version.py +2 -2
  2. swcgeom/analysis/__init__.py +1 -3
  3. swcgeom/analysis/feature_extractor.py +16 -15
  4. swcgeom/analysis/{node_features.py → features.py} +105 -3
  5. swcgeom/analysis/lmeasure.py +5 -5
  6. swcgeom/analysis/sholl.py +4 -4
  7. swcgeom/analysis/trunk.py +12 -11
  8. swcgeom/analysis/visualization.py +9 -9
  9. swcgeom/analysis/visualization3d.py +85 -0
  10. swcgeom/analysis/volume.py +4 -4
  11. swcgeom/core/branch.py +4 -3
  12. swcgeom/core/branch_tree.py +3 -4
  13. swcgeom/core/compartment.py +3 -2
  14. swcgeom/core/node.py +2 -2
  15. swcgeom/core/path.py +3 -2
  16. swcgeom/core/population.py +16 -27
  17. swcgeom/core/swc.py +11 -10
  18. swcgeom/core/swc_utils/base.py +8 -17
  19. swcgeom/core/swc_utils/io.py +7 -6
  20. swcgeom/core/swc_utils/normalizer.py +4 -3
  21. swcgeom/core/swc_utils/subtree.py +2 -2
  22. swcgeom/core/tree.py +22 -34
  23. swcgeom/core/tree_utils.py +11 -10
  24. swcgeom/core/tree_utils_impl.py +3 -3
  25. swcgeom/images/augmentation.py +3 -3
  26. swcgeom/images/folder.py +10 -16
  27. swcgeom/images/io.py +76 -111
  28. swcgeom/transforms/image_stack.py +6 -5
  29. swcgeom/transforms/images.py +105 -5
  30. swcgeom/transforms/neurolucida_asc.py +4 -6
  31. swcgeom/transforms/population.py +1 -3
  32. swcgeom/transforms/tree.py +8 -7
  33. swcgeom/transforms/tree_assembler.py +4 -3
  34. swcgeom/utils/ellipse.py +3 -4
  35. swcgeom/utils/neuromorpho.py +17 -16
  36. swcgeom/utils/plotter_2d.py +12 -6
  37. swcgeom/utils/plotter_3d.py +31 -0
  38. swcgeom/utils/renderer.py +6 -6
  39. swcgeom/utils/sdf.py +2 -2
  40. swcgeom/utils/solid_geometry.py +1 -3
  41. swcgeom/utils/transforms.py +1 -3
  42. swcgeom/utils/volumetric_object.py +8 -10
  43. {swcgeom-0.16.0.dist-info → swcgeom-0.17.1.dist-info}/METADATA +1 -1
  44. swcgeom-0.17.1.dist-info/RECORD +67 -0
  45. swcgeom/analysis/branch_features.py +0 -67
  46. swcgeom/analysis/path_features.py +0 -37
  47. swcgeom-0.16.0.dist-info/RECORD +0 -67
  48. {swcgeom-0.16.0.dist-info → swcgeom-0.17.1.dist-info}/LICENSE +0 -0
  49. {swcgeom-0.16.0.dist-info → swcgeom-0.17.1.dist-info}/WHEEL +0 -0
  50. {swcgeom-0.16.0.dist-info → swcgeom-0.17.1.dist-info}/top_level.txt +0 -0
@@ -5,7 +5,7 @@ Notes
5
5
  Do not import `Tree` and keep this file minimized.
6
6
  """
7
7
 
8
- from typing import Any, Dict, List, Optional, Tuple
8
+ from typing import Any, Optional
9
9
 
10
10
  import numpy as np
11
11
  import numpy.typing as npt
@@ -15,8 +15,8 @@ from swcgeom.core.swc_utils import Topology, to_sub_topology, traverse
15
15
 
16
16
  __all__ = ["get_subtree_impl", "to_subtree_impl"]
17
17
 
18
- Mapping = Dict[int, int] | List[int]
19
- TreeArgs = Tuple[int, Dict[str, npt.NDArray[Any]], str, SWCNames]
18
+ Mapping = dict[int, int] | list[int]
19
+ TreeArgs = tuple[int, dict[str, npt.NDArray[Any]], str, SWCNames]
20
20
 
21
21
 
22
22
  def get_subtree_impl(
@@ -6,7 +6,7 @@ This is expremental code, and the API is subject to change.
6
6
  """
7
7
 
8
8
  import random
9
- from typing import List, Literal, Optional
9
+ from typing import Literal, Optional
10
10
 
11
11
  import numpy as np
12
12
  import numpy.typing as npt
@@ -54,7 +54,7 @@ class Augmentation:
54
54
 
55
55
  def swapaxes(self, x, mode: Optional[Literal["xy", "xz", "yz"]] = None) -> NDArrf32:
56
56
  if mode is None:
57
- modes: List[Literal["xy", "xz", "yz"]] = ["xy", "xz", "yz"]
57
+ modes: list[Literal["xy", "xz", "yz"]] = ["xy", "xz", "yz"]
58
58
  mode = modes[self.rand.randint(0, 2)]
59
59
 
60
60
  match mode:
@@ -69,7 +69,7 @@ class Augmentation:
69
69
 
70
70
  def flip(self, x, mode: Optional[Literal["xy", "xz", "yz"]] = None) -> NDArrf32:
71
71
  if mode is None:
72
- modes: List[Literal["xy", "xz", "yz"]] = ["xy", "xz", "yz"]
72
+ modes: list[Literal["xy", "xz", "yz"]] = ["xy", "xz", "yz"]
73
73
  mode = modes[random.randint(0, 2)]
74
74
 
75
75
  match mode:
swcgeom/images/folder.py CHANGED
@@ -4,18 +4,9 @@ import math
4
4
  import os
5
5
  import re
6
6
  import warnings
7
+ from collections.abc import Callable, Iterable
7
8
  from dataclasses import dataclass
8
- from typing import (
9
- Callable,
10
- Generic,
11
- Iterable,
12
- List,
13
- Literal,
14
- Optional,
15
- Tuple,
16
- TypeVar,
17
- overload,
18
- )
9
+ from typing import Generic, Literal, Optional, TypeVar, overload
19
10
 
20
11
  import numpy as np
21
12
  import numpy.typing as npt
@@ -33,7 +24,7 @@ T = TypeVar("T")
33
24
  class ImageStackFolderBase(Generic[ScalarType, T]):
34
25
  """Image stack folder base."""
35
26
 
36
- files: List[str]
27
+ files: list[str]
37
28
  transform: Transform[npt.NDArray[ScalarType], T]
38
29
 
39
30
  # fmt: off
@@ -61,7 +52,10 @@ class ImageStackFolderBase(Generic[ScalarType, T]):
61
52
  return read_imgs(fname, dtype=self.dtype).get_full() # type: ignore
62
53
 
63
54
  @staticmethod
64
- def scan(root: str, *, pattern: Optional[str] = None) -> List[str]:
55
+ def scan(root: str, *, pattern: Optional[str] = None) -> list[str]:
56
+ if not os.path.isdir(root):
57
+ raise NotADirectoryError(f"not a directory: {root}")
58
+
65
59
  is_valid = re.compile(pattern).match if pattern is not None else truthly
66
60
 
67
61
  fs = []
@@ -168,13 +162,13 @@ class ImageStackFolder(ImageStackFolderBase[ScalarType, T]):
168
162
  class LabeledImageStackFolder(ImageStackFolderBase[ScalarType, T]):
169
163
  """Image stack folder with label."""
170
164
 
171
- labels: List[int]
165
+ labels: list[int]
172
166
 
173
167
  def __init__(self, files: Iterable[str], labels: Iterable[int], **kwargs):
174
168
  super().__init__(files, **kwargs)
175
169
  self.labels = list(labels)
176
170
 
177
- def __getitem__(self, idx: int) -> Tuple[T, int]:
171
+ def __getitem__(self, idx: int) -> tuple[T, int]:
178
172
  return self._get(self.files[idx]), self.labels[idx]
179
173
 
180
174
  @classmethod
@@ -205,7 +199,7 @@ class PathImageStackFolder(ImageStackFolderBase[ScalarType, T]):
205
199
  super().__init__(files, **kwargs)
206
200
  self.root = root
207
201
 
208
- def __getitem__(self, idx: int) -> Tuple[T, str]:
202
+ def __getitem__(self, idx: int) -> tuple[T, str]:
209
203
  relpath = os.path.relpath(self.files[idx], self.root)
210
204
  return self._get(self.files[idx]), relpath
211
205
 
swcgeom/images/io.py CHANGED
@@ -4,20 +4,9 @@ import os
4
4
  import re
5
5
  import warnings
6
6
  from abc import ABC, abstractmethod
7
+ from collections.abc import Callable, Iterable
7
8
  from functools import cache, lru_cache
8
- from typing import (
9
- Any,
10
- Callable,
11
- Generic,
12
- Iterable,
13
- List,
14
- Literal,
15
- Optional,
16
- Tuple,
17
- TypeVar,
18
- cast,
19
- overload,
20
- )
9
+ from typing import Any, Generic, Literal, Optional, TypeVar, cast, overload
21
10
 
22
11
  import nrrd
23
12
  import numpy as np
@@ -27,7 +16,7 @@ from v3dpy.loaders import PBD, Raw
27
16
 
28
17
  __all__ = ["read_imgs", "save_tiff", "read_images"]
29
18
 
30
- Vec3i = Tuple[int, int, int]
19
+ Vec3i = tuple[int, int, int]
31
20
  ScalarType = TypeVar("ScalarType", bound=np.generic, covariant=True)
32
21
 
33
22
  RE_TERAFLY_ROOT = re.compile(r"^RES\((\d+)x(\d+)x(\d+)\)$")
@@ -58,17 +47,17 @@ class ImageStack(ABC, Generic[ScalarType]):
58
47
  def __getitem__(self, key: int) -> npt.NDArray[ScalarType]: ... # array of shape (Y, Z, C)
59
48
  @overload
60
49
  @abstractmethod
61
- def __getitem__(self, key: Tuple[int, int]) -> npt.NDArray[ScalarType]: ... # array of shape (Z, C)
50
+ def __getitem__(self, key: tuple[int, int]) -> npt.NDArray[ScalarType]: ... # array of shape (Z, C)
62
51
  @overload
63
52
  @abstractmethod
64
- def __getitem__(self, key: Tuple[int, int, int]) -> npt.NDArray[ScalarType]: ... # array of shape (C,)
53
+ def __getitem__(self, key: tuple[int, int, int]) -> npt.NDArray[ScalarType]: ... # array of shape (C,)
65
54
  @overload
66
55
  @abstractmethod
67
- def __getitem__(self, key: Tuple[int, int, int, int]) -> ScalarType: ... # value
56
+ def __getitem__(self, key: tuple[int, int, int, int]) -> ScalarType: ... # value
68
57
  @overload
69
58
  @abstractmethod
70
59
  def __getitem__(
71
- self, key: slice | Tuple[slice, slice] | Tuple[slice, slice, slice] | Tuple[slice, slice, slice, slice],
60
+ self, key: slice | tuple[slice, slice] | tuple[slice, slice, slice] | tuple[slice, slice, slice, slice],
72
61
  ) -> npt.NDArray[ScalarType]: ... # array of shape (X, Y, Z, C)
73
62
  @overload
74
63
  @abstractmethod
@@ -95,7 +84,7 @@ class ImageStack(ABC, Generic[ScalarType]):
95
84
  return self[:, :, :, :]
96
85
 
97
86
  @property
98
- def shape(self) -> Tuple[int, int, int, int]:
87
+ def shape(self) -> tuple[int, int, int, int]:
99
88
  raise NotImplementedError()
100
89
 
101
90
 
@@ -107,26 +96,42 @@ def read_imgs(fname: str, *, dtype: None =..., **kwargs) -> ImageStack[np.float3
107
96
  # fmt:on
108
97
 
109
98
 
110
- def read_imgs(fname: str, *, dtype=None, **kwargs): # type: ignore
111
- """Read image stack."""
99
+ def read_imgs(fname: str, **kwargs): # type: ignore
100
+ """Read image stack.
112
101
 
113
- kwargs["dtype"] = dtype or np.float32
102
+ Parameters
103
+ ----------
104
+ fname : str
105
+ The path of image stack.
106
+ dtype : np.dtype, default to `np.float32`
107
+ Casting data to specified dtype. If integer and float
108
+ conversions occur, they will be scaled (assuming floats are
109
+ between 0 and 1).
110
+ **kwargs : dict[str, Any]
111
+ Forwarding to the corresponding reader.
112
+ """
114
113
 
115
- ext = os.path.splitext(fname)[-1]
116
- if ext in [".tif", ".tiff"]:
117
- return TiffImageStack(fname, **kwargs)
118
- if ext in [".nrrd"]:
119
- return NrrdImageStack(fname, **kwargs)
120
- if ext in [".v3dpbd"]:
121
- return V3dpbdImageStack(fname, **kwargs)
122
- if ext in [".v3draw"]:
123
- return V3drawImageStack(fname, **kwargs)
124
- if ext in [".npy", ".npz"]:
125
- return NDArrayImageStack(np.load(fname), **kwargs)
114
+ kwargs.setdefault("dtype", np.float32)
115
+ if not os.path.exists(fname):
116
+ raise ValueError(f"image stack not exists: {fname}")
117
+
118
+ # match file extension
119
+ match os.path.splitext(fname)[-1]:
120
+ case ".tif" | ".tiff":
121
+ return TiffImageStack(fname, **kwargs)
122
+ case ".nrrd":
123
+ return NrrdImageStack(fname, **kwargs)
124
+ case ".v3dpbd":
125
+ return V3dpbdImageStack(fname, **kwargs)
126
+ case ".v3draw":
127
+ return V3drawImageStack(fname, **kwargs)
128
+ case ".npy":
129
+ return NDArrayImageStack(np.load(fname), **kwargs)
130
+
131
+ # try to read as terafly
126
132
  if TeraflyImageStack.is_root(fname):
127
133
  return TeraflyImageStack(fname, **kwargs)
128
- if not os.path.exists(fname):
129
- raise ValueError("image stack not exists")
134
+
130
135
  raise ValueError("unsupported image stack")
131
136
 
132
137
 
@@ -135,7 +140,6 @@ def save_tiff(
135
140
  fname: str,
136
141
  *,
137
142
  dtype: Optional[np.unsignedinteger | np.floating] = None,
138
- swap_xy: Optional[bool] = None,
139
143
  compression: str | Literal[False] = "zlib",
140
144
  **kwargs,
141
145
  ) -> None:
@@ -154,7 +158,7 @@ def save_tiff(
154
158
  Compression algorithm, forwarding to `tifffile.imwrite`. If no
155
159
  algorithnm is specify specified, we will use the zlib algorithm
156
160
  with compression level 6 by default.
157
- **kwargs : Dict[str, Any]
161
+ **kwargs : dict[str, Any]
158
162
  Forwarding to `tifffile.imwrite`
159
163
  """
160
164
  if isinstance(data, ImageStack):
@@ -164,17 +168,6 @@ def save_tiff(
164
168
  data = np.expand_dims(data, -1) # (_, _, _) -> (_, _, _, C), C === 1
165
169
 
166
170
  axes = "ZXYC"
167
- if swap_xy is not None:
168
- warnings.warn(
169
- "flag `swap_xy` is easy to implement in user space and "
170
- "is more flexiable. Since this flag is rarely used, we "
171
- "decided to remove it in the next version",
172
- DeprecationWarning,
173
- )
174
- if swap_xy is True:
175
- axes = "ZYXC"
176
- data = data.swapaxes(0, 1) # (X, Y, _, _) -> (Y, X, _, _)
177
-
178
171
  assert data.ndim == 4, "should be an array of shape (X, Y, Z, C)"
179
172
  assert data.shape[-1] in [1, 3], "support 'miniblack' or 'rgb'"
180
173
 
@@ -209,12 +202,7 @@ class NDArrayImageStack(ImageStack[ScalarType]):
209
202
  """NDArray image stack."""
210
203
 
211
204
  def __init__(
212
- self,
213
- imgs: npt.NDArray[Any],
214
- swap_xy: Optional[bool] = None,
215
- filp_xy: Optional[bool] = None,
216
- *,
217
- dtype: ScalarType,
205
+ self, imgs: npt.NDArray[Any], *, dtype: Optional[ScalarType] = None
218
206
  ) -> None:
219
207
  super().__init__()
220
208
 
@@ -222,34 +210,22 @@ class NDArrayImageStack(ImageStack[ScalarType]):
222
210
  imgs = np.expand_dims(imgs, -1)
223
211
  assert imgs.ndim == 4, "Should be shape of (X, Y, Z, C)"
224
212
 
225
- if swap_xy is not None:
226
- warnings.warn(
227
- "flag `swap_xy` now is unnecessary, tifffile will "
228
- "automatically adjust dimensions according to "
229
- "`tags.axes`, so this flag will be removed in the next "
230
- " version",
231
- DeprecationWarning,
232
- )
233
- if swap_xy is True:
234
- imgs = imgs.swapaxes(0, 1) # (Y, X, _, _) -> (X, Y, _, _)
235
-
236
- if filp_xy is not None:
237
- warnings.warn(
238
- "flag `filp_xy` is easy to implement in user space and "
239
- "is more flexiable. Since this flag is rarely used, we "
240
- "decided to remove it in the next version",
241
- DeprecationWarning,
242
- )
243
- if filp_xy is True:
244
- imgs = np.flip(imgs, (0, 1)) # (X, Y, Z, C)
213
+ if dtype is not None:
214
+ dtype_raw = imgs.dtype
215
+ if np.issubdtype(dtype, np.floating) and np.issubdtype(
216
+ dtype_raw, np.unsignedinteger
217
+ ):
218
+ sclar_factor = 1.0 / UINT_MAX[dtype_raw]
219
+ imgs = sclar_factor * imgs.astype(dtype)
220
+ elif np.issubdtype(dtype, np.unsignedinteger) and np.issubdtype(
221
+ dtype_raw, np.floating
222
+ ):
223
+ sclar_factor = UINT_MAX[dtype] # type: ignore
224
+ imgs *= (sclar_factor * imgs).astype(dtype)
225
+ else:
226
+ imgs = imgs.astype(dtype)
245
227
 
246
- dtype_raw = imgs.dtype
247
- self.imgs = imgs.astype(dtype)
248
- if np.issubdtype(dtype, np.floating) and np.issubdtype(
249
- dtype_raw, np.unsignedinteger
250
- ): # TODO: add a option to disable this
251
- sclar_factor = 1.0 / UINT_MAX[imgs.dtype]
252
- self.imgs *= sclar_factor
228
+ self.imgs = imgs
253
229
 
254
230
  def __getitem__(self, key):
255
231
  return self.imgs.__getitem__(key)
@@ -258,22 +234,14 @@ class NDArrayImageStack(ImageStack[ScalarType]):
258
234
  return self.imgs
259
235
 
260
236
  @property
261
- def shape(self) -> Tuple[int, int, int, int]:
262
- return cast(Tuple[int, int, int, int], self.imgs.shape)
237
+ def shape(self) -> tuple[int, int, int, int]:
238
+ return cast(tuple[int, int, int, int], self.imgs.shape)
263
239
 
264
240
 
265
241
  class TiffImageStack(NDArrayImageStack[ScalarType]):
266
242
  """Tiff image stack."""
267
243
 
268
- def __init__(
269
- self,
270
- fname: str,
271
- swap_xy: Optional[bool] = None,
272
- filp_xy: Optional[bool] = None,
273
- *,
274
- dtype: ScalarType,
275
- **kwargs,
276
- ) -> None:
244
+ def __init__(self, fname: str, *, dtype: ScalarType, **kwargs) -> None:
277
245
  with tifffile.TiffFile(fname, **kwargs) as f:
278
246
  s = f.series[0]
279
247
  imgs, axes = s.asarray(), s.axes
@@ -285,23 +253,15 @@ class TiffImageStack(NDArrayImageStack[ScalarType]):
285
253
 
286
254
  orders = [AXES_ORDER[c] for c in axes]
287
255
  imgs = imgs.transpose(np.argsort(orders))
288
- super().__init__(imgs, swap_xy=swap_xy, filp_xy=filp_xy, dtype=dtype)
256
+ super().__init__(imgs, dtype=dtype)
289
257
 
290
258
 
291
259
  class NrrdImageStack(NDArrayImageStack[ScalarType]):
292
260
  """Nrrd image stack."""
293
261
 
294
- def __init__(
295
- self,
296
- fname: str,
297
- swap_xy: Optional[bool] = None,
298
- filp_xy: Optional[bool] = None,
299
- *,
300
- dtype: ScalarType,
301
- **kwargs,
302
- ) -> None:
262
+ def __init__(self, fname: str, *, dtype: ScalarType, **kwargs) -> None:
303
263
  imgs, header = nrrd.read(fname, **kwargs)
304
- super().__init__(imgs, swap_xy=swap_xy, filp_xy=filp_xy, dtype=dtype)
264
+ super().__init__(imgs, dtype=dtype)
305
265
  self.header = header
306
266
 
307
267
 
@@ -353,7 +313,7 @@ class TeraflyImageStack(ImageStack[ScalarType]):
353
313
  use its coordinate system, remember to FLIP Y-AXIS BACK.
354
314
  """
355
315
 
356
- _listdir: Callable[[str], List[str]]
316
+ _listdir: Callable[[str], list[str]]
357
317
  _read_patch: Callable[[str], npt.NDArray]
358
318
 
359
319
  def __init__(
@@ -379,12 +339,17 @@ class TeraflyImageStack(ImageStack[ScalarType]):
379
339
  self.res, self.res_dirs, self.res_patch_sizes = self.get_resolutions(root)
380
340
 
381
341
  @cache
382
- def listdir(path: str) -> List[str]:
342
+ def listdir(path: str) -> list[str]:
383
343
  return os.listdir(path)
384
344
 
385
345
  @lru_cache(maxsize=lru_maxsize)
386
346
  def read_patch(path: str) -> npt.NDArray[ScalarType]:
387
- return read_imgs(path, dtype=dtype).get_full()
347
+ match os.path.splitext(path)[-1]:
348
+ case "raw":
349
+ # Treat it as a v3draw file
350
+ return V3drawImageStack(path, dtype=dtype).get_full()
351
+ case _:
352
+ return read_imgs(path, dtype=dtype).get_full()
388
353
 
389
354
  self._listdir, self._read_patch = listdir, read_patch
390
355
 
@@ -453,19 +418,19 @@ class TeraflyImageStack(ImageStack[ScalarType]):
453
418
  raise NotImplementedError() # TODO
454
419
 
455
420
  @property
456
- def shape(self) -> Tuple[int, int, int, int]:
421
+ def shape(self) -> tuple[int, int, int, int]:
457
422
  res_max = self.res[-1]
458
423
  return res_max[0], res_max[1], res_max[2], 1
459
424
 
460
425
  @classmethod
461
- def get_resolutions(cls, root: str) -> Tuple[List[Vec3i], List[str], List[Vec3i]]:
426
+ def get_resolutions(cls, root: str) -> tuple[list[Vec3i], list[str], list[Vec3i]]:
462
427
  """Get all resolutions.
463
428
 
464
429
  Returns
465
430
  -------
466
431
  resolutions : List of (int, int, int)
467
432
  Sequence of sorted resolutions (from small to large).
468
- roots : List[str]
433
+ roots : list[str]
469
434
  Sequence of root of resolutions respectively.
470
435
  patch_sizes : List of (int, int, int)
471
436
  Sequence of patch size of resolutions respectively.
@@ -605,7 +570,7 @@ class GrayImageStack:
605
570
  @overload
606
571
  def __getitem__(self, key: npt.NDArray[np.integer[Any]]) -> np.float32: ...
607
572
  @overload
608
- def __getitem__(self, key: slice | Tuple[slice, slice] | Tuple[slice, slice, slice]) -> npt.NDArray[np.float32]: ...
573
+ def __getitem__(self, key: slice | tuple[slice, slice] | tuple[slice, slice, slice]) -> npt.NDArray[np.float32]: ...
609
574
  # fmt: on
610
575
  def __getitem__(self, key):
611
576
  """Get pixel/patch of image stack."""
@@ -632,7 +597,7 @@ class GrayImageStack:
632
597
  return self.imgs.get_full()[:, :, :, 0]
633
598
 
634
599
  @property
635
- def shape(self) -> Tuple[int, int, int]:
600
+ def shape(self) -> tuple[int, int, int]:
636
601
  return self.imgs.shape[:-1]
637
602
 
638
603
 
@@ -12,7 +12,8 @@ pip install swcgeom[all]
12
12
  import os
13
13
  import re
14
14
  import time
15
- from typing import Iterable, List, Optional, Tuple
15
+ from collections.abc import Iterable
16
+ from typing import Optional
16
17
 
17
18
  import numpy as np
18
19
  import numpy.typing as npt
@@ -69,7 +70,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
69
70
  x: Tree,
70
71
  verbose: bool = True,
71
72
  *,
72
- ranges: Optional[Tuple[npt.ArrayLike, npt.ArrayLike]] = None,
73
+ ranges: Optional[tuple[npt.ArrayLike, npt.ArrayLike]] = None,
73
74
  ) -> Iterable[npt.NDArray[np.uint8]]:
74
75
  if verbose:
75
76
  print("To image stack: " + x.source)
@@ -133,7 +134,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
133
134
  scene = ObjectsScene()
134
135
  scene.set_background((0, 0, 0))
135
136
 
136
- def leave(n: Tree.Node, children: List[Tree.Node]) -> Tree.Node:
137
+ def leave(n: Tree.Node, children: list[Tree.Node]) -> Tree.Node:
137
138
  for c in children:
138
139
  sdf = RoundCone(_tp3f(n.xyz()), _tp3f(c.xyz()), n.r, c.r).into()
139
140
  scene.add_object(SDFObject(sdf, material).into())
@@ -175,7 +176,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
175
176
  def save_tif(
176
177
  fname: str,
177
178
  frames: Iterable[npt.NDArray[np.uint8]],
178
- resolution: Tuple[float, float] = (1, 1),
179
+ resolution: tuple[float, float] = (1, 1),
179
180
  ) -> None:
180
181
  with tifffile.TiffWriter(fname) as tif:
181
182
  for frame in frames:
@@ -191,7 +192,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
191
192
  )
192
193
 
193
194
 
194
- def _tp3f(x: npt.NDArray) -> Tuple[float, float, float]:
195
+ def _tp3f(x: npt.NDArray) -> tuple[float, float, float]:
195
196
  """Convert to tuple of 3 floats."""
196
197
  assert len(x) == 3
197
198
  return (float(x[0]), float(x[1]), float(x[2]))
@@ -1,19 +1,22 @@
1
1
  """Image stack related transform."""
2
2
 
3
3
  import warnings
4
- from typing import Tuple
5
4
 
6
5
  import numpy as np
7
6
  import numpy.typing as npt
8
7
 
9
- from swcgeom.transforms.base import Transform
8
+ from swcgeom.transforms.base import Identity, Transform
10
9
 
11
10
  __all__ = [
12
11
  "ImagesCenterCrop",
13
12
  "ImagesScale",
14
13
  "ImagesClip",
14
+ "ImagesFlip",
15
+ "ImagesFlipY",
15
16
  "ImagesNormalizer",
16
17
  "ImagesMeanVarianceAdjustment",
18
+ "ImagesScaleToUnitRange",
19
+ "ImagesHistogramEqualization",
17
20
  "Center", # legacy
18
21
  ]
19
22
 
@@ -24,7 +27,7 @@ NDArrayf32 = npt.NDArray[np.float32]
24
27
  class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
25
28
  """Get image stack center."""
26
29
 
27
- def __init__(self, shape_out: int | Tuple[int, int, int]):
30
+ def __init__(self, shape_out: int | tuple[int, int, int]):
28
31
  super().__init__()
29
32
  self.shape_out = (
30
33
  shape_out
@@ -45,11 +48,11 @@ class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
45
48
  class Center(ImagesCenterCrop):
46
49
  """Get image stack center.
47
50
 
48
- .. deprecated:: 0.5.0
51
+ .. deprecated:: 0.16.0
49
52
  Use :class:`ImagesCenterCrop` instead.
50
53
  """
51
54
 
52
- def __init__(self, shape_out: int | Tuple[int, int, int]):
55
+ def __init__(self, shape_out: int | tuple[int, int, int]):
53
56
  warnings.warn(
54
57
  "`Center` is deprecated, use `ImagesCenterCrop` instead",
55
58
  DeprecationWarning,
@@ -66,6 +69,9 @@ class ImagesScale(Transform[NDArrayf32, NDArrayf32]):
66
69
  def __call__(self, x: NDArrayf32) -> NDArrayf32:
67
70
  return self.scaler * x
68
71
 
72
+ def extra_repr(self) -> str:
73
+ return f"scaler={self.scaler}"
74
+
69
75
 
70
76
  class ImagesClip(Transform[NDArrayf32, NDArrayf32]):
71
77
  def __init__(self, vmin: float = 0, vmax: float = 1, /) -> None:
@@ -75,6 +81,41 @@ class ImagesClip(Transform[NDArrayf32, NDArrayf32]):
75
81
  def __call__(self, x: NDArrayf32) -> NDArrayf32:
76
82
  return np.clip(x, self.vmin, self.vmax)
77
83
 
84
+ def extra_repr(self) -> str:
85
+ return f"vmin={self.vmin}, vmax={self.vmax}"
86
+
87
+
88
+ class ImagesFlip(Transform[NDArrayf32, NDArrayf32]):
89
+ """Flip image stack along axis."""
90
+
91
+ def __init__(self, axis: int, /) -> None:
92
+ super().__init__()
93
+ self.axis = axis
94
+
95
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
96
+ return np.flip(x, axis=self.axis)
97
+
98
+ def extra_repr(self) -> str:
99
+ return f"axis={self.axis}"
100
+
101
+
102
+ class ImagesFlipY(ImagesFlip):
103
+ """Flip image stack along Y-axis.
104
+
105
+ See Also
106
+ --------
107
+ ~.images.io.TeraflyImageStack:
108
+ Terafly and Vaa3d use a especial right-handed coordinate system
109
+ (with origin point in the left-top and z-axis points front),
110
+ but we flip y-axis to makes it a left-handed coordinate system
111
+ (with orgin point in the left-bottom and z-axis points front).
112
+ If you need to use its coordinate system, remember to FLIP
113
+ Y-AXIS BACK.
114
+ """
115
+
116
+ def __init__(self, axis: int = 1, /) -> None:
117
+ super().__init__(axis) # (X, Y, Z, C)
118
+
78
119
 
79
120
  class ImagesNormalizer(Transform[NDArrayf32, NDArrayf32]):
80
121
  """Normalize image stack."""
@@ -100,3 +141,62 @@ class ImagesMeanVarianceAdjustment(Transform[NDArrayf32, NDArrayf32]):
100
141
 
101
142
  def __call__(self, x: NDArrayf32) -> NDArrayf32:
102
143
  return (x - self.mean) / self.variance
144
+
145
+ def extra_repr(self) -> str:
146
+ return f"mean={self.mean}, variance={self.variance}"
147
+
148
+
149
+ class ImagesScaleToUnitRange(Transform[NDArrayf32, NDArrayf32]):
150
+ """Scale image stack to unit range."""
151
+
152
+ def __init__(self, vmin: float, vmax: float, *, clip: bool = True) -> None:
153
+ """Scale image stack to unit range.
154
+
155
+ Parameters
156
+ ----------
157
+ vmin : float
158
+ Minimum value.
159
+ vmax : float
160
+ Maximum value.
161
+ clip : bool, default True
162
+ Clip values to [0, 1] to avoid numerical issues.
163
+ """
164
+
165
+ super().__init__()
166
+ self.vmin = vmin
167
+ self.vmax = vmax
168
+ self.diff = vmax - vmin
169
+ self.clip = clip
170
+ self.post = ImagesClip(0, 1) if self.clip else Identity()
171
+
172
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
173
+ return self.post((x - self.vmin) / self.diff)
174
+
175
+ def extra_repr(self) -> str:
176
+ return f"vmin={self.vmin}, vmax={self.vmax}, clip={self.clip}"
177
+
178
+
179
+ class ImagesHistogramEqualization(Transform[NDArrayf32, NDArrayf32]):
180
+ """Image histogram equalization.
181
+
182
+ References
183
+ ----------
184
+ http://www.janeriksolem.net/histogram-equalization-with-python-and.html
185
+ """
186
+
187
+ def __init__(self, bins: int = 256) -> None:
188
+ super().__init__()
189
+ self.bins = bins
190
+
191
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
192
+ # get image histogram
193
+ hist, bin_edges = np.histogram(x.flatten(), self.bins, density=True)
194
+ cdf = hist.cumsum() # cumulative distribution function
195
+ cdf = cdf / cdf[-1] # normalize
196
+
197
+ # use linear interpolation of cdf to find new pixel values
198
+ equalized = np.interp(x.flatten(), bin_edges[:-1], cdf)
199
+ return equalized.reshape(x.shape).astype(np.float32)
200
+
201
+ def extra_repr(self) -> str:
202
+ return f"bins={self.bins}"