swcgeom 0.17.1__py3-none-any.whl → 0.18.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.
- swcgeom/__init__.py +14 -0
- swcgeom/_version.py +2 -2
- swcgeom/analysis/__init__.py +15 -0
- swcgeom/analysis/feature_extractor.py +27 -3
- swcgeom/analysis/features.py +31 -4
- swcgeom/analysis/lmeasure.py +43 -7
- swcgeom/analysis/sholl.py +21 -24
- swcgeom/analysis/trunk.py +15 -0
- swcgeom/analysis/visualization.py +15 -0
- swcgeom/analysis/visualization3d.py +15 -0
- swcgeom/analysis/volume.py +15 -0
- swcgeom/core/__init__.py +15 -0
- swcgeom/core/branch.py +15 -0
- swcgeom/core/branch_tree.py +15 -0
- swcgeom/core/compartment.py +15 -0
- swcgeom/core/node.py +30 -1
- swcgeom/core/path.py +18 -7
- swcgeom/core/population.py +43 -3
- swcgeom/core/swc.py +15 -0
- swcgeom/core/swc_utils/__init__.py +15 -1
- swcgeom/core/swc_utils/assembler.py +15 -0
- swcgeom/core/swc_utils/base.py +15 -0
- swcgeom/core/swc_utils/checker.py +19 -12
- swcgeom/core/swc_utils/io.py +17 -1
- swcgeom/core/swc_utils/normalizer.py +16 -1
- swcgeom/core/swc_utils/subtree.py +15 -0
- swcgeom/core/tree.py +37 -9
- swcgeom/core/tree_utils.py +17 -7
- swcgeom/core/tree_utils_impl.py +15 -0
- swcgeom/images/__init__.py +15 -0
- swcgeom/images/augmentation.py +15 -0
- swcgeom/images/contrast.py +15 -0
- swcgeom/images/folder.py +17 -10
- swcgeom/images/io.py +18 -6
- swcgeom/transforms/__init__.py +16 -0
- swcgeom/transforms/base.py +17 -2
- swcgeom/transforms/branch.py +74 -8
- swcgeom/transforms/branch_tree.py +82 -0
- swcgeom/transforms/geometry.py +22 -7
- swcgeom/transforms/image_preprocess.py +15 -0
- swcgeom/transforms/image_stack.py +30 -4
- swcgeom/transforms/images.py +17 -10
- swcgeom/transforms/mst.py +15 -0
- swcgeom/transforms/neurolucida_asc.py +16 -1
- swcgeom/transforms/path.py +15 -0
- swcgeom/transforms/population.py +15 -0
- swcgeom/transforms/tree.py +76 -23
- swcgeom/transforms/tree_assembler.py +19 -4
- swcgeom/utils/__init__.py +15 -0
- swcgeom/utils/debug.py +15 -0
- swcgeom/utils/download.py +59 -21
- swcgeom/utils/dsu.py +15 -0
- swcgeom/utils/ellipse.py +15 -0
- swcgeom/utils/file.py +15 -0
- swcgeom/utils/neuromorpho.py +18 -7
- swcgeom/utils/numpy_helper.py +15 -0
- swcgeom/utils/plotter_2d.py +15 -0
- swcgeom/utils/plotter_3d.py +18 -1
- swcgeom/utils/renderer.py +15 -0
- swcgeom/utils/sdf.py +17 -5
- swcgeom/utils/solid_geometry.py +15 -0
- swcgeom/utils/transforms.py +16 -1
- swcgeom/utils/volumetric_object.py +15 -0
- {swcgeom-0.17.1.dist-info → swcgeom-0.18.1.dist-info}/LICENSE +1 -1
- {swcgeom-0.17.1.dist-info → swcgeom-0.18.1.dist-info}/METADATA +26 -22
- swcgeom-0.18.1.dist-info/RECORD +68 -0
- {swcgeom-0.17.1.dist-info → swcgeom-0.18.1.dist-info}/WHEEL +1 -1
- swcgeom-0.17.1.dist-info/RECORD +0 -67
- {swcgeom-0.17.1.dist-info → swcgeom-0.18.1.dist-info}/top_level.txt +0 -0
swcgeom/transforms/branch.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
|
"""Transformation in branch."""
|
|
2
17
|
|
|
3
18
|
from abc import ABC, abstractmethod
|
|
@@ -72,10 +87,61 @@ class BranchLinearResampler(_BranchResampler):
|
|
|
72
87
|
r = np.interp(xvals, xp, xyzr[:, 3])
|
|
73
88
|
return cast(npt.NDArray[np.float32], np.stack([x, y, z, r], axis=1))
|
|
74
89
|
|
|
75
|
-
def extra_repr(self):
|
|
90
|
+
def extra_repr(self) -> str:
|
|
76
91
|
return f"n_nodes={self.n_nodes}"
|
|
77
92
|
|
|
78
93
|
|
|
94
|
+
class BranchIsometricResampler(_BranchResampler):
|
|
95
|
+
def __init__(self, distance: float, *, adjust_last_gap: bool = True) -> None:
|
|
96
|
+
super().__init__()
|
|
97
|
+
self.distance = distance
|
|
98
|
+
self.adjust_last_gap = adjust_last_gap
|
|
99
|
+
|
|
100
|
+
def resample(self, xyzr: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
|
|
101
|
+
"""Resampling by isometric interpolation, DO NOT keep original node.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
xyzr : np.ndarray[np.float32]
|
|
106
|
+
The array of shape (N, 4).
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
new_xyzr : ~numpy.NDArray[float32]
|
|
111
|
+
An array of shape (n_nodes, 4).
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
# Compute the cumulative distances between consecutive points
|
|
115
|
+
diffs = np.diff(xyzr[:, :3], axis=0)
|
|
116
|
+
distances = np.sqrt((diffs**2).sum(axis=1))
|
|
117
|
+
cumulative_distances = np.concatenate([[0], np.cumsum(distances)])
|
|
118
|
+
|
|
119
|
+
total_length = cumulative_distances[-1]
|
|
120
|
+
n_nodes = int(np.ceil(total_length / self.distance)) + 1
|
|
121
|
+
|
|
122
|
+
# Determine the new distances
|
|
123
|
+
if self.adjust_last_gap and n_nodes > 1:
|
|
124
|
+
new_distances = np.linspace(0, total_length, n_nodes)
|
|
125
|
+
else:
|
|
126
|
+
new_distances = np.arange(0, total_length, self.distance)
|
|
127
|
+
# keep endpoint
|
|
128
|
+
new_distances = np.concatenate([new_distances, total_length])
|
|
129
|
+
|
|
130
|
+
# Interpolate the new points
|
|
131
|
+
new_xyzr = np.zeros((n_nodes, 4), dtype=np.float32)
|
|
132
|
+
new_xyzr[:, :3] = np.array(
|
|
133
|
+
[
|
|
134
|
+
np.interp(new_distances, cumulative_distances, xyzr[:, i])
|
|
135
|
+
for i in range(3)
|
|
136
|
+
]
|
|
137
|
+
).T
|
|
138
|
+
new_xyzr[:, 3] = np.interp(new_distances, cumulative_distances, xyzr[:, 3])
|
|
139
|
+
return new_xyzr
|
|
140
|
+
|
|
141
|
+
def extra_repr(self) -> str:
|
|
142
|
+
return f"distance={self.distance},adjust_last_gap={self.adjust_last_gap}"
|
|
143
|
+
|
|
144
|
+
|
|
79
145
|
class BranchConvSmoother(Transform[Branch, Branch[DictSWC]]):
|
|
80
146
|
r"""Smooth the branch by sliding window."""
|
|
81
147
|
|
|
@@ -88,24 +154,24 @@ class BranchConvSmoother(Transform[Branch, Branch[DictSWC]]):
|
|
|
88
154
|
"""
|
|
89
155
|
super().__init__()
|
|
90
156
|
self.n_nodes = n_nodes
|
|
91
|
-
self.
|
|
157
|
+
self.kernel = np.ones(n_nodes)
|
|
92
158
|
|
|
93
159
|
def __call__(self, x: Branch) -> Branch[DictSWC]:
|
|
94
160
|
x = x.detach()
|
|
95
|
-
c = signal.convolve(np.ones(x.number_of_nodes()), self.
|
|
161
|
+
c = signal.convolve(np.ones(x.number_of_nodes()), self.kernel, mode="same")
|
|
96
162
|
for k in ["x", "y", "z"]:
|
|
97
163
|
v = x.get_ndata(k)
|
|
98
|
-
s = signal.convolve(v, self.
|
|
164
|
+
s = signal.convolve(v, self.kernel, mode="same")
|
|
99
165
|
x.attach.ndata[k][1:-1] = (s / c)[1:-1]
|
|
100
166
|
|
|
101
167
|
return x
|
|
102
168
|
|
|
103
|
-
def extra_repr(self):
|
|
169
|
+
def extra_repr(self) -> str:
|
|
104
170
|
return f"n_nodes={self.n_nodes}"
|
|
105
171
|
|
|
106
172
|
|
|
107
173
|
class BranchStandardizer(Transform[Branch, Branch[DictSWC]]):
|
|
108
|
-
r"""
|
|
174
|
+
r"""Standardize branch.
|
|
109
175
|
|
|
110
176
|
Standardized branch starts at (0, 0, 0), ends at (1, 0, 0), up at
|
|
111
177
|
y, and scale max radius to 1.
|
|
@@ -123,7 +189,7 @@ class BranchStandardizer(Transform[Branch, Branch[DictSWC]]):
|
|
|
123
189
|
|
|
124
190
|
@staticmethod
|
|
125
191
|
def get_matrix(xyz: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
|
|
126
|
-
r"""Get
|
|
192
|
+
r"""Get standardize transformation matrix.
|
|
127
193
|
|
|
128
194
|
Standardized branch starts at (0, 0, 0), ends at (1, 0, 0), up
|
|
129
195
|
at y.
|
|
@@ -136,7 +202,7 @@ class BranchStandardizer(Transform[Branch, Branch[DictSWC]]):
|
|
|
136
202
|
Returns
|
|
137
203
|
-------
|
|
138
204
|
T : np.ndarray[np.float32]
|
|
139
|
-
An homogeneous
|
|
205
|
+
An homogeneous transformation matrix of shape (4, 4).
|
|
140
206
|
"""
|
|
141
207
|
|
|
142
208
|
assert (
|
|
@@ -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
|
swcgeom/transforms/geometry.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
|
"""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:
|
|
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
|
|
@@ -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
|
"""Image stack pre-processing."""
|
|
2
17
|
|
|
3
18
|
import numpy as np
|
|
@@ -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
|
|
@@ -27,6 +42,7 @@ from sdflit import (
|
|
|
27
42
|
SDFObject,
|
|
28
43
|
)
|
|
29
44
|
from tqdm import tqdm
|
|
45
|
+
from typing_extensions import deprecated
|
|
30
46
|
|
|
31
47
|
from swcgeom.core import Population, Tree
|
|
32
48
|
from swcgeom.transforms.base import Transform
|
|
@@ -63,9 +79,9 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
|
|
|
63
79
|
ONLY works for small image stacks, use :meth`transform_and_save`
|
|
64
80
|
for big image stack.
|
|
65
81
|
"""
|
|
66
|
-
return np.stack(list(self.
|
|
82
|
+
return np.stack(list(self.transform(x, verbose=False)), axis=0)
|
|
67
83
|
|
|
68
|
-
def
|
|
84
|
+
def transform(
|
|
69
85
|
self,
|
|
70
86
|
x: Tree,
|
|
71
87
|
verbose: bool = True,
|
|
@@ -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.
|
|
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
|
|
@@ -125,7 +151,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
|
|
|
125
151
|
if not os.path.isfile(tif):
|
|
126
152
|
self.transform_and_save(tif, tree, verbose=False)
|
|
127
153
|
|
|
128
|
-
def extra_repr(self):
|
|
154
|
+
def extra_repr(self) -> str:
|
|
129
155
|
res = ",".join(f"{a:.4f}" for a in self.resolution)
|
|
130
156
|
return f"resolution=({res})"
|
|
131
157
|
|
swcgeom/transforms/images.py
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
|
|
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
|
+
|
|
2
15
|
|
|
3
|
-
|
|
16
|
+
"""Image stack related transform."""
|
|
4
17
|
|
|
5
18
|
import numpy as np
|
|
6
19
|
import numpy.typing as npt
|
|
20
|
+
from typing_extensions import deprecated
|
|
7
21
|
|
|
8
22
|
from swcgeom.transforms.base import Identity, Transform
|
|
9
23
|
|
|
@@ -45,6 +59,7 @@ class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
|
|
|
45
59
|
return f"shape_out=({','.join(str(a) for a in self.shape_out)})"
|
|
46
60
|
|
|
47
61
|
|
|
62
|
+
@deprecated("use `ImagesCenterCrop` instead", stacklevel=2)
|
|
48
63
|
class Center(ImagesCenterCrop):
|
|
49
64
|
"""Get image stack center.
|
|
50
65
|
|
|
@@ -52,14 +67,6 @@ class Center(ImagesCenterCrop):
|
|
|
52
67
|
Use :class:`ImagesCenterCrop` instead.
|
|
53
68
|
"""
|
|
54
69
|
|
|
55
|
-
def __init__(self, shape_out: int | tuple[int, int, int]):
|
|
56
|
-
warnings.warn(
|
|
57
|
-
"`Center` is deprecated, use `ImagesCenterCrop` instead",
|
|
58
|
-
DeprecationWarning,
|
|
59
|
-
stacklevel=2,
|
|
60
|
-
)
|
|
61
|
-
super().__init__(shape_out)
|
|
62
|
-
|
|
63
70
|
|
|
64
71
|
class ImagesScale(Transform[NDArrayf32, NDArrayf32]):
|
|
65
72
|
def __init__(self, scaler: float) -> None:
|
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
|
|
@@ -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
|
"""Neurolucida related transformation."""
|
|
2
17
|
|
|
3
18
|
import os
|
|
@@ -421,7 +436,7 @@ class Lexer:
|
|
|
421
436
|
return self
|
|
422
437
|
|
|
423
438
|
def __next__(self) -> Token:
|
|
424
|
-
match
|
|
439
|
+
match word := self._read_word():
|
|
425
440
|
case "":
|
|
426
441
|
raise StopIteration
|
|
427
442
|
|
swcgeom/transforms/path.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
|
"""Transformation in path."""
|
|
2
17
|
|
|
3
18
|
from swcgeom.core import Path, Tree, redirect_tree
|
swcgeom/transforms/population.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
|
"""Transformation in population."""
|
|
2
17
|
|
|
3
18
|
from swcgeom.core import Population, Tree
|
swcgeom/transforms/tree.py
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
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
|
"""Transformation in tree."""
|
|
2
17
|
|
|
3
|
-
import warnings
|
|
4
18
|
from collections.abc import Callable
|
|
5
19
|
from typing import Optional
|
|
6
20
|
|
|
7
21
|
import numpy as np
|
|
22
|
+
from typing_extensions import deprecated
|
|
8
23
|
|
|
9
|
-
from swcgeom.core import BranchTree, DictSWC, Path, Tree, cut_tree, to_subtree
|
|
24
|
+
from swcgeom.core import Branch, BranchTree, DictSWC, Path, Tree, cut_tree, to_subtree
|
|
10
25
|
from swcgeom.core.swc_utils import SWCTypes, get_types
|
|
11
26
|
from swcgeom.transforms.base import Transform
|
|
12
|
-
from swcgeom.transforms.branch import BranchConvSmoother
|
|
27
|
+
from swcgeom.transforms.branch import BranchConvSmoother, BranchIsometricResampler
|
|
28
|
+
from swcgeom.transforms.branch_tree import BranchTreeAssembler
|
|
13
29
|
from swcgeom.transforms.geometry import Normalizer
|
|
14
30
|
|
|
15
31
|
__all__ = [
|
|
@@ -20,8 +36,9 @@ __all__ = [
|
|
|
20
36
|
"CutByType",
|
|
21
37
|
"CutAxonTree",
|
|
22
38
|
"CutDendriteTree",
|
|
23
|
-
"
|
|
39
|
+
"CutByFurcationOrder",
|
|
24
40
|
"CutShortTipBranch",
|
|
41
|
+
"IsometricResampler",
|
|
25
42
|
]
|
|
26
43
|
|
|
27
44
|
|
|
@@ -65,10 +82,11 @@ class TreeSmoother(Transform[Tree, Tree]): # pylint: disable=missing-class-docs
|
|
|
65
82
|
|
|
66
83
|
return x
|
|
67
84
|
|
|
68
|
-
def extra_repr(self):
|
|
85
|
+
def extra_repr(self) -> str:
|
|
69
86
|
return f"n_nodes={self.n_nodes}"
|
|
70
87
|
|
|
71
88
|
|
|
89
|
+
@deprecated("Use `Normalizer` instead")
|
|
72
90
|
class TreeNormalizer(Normalizer[Tree]):
|
|
73
91
|
"""Noramlize coordinates and radius to 0-1.
|
|
74
92
|
|
|
@@ -76,15 +94,6 @@ class TreeNormalizer(Normalizer[Tree]):
|
|
|
76
94
|
Use :cls:`Normalizer` instead.
|
|
77
95
|
"""
|
|
78
96
|
|
|
79
|
-
def __init__(self, *args, **kwargs) -> None:
|
|
80
|
-
warnings.warn(
|
|
81
|
-
"`TreeNormalizer` has been replaced by `Normalizer` since "
|
|
82
|
-
"v0.6.0 beacuse it applies more widely, and this will be "
|
|
83
|
-
"removed in next version",
|
|
84
|
-
DeprecationWarning,
|
|
85
|
-
)
|
|
86
|
-
super().__init__(*args, **kwargs)
|
|
87
|
-
|
|
88
97
|
|
|
89
98
|
class CutByType(Transform[Tree, Tree]):
|
|
90
99
|
"""Cut tree by type.
|
|
@@ -112,7 +121,7 @@ class CutByType(Transform[Tree, Tree]):
|
|
|
112
121
|
y = to_subtree(x, removals)
|
|
113
122
|
return y
|
|
114
123
|
|
|
115
|
-
def extra_repr(self):
|
|
124
|
+
def extra_repr(self) -> str:
|
|
116
125
|
return f"type={self.type}"
|
|
117
126
|
|
|
118
127
|
|
|
@@ -132,28 +141,48 @@ class CutDendriteTree(CutByType):
|
|
|
132
141
|
super().__init__(type=types.basal_dendrite) # TODO: apical dendrite
|
|
133
142
|
|
|
134
143
|
|
|
135
|
-
class
|
|
136
|
-
"""Cut tree by
|
|
144
|
+
class CutByFurcationOrder(Transform[Tree, Tree]):
|
|
145
|
+
"""Cut tree by furcation order."""
|
|
137
146
|
|
|
138
|
-
|
|
147
|
+
max_furcation_order: int
|
|
139
148
|
|
|
140
149
|
def __init__(self, max_bifurcation_order: int) -> None:
|
|
141
|
-
self.
|
|
150
|
+
self.max_furcation_order = max_bifurcation_order
|
|
142
151
|
|
|
143
152
|
def __call__(self, x: Tree) -> Tree:
|
|
144
153
|
return cut_tree(x, enter=self._enter)
|
|
145
154
|
|
|
146
155
|
def __repr__(self) -> str:
|
|
147
|
-
return f"CutByBifurcationOrder-{self.
|
|
156
|
+
return f"CutByBifurcationOrder-{self.max_furcation_order}"
|
|
148
157
|
|
|
149
158
|
def _enter(self, n: Tree.Node, parent_level: int | None) -> tuple[int, bool]:
|
|
150
159
|
if parent_level is None:
|
|
151
160
|
level = 0
|
|
152
|
-
elif n.
|
|
161
|
+
elif n.is_furcation():
|
|
153
162
|
level = parent_level + 1
|
|
154
163
|
else:
|
|
155
164
|
level = parent_level
|
|
156
|
-
return (level, level >= self.
|
|
165
|
+
return (level, level >= self.max_furcation_order)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@deprecated("Use CutByFurcationOrder instead")
|
|
169
|
+
class CutByBifurcationOrder(CutByFurcationOrder):
|
|
170
|
+
"""Cut tree by bifurcation order.
|
|
171
|
+
|
|
172
|
+
Notes
|
|
173
|
+
-----
|
|
174
|
+
Deprecated due to the wrong spelling of furcation. For now, it
|
|
175
|
+
is just an alias of `CutByFurcationOrder` and raise a warning. It
|
|
176
|
+
will be change to raise an error in the future.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
max_furcation_order: int
|
|
180
|
+
|
|
181
|
+
def __init__(self, max_bifurcation_order: int) -> None:
|
|
182
|
+
super().__init__(max_bifurcation_order)
|
|
183
|
+
|
|
184
|
+
def __repr__(self) -> str:
|
|
185
|
+
return f"CutByBifurcationOrder-{self.max_furcation_order}"
|
|
157
186
|
|
|
158
187
|
|
|
159
188
|
class CutShortTipBranch(Transform[Tree, Tree]):
|
|
@@ -183,7 +212,7 @@ class CutShortTipBranch(Transform[Tree, Tree]):
|
|
|
183
212
|
self.callbacks.pop()
|
|
184
213
|
return to_subtree(x, removals)
|
|
185
214
|
|
|
186
|
-
def extra_repr(self):
|
|
215
|
+
def extra_repr(self) -> str:
|
|
187
216
|
return f"threshold={self.thre}"
|
|
188
217
|
|
|
189
218
|
def _leave(
|
|
@@ -215,3 +244,27 @@ class CutShortTipBranch(Transform[Tree, Tree]):
|
|
|
215
244
|
cb(br)
|
|
216
245
|
|
|
217
246
|
return None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class Resampler(Transform[Tree, Tree]):
|
|
250
|
+
def __init__(self, branch_resampler: Transform[Branch, Branch]) -> None:
|
|
251
|
+
super().__init__()
|
|
252
|
+
self.resampler = branch_resampler
|
|
253
|
+
self.assembler = BranchTreeAssembler()
|
|
254
|
+
|
|
255
|
+
def __call__(self, x: Tree) -> Tree:
|
|
256
|
+
t = BranchTree.from_tree(x)
|
|
257
|
+
t.branches = {
|
|
258
|
+
k: [self.resampler(br) for br in brs] for k, brs in t.branches.items()
|
|
259
|
+
}
|
|
260
|
+
return self.assembler(t)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class IsometricResampler(Resampler):
|
|
264
|
+
def __init__(
|
|
265
|
+
self, distance: float, *, adjust_last_gap: bool = True, **kwargs
|
|
266
|
+
) -> None:
|
|
267
|
+
branch_resampler = BranchIsometricResampler(
|
|
268
|
+
distance, adjust_last_gap=adjust_last_gap, **kwargs
|
|
269
|
+
)
|
|
270
|
+
super().__init__(branch_resampler)
|