swcgeom 0.15.0__py3-none-any.whl → 0.18.3__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 (72) hide show
  1. swcgeom/__init__.py +26 -1
  2. swcgeom/analysis/__init__.py +21 -8
  3. swcgeom/analysis/feature_extractor.py +43 -18
  4. swcgeom/analysis/features.py +250 -0
  5. swcgeom/analysis/lmeasure.py +857 -0
  6. swcgeom/analysis/sholl.py +55 -29
  7. swcgeom/analysis/trunk.py +27 -11
  8. swcgeom/analysis/visualization.py +24 -9
  9. swcgeom/analysis/visualization3d.py +100 -0
  10. swcgeom/analysis/volume.py +19 -4
  11. swcgeom/core/__init__.py +32 -9
  12. swcgeom/core/branch.py +28 -7
  13. swcgeom/core/branch_tree.py +18 -4
  14. swcgeom/core/{segment.py → compartment.py} +31 -10
  15. swcgeom/core/node.py +31 -10
  16. swcgeom/core/path.py +37 -10
  17. swcgeom/core/population.py +103 -34
  18. swcgeom/core/swc.py +26 -10
  19. swcgeom/core/swc_utils/__init__.py +21 -7
  20. swcgeom/core/swc_utils/assembler.py +27 -1
  21. swcgeom/core/swc_utils/base.py +25 -12
  22. swcgeom/core/swc_utils/checker.py +31 -14
  23. swcgeom/core/swc_utils/io.py +24 -7
  24. swcgeom/core/swc_utils/normalizer.py +20 -4
  25. swcgeom/core/swc_utils/subtree.py +17 -2
  26. swcgeom/core/tree.py +85 -72
  27. swcgeom/core/tree_utils.py +31 -16
  28. swcgeom/core/tree_utils_impl.py +18 -3
  29. swcgeom/images/__init__.py +17 -2
  30. swcgeom/images/augmentation.py +24 -4
  31. swcgeom/images/contrast.py +122 -0
  32. swcgeom/images/folder.py +97 -39
  33. swcgeom/images/io.py +108 -121
  34. swcgeom/transforms/__init__.py +28 -10
  35. swcgeom/transforms/base.py +17 -2
  36. swcgeom/transforms/branch.py +74 -8
  37. swcgeom/transforms/branch_tree.py +82 -0
  38. swcgeom/transforms/geometry.py +22 -7
  39. swcgeom/transforms/image_preprocess.py +115 -0
  40. swcgeom/transforms/image_stack.py +37 -13
  41. swcgeom/transforms/images.py +184 -7
  42. swcgeom/transforms/mst.py +20 -5
  43. swcgeom/transforms/neurolucida_asc.py +508 -0
  44. swcgeom/transforms/path.py +15 -0
  45. swcgeom/transforms/population.py +16 -3
  46. swcgeom/transforms/tree.py +89 -31
  47. swcgeom/transforms/tree_assembler.py +23 -7
  48. swcgeom/utils/__init__.py +27 -11
  49. swcgeom/utils/debug.py +15 -0
  50. swcgeom/utils/download.py +59 -21
  51. swcgeom/utils/dsu.py +15 -0
  52. swcgeom/utils/ellipse.py +18 -4
  53. swcgeom/utils/file.py +15 -0
  54. swcgeom/utils/neuromorpho.py +439 -302
  55. swcgeom/utils/numpy_helper.py +29 -4
  56. swcgeom/utils/plotter_2d.py +151 -0
  57. swcgeom/utils/plotter_3d.py +48 -0
  58. swcgeom/utils/renderer.py +49 -145
  59. swcgeom/utils/sdf.py +24 -8
  60. swcgeom/utils/solid_geometry.py +16 -3
  61. swcgeom/utils/transforms.py +17 -4
  62. swcgeom/utils/volumetric_object.py +23 -10
  63. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/LICENSE +1 -1
  64. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/METADATA +28 -24
  65. swcgeom-0.18.3.dist-info/RECORD +67 -0
  66. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/WHEEL +1 -1
  67. swcgeom/_version.py +0 -16
  68. swcgeom/analysis/branch_features.py +0 -67
  69. swcgeom/analysis/node_features.py +0 -121
  70. swcgeom/analysis/path_features.py +0 -37
  71. swcgeom-0.15.0.dist-info/RECORD +0 -62
  72. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,82 @@
1
+ # Copyright 2022-2025 Zexin Yuan
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from typing import Iterable
17
+
18
+ import numpy as np
19
+
20
+ from swcgeom.core import Branch, BranchTree, Node, Tree
21
+ from swcgeom.transforms.base import Transform
22
+
23
+ __all__ = ["BranchTreeAssembler"]
24
+
25
+
26
+ class BranchTreeAssembler(Transform[BranchTree, Tree]):
27
+ EPS = 1e-6
28
+
29
+ def __call__(self, x: BranchTree) -> Tree:
30
+ nodes = [x.soma().detach()]
31
+ stack = [(x.soma(), 0)] # n_orig, id_new
32
+ while len(stack):
33
+ n_orig, pid_new = stack.pop()
34
+ children = n_orig.children()
35
+
36
+ for br, c in self.pair(x.branches.get(n_orig.id, []), children):
37
+ s = 1 if np.linalg.norm(br[0].xyz() - n_orig.xyz()) < self.EPS else 0
38
+ e = -2 if np.linalg.norm(br[-1].xyz() - c.xyz()) < self.EPS else -1
39
+
40
+ br_nodes = [n.detach() for n in br[s:e]] + [c.detach()]
41
+ for i, n in enumerate(br_nodes):
42
+ # reindex
43
+ n.id = len(nodes) + i
44
+ n.pid = len(nodes) + i - 1
45
+
46
+ br_nodes[0].pid = pid_new
47
+ nodes.extend(br_nodes)
48
+ stack.append((c, br_nodes[-1].id))
49
+
50
+ return Tree(
51
+ len(nodes),
52
+ source=x.source,
53
+ comments=x.comments,
54
+ names=x.names,
55
+ **{
56
+ k: np.array([n.__getattribute__(k) for n in nodes])
57
+ for k in x.names.cols()
58
+ },
59
+ )
60
+
61
+ def pair(
62
+ self, branches: list[Branch], endpoints: list[Node]
63
+ ) -> Iterable[tuple[Branch, Node]]:
64
+ assert len(branches) == len(endpoints)
65
+ xyz1 = [br[-1].xyz() for br in branches]
66
+ xyz2 = [n.xyz() for n in endpoints]
67
+ v = np.reshape(xyz1, (-1, 1, 3)) - np.reshape(xyz2, (1, -1, 3))
68
+ dis = np.linalg.norm(v, axis=-1)
69
+
70
+ # greedy algorithm
71
+ pairs = []
72
+ for _ in range(len(branches)):
73
+ # find minimal
74
+ min_idx = np.argmin(dis)
75
+ min_branch_idx, min_endpoint_idx = np.unravel_index(min_idx, dis.shape)
76
+ pairs.append((branches[min_branch_idx], endpoints[min_endpoint_idx]))
77
+
78
+ # remove current node
79
+ dis[min_branch_idx, :] = np.inf
80
+ dis[:, min_endpoint_idx] = np.inf
81
+
82
+ return pairs
@@ -1,3 +1,18 @@
1
+ # Copyright 2022-2025 Zexin Yuan
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
1
16
  """SWC geometry operations."""
2
17
 
3
18
  import warnings
@@ -72,7 +87,7 @@ class RadiusReseter(Generic[T], Transform[T, T]):
72
87
  new_tree.ndata[new_tree.names.r] = r
73
88
  return new_tree
74
89
 
75
- def extra_repr(self):
90
+ def extra_repr(self) -> str:
76
91
  return f"r={self.r:.4f}"
77
92
 
78
93
 
@@ -141,7 +156,7 @@ class Translate(Generic[T], AffineTransform[T]):
141
156
  super().__init__(translate3d(tx, ty, tz), **kwargs)
142
157
  self.tx, self.ty, self.tz = tx, ty, tz
143
158
 
144
- def extra_repr(self):
159
+ def extra_repr(self) -> str:
145
160
  return f"tx={self.tx:.4f}, ty={self.ty:.4f}, tz={self.tz:.4f}"
146
161
 
147
162
  @classmethod
@@ -194,8 +209,8 @@ class Rotate(Generic[T], AffineTransform[T]):
194
209
  self.theta = theta
195
210
  self.center = center
196
211
 
197
- def extra_repr(self):
198
- return f"n={self.n}, theta={self.theta:.4f}, center={self.center}" # TODO: imporve format of n
212
+ def extra_repr(self) -> str:
213
+ return f"n={self.n}, theta={self.theta:.4f}, center={self.center}" # TODO: improve format of n
199
214
 
200
215
  @classmethod
201
216
  def transform(
@@ -216,7 +231,7 @@ class RotateX(Generic[T], AffineTransform[T]):
216
231
  super().__init__(rotate3d_x(theta), center=center, **kwargs)
217
232
  self.theta = theta
218
233
 
219
- def extra_repr(self):
234
+ def extra_repr(self) -> str:
220
235
  return f"center={self.center}, theta={self.theta:.4f}"
221
236
 
222
237
  @classmethod
@@ -232,7 +247,7 @@ class RotateY(Generic[T], AffineTransform[T]):
232
247
  self.theta = theta
233
248
  self.center = center
234
249
 
235
- def extra_repr(self):
250
+ def extra_repr(self) -> str:
236
251
  return f"theta={self.theta:.4f}, center={self.center}"
237
252
 
238
253
  @classmethod
@@ -248,7 +263,7 @@ class RotateZ(Generic[T], AffineTransform[T]):
248
263
  self.theta = theta
249
264
  self.center = center
250
265
 
251
- def extra_repr(self):
266
+ def extra_repr(self) -> str:
252
267
  return f"theta={self.theta:.4f}, center={self.center}"
253
268
 
254
269
  @classmethod
@@ -0,0 +1,115 @@
1
+ # Copyright 2022-2025 Zexin Yuan
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ """Image stack pre-processing."""
17
+
18
+ import numpy as np
19
+ import numpy.typing as npt
20
+ from scipy.fftpack import fftn, fftshift, ifftn
21
+ from scipy.ndimage import gaussian_filter, minimum_filter
22
+
23
+ from swcgeom.transforms.base import Transform
24
+
25
+ __all__ = ["SGuoImPreProcess"]
26
+
27
+
28
+ class SGuoImPreProcess(Transform[npt.NDArray[np.uint8], npt.NDArray[np.uint8]]):
29
+ """Single-Neuron Image Enhancement.
30
+
31
+ Implementation of the image enhancement method described in the paper:
32
+
33
+ Shuxia Guo, Xuan Zhao, Shengdian Jiang, Liya Ding, Hanchuan Peng,
34
+ Image enhancement to leverage the 3D morphological reconstruction
35
+ of single-cell neurons, Bioinformatics, Volume 38, Issue 2,
36
+ January 2022, Pages 503–512, https://doi.org/10.1093/bioinformatics/btab638
37
+ """
38
+
39
+ def __call__(self, x: npt.NDArray[np.uint8]) -> npt.NDArray[np.uint8]:
40
+ # TODO: support np.float32
41
+ assert x.dtype == np.uint8, "Image must be in uint8 format"
42
+ x = self.sigmoid_adjustment(x)
43
+ x = self.subtract_min_along_z(x)
44
+ x = self.bilateral_filter_3d(x)
45
+ x = self.high_pass_fft(x)
46
+ return x
47
+
48
+ @staticmethod
49
+ def sigmoid_adjustment(
50
+ image: npt.NDArray[np.uint8], sigma: float = 3, percentile: float = 25
51
+ ) -> npt.NDArray[np.uint8]:
52
+ image_normalized = image / 255.0
53
+ u = np.percentile(image_normalized, percentile)
54
+ adjusted = 1 / (1 + np.exp(-sigma * (image_normalized - u)))
55
+ adjusted_rescaled = (adjusted * 255).astype(np.uint8)
56
+ return adjusted_rescaled
57
+
58
+ @staticmethod
59
+ def subtract_min_along_z(image: npt.NDArray[np.uint8]) -> npt.NDArray[np.uint8]:
60
+ min_along_z = minimum_filter(
61
+ image,
62
+ size=(1, 1, image.shape[2], 1),
63
+ mode="constant",
64
+ cval=np.max(image).item(),
65
+ )
66
+ subtracted = image - min_along_z
67
+ return subtracted
68
+
69
+ @staticmethod
70
+ def bilateral_filter_3d(
71
+ image: npt.NDArray[np.uint8], spatial_sigma=(1, 1, 0.33), range_sigma=35
72
+ ) -> npt.NDArray[np.uint8]:
73
+ # initialize the output image
74
+ filtered_image = np.zeros_like(image)
75
+
76
+ spatial_gaussian = gaussian_filter(image, spatial_sigma)
77
+
78
+ # traverse each pixel to perform bilateral filtering
79
+ # TODO: optimization is needed
80
+ for z in range(image.shape[2]):
81
+ for y in range(image.shape[1]):
82
+ for x in range(image.shape[0]):
83
+ value = image[x, y, z]
84
+ range_weight = np.exp(
85
+ -((image - value) ** 2) / (2 * range_sigma**2)
86
+ )
87
+ weights = spatial_gaussian * range_weight
88
+ filtered_image[x, y, z] = np.sum(image * weights) / np.sum(weights)
89
+
90
+ return filtered_image
91
+
92
+ @staticmethod
93
+ def high_pass_fft(image: npt.NDArray[np.uint8]) -> npt.NDArray[np.uint8]:
94
+ # fft
95
+ fft_image = fftn(image)
96
+ fft_shifted = fftshift(fft_image)
97
+
98
+ # create a high-pass filter
99
+ h, w, d, _ = image.shape
100
+ x, y, z = np.ogrid[:h, :w, :d]
101
+ center = (h / 2, w / 2, d / 2)
102
+ distance = np.sqrt(
103
+ (x - center[0]) ** 2 + (y - center[1]) ** 2 + (z - center[2]) ** 2
104
+ )
105
+ # adjust this threshold to control the filtering strength
106
+ high_pass_mask = distance > (d // 4)
107
+ # apply the high-pass filter
108
+ fft_shifted *= high_pass_mask
109
+
110
+ # inverse fft
111
+ fft_unshifted = np.fft.ifftshift(fft_shifted)
112
+ filtered_image = np.real(ifftn(fft_unshifted))
113
+
114
+ filtered_rescaled = np.clip(filtered_image, 0, 255).astype(np.uint8)
115
+ return filtered_rescaled
@@ -1,3 +1,18 @@
1
+ # Copyright 2022-2025 Zexin Yuan
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
1
16
  """Create image stack from morphology.
2
17
 
3
18
  Notes
@@ -12,7 +27,8 @@ pip install swcgeom[all]
12
27
  import os
13
28
  import re
14
29
  import time
15
- from typing import Iterable, List, Optional, Tuple
30
+ from collections.abc import Iterable
31
+ from typing import Optional
16
32
 
17
33
  import numpy as np
18
34
  import numpy.typing as npt
@@ -25,6 +41,8 @@ from sdflit import (
25
41
  Scene,
26
42
  SDFObject,
27
43
  )
44
+ from tqdm import tqdm
45
+ from typing_extensions import deprecated
28
46
 
29
47
  from swcgeom.core import Population, Tree
30
48
  from swcgeom.transforms.base import Transform
@@ -61,14 +79,14 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
61
79
  ONLY works for small image stacks, use :meth`transform_and_save`
62
80
  for big image stack.
63
81
  """
64
- return np.stack(list(self.transfrom(x, verbose=False)), axis=0)
82
+ return np.stack(list(self.transform(x, verbose=False)), axis=0)
65
83
 
66
- def transfrom(
84
+ def transform(
67
85
  self,
68
86
  x: Tree,
69
87
  verbose: bool = True,
70
88
  *,
71
- ranges: Optional[Tuple[npt.ArrayLike, npt.ArrayLike]] = None,
89
+ ranges: Optional[tuple[npt.ArrayLike, npt.ArrayLike]] = None,
72
90
  ) -> Iterable[npt.NDArray[np.uint8]]:
73
91
  if verbose:
74
92
  print("To image stack: " + x.source)
@@ -89,8 +107,6 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
89
107
  samplers = self._get_samplers(coord_min, coord_max)
90
108
 
91
109
  if verbose:
92
- from tqdm import tqdm
93
-
94
110
  total = (coord_max[2] - coord_min[2]) / self.resolution[2]
95
111
  samplers = tqdm(samplers, total=total.astype(np.int64).item())
96
112
 
@@ -102,10 +118,20 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
102
118
  frame = (255 * voxel[..., 0, 0]).astype(np.uint8)
103
119
  yield frame
104
120
 
121
+ @deprecated("Use transform instead")
122
+ def transfrom(
123
+ self,
124
+ x: Tree,
125
+ verbose: bool = True,
126
+ *,
127
+ ranges: Optional[tuple[npt.ArrayLike, npt.ArrayLike]] = None,
128
+ ) -> Iterable[npt.NDArray[np.uint8]]:
129
+ return self.transform(x, verbose, ranges=ranges)
130
+
105
131
  def transform_and_save(
106
132
  self, fname: str, x: Tree, verbose: bool = True, **kwargs
107
133
  ) -> None:
108
- self.save_tif(fname, self.transfrom(x, verbose=verbose, **kwargs))
134
+ self.save_tif(fname, self.transform(x, verbose=verbose, **kwargs))
109
135
 
110
136
  def transform_population(
111
137
  self, population: Population | str, verbose: bool = True
@@ -117,8 +143,6 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
117
143
  )
118
144
 
119
145
  if verbose:
120
- from tqdm import tqdm
121
-
122
146
  trees = tqdm(trees)
123
147
 
124
148
  # TODO: multiprocess
@@ -127,7 +151,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
127
151
  if not os.path.isfile(tif):
128
152
  self.transform_and_save(tif, tree, verbose=False)
129
153
 
130
- def extra_repr(self):
154
+ def extra_repr(self) -> str:
131
155
  res = ",".join(f"{a:.4f}" for a in self.resolution)
132
156
  return f"resolution=({res})"
133
157
 
@@ -136,7 +160,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
136
160
  scene = ObjectsScene()
137
161
  scene.set_background((0, 0, 0))
138
162
 
139
- def leave(n: Tree.Node, children: List[Tree.Node]) -> Tree.Node:
163
+ def leave(n: Tree.Node, children: list[Tree.Node]) -> Tree.Node:
140
164
  for c in children:
141
165
  sdf = RoundCone(_tp3f(n.xyz()), _tp3f(c.xyz()), n.r, c.r).into()
142
166
  scene.add_object(SDFObject(sdf, material).into())
@@ -178,7 +202,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
178
202
  def save_tif(
179
203
  fname: str,
180
204
  frames: Iterable[npt.NDArray[np.uint8]],
181
- resolution: Tuple[float, float] = (1, 1),
205
+ resolution: tuple[float, float] = (1, 1),
182
206
  ) -> None:
183
207
  with tifffile.TiffWriter(fname) as tif:
184
208
  for frame in frames:
@@ -194,7 +218,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
194
218
  )
195
219
 
196
220
 
197
- def _tp3f(x: npt.NDArray) -> Tuple[float, float, float]:
221
+ def _tp3f(x: npt.NDArray) -> tuple[float, float, float]:
198
222
  """Convert to tuple of 3 floats."""
199
223
  assert len(x) == 3
200
224
  return (float(x[0]), float(x[1]), float(x[2]))
@@ -1,20 +1,47 @@
1
- """Image stack related transform."""
1
+ # Copyright 2022-2025 Zexin Yuan
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
2
14
 
3
15
 
4
- from typing import Tuple
16
+ """Image stack related transform."""
5
17
 
6
18
  import numpy as np
7
19
  import numpy.typing as npt
20
+ from typing_extensions import deprecated
21
+
22
+ from swcgeom.transforms.base import Identity, Transform
23
+
24
+ __all__ = [
25
+ "ImagesCenterCrop",
26
+ "ImagesScale",
27
+ "ImagesClip",
28
+ "ImagesFlip",
29
+ "ImagesFlipY",
30
+ "ImagesNormalizer",
31
+ "ImagesMeanVarianceAdjustment",
32
+ "ImagesScaleToUnitRange",
33
+ "ImagesHistogramEqualization",
34
+ "Center", # legacy
35
+ ]
8
36
 
9
- from swcgeom.transforms.base import Transform
10
37
 
11
- __all__ = ["Center"]
38
+ NDArrayf32 = npt.NDArray[np.float32]
12
39
 
13
40
 
14
- class Center(Transform[npt.NDArray[np.float32], npt.NDArray[np.float32]]):
41
+ class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
15
42
  """Get image stack center."""
16
43
 
17
- def __init__(self, shape_out: int | Tuple[int, int, int]):
44
+ def __init__(self, shape_out: int | tuple[int, int, int]):
18
45
  super().__init__()
19
46
  self.shape_out = (
20
47
  shape_out
@@ -22,7 +49,7 @@ class Center(Transform[npt.NDArray[np.float32], npt.NDArray[np.float32]]):
22
49
  else (shape_out, shape_out, shape_out)
23
50
  )
24
51
 
25
- def __call__(self, x: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
52
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
26
53
  diff = np.subtract(x.shape[:3], self.shape_out)
27
54
  s = diff // 2
28
55
  e = np.add(s, self.shape_out)
@@ -30,3 +57,153 @@ class Center(Transform[npt.NDArray[np.float32], npt.NDArray[np.float32]]):
30
57
 
31
58
  def extra_repr(self) -> str:
32
59
  return f"shape_out=({','.join(str(a) for a in self.shape_out)})"
60
+
61
+
62
+ @deprecated("use `ImagesCenterCrop` instead", stacklevel=2)
63
+ class Center(ImagesCenterCrop):
64
+ """Get image stack center.
65
+
66
+ .. deprecated:: 0.16.0
67
+ Use :class:`ImagesCenterCrop` instead.
68
+ """
69
+
70
+
71
+ class ImagesScale(Transform[NDArrayf32, NDArrayf32]):
72
+ def __init__(self, scaler: float) -> None:
73
+ super().__init__()
74
+ self.scaler = scaler
75
+
76
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
77
+ return self.scaler * x
78
+
79
+ def extra_repr(self) -> str:
80
+ return f"scaler={self.scaler}"
81
+
82
+
83
+ class ImagesClip(Transform[NDArrayf32, NDArrayf32]):
84
+ def __init__(self, vmin: float = 0, vmax: float = 1, /) -> None:
85
+ super().__init__()
86
+ self.vmin, self.vmax = vmin, vmax
87
+
88
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
89
+ return np.clip(x, self.vmin, self.vmax)
90
+
91
+ def extra_repr(self) -> str:
92
+ return f"vmin={self.vmin}, vmax={self.vmax}"
93
+
94
+
95
+ class ImagesFlip(Transform[NDArrayf32, NDArrayf32]):
96
+ """Flip image stack along axis."""
97
+
98
+ def __init__(self, axis: int, /) -> None:
99
+ super().__init__()
100
+ self.axis = axis
101
+
102
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
103
+ return np.flip(x, axis=self.axis)
104
+
105
+ def extra_repr(self) -> str:
106
+ return f"axis={self.axis}"
107
+
108
+
109
+ class ImagesFlipY(ImagesFlip):
110
+ """Flip image stack along Y-axis.
111
+
112
+ See Also
113
+ --------
114
+ ~.images.io.TeraflyImageStack:
115
+ Terafly and Vaa3d use a especial right-handed coordinate system
116
+ (with origin point in the left-top and z-axis points front),
117
+ but we flip y-axis to makes it a left-handed coordinate system
118
+ (with orgin point in the left-bottom and z-axis points front).
119
+ If you need to use its coordinate system, remember to FLIP
120
+ Y-AXIS BACK.
121
+ """
122
+
123
+ def __init__(self, axis: int = 1, /) -> None:
124
+ super().__init__(axis) # (X, Y, Z, C)
125
+
126
+
127
+ class ImagesNormalizer(Transform[NDArrayf32, NDArrayf32]):
128
+ """Normalize image stack."""
129
+
130
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
131
+ mean = np.mean(x)
132
+ variance = np.var(x)
133
+ return (x - mean) / variance
134
+
135
+
136
+ class ImagesMeanVarianceAdjustment(Transform[NDArrayf32, NDArrayf32]):
137
+ """Adjust image stack mean and variance.
138
+
139
+ See Also
140
+ --------
141
+ ~swcgeom.images.ImageStackFolder.stat
142
+ """
143
+
144
+ def __init__(self, mean: float, variance: float) -> None:
145
+ super().__init__()
146
+ self.mean = mean
147
+ self.variance = variance
148
+
149
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
150
+ return (x - self.mean) / self.variance
151
+
152
+ def extra_repr(self) -> str:
153
+ return f"mean={self.mean}, variance={self.variance}"
154
+
155
+
156
+ class ImagesScaleToUnitRange(Transform[NDArrayf32, NDArrayf32]):
157
+ """Scale image stack to unit range."""
158
+
159
+ def __init__(self, vmin: float, vmax: float, *, clip: bool = True) -> None:
160
+ """Scale image stack to unit range.
161
+
162
+ Parameters
163
+ ----------
164
+ vmin : float
165
+ Minimum value.
166
+ vmax : float
167
+ Maximum value.
168
+ clip : bool, default True
169
+ Clip values to [0, 1] to avoid numerical issues.
170
+ """
171
+
172
+ super().__init__()
173
+ self.vmin = vmin
174
+ self.vmax = vmax
175
+ self.diff = vmax - vmin
176
+ self.clip = clip
177
+ self.post = ImagesClip(0, 1) if self.clip else Identity()
178
+
179
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
180
+ return self.post((x - self.vmin) / self.diff)
181
+
182
+ def extra_repr(self) -> str:
183
+ return f"vmin={self.vmin}, vmax={self.vmax}, clip={self.clip}"
184
+
185
+
186
+ class ImagesHistogramEqualization(Transform[NDArrayf32, NDArrayf32]):
187
+ """Image histogram equalization.
188
+
189
+ References
190
+ ----------
191
+ http://www.janeriksolem.net/histogram-equalization-with-python-and.html
192
+ """
193
+
194
+ def __init__(self, bins: int = 256) -> None:
195
+ super().__init__()
196
+ self.bins = bins
197
+
198
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
199
+ # get image histogram
200
+ hist, bin_edges = np.histogram(x.flatten(), self.bins, density=True)
201
+ cdf = hist.cumsum() # cumulative distribution function
202
+ cdf = cdf / cdf[-1] # normalize
203
+
204
+ # use linear interpolation of cdf to find new pixel values
205
+ equalized = np.interp(x.flatten(), bin_edges[:-1], cdf)
206
+ return equalized.reshape(x.shape).astype(np.float32)
207
+
208
+ def extra_repr(self) -> str:
209
+ return f"bins={self.bins}"
swcgeom/transforms/mst.py CHANGED
@@ -1,3 +1,18 @@
1
+ # Copyright 2022-2025 Zexin Yuan
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
1
16
  """Minimum spanning tree."""
2
17
 
3
18
  import warnings
@@ -23,11 +38,11 @@ class PointsToCuntzMST(Transform[npt.NDArray[np.float32], Tree]):
23
38
 
24
39
  References
25
40
  ----------
26
- [1] Cuntz, H., Forstner, F., Borst, A. & Häusser, M. One Rule to
27
- Grow Them Al: A General Theory of Neuronal Branching and Its
28
- Practical Application. PLOS Comput Biol 6, e1000877 (2010).
29
- [2] Cuntz, H., Borst, A. & Segev, I. Optimization principles of
30
- dendritic structure. Theor Biol Med Model 4, 21 (2007).
41
+ .. [1] Cuntz, H., Forstner, F., Borst, A. & Häusser, M. One Rule to
42
+ Grow Them Al: A General Theory of Neuronal Branching and Its
43
+ Practical Application. PLOS Comput Biol 6, e1000877 (2010).
44
+ .. [2] Cuntz, H., Borst, A. & Segev, I. Optimization principles of
45
+ dendritic structure. Theor Biol Med Model 4, 21 (2007).
31
46
  """
32
47
 
33
48
  def __init__(