swcgeom 0.19.4__cp312-cp312-macosx_14_0_arm64.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.
- swcgeom/__init__.py +21 -0
- swcgeom/analysis/__init__.py +13 -0
- swcgeom/analysis/feature_extractor.py +454 -0
- swcgeom/analysis/features.py +218 -0
- swcgeom/analysis/lmeasure.py +750 -0
- swcgeom/analysis/sholl.py +201 -0
- swcgeom/analysis/trunk.py +183 -0
- swcgeom/analysis/visualization.py +191 -0
- swcgeom/analysis/visualization3d.py +81 -0
- swcgeom/analysis/volume.py +143 -0
- swcgeom/core/__init__.py +19 -0
- swcgeom/core/branch.py +129 -0
- swcgeom/core/branch_tree.py +65 -0
- swcgeom/core/compartment.py +107 -0
- swcgeom/core/node.py +130 -0
- swcgeom/core/path.py +155 -0
- swcgeom/core/population.py +341 -0
- swcgeom/core/swc.py +247 -0
- swcgeom/core/swc_utils/__init__.py +19 -0
- swcgeom/core/swc_utils/assembler.py +35 -0
- swcgeom/core/swc_utils/base.py +180 -0
- swcgeom/core/swc_utils/checker.py +107 -0
- swcgeom/core/swc_utils/io.py +204 -0
- swcgeom/core/swc_utils/normalizer.py +163 -0
- swcgeom/core/swc_utils/subtree.py +70 -0
- swcgeom/core/tree.py +384 -0
- swcgeom/core/tree_utils.py +277 -0
- swcgeom/core/tree_utils_impl.py +58 -0
- swcgeom/images/__init__.py +9 -0
- swcgeom/images/augmentation.py +149 -0
- swcgeom/images/contrast.py +87 -0
- swcgeom/images/folder.py +217 -0
- swcgeom/images/io.py +578 -0
- swcgeom/images/loaders/__init__.py +8 -0
- swcgeom/images/loaders/pbd.cpython-312-darwin.so +0 -0
- swcgeom/images/loaders/pbd.pyx +523 -0
- swcgeom/images/loaders/raw.cpython-312-darwin.so +0 -0
- swcgeom/images/loaders/raw.pyx +183 -0
- swcgeom/transforms/__init__.py +20 -0
- swcgeom/transforms/base.py +136 -0
- swcgeom/transforms/branch.py +223 -0
- swcgeom/transforms/branch_tree.py +74 -0
- swcgeom/transforms/geometry.py +270 -0
- swcgeom/transforms/image_preprocess.py +107 -0
- swcgeom/transforms/image_stack.py +219 -0
- swcgeom/transforms/images.py +206 -0
- swcgeom/transforms/mst.py +183 -0
- swcgeom/transforms/neurolucida_asc.py +498 -0
- swcgeom/transforms/path.py +56 -0
- swcgeom/transforms/population.py +36 -0
- swcgeom/transforms/tree.py +265 -0
- swcgeom/transforms/tree_assembler.py +161 -0
- swcgeom/utils/__init__.py +18 -0
- swcgeom/utils/debug.py +23 -0
- swcgeom/utils/download.py +119 -0
- swcgeom/utils/dsu.py +58 -0
- swcgeom/utils/ellipse.py +131 -0
- swcgeom/utils/file.py +90 -0
- swcgeom/utils/neuromorpho.py +581 -0
- swcgeom/utils/numpy_helper.py +70 -0
- swcgeom/utils/plotter_2d.py +134 -0
- swcgeom/utils/plotter_3d.py +35 -0
- swcgeom/utils/renderer.py +145 -0
- swcgeom/utils/sdf.py +324 -0
- swcgeom/utils/solid_geometry.py +154 -0
- swcgeom/utils/transforms.py +367 -0
- swcgeom/utils/volumetric_object.py +483 -0
- swcgeom-0.19.4.dist-info/METADATA +86 -0
- swcgeom-0.19.4.dist-info/RECORD +72 -0
- swcgeom-0.19.4.dist-info/WHEEL +5 -0
- swcgeom-0.19.4.dist-info/licenses/LICENSE +201 -0
- swcgeom-0.19.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""Image stack related transform."""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import numpy.typing as npt
|
|
10
|
+
from typing_extensions import deprecated, override
|
|
11
|
+
|
|
12
|
+
from swcgeom.transforms.base import Identity, Transform
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ImagesCenterCrop",
|
|
16
|
+
"ImagesScale",
|
|
17
|
+
"ImagesClip",
|
|
18
|
+
"ImagesFlip",
|
|
19
|
+
"ImagesFlipY",
|
|
20
|
+
"ImagesNormalizer",
|
|
21
|
+
"ImagesMeanVarianceAdjustment",
|
|
22
|
+
"ImagesScaleToUnitRange",
|
|
23
|
+
"ImagesHistogramEqualization",
|
|
24
|
+
"Center", # legacy
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
NDArrayf32 = npt.NDArray[np.float32]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
|
|
32
|
+
"""Get image stack center."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, shape_out: int | tuple[int, int, int]):
|
|
35
|
+
super().__init__()
|
|
36
|
+
self.shape_out = (
|
|
37
|
+
shape_out
|
|
38
|
+
if isinstance(shape_out, tuple)
|
|
39
|
+
else (shape_out, shape_out, shape_out)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@override
|
|
43
|
+
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
44
|
+
diff = np.subtract(x.shape[:3], self.shape_out)
|
|
45
|
+
s = diff // 2
|
|
46
|
+
e = np.add(s, self.shape_out)
|
|
47
|
+
return x[s[0] : e[0], s[1] : e[1], s[2] : e[2], :]
|
|
48
|
+
|
|
49
|
+
@override
|
|
50
|
+
def extra_repr(self) -> str:
|
|
51
|
+
return f"shape_out=({','.join(str(a) for a in self.shape_out)})"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@deprecated("use `ImagesCenterCrop` instead", stacklevel=2)
|
|
55
|
+
class Center(ImagesCenterCrop):
|
|
56
|
+
"""Get image stack center.
|
|
57
|
+
|
|
58
|
+
.. deprecated:: 0.16.0
|
|
59
|
+
Use :class:`ImagesCenterCrop` instead.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ImagesScale(Transform[NDArrayf32, NDArrayf32]):
|
|
64
|
+
def __init__(self, scaler: float) -> None:
|
|
65
|
+
super().__init__()
|
|
66
|
+
self.scaler = scaler
|
|
67
|
+
|
|
68
|
+
@override
|
|
69
|
+
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
70
|
+
return self.scaler * x
|
|
71
|
+
|
|
72
|
+
@override
|
|
73
|
+
def extra_repr(self) -> str:
|
|
74
|
+
return f"scaler={self.scaler}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ImagesClip(Transform[NDArrayf32, NDArrayf32]):
|
|
78
|
+
def __init__(self, vmin: float = 0, vmax: float = 1, /) -> None:
|
|
79
|
+
super().__init__()
|
|
80
|
+
self.vmin, self.vmax = vmin, vmax
|
|
81
|
+
|
|
82
|
+
@override
|
|
83
|
+
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
84
|
+
return np.clip(x, self.vmin, self.vmax)
|
|
85
|
+
|
|
86
|
+
@override
|
|
87
|
+
def extra_repr(self) -> str:
|
|
88
|
+
return f"vmin={self.vmin}, vmax={self.vmax}"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ImagesFlip(Transform[NDArrayf32, NDArrayf32]):
|
|
92
|
+
"""Flip image stack along axis."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, axis: int, /) -> None:
|
|
95
|
+
super().__init__()
|
|
96
|
+
self.axis = axis
|
|
97
|
+
|
|
98
|
+
@override
|
|
99
|
+
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
100
|
+
return np.flip(x, axis=self.axis)
|
|
101
|
+
|
|
102
|
+
@override
|
|
103
|
+
def extra_repr(self) -> str:
|
|
104
|
+
return f"axis={self.axis}"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ImagesFlipY(ImagesFlip):
|
|
108
|
+
"""Flip image stack along Y-axis.
|
|
109
|
+
|
|
110
|
+
See Also:
|
|
111
|
+
~.images.io.TeraflyImageStack:
|
|
112
|
+
Terafly and Vaa3d use a especial right-handed coordinate system (with
|
|
113
|
+
origin point in the left-top and z-axis points front), but we flip y-axis
|
|
114
|
+
to makes it a left-handed coordinate system (with origin point in the
|
|
115
|
+
left-bottom and z-axis points front). If you need to use its coordinate
|
|
116
|
+
system, remember to FLIP Y-AXIS BACK.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(self, axis: int = 1, /) -> None:
|
|
120
|
+
super().__init__(axis) # (X, Y, Z, C)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ImagesNormalizer(Transform[NDArrayf32, NDArrayf32]):
|
|
124
|
+
"""Normalize image stack."""
|
|
125
|
+
|
|
126
|
+
@override
|
|
127
|
+
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
128
|
+
mean = np.mean(x)
|
|
129
|
+
variance = np.var(x)
|
|
130
|
+
return (x - mean) / variance
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class ImagesMeanVarianceAdjustment(Transform[NDArrayf32, NDArrayf32]):
|
|
134
|
+
"""Adjust image stack mean and variance.
|
|
135
|
+
|
|
136
|
+
See Also:
|
|
137
|
+
~swcgeom.images.ImageStackFolder.stat
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(self, mean: float, variance: float) -> None:
|
|
141
|
+
super().__init__()
|
|
142
|
+
self.mean = mean
|
|
143
|
+
self.variance = variance
|
|
144
|
+
|
|
145
|
+
@override
|
|
146
|
+
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
147
|
+
return (x - self.mean) / self.variance
|
|
148
|
+
|
|
149
|
+
@override
|
|
150
|
+
def extra_repr(self) -> str:
|
|
151
|
+
return f"mean={self.mean}, variance={self.variance}"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class ImagesScaleToUnitRange(Transform[NDArrayf32, NDArrayf32]):
|
|
155
|
+
"""Scale image stack to unit range."""
|
|
156
|
+
|
|
157
|
+
def __init__(self, vmin: float, vmax: float, *, clip: bool = True) -> None:
|
|
158
|
+
"""Scale image stack to unit range.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
vmin: Minimum value.
|
|
162
|
+
vmax: Maximum value.
|
|
163
|
+
clip: Clip values to [0, 1] to avoid numerical issues.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
super().__init__()
|
|
167
|
+
self.vmin = vmin
|
|
168
|
+
self.vmax = vmax
|
|
169
|
+
self.diff = vmax - vmin
|
|
170
|
+
self.clip = clip
|
|
171
|
+
self.post = ImagesClip(0, 1) if self.clip else Identity()
|
|
172
|
+
|
|
173
|
+
@override
|
|
174
|
+
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
175
|
+
return self.post((x - self.vmin) / self.diff)
|
|
176
|
+
|
|
177
|
+
@override
|
|
178
|
+
def extra_repr(self) -> str:
|
|
179
|
+
return f"vmin={self.vmin}, vmax={self.vmax}, clip={self.clip}"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class ImagesHistogramEqualization(Transform[NDArrayf32, NDArrayf32]):
|
|
183
|
+
"""Image histogram equalization.
|
|
184
|
+
|
|
185
|
+
References:
|
|
186
|
+
http://www.janeriksolem.net/histogram-equalization-with-python-and.html
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
def __init__(self, bins: int = 256) -> None:
|
|
190
|
+
super().__init__()
|
|
191
|
+
self.bins = bins
|
|
192
|
+
|
|
193
|
+
@override
|
|
194
|
+
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
195
|
+
# get image histogram
|
|
196
|
+
hist, bin_edges = np.histogram(x.flatten(), self.bins, density=True)
|
|
197
|
+
cdf = hist.cumsum() # cumulative distribution function
|
|
198
|
+
cdf = cdf / cdf[-1] # normalize
|
|
199
|
+
|
|
200
|
+
# use linear interpolation of cdf to find new pixel values
|
|
201
|
+
equalized = np.interp(x.flatten(), bin_edges[:-1], cdf)
|
|
202
|
+
return equalized.reshape(x.shape).astype(np.float32)
|
|
203
|
+
|
|
204
|
+
@override
|
|
205
|
+
def extra_repr(self) -> str:
|
|
206
|
+
return f"bins={self.bins}"
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""Minimum spanning tree."""
|
|
7
|
+
|
|
8
|
+
import warnings
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
from numpy import ma
|
|
13
|
+
from numpy import typing as npt
|
|
14
|
+
from typing_extensions import override
|
|
15
|
+
|
|
16
|
+
from swcgeom.core import Tree, sort_tree
|
|
17
|
+
from swcgeom.core.swc_utils import SWCNames, SWCTypes, get_names, get_types
|
|
18
|
+
from swcgeom.transforms.base import Transform
|
|
19
|
+
|
|
20
|
+
__all__ = ["PointsToCuntzMST", "PointsToMST"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PointsToCuntzMST(Transform[npt.NDArray[np.float32], Tree]):
|
|
24
|
+
"""Create tree from points.
|
|
25
|
+
|
|
26
|
+
Creates trees corresponding to the minimum spanning tree keeping
|
|
27
|
+
the path length to the root small (with balancing factor bf).
|
|
28
|
+
|
|
29
|
+
References:
|
|
30
|
+
.. [1] Cuntz, H., Forstner, F., Borst, A. & Häusser, M. One Rule to
|
|
31
|
+
Grow Them Al: A General Theory of Neuronal Branching and Its
|
|
32
|
+
Practical Application. PLOS Comput Biol 6, e1000877 (2010).
|
|
33
|
+
.. [2] Cuntz, H., Borst, A. & Segev, I. Optimization principles of
|
|
34
|
+
dendritic structure. Theor Biol Med Model 4, 21 (2007).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
bf: float = 0.4,
|
|
41
|
+
furcations: int = 2,
|
|
42
|
+
exclude_soma: bool = True,
|
|
43
|
+
sort: bool = True,
|
|
44
|
+
names: SWCNames | None = None,
|
|
45
|
+
types: SWCTypes | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Args:
|
|
49
|
+
bf: Balancing factor between 0~1.
|
|
50
|
+
furcations: Suppress multi-furcations which more than k.
|
|
51
|
+
If set to -1, no suppression.
|
|
52
|
+
exclude_soma: Suppress multi-furcations exclude soma.
|
|
53
|
+
"""
|
|
54
|
+
self.bf = np.clip(bf, 0, 1)
|
|
55
|
+
self.furcations = furcations
|
|
56
|
+
self.exclude_soma = exclude_soma
|
|
57
|
+
self.sort = sort
|
|
58
|
+
self.names = get_names(names)
|
|
59
|
+
self.types = get_types(types)
|
|
60
|
+
|
|
61
|
+
@override
|
|
62
|
+
def __call__( # pylint: disable=too-many-locals
|
|
63
|
+
self,
|
|
64
|
+
points: npt.NDArray[np.floating],
|
|
65
|
+
soma: npt.ArrayLike | None = None,
|
|
66
|
+
*,
|
|
67
|
+
names: SWCNames | None = None,
|
|
68
|
+
) -> Tree:
|
|
69
|
+
"""
|
|
70
|
+
Args:
|
|
71
|
+
points: Positions of points cloud.
|
|
72
|
+
soma: Position of soma. If none, use the first point as soma.
|
|
73
|
+
"""
|
|
74
|
+
if names is None:
|
|
75
|
+
names = self.names
|
|
76
|
+
else:
|
|
77
|
+
warnings.warn(
|
|
78
|
+
"`PointsToCuntzMST(...)(names=...)` has been replaced by "
|
|
79
|
+
"`PointsToCuntzMST(...,names=...)` since v0.12.0, and will be removed "
|
|
80
|
+
"in next version",
|
|
81
|
+
DeprecationWarning,
|
|
82
|
+
)
|
|
83
|
+
names = get_names(names) # TODO: remove it
|
|
84
|
+
|
|
85
|
+
if soma is not None:
|
|
86
|
+
soma = np.array(soma)
|
|
87
|
+
assert soma.shape == (3,)
|
|
88
|
+
points = np.concatenate([[soma], points])
|
|
89
|
+
|
|
90
|
+
n = points.shape[0]
|
|
91
|
+
dis = np.linalg.norm(
|
|
92
|
+
points.reshape((-1, 1, 3)) - points.reshape((1, -1, 3)), axis=2
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
pid = np.full(n, fill_value=-1)
|
|
96
|
+
acc = np.zeros(n)
|
|
97
|
+
furcations = np.zeros(n, dtype=np.int32)
|
|
98
|
+
conn = np.zeros(n, dtype=np.bool_) # connect points
|
|
99
|
+
conn[0] = True
|
|
100
|
+
mask = np.ones((n, n), dtype=np.bool_)
|
|
101
|
+
mask[0, :] = False
|
|
102
|
+
mask[0, 0] = True
|
|
103
|
+
for _ in range(n - 1): # for tree: e = n-1
|
|
104
|
+
cost = ma.array(dis + self.bf * acc, mask=mask)
|
|
105
|
+
(i, j) = np.unravel_index(cost.argmin(), cost.shape)
|
|
106
|
+
|
|
107
|
+
furcations[i] += 1
|
|
108
|
+
if (
|
|
109
|
+
self.furcations != -1
|
|
110
|
+
and furcations[i] >= self.furcations
|
|
111
|
+
and (not self.exclude_soma or i != 0)
|
|
112
|
+
):
|
|
113
|
+
mask[i, :] = True # avoid it link to any point
|
|
114
|
+
mask[:, i] = True # avoid any point lint to it
|
|
115
|
+
|
|
116
|
+
pid[j] = i
|
|
117
|
+
acc[j] = acc[i] + dis[i, j]
|
|
118
|
+
conn[j] = True
|
|
119
|
+
mask[j, :] = conn # enable link to other points
|
|
120
|
+
mask[:, j] = True # avoid any point lint to it
|
|
121
|
+
|
|
122
|
+
dic = {
|
|
123
|
+
names.id: np.arange(n),
|
|
124
|
+
names.type: np.full(n, fill_value=self.types.glia_processes),
|
|
125
|
+
names.x: points[:, 0],
|
|
126
|
+
names.y: points[:, 1],
|
|
127
|
+
names.z: points[:, 2],
|
|
128
|
+
names.r: 1,
|
|
129
|
+
names.pid: pid,
|
|
130
|
+
}
|
|
131
|
+
dic[names.type][0] = self.types.soma
|
|
132
|
+
df = pd.DataFrame.from_dict(dic)
|
|
133
|
+
t = Tree.from_data_frame(df, names=names)
|
|
134
|
+
if self.sort:
|
|
135
|
+
t = sort_tree(t)
|
|
136
|
+
return t
|
|
137
|
+
|
|
138
|
+
@override
|
|
139
|
+
def extra_repr(self) -> str: # TODO: names, types
|
|
140
|
+
return f"bf={self.bf:.4f}, furcations={self.furcations}, exclude_soma={self.exclude_soma}, sort={self.sort}"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class PointsToMST(PointsToCuntzMST): # pylint: disable=too-few-public-methods
|
|
144
|
+
"""Create minimum spanning tree from points."""
|
|
145
|
+
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
furcations: int = 2,
|
|
149
|
+
*,
|
|
150
|
+
k_furcations: int | None = None,
|
|
151
|
+
exclude_soma: bool = True,
|
|
152
|
+
names: SWCNames | None = None,
|
|
153
|
+
types: SWCTypes | None = None,
|
|
154
|
+
**kwargs,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""
|
|
157
|
+
Args:
|
|
158
|
+
furcations: Suppress multifurcations which more than k.
|
|
159
|
+
If set to -1, no suppression.
|
|
160
|
+
exclude_soma: Suppress multi-furcations exclude soma.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
if k_furcations is not None:
|
|
164
|
+
warnings.warn(
|
|
165
|
+
"`PointsToMST(k_furcations=...)` has been renamed to "
|
|
166
|
+
"`PointsToMST(furcations=...)` since v0.12.0, and will "
|
|
167
|
+
"be removed in next version",
|
|
168
|
+
DeprecationWarning,
|
|
169
|
+
)
|
|
170
|
+
furcations = k_furcations
|
|
171
|
+
|
|
172
|
+
super().__init__(
|
|
173
|
+
bf=0,
|
|
174
|
+
furcations=furcations,
|
|
175
|
+
exclude_soma=exclude_soma,
|
|
176
|
+
names=names,
|
|
177
|
+
types=types,
|
|
178
|
+
**kwargs,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
@override
|
|
182
|
+
def extra_repr(self) -> str:
|
|
183
|
+
return f"furcations-{self.furcations}, exclude-soma={self.exclude_soma}"
|