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.
- swcgeom/__init__.py +26 -1
- swcgeom/analysis/__init__.py +21 -8
- swcgeom/analysis/feature_extractor.py +43 -18
- swcgeom/analysis/features.py +250 -0
- swcgeom/analysis/lmeasure.py +857 -0
- swcgeom/analysis/sholl.py +55 -29
- swcgeom/analysis/trunk.py +27 -11
- swcgeom/analysis/visualization.py +24 -9
- swcgeom/analysis/visualization3d.py +100 -0
- swcgeom/analysis/volume.py +19 -4
- swcgeom/core/__init__.py +32 -9
- swcgeom/core/branch.py +28 -7
- swcgeom/core/branch_tree.py +18 -4
- swcgeom/core/{segment.py → compartment.py} +31 -10
- swcgeom/core/node.py +31 -10
- swcgeom/core/path.py +37 -10
- swcgeom/core/population.py +103 -34
- swcgeom/core/swc.py +26 -10
- swcgeom/core/swc_utils/__init__.py +21 -7
- swcgeom/core/swc_utils/assembler.py +27 -1
- swcgeom/core/swc_utils/base.py +25 -12
- swcgeom/core/swc_utils/checker.py +31 -14
- swcgeom/core/swc_utils/io.py +24 -7
- swcgeom/core/swc_utils/normalizer.py +20 -4
- swcgeom/core/swc_utils/subtree.py +17 -2
- swcgeom/core/tree.py +85 -72
- swcgeom/core/tree_utils.py +31 -16
- swcgeom/core/tree_utils_impl.py +18 -3
- swcgeom/images/__init__.py +17 -2
- swcgeom/images/augmentation.py +24 -4
- swcgeom/images/contrast.py +122 -0
- swcgeom/images/folder.py +97 -39
- swcgeom/images/io.py +108 -121
- swcgeom/transforms/__init__.py +28 -10
- 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 +115 -0
- swcgeom/transforms/image_stack.py +37 -13
- swcgeom/transforms/images.py +184 -7
- swcgeom/transforms/mst.py +20 -5
- swcgeom/transforms/neurolucida_asc.py +508 -0
- swcgeom/transforms/path.py +15 -0
- swcgeom/transforms/population.py +16 -3
- swcgeom/transforms/tree.py +89 -31
- swcgeom/transforms/tree_assembler.py +23 -7
- swcgeom/utils/__init__.py +27 -11
- swcgeom/utils/debug.py +15 -0
- swcgeom/utils/download.py +59 -21
- swcgeom/utils/dsu.py +15 -0
- swcgeom/utils/ellipse.py +18 -4
- swcgeom/utils/file.py +15 -0
- swcgeom/utils/neuromorpho.py +439 -302
- swcgeom/utils/numpy_helper.py +29 -4
- swcgeom/utils/plotter_2d.py +151 -0
- swcgeom/utils/plotter_3d.py +48 -0
- swcgeom/utils/renderer.py +49 -145
- swcgeom/utils/sdf.py +24 -8
- swcgeom/utils/solid_geometry.py +16 -3
- swcgeom/utils/transforms.py +17 -4
- swcgeom/utils/volumetric_object.py +23 -10
- {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/LICENSE +1 -1
- {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/METADATA +28 -24
- swcgeom-0.18.3.dist-info/RECORD +67 -0
- {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/WHEEL +1 -1
- swcgeom/_version.py +0 -16
- swcgeom/analysis/branch_features.py +0 -67
- swcgeom/analysis/node_features.py +0 -121
- swcgeom/analysis/path_features.py +0 -37
- swcgeom-0.15.0.dist-info/RECORD +0 -62
- {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
|
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
|
|
@@ -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
|
|
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.
|
|
82
|
+
return np.stack(list(self.transform(x, verbose=False)), axis=0)
|
|
65
83
|
|
|
66
|
-
def
|
|
84
|
+
def transform(
|
|
67
85
|
self,
|
|
68
86
|
x: Tree,
|
|
69
87
|
verbose: bool = True,
|
|
70
88
|
*,
|
|
71
|
-
ranges: Optional[
|
|
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.
|
|
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:
|
|
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:
|
|
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) ->
|
|
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]))
|
swcgeom/transforms/images.py
CHANGED
|
@@ -1,20 +1,47 @@
|
|
|
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.
|
|
2
14
|
|
|
3
15
|
|
|
4
|
-
|
|
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
|
-
|
|
38
|
+
NDArrayf32 = npt.NDArray[np.float32]
|
|
12
39
|
|
|
13
40
|
|
|
14
|
-
class
|
|
41
|
+
class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
|
|
15
42
|
"""Get image stack center."""
|
|
16
43
|
|
|
17
|
-
def __init__(self, shape_out: 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:
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
[2] Cuntz, H., Borst, A. & Segev, I. Optimization principles of
|
|
30
|
-
|
|
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__(
|