swcgeom 0.13.2__py3-none-any.whl → 0.15.0__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/_version.py +2 -2
- swcgeom/analysis/volume.py +105 -20
- swcgeom/core/branch_tree.py +2 -3
- swcgeom/core/population.py +6 -7
- swcgeom/core/swc_utils/assembler.py +12 -141
- swcgeom/core/swc_utils/subtree.py +2 -2
- swcgeom/core/tree.py +19 -12
- swcgeom/core/tree_utils.py +23 -5
- swcgeom/core/tree_utils_impl.py +22 -6
- swcgeom/images/folder.py +42 -17
- swcgeom/images/io.py +65 -36
- swcgeom/transforms/base.py +41 -21
- swcgeom/transforms/branch.py +5 -5
- swcgeom/transforms/geometry.py +42 -18
- swcgeom/transforms/image_stack.py +104 -124
- swcgeom/transforms/images.py +2 -2
- swcgeom/transforms/mst.py +5 -13
- swcgeom/transforms/population.py +2 -2
- swcgeom/transforms/tree.py +7 -13
- swcgeom/transforms/tree_assembler.py +85 -19
- swcgeom/utils/__init__.py +1 -1
- swcgeom/utils/sdf.py +167 -10
- swcgeom/utils/solid_geometry.py +26 -0
- swcgeom/utils/volumetric_object.py +504 -0
- swcgeom-0.15.0.dist-info/LICENSE +201 -0
- {swcgeom-0.13.2.dist-info → swcgeom-0.15.0.dist-info}/METADATA +6 -5
- {swcgeom-0.13.2.dist-info → swcgeom-0.15.0.dist-info}/RECORD +29 -29
- swcgeom/utils/geometry_object.py +0 -255
- swcgeom-0.13.2.dist-info/LICENSE +0 -3
- {swcgeom-0.13.2.dist-info → swcgeom-0.15.0.dist-info}/WHEEL +0 -0
- {swcgeom-0.13.2.dist-info → swcgeom-0.15.0.dist-info}/top_level.txt +0 -0
|
@@ -2,60 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
Notes
|
|
4
4
|
-----
|
|
5
|
-
|
|
6
|
-
- All denpendencies need to be installed, try:
|
|
5
|
+
All denpendencies need to be installed, try:
|
|
7
6
|
|
|
8
7
|
```sh
|
|
9
8
|
pip install swcgeom[all]
|
|
10
9
|
```
|
|
11
10
|
"""
|
|
12
11
|
|
|
13
|
-
import math
|
|
14
12
|
import os
|
|
15
13
|
import re
|
|
16
14
|
import time
|
|
17
|
-
from typing import
|
|
15
|
+
from typing import Iterable, List, Optional, Tuple
|
|
18
16
|
|
|
19
17
|
import numpy as np
|
|
20
18
|
import numpy.typing as npt
|
|
21
19
|
import tifffile
|
|
20
|
+
from sdflit import (
|
|
21
|
+
ColoredMaterial,
|
|
22
|
+
ObjectsScene,
|
|
23
|
+
RangeSampler,
|
|
24
|
+
RoundCone,
|
|
25
|
+
Scene,
|
|
26
|
+
SDFObject,
|
|
27
|
+
)
|
|
22
28
|
|
|
23
29
|
from swcgeom.core import Population, Tree
|
|
24
30
|
from swcgeom.transforms.base import Transform
|
|
25
|
-
from swcgeom.utils import SDF, SDFCompose, SDFRoundCone
|
|
26
31
|
|
|
27
32
|
__all__ = ["ToImageStack"]
|
|
28
33
|
|
|
29
34
|
|
|
30
|
-
# TODO: migrate to github.com/yzx9/swc2skeleton
|
|
31
35
|
class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
|
|
32
36
|
r"""Transform tree to image stack."""
|
|
33
37
|
|
|
34
38
|
resolution: npt.NDArray[np.float32]
|
|
35
|
-
msaa: int
|
|
36
|
-
z_per_iter: int = 1
|
|
37
39
|
|
|
38
|
-
def __init__(
|
|
39
|
-
self, resolution: float | npt.ArrayLike = 1, msaa: int = 8, z_per_iter: int = 1
|
|
40
|
-
) -> None:
|
|
40
|
+
def __init__(self, resolution: int | float | npt.ArrayLike = 1) -> None:
|
|
41
41
|
"""Transform tree to image stack.
|
|
42
42
|
|
|
43
43
|
Parameters
|
|
44
44
|
----------
|
|
45
45
|
resolution : int | (x, y, z), default `(1, 1, 1)`
|
|
46
46
|
Resolution of image stack.
|
|
47
|
-
mass : int, default `8`
|
|
48
|
-
Multi-sample anti-aliasing.
|
|
49
47
|
"""
|
|
50
48
|
|
|
51
|
-
if isinstance(resolution, float):
|
|
52
|
-
resolution = [resolution, resolution, resolution]
|
|
53
|
-
|
|
54
|
-
self.resolution = (resolution := np.array(resolution, dtype=np.float32))
|
|
55
|
-
assert tuple(resolution.shape) == (3,), "resolution shoule be vector of 3d."
|
|
49
|
+
if isinstance(resolution, (int, float, np.integer, np.floating)):
|
|
50
|
+
resolution = [resolution, resolution, resolution] # type: ignore
|
|
56
51
|
|
|
57
|
-
self.
|
|
58
|
-
self.
|
|
52
|
+
self.resolution = np.array(resolution, dtype=np.float32)
|
|
53
|
+
assert len(self.resolution) == 3, "resolution shoule be vector of 3d."
|
|
59
54
|
|
|
60
55
|
def __call__(self, x: Tree) -> npt.NDArray[np.uint8]:
|
|
61
56
|
"""Transform tree to image stack.
|
|
@@ -63,142 +58,121 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
|
|
|
63
58
|
Notes
|
|
64
59
|
-----
|
|
65
60
|
This method loads the entire image stack into memory, so it
|
|
66
|
-
ONLY works for small image stacks, use
|
|
67
|
-
|
|
61
|
+
ONLY works for small image stacks, use :meth`transform_and_save`
|
|
62
|
+
for big image stack.
|
|
68
63
|
"""
|
|
69
|
-
return np.
|
|
70
|
-
|
|
71
|
-
def __repr__(self) -> str:
|
|
72
|
-
return (
|
|
73
|
-
"ToImageStack"
|
|
74
|
-
+ f"-resolution-{'-'.join(self.resolution)}"
|
|
75
|
-
+ f"-mass-{self.msaa}"
|
|
76
|
-
+ f"-z-{self.z_per_iter}"
|
|
77
|
-
)
|
|
64
|
+
return np.stack(list(self.transfrom(x, verbose=False)), axis=0)
|
|
78
65
|
|
|
79
66
|
def transfrom(
|
|
80
|
-
self,
|
|
67
|
+
self,
|
|
68
|
+
x: Tree,
|
|
69
|
+
verbose: bool = True,
|
|
70
|
+
*,
|
|
71
|
+
ranges: Optional[Tuple[npt.ArrayLike, npt.ArrayLike]] = None,
|
|
81
72
|
) -> Iterable[npt.NDArray[np.uint8]]:
|
|
82
|
-
# pylint: disable=too-many-locals
|
|
83
|
-
from tqdm import tqdm
|
|
84
|
-
|
|
85
73
|
if verbose:
|
|
86
74
|
print("To image stack: " + x.source)
|
|
87
75
|
time_start = time.time()
|
|
88
76
|
|
|
89
|
-
|
|
77
|
+
scene = self._get_scene(x)
|
|
78
|
+
|
|
79
|
+
if ranges is None:
|
|
80
|
+
xyz, r = x.xyz(), x.r().reshape(-1, 1)
|
|
81
|
+
coord_min = np.floor(np.min(xyz - r, axis=0))
|
|
82
|
+
coord_max = np.ceil(np.max(xyz + r, axis=0))
|
|
83
|
+
else:
|
|
84
|
+
assert len(ranges) == 2
|
|
85
|
+
coord_min = np.array(ranges[0])
|
|
86
|
+
coord_max = np.array(ranges[1])
|
|
87
|
+
assert len(coord_min) == len(coord_max) == 3
|
|
90
88
|
|
|
91
|
-
|
|
92
|
-
coord_min = np.floor(np.min(xyz - r, axis=0)) # TODO: snap to grid
|
|
93
|
-
coord_max = np.ceil(np.max(xyz + r, axis=0))
|
|
94
|
-
grids, total = self.get_grids(coord_min, coord_max)
|
|
89
|
+
samplers = self._get_samplers(coord_min, coord_max)
|
|
95
90
|
|
|
96
91
|
if verbose:
|
|
92
|
+
from tqdm import tqdm
|
|
93
|
+
|
|
94
|
+
total = (coord_max[2] - coord_min[2]) / self.resolution[2]
|
|
95
|
+
samplers = tqdm(samplers, total=total.astype(np.int64).item())
|
|
96
|
+
|
|
97
97
|
time_end = time.time()
|
|
98
98
|
print("Prepare in: ", time_end - time_start, "s") # type: ignore
|
|
99
99
|
|
|
100
|
-
for
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
for i in range(voxel.shape[2]):
|
|
105
|
-
yield voxel[:, :, i]
|
|
100
|
+
for sampler in samplers:
|
|
101
|
+
voxel = sampler.sample(scene) # should be shape of (x, y, z, 3) and z = 1
|
|
102
|
+
frame = (255 * voxel[..., 0, 0]).astype(np.uint8)
|
|
103
|
+
yield frame
|
|
106
104
|
|
|
107
|
-
def transform_and_save(
|
|
108
|
-
self
|
|
105
|
+
def transform_and_save(
|
|
106
|
+
self, fname: str, x: Tree, verbose: bool = True, **kwargs
|
|
107
|
+
) -> None:
|
|
108
|
+
self.save_tif(fname, self.transfrom(x, verbose=verbose, **kwargs))
|
|
109
109
|
|
|
110
110
|
def transform_population(
|
|
111
111
|
self, population: Population | str, verbose: bool = True
|
|
112
112
|
) -> None:
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
trees = (
|
|
114
|
+
Population.from_swc(population)
|
|
115
|
+
if isinstance(population, str)
|
|
116
|
+
else population
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if verbose:
|
|
120
|
+
from tqdm import tqdm
|
|
121
|
+
|
|
122
|
+
trees = tqdm(trees)
|
|
115
123
|
|
|
116
124
|
# TODO: multiprocess
|
|
117
|
-
for tree in
|
|
125
|
+
for tree in trees:
|
|
118
126
|
tif = re.sub(r".swc$", ".tif", tree.source)
|
|
119
127
|
if not os.path.isfile(tif):
|
|
120
|
-
self.transform_and_save(tif, tree, verbose=
|
|
128
|
+
self.transform_and_save(tif, tree, verbose=False)
|
|
129
|
+
|
|
130
|
+
def extra_repr(self):
|
|
131
|
+
res = ",".join(f"{a:.4f}" for a in self.resolution)
|
|
132
|
+
return f"resolution=({res})"
|
|
133
|
+
|
|
134
|
+
def _get_scene(self, x: Tree) -> Scene:
|
|
135
|
+
material = ColoredMaterial((1, 0, 0)).into()
|
|
136
|
+
scene = ObjectsScene()
|
|
137
|
+
scene.set_background((0, 0, 0))
|
|
138
|
+
|
|
139
|
+
def leave(n: Tree.Node, children: List[Tree.Node]) -> Tree.Node:
|
|
140
|
+
for c in children:
|
|
141
|
+
sdf = RoundCone(_tp3f(n.xyz()), _tp3f(c.xyz()), n.r, c.r).into()
|
|
142
|
+
scene.add_object(SDFObject(sdf, material).into())
|
|
121
143
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
144
|
+
return n
|
|
145
|
+
|
|
146
|
+
x.traverse(leave=leave)
|
|
147
|
+
scene.build_bvh()
|
|
148
|
+
return scene.into()
|
|
149
|
+
|
|
150
|
+
def _get_samplers(
|
|
151
|
+
self,
|
|
152
|
+
coord_min: npt.NDArray,
|
|
153
|
+
coord_max: npt.NDArray,
|
|
154
|
+
offset: Optional[npt.NDArray] = None,
|
|
155
|
+
) -> Iterable[RangeSampler]:
|
|
156
|
+
"""Get Samplers.
|
|
126
157
|
|
|
127
158
|
Parameters
|
|
128
159
|
----------
|
|
129
160
|
coord_min, coord_max: npt.ArrayLike
|
|
130
161
|
Coordinates array of shape (3,).
|
|
131
|
-
z_per_iter : int
|
|
132
|
-
Yeild z per iter, raising this option speeds up processing,
|
|
133
|
-
but consumes more memory.
|
|
134
|
-
|
|
135
|
-
Returns
|
|
136
|
-
-------
|
|
137
|
-
grid : npt.NDArray[np.float32]
|
|
138
|
-
Array of shape (nx, ny, z_per_iter, k, 3).
|
|
139
162
|
"""
|
|
140
163
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
assert tuple(coord_max.shape) == (3,), "coord_max shoule be vector of 3d."
|
|
145
|
-
|
|
146
|
-
point_grid = np.mgrid[
|
|
147
|
-
coord_min[0] : coord_max[0] : self.resolution[0],
|
|
148
|
-
coord_min[1] : coord_max[1] : self.resolution[1],
|
|
149
|
-
coord_min[2] : coord_max[2] : self.resolution[2],
|
|
150
|
-
] # (3, nx, ny, nz)
|
|
151
|
-
point_grid = np.rollaxis(point_grid, 0, 4) # (nx, ny, nz, 3)
|
|
152
|
-
|
|
153
|
-
step = self.resolution / (k + 1)
|
|
154
|
-
ends = self.resolution - step / 2
|
|
155
|
-
inter_grid = np.mgrid[
|
|
156
|
-
step[0] : ends[0] : step[0],
|
|
157
|
-
step[1] : ends[1] : step[1],
|
|
158
|
-
step[2] : ends[2] : step[2],
|
|
159
|
-
] # (3, kx, ky, kz)
|
|
160
|
-
inter_grid = np.rollaxis(inter_grid, 0, 4).reshape(-1, 3) # (k, 3)
|
|
161
|
-
|
|
162
|
-
grids = np.expand_dims(point_grid, 3).repeat(k**3, axis=3)
|
|
163
|
-
grids = cast(Any, grids + inter_grid)
|
|
164
|
-
|
|
165
|
-
return (
|
|
166
|
-
grids[:, :, i : i + self.z_per_iter]
|
|
167
|
-
for i in range(0, grids.shape[2], self.z_per_iter)
|
|
168
|
-
), math.ceil(grids.shape[2] / self.z_per_iter)
|
|
169
|
-
|
|
170
|
-
def get_sdf(self, x: Tree) -> SDF:
|
|
171
|
-
T = Tuple[Tree.Node, List[SDF], SDF | None]
|
|
172
|
-
|
|
173
|
-
def collect(n: Tree.Node, pre: List[T]) -> T:
|
|
174
|
-
if len(pre) == 0:
|
|
175
|
-
return (n, [], None)
|
|
176
|
-
|
|
177
|
-
if len(pre) == 1:
|
|
178
|
-
child, sub_sdfs, last = pre[0]
|
|
179
|
-
sub_sdfs.append(SDFRoundCone(n.xyz(), child.xyz(), n.r, child.r))
|
|
180
|
-
return (n, sub_sdfs, last)
|
|
181
|
-
|
|
182
|
-
sdfs: List[SDF] = []
|
|
183
|
-
for child, sub_sdfs, last in pre:
|
|
184
|
-
sub_sdfs.append(SDFRoundCone(n.xyz(), child.xyz(), n.r, child.r))
|
|
185
|
-
sdfs.append(SDFCompose.compose(sub_sdfs))
|
|
186
|
-
if last is not None:
|
|
187
|
-
sdfs.append(last)
|
|
188
|
-
|
|
189
|
-
return (n, [], SDFCompose.compose(sdfs))
|
|
190
|
-
|
|
191
|
-
_, sdfs, last = x.traverse(leave=collect)
|
|
192
|
-
if len(sdfs) != 0:
|
|
193
|
-
sdf = SDFCompose.compose(sdfs)
|
|
194
|
-
if last is not None:
|
|
195
|
-
sdf = SDFCompose.compose([sdf, last])
|
|
196
|
-
elif last is not None:
|
|
197
|
-
sdf = last
|
|
198
|
-
else:
|
|
199
|
-
raise ValueError("empty tree")
|
|
164
|
+
eps = 1e-6
|
|
165
|
+
stride = self.resolution
|
|
166
|
+
offset = offset or (stride / 2)
|
|
200
167
|
|
|
201
|
-
|
|
168
|
+
xmin, ymin, zmin = _tp3f(coord_min + offset)
|
|
169
|
+
xmax, ymax, zmax = _tp3f(coord_max)
|
|
170
|
+
z = zmin
|
|
171
|
+
while z < zmax:
|
|
172
|
+
yield RangeSampler(
|
|
173
|
+
(xmin, ymin, z), (xmax, ymax, z + stride[2] - eps), _tp3f(stride)
|
|
174
|
+
)
|
|
175
|
+
z += stride[2]
|
|
202
176
|
|
|
203
177
|
@staticmethod
|
|
204
178
|
def save_tif(
|
|
@@ -218,3 +192,9 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
|
|
|
218
192
|
"axes": "ZXY",
|
|
219
193
|
},
|
|
220
194
|
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _tp3f(x: npt.NDArray) -> Tuple[float, float, float]:
|
|
198
|
+
"""Convert to tuple of 3 floats."""
|
|
199
|
+
assert len(x) == 3
|
|
200
|
+
return (float(x[0]), float(x[1]), float(x[2]))
|
swcgeom/transforms/images.py
CHANGED
|
@@ -28,5 +28,5 @@ class Center(Transform[npt.NDArray[np.float32], npt.NDArray[np.float32]]):
|
|
|
28
28
|
e = np.add(s, self.shape_out)
|
|
29
29
|
return x[s[0] : e[0], s[1] : e[1], s[2] : e[2], :]
|
|
30
30
|
|
|
31
|
-
def
|
|
32
|
-
return f"
|
|
31
|
+
def extra_repr(self) -> str:
|
|
32
|
+
return f"shape_out=({','.join(str(a) for a in self.shape_out)})"
|
swcgeom/transforms/mst.py
CHANGED
|
@@ -140,13 +140,8 @@ class PointsToCuntzMST(Transform[npt.NDArray[np.float32], Tree]):
|
|
|
140
140
|
t = sort_tree(t)
|
|
141
141
|
return t
|
|
142
142
|
|
|
143
|
-
def
|
|
144
|
-
return
|
|
145
|
-
f"PointsToCuntzMST"
|
|
146
|
-
f"-bf-{self.bf}"
|
|
147
|
-
f"-furcations-{self.furcations}"
|
|
148
|
-
f"-{'exclude-soma' if self.exclude_soma else 'include-soma'}"
|
|
149
|
-
) # TODO: names, types
|
|
143
|
+
def extra_repr(self) -> str: # TODO: names, types
|
|
144
|
+
return f"bf={self.bf:.4f}, furcations={self.furcations}, exclude_soma={self.exclude_soma}, sort={self.sort}"
|
|
150
145
|
|
|
151
146
|
|
|
152
147
|
class PointsToMST(PointsToCuntzMST): # pylint: disable=too-few-public-methods
|
|
@@ -173,6 +168,7 @@ class PointsToMST(PointsToCuntzMST): # pylint: disable=too-few-public-methods
|
|
|
173
168
|
names : SWCNames, optional
|
|
174
169
|
types : SWCTypes, optional
|
|
175
170
|
"""
|
|
171
|
+
|
|
176
172
|
if k_furcations is not None:
|
|
177
173
|
warnings.warn(
|
|
178
174
|
"`PointsToMST(k_furcations=...)` has been renamed to "
|
|
@@ -191,9 +187,5 @@ class PointsToMST(PointsToCuntzMST): # pylint: disable=too-few-public-methods
|
|
|
191
187
|
**kwargs,
|
|
192
188
|
)
|
|
193
189
|
|
|
194
|
-
def
|
|
195
|
-
return
|
|
196
|
-
f"PointsToMST"
|
|
197
|
-
f"-furcations-{self.furcations}"
|
|
198
|
-
f"-{'exclude-soma' if self.exclude_soma else 'include-soma'}"
|
|
199
|
-
)
|
|
190
|
+
def extra_repr(self) -> str:
|
|
191
|
+
return f"furcations-{self.furcations}, exclude-soma={self.exclude_soma}"
|
swcgeom/transforms/population.py
CHANGED
swcgeom/transforms/tree.py
CHANGED
|
@@ -64,8 +64,8 @@ class TreeSmoother(Transform[Tree, Tree]): # pylint: disable=missing-class-docs
|
|
|
64
64
|
|
|
65
65
|
return x
|
|
66
66
|
|
|
67
|
-
def
|
|
68
|
-
return f"
|
|
67
|
+
def extra_repr(self):
|
|
68
|
+
return f"n_nodes={self.n_nodes}"
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
class TreeNormalizer(Normalizer[Tree]):
|
|
@@ -107,8 +107,8 @@ class CutByType(Transform[Tree, Tree]):
|
|
|
107
107
|
y = to_subtree(x, removals)
|
|
108
108
|
return y
|
|
109
109
|
|
|
110
|
-
def
|
|
111
|
-
return f"
|
|
110
|
+
def extra_repr(self):
|
|
111
|
+
return f"type={self.type}"
|
|
112
112
|
|
|
113
113
|
|
|
114
114
|
class CutAxonTree(CutByType):
|
|
@@ -118,9 +118,6 @@ class CutAxonTree(CutByType):
|
|
|
118
118
|
types = get_types(types)
|
|
119
119
|
super().__init__(type=types.axon)
|
|
120
120
|
|
|
121
|
-
def __repr__(self) -> str:
|
|
122
|
-
return "CutAxonTree"
|
|
123
|
-
|
|
124
121
|
|
|
125
122
|
class CutDendriteTree(CutByType):
|
|
126
123
|
"""Cut dendrite tree."""
|
|
@@ -129,9 +126,6 @@ class CutDendriteTree(CutByType):
|
|
|
129
126
|
types = get_types(types)
|
|
130
127
|
super().__init__(type=types.basal_dendrite) # TODO: apical dendrite
|
|
131
128
|
|
|
132
|
-
def __repr__(self) -> str:
|
|
133
|
-
return "CutDenriteTree"
|
|
134
|
-
|
|
135
129
|
|
|
136
130
|
class CutByBifurcationOrder(Transform[Tree, Tree]):
|
|
137
131
|
"""Cut tree by bifurcation order."""
|
|
@@ -177,9 +171,6 @@ class CutShortTipBranch(Transform[Tree, Tree]):
|
|
|
177
171
|
if callback is not None:
|
|
178
172
|
self.callbacks.append(callback)
|
|
179
173
|
|
|
180
|
-
def __repr__(self) -> str:
|
|
181
|
-
return f"CutShortTipBranch-{self.thre}"
|
|
182
|
-
|
|
183
174
|
def __call__(self, x: Tree) -> Tree:
|
|
184
175
|
removals: List[int] = []
|
|
185
176
|
self.callbacks.append(lambda br: removals.append(br[1].id))
|
|
@@ -187,6 +178,9 @@ class CutShortTipBranch(Transform[Tree, Tree]):
|
|
|
187
178
|
self.callbacks.pop()
|
|
188
179
|
return to_subtree(x, removals)
|
|
189
180
|
|
|
181
|
+
def extra_repr(self):
|
|
182
|
+
return f"threshold={self.thre}"
|
|
183
|
+
|
|
190
184
|
def _leave(
|
|
191
185
|
self, n: Tree.Node, children: List[Tuple[float, Tree.Node] | None]
|
|
192
186
|
) -> Tuple[float, Tree.Node] | None:
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
"""Assemble a tree."""
|
|
2
2
|
|
|
3
|
+
from copy import copy
|
|
3
4
|
from typing import Iterable, List, Optional, Tuple
|
|
4
5
|
|
|
6
|
+
import numpy as np
|
|
5
7
|
import pandas as pd
|
|
6
8
|
|
|
7
9
|
from swcgeom.core import Tree
|
|
8
|
-
from swcgeom.core.swc_utils import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
from swcgeom.core.swc_utils import (
|
|
11
|
+
SWCNames,
|
|
12
|
+
get_names,
|
|
13
|
+
link_roots_to_nearest_,
|
|
14
|
+
sort_nodes_,
|
|
12
15
|
)
|
|
13
16
|
from swcgeom.transforms.base import Transform
|
|
14
17
|
|
|
18
|
+
EPS = 1e-5
|
|
19
|
+
|
|
15
20
|
|
|
16
21
|
class LinesToTree(Transform[List[pd.DataFrame], Tree]):
|
|
17
22
|
"""Assemble lines to swc."""
|
|
@@ -35,11 +40,12 @@ class LinesToTree(Transform[List[pd.DataFrame], Tree]):
|
|
|
35
40
|
):
|
|
36
41
|
return self.assemble(lines, names=names)
|
|
37
42
|
|
|
38
|
-
def __repr__(self) -> str:
|
|
39
|
-
return f"LinesToTree-thre-{self.thre}-{'undirected' if self.undirected else 'directed'}"
|
|
40
|
-
|
|
41
43
|
def assemble(
|
|
42
|
-
self,
|
|
44
|
+
self,
|
|
45
|
+
lines: Iterable[pd.DataFrame],
|
|
46
|
+
*,
|
|
47
|
+
undirected: bool = True,
|
|
48
|
+
names: Optional[SWCNames] = None,
|
|
43
49
|
) -> pd.DataFrame:
|
|
44
50
|
"""Assemble lines to a tree.
|
|
45
51
|
|
|
@@ -51,6 +57,8 @@ class LinesToTree(Transform[List[pd.DataFrame], Tree]):
|
|
|
51
57
|
lines : List of ~pd.DataFrame
|
|
52
58
|
An array of tables containing a line, columns should follwing
|
|
53
59
|
the swc.
|
|
60
|
+
undirected : bool, default `True`
|
|
61
|
+
Forwarding to `self.try_assemble`.
|
|
54
62
|
names : SWCNames, optional
|
|
55
63
|
Forwarding to `self.try_assemble`.
|
|
56
64
|
|
|
@@ -60,17 +68,33 @@ class LinesToTree(Transform[List[pd.DataFrame], Tree]):
|
|
|
60
68
|
|
|
61
69
|
See Also
|
|
62
70
|
--------
|
|
63
|
-
self.
|
|
71
|
+
self.try_assemble
|
|
64
72
|
"""
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
|
|
74
|
+
tree, lines = self.try_assemble(
|
|
75
|
+
lines, sort_nodes=False, undirected=undirected, names=names
|
|
67
76
|
)
|
|
77
|
+
while len(lines) > 0:
|
|
78
|
+
t, lines = self.try_assemble(
|
|
79
|
+
lines,
|
|
80
|
+
id_offset=len(tree),
|
|
81
|
+
sort_nodes=False,
|
|
82
|
+
undirected=undirected,
|
|
83
|
+
names=names,
|
|
84
|
+
)
|
|
85
|
+
tree = pd.concat([tree, t])
|
|
86
|
+
|
|
87
|
+
tree = tree.reset_index()
|
|
88
|
+
link_roots_to_nearest_(tree)
|
|
89
|
+
sort_nodes_(tree)
|
|
90
|
+
return tree
|
|
68
91
|
|
|
69
92
|
def try_assemble(
|
|
70
93
|
self,
|
|
71
94
|
lines: Iterable[pd.DataFrame],
|
|
72
95
|
*,
|
|
73
96
|
id_offset: int = 0,
|
|
97
|
+
undirected: bool = True,
|
|
74
98
|
sort_nodes: bool = True,
|
|
75
99
|
names: Optional[SWCNames] = None,
|
|
76
100
|
) -> Tuple[pd.DataFrame, List[pd.DataFrame]]:
|
|
@@ -88,6 +112,9 @@ class LinesToTree(Transform[List[pd.DataFrame], Tree]):
|
|
|
88
112
|
the swc.
|
|
89
113
|
id_offset : int, default `0`
|
|
90
114
|
The offset of the line node id.
|
|
115
|
+
undirected : bool, default `True`
|
|
116
|
+
Both ends of a line can be considered connection point. If
|
|
117
|
+
`False`, only the starting point.
|
|
91
118
|
sort_nodes : bool, default `True`
|
|
92
119
|
sort nodes of subtree.
|
|
93
120
|
names : SWCNames, optional
|
|
@@ -97,11 +124,50 @@ class LinesToTree(Transform[List[pd.DataFrame], Tree]):
|
|
|
97
124
|
tree : ~pandas.DataFrame
|
|
98
125
|
remaining_lines : List of ~pandas.DataFrame
|
|
99
126
|
"""
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
127
|
+
|
|
128
|
+
names = get_names(names)
|
|
129
|
+
lines = copy(list(lines))
|
|
130
|
+
|
|
131
|
+
tree = lines[0]
|
|
132
|
+
tree[names.id] = id_offset + np.arange(len(tree))
|
|
133
|
+
tree[names.pid] = tree[names.id] - 1
|
|
134
|
+
tree.at[0, names.pid] = -1
|
|
135
|
+
del lines[0]
|
|
136
|
+
|
|
137
|
+
while True:
|
|
138
|
+
for i, line in enumerate(lines):
|
|
139
|
+
for p in [0, -1] if undirected else [0]:
|
|
140
|
+
xyz = [names.x, names.y, names.z]
|
|
141
|
+
vs = tree[xyz] - line.iloc[p][xyz]
|
|
142
|
+
dis = np.linalg.norm(vs, axis=1)
|
|
143
|
+
ind = np.argmin(dis)
|
|
144
|
+
if dis[ind] > self.thre:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
if dis[ind] < EPS:
|
|
148
|
+
line = line.drop((p + len(line)) % len(line)).reset_index(
|
|
149
|
+
drop=True
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
line[names.id] = id_offset + len(tree) + np.arange(len(line))
|
|
153
|
+
line[names.pid] = line[names.id] + (-1 if p == 0 else 1)
|
|
154
|
+
line.at[(p + len(line)) % len(line), names.pid] = tree.iloc[ind][
|
|
155
|
+
names.id
|
|
156
|
+
]
|
|
157
|
+
tree = pd.concat([tree, line])
|
|
158
|
+
del lines[i]
|
|
159
|
+
break
|
|
160
|
+
else:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
break
|
|
164
|
+
else:
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
if sort_nodes:
|
|
168
|
+
sort_nodes_(tree)
|
|
169
|
+
|
|
170
|
+
return tree, lines
|
|
171
|
+
|
|
172
|
+
def extra_repr(self):
|
|
173
|
+
return f"thre={self.thre}, undirected={self.undirected}"
|
swcgeom/utils/__init__.py
CHANGED
|
@@ -4,10 +4,10 @@ from swcgeom.utils.debug import *
|
|
|
4
4
|
from swcgeom.utils.dsu import *
|
|
5
5
|
from swcgeom.utils.ellipse import *
|
|
6
6
|
from swcgeom.utils.file import *
|
|
7
|
-
from swcgeom.utils.geometry_object import *
|
|
8
7
|
from swcgeom.utils.neuromorpho import *
|
|
9
8
|
from swcgeom.utils.numpy_helper import *
|
|
10
9
|
from swcgeom.utils.renderer import *
|
|
11
10
|
from swcgeom.utils.sdf import *
|
|
12
11
|
from swcgeom.utils.solid_geometry import *
|
|
13
12
|
from swcgeom.utils.transforms import *
|
|
13
|
+
from swcgeom.utils.volumetric_object import *
|