swcgeom 0.11.1__py3-none-any.whl → 0.13.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/__init__.py +4 -4
- swcgeom/_version.py +14 -2
- swcgeom/analysis/__init__.py +7 -7
- swcgeom/analysis/branch_features.py +1 -1
- swcgeom/analysis/feature_extractor.py +25 -12
- swcgeom/analysis/node_features.py +1 -1
- swcgeom/analysis/path_features.py +1 -1
- swcgeom/analysis/sholl.py +11 -7
- swcgeom/analysis/trunk.py +5 -5
- swcgeom/analysis/visualization.py +2 -2
- swcgeom/analysis/volume.py +80 -0
- swcgeom/core/__init__.py +9 -9
- swcgeom/core/branch.py +8 -4
- swcgeom/core/branch_tree.py +4 -5
- swcgeom/core/node.py +5 -3
- swcgeom/core/path.py +6 -3
- swcgeom/core/population.py +2 -2
- swcgeom/core/segment.py +8 -4
- swcgeom/core/swc.py +24 -3
- swcgeom/core/swc_utils/__init__.py +6 -6
- swcgeom/core/swc_utils/assembler.py +2 -2
- swcgeom/core/swc_utils/base.py +30 -1
- swcgeom/core/swc_utils/checker.py +30 -6
- swcgeom/core/swc_utils/io.py +31 -30
- swcgeom/core/swc_utils/normalizer.py +1 -1
- swcgeom/core/swc_utils/subtree.py +1 -1
- swcgeom/core/tree.py +38 -14
- swcgeom/core/tree_utils.py +47 -41
- swcgeom/core/tree_utils_impl.py +39 -0
- swcgeom/images/__init__.py +2 -2
- swcgeom/images/folder.py +2 -2
- swcgeom/images/io.py +48 -9
- swcgeom/transforms/__init__.py +10 -8
- swcgeom/transforms/branch.py +3 -3
- swcgeom/transforms/geometry.py +11 -4
- swcgeom/transforms/image_stack.py +3 -3
- swcgeom/transforms/images.py +1 -1
- swcgeom/transforms/mst.py +68 -13
- swcgeom/transforms/path.py +48 -0
- swcgeom/transforms/population.py +2 -2
- swcgeom/transforms/tree.py +18 -9
- swcgeom/transforms/tree_assembler.py +7 -4
- swcgeom/utils/__init__.py +10 -7
- swcgeom/utils/dsu.py +42 -0
- swcgeom/utils/file.py +91 -0
- swcgeom/utils/geometry_object.py +299 -0
- swcgeom/utils/neuromorpho.py +33 -11
- swcgeom/utils/renderer.py +5 -4
- swcgeom/utils/transforms.py +26 -1
- {swcgeom-0.11.1.dist-info → swcgeom-0.13.0.dist-info}/METADATA +8 -8
- swcgeom-0.13.0.dist-info/RECORD +61 -0
- {swcgeom-0.11.1.dist-info → swcgeom-0.13.0.dist-info}/WHEEL +1 -1
- swcgeom-0.11.1.dist-info/RECORD +0 -55
- /swcgeom/utils/{numpy.py → numpy_helper.py} +0 -0
- {swcgeom-0.11.1.dist-info → swcgeom-0.13.0.dist-info}/LICENSE +0 -0
- {swcgeom-0.11.1.dist-info → swcgeom-0.13.0.dist-info}/top_level.txt +0 -0
swcgeom/core/swc_utils/io.py
CHANGED
|
@@ -4,25 +4,25 @@ import re
|
|
|
4
4
|
import warnings
|
|
5
5
|
from typing import Callable, Iterable, List, Literal, Optional, Tuple
|
|
6
6
|
|
|
7
|
-
import chardet
|
|
8
7
|
import numpy as np
|
|
9
8
|
import numpy.typing as npt
|
|
10
9
|
import pandas as pd
|
|
11
10
|
|
|
12
|
-
from .base import SWCNames, get_names
|
|
13
|
-
from .checker import is_single_root
|
|
14
|
-
from .normalizer import (
|
|
11
|
+
from swcgeom.core.swc_utils.base import SWCNames, get_names
|
|
12
|
+
from swcgeom.core.swc_utils.checker import is_single_root
|
|
13
|
+
from swcgeom.core.swc_utils.normalizer import (
|
|
15
14
|
link_roots_to_nearest_,
|
|
16
15
|
mark_roots_as_somas_,
|
|
17
16
|
reset_index_,
|
|
18
17
|
sort_nodes_,
|
|
19
18
|
)
|
|
19
|
+
from swcgeom.utils import FileReader, PathOrIO
|
|
20
20
|
|
|
21
21
|
__all__ = ["read_swc", "to_swc"]
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def read_swc(
|
|
25
|
-
swc_file:
|
|
25
|
+
swc_file: PathOrIO,
|
|
26
26
|
extra_cols: Optional[Iterable[str]] = None,
|
|
27
27
|
fix_roots: Literal["somas", "nearest", False] = False,
|
|
28
28
|
sort_nodes: bool = False,
|
|
@@ -35,7 +35,7 @@ def read_swc(
|
|
|
35
35
|
|
|
36
36
|
Parameters
|
|
37
37
|
----------
|
|
38
|
-
swc_file :
|
|
38
|
+
swc_file : PathOrIO
|
|
39
39
|
Path of swc file, the id should be consecutively incremented.
|
|
40
40
|
extra_cols : Iterable[str], optional
|
|
41
41
|
Read more cols in swc file.
|
|
@@ -47,7 +47,7 @@ def read_swc(
|
|
|
47
47
|
reset_index : bool, default `True`
|
|
48
48
|
Reset node index to start with zero, DO NOT set to false if
|
|
49
49
|
you are not sure what will happend.
|
|
50
|
-
encoding : str, default `utf-8`
|
|
50
|
+
encoding : str | 'detect', default `utf-8`
|
|
51
51
|
The name of the encoding used to decode the file. If is
|
|
52
52
|
`detect`, we will try to detect the character encoding.
|
|
53
53
|
names : SWCNames, optional
|
|
@@ -59,19 +59,6 @@ def read_swc(
|
|
|
59
59
|
"""
|
|
60
60
|
|
|
61
61
|
names = get_names(names)
|
|
62
|
-
|
|
63
|
-
if encoding == "detect":
|
|
64
|
-
with open(swc_file, "rb") as f:
|
|
65
|
-
data = f.read()
|
|
66
|
-
|
|
67
|
-
result = chardet.detect(data)
|
|
68
|
-
encoding = result["encoding"] or "utf-8"
|
|
69
|
-
if result["confidence"] < 0.9:
|
|
70
|
-
warnings.warn(
|
|
71
|
-
f"parse as `{encoding}` with low confidence "
|
|
72
|
-
f"{result['confidence']} in `{swc_file}`"
|
|
73
|
-
)
|
|
74
|
-
|
|
75
62
|
df, comments = parse_swc(
|
|
76
63
|
swc_file, names=names, extra_cols=extra_cols, encoding=encoding
|
|
77
64
|
)
|
|
@@ -145,14 +132,23 @@ RE_FLOAT = r"([+-]?(?:\d+(?:[.]\d*)?(?:[eE][+-]?\d+)?|[.]\d+(?:[eE][+-]?\d+)?))"
|
|
|
145
132
|
|
|
146
133
|
|
|
147
134
|
def parse_swc(
|
|
148
|
-
|
|
135
|
+
fname: PathOrIO,
|
|
149
136
|
*,
|
|
150
137
|
names: SWCNames,
|
|
151
138
|
extra_cols: Iterable[str] | None = None,
|
|
152
|
-
encoding: str = "utf-8",
|
|
139
|
+
encoding: Literal["detect"] | str = "utf-8",
|
|
153
140
|
) -> Tuple[pd.DataFrame, List[str]]:
|
|
154
141
|
"""Parse swc file.
|
|
155
142
|
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
fname : PathOrIO
|
|
146
|
+
names : SWCNames
|
|
147
|
+
extra_cols : list of str, optional
|
|
148
|
+
encoding : str | 'detect', default `utf-8`
|
|
149
|
+
The name of the encoding used to decode the file. If is
|
|
150
|
+
`detect`, we will try to detect the character encoding.
|
|
151
|
+
|
|
156
152
|
Returns
|
|
157
153
|
-------
|
|
158
154
|
df : ~pandas.DataFrame
|
|
@@ -183,17 +179,19 @@ def parse_swc(
|
|
|
183
179
|
# neuromorpho.org. More fields at the end is allowed, such as
|
|
184
180
|
# reading eswc as swc, but with a warning.
|
|
185
181
|
re_swc = re.compile(rf"^\s*{re_swc_cols_str}\s*([\s+-.0-9]*)$")
|
|
182
|
+
|
|
186
183
|
last_group = 7 + len(extras) + 1
|
|
184
|
+
ignored_comment = f"# {' '.join(names.cols())}"
|
|
185
|
+
flag = True
|
|
187
186
|
|
|
188
187
|
comments = []
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
flag = True
|
|
188
|
+
with FileReader(fname, encoding=encoding) as f:
|
|
189
|
+
try:
|
|
192
190
|
for i, line in enumerate(f):
|
|
193
191
|
if (match := re_swc.search(line)) is not None:
|
|
194
192
|
if flag and match.group(last_group):
|
|
195
193
|
warnings.warn(
|
|
196
|
-
f"some fields are ignored in row {i} of `{
|
|
194
|
+
f"some fields are ignored in row {i+1} of `{fname}`"
|
|
197
195
|
)
|
|
198
196
|
flag = False
|
|
199
197
|
|
|
@@ -201,11 +199,14 @@ def parse_swc(
|
|
|
201
199
|
vals[i].append(trans(match.group(i + 1)))
|
|
202
200
|
elif match := RE_COMMENT.match(line):
|
|
203
201
|
comment = line[len(match.group(0)) :].removesuffix("\n")
|
|
204
|
-
|
|
202
|
+
if not comment.startswith(ignored_comment):
|
|
203
|
+
comments.append(comment)
|
|
205
204
|
elif not line.isspace():
|
|
206
|
-
raise ValueError(f"invalid row {i} in `{
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
raise ValueError(f"invalid row {i+1} in `{fname}`")
|
|
206
|
+
except UnicodeDecodeError as e:
|
|
207
|
+
raise ValueError(
|
|
208
|
+
f"decode failed, try to enable auto detect `encoding='detect'`"
|
|
209
|
+
) from e
|
|
209
210
|
|
|
210
211
|
df = pd.DataFrame.from_dict(dict(zip(keys, vals)))
|
|
211
212
|
return df, comments
|
swcgeom/core/tree.py
CHANGED
|
@@ -22,13 +22,14 @@ import numpy.typing as npt
|
|
|
22
22
|
import pandas as pd
|
|
23
23
|
from typing_extensions import Self
|
|
24
24
|
|
|
25
|
-
from
|
|
26
|
-
from .
|
|
27
|
-
from .
|
|
28
|
-
from .
|
|
29
|
-
from .
|
|
30
|
-
from .
|
|
31
|
-
from .
|
|
25
|
+
from swcgeom.core.branch import Branch
|
|
26
|
+
from swcgeom.core.node import Node
|
|
27
|
+
from swcgeom.core.path import Path
|
|
28
|
+
from swcgeom.core.segment import Segment, Segments
|
|
29
|
+
from swcgeom.core.swc import DictSWC, eswc_cols
|
|
30
|
+
from swcgeom.core.swc_utils import SWCNames, get_names, read_swc, traverse
|
|
31
|
+
from swcgeom.core.tree_utils_impl import get_subtree_impl
|
|
32
|
+
from swcgeom.utils import PathOrIO, padding1d
|
|
32
33
|
|
|
33
34
|
__all__ = ["Tree"]
|
|
34
35
|
|
|
@@ -68,13 +69,21 @@ class Tree(DictSWC):
|
|
|
68
69
|
|
|
69
70
|
return Tree.Branch(self.attach, [n.id for n in ns])
|
|
70
71
|
|
|
71
|
-
def is_soma(self) -> bool:
|
|
72
|
-
return self.id == 0
|
|
73
|
-
|
|
74
72
|
def radial_distance(self) -> float:
|
|
75
73
|
"""The end-to-end straight-line distance to soma."""
|
|
76
74
|
return self.distance(self.attach.soma())
|
|
77
75
|
|
|
76
|
+
def subtree(self) -> "Tree":
|
|
77
|
+
"""Get subtree from node."""
|
|
78
|
+
n_nodes, ndata, source, names = get_subtree_impl(self.attach, self.id)
|
|
79
|
+
return Tree(n_nodes, **ndata, source=source, names=names)
|
|
80
|
+
|
|
81
|
+
def is_root(self) -> bool:
|
|
82
|
+
return self.parent() is None
|
|
83
|
+
|
|
84
|
+
def is_soma(self) -> bool: # TODO: support multi soma, e.g. 3 points
|
|
85
|
+
return self.type == self.attach.types.soma and self.is_root()
|
|
86
|
+
|
|
78
87
|
# fmt: off
|
|
79
88
|
@overload
|
|
80
89
|
def traverse(self, *, enter: Callable[[Node, T | None], T], mode: Literal["dfs"] = ...) -> None: ...
|
|
@@ -181,8 +190,13 @@ class Tree(DictSWC):
|
|
|
181
190
|
def node(self, idx: int | np.integer) -> Node:
|
|
182
191
|
return self.Node(self, idx)
|
|
183
192
|
|
|
184
|
-
def soma(self) -> Node:
|
|
185
|
-
|
|
193
|
+
def soma(self, type_check: bool = True) -> Node:
|
|
194
|
+
"""Get soma of neuron."""
|
|
195
|
+
# TODO: find soma, see also: https://neuromorpho.org/myfaq.jsp
|
|
196
|
+
n = self.node(0)
|
|
197
|
+
if type_check and n.type != self.types.soma:
|
|
198
|
+
raise ValueError(f"no soma found in: {self.source}")
|
|
199
|
+
return n
|
|
186
200
|
|
|
187
201
|
def get_bifurcations(self) -> List[Node]:
|
|
188
202
|
"""Get all node of bifurcations."""
|
|
@@ -246,6 +260,16 @@ class Tree(DictSWC):
|
|
|
246
260
|
paths = self.traverse(enter=assign_path, leave=collect_path)
|
|
247
261
|
return [self.Path(self, idx) for idx in paths]
|
|
248
262
|
|
|
263
|
+
def get_neurites(self, type_check: bool = True) -> Iterable[Self]:
|
|
264
|
+
"""Get neurites from soma."""
|
|
265
|
+
return (n.subtree() for n in self.soma(type_check).children())
|
|
266
|
+
|
|
267
|
+
def get_dendrites(self, type_check: bool = True) -> Iterable[Self]:
|
|
268
|
+
"""Get dendrites."""
|
|
269
|
+
types = [self.types.apical_dendrite, self.types.basal_dendrite]
|
|
270
|
+
children = self.soma(type_check).children()
|
|
271
|
+
return (n.subtree() for n in children if n.type in types)
|
|
272
|
+
|
|
249
273
|
# fmt: off
|
|
250
274
|
@overload
|
|
251
275
|
def traverse(self, *,
|
|
@@ -309,7 +333,7 @@ class Tree(DictSWC):
|
|
|
309
333
|
return tree
|
|
310
334
|
|
|
311
335
|
@classmethod
|
|
312
|
-
def from_swc(cls, swc_file:
|
|
336
|
+
def from_swc(cls, swc_file: PathOrIO, **kwargs) -> Self:
|
|
313
337
|
"""Read neuron tree from swc file.
|
|
314
338
|
|
|
315
339
|
See Also
|
|
@@ -322,7 +346,7 @@ class Tree(DictSWC):
|
|
|
322
346
|
except Exception as e: # pylint: disable=broad-except
|
|
323
347
|
raise ValueError(f"fails to read swc: {swc_file}") from e
|
|
324
348
|
|
|
325
|
-
source = os.path.abspath(swc_file)
|
|
349
|
+
source = os.path.abspath(swc_file) if isinstance(swc_file, str) else ""
|
|
326
350
|
return cls.from_data_frame(df, source=source, comments=comments)
|
|
327
351
|
|
|
328
352
|
@classmethod
|
swcgeom/core/tree_utils.py
CHANGED
|
@@ -5,8 +5,8 @@ from typing import Callable, Dict, Iterable, List, Optional, Tuple, TypeVar, ove
|
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
|
|
8
|
-
from .swc import SWCLike
|
|
9
|
-
from .swc_utils import (
|
|
8
|
+
from swcgeom.core.swc import SWCLike
|
|
9
|
+
from swcgeom.core.swc_utils import (
|
|
10
10
|
REMOVAL,
|
|
11
11
|
SWCNames,
|
|
12
12
|
Topology,
|
|
@@ -15,9 +15,9 @@ from .swc_utils import (
|
|
|
15
15
|
propagate_removal,
|
|
16
16
|
sort_nodes_impl,
|
|
17
17
|
to_sub_topology,
|
|
18
|
-
traverse,
|
|
19
18
|
)
|
|
20
|
-
from .tree import Tree
|
|
19
|
+
from swcgeom.core.tree import Tree
|
|
20
|
+
from swcgeom.core.tree_utils_impl import get_subtree_impl, to_subtree_impl
|
|
21
21
|
|
|
22
22
|
__all__ = [
|
|
23
23
|
"sort_tree",
|
|
@@ -120,8 +120,7 @@ def to_sub_tree(swc_like: SWCLike, sub: Topology) -> Tuple[Tree, Dict[int, int]]
|
|
|
120
120
|
ndata = {k: swc_like.get_ndata(k)[id_map_arr].copy() for k in swc_like.keys()}
|
|
121
121
|
ndata.update(id=new_id, pid=new_pid)
|
|
122
122
|
|
|
123
|
-
subtree = Tree(n_nodes, **ndata, names=swc_like.names)
|
|
124
|
-
subtree.source = swc_like.source
|
|
123
|
+
subtree = Tree(n_nodes, **ndata, source=swc_like.source, names=swc_like.names)
|
|
125
124
|
|
|
126
125
|
id_map = {}
|
|
127
126
|
for i, idx in enumerate(id_map_arr):
|
|
@@ -143,7 +142,8 @@ def to_subtree(swc_like: SWCLike, removals: Iterable[int]) -> Tree:
|
|
|
143
142
|
new_ids[i] = REMOVAL
|
|
144
143
|
|
|
145
144
|
sub = propagate_removal((new_ids, swc_like.pid()))
|
|
146
|
-
|
|
145
|
+
n_nodes, ndata, source, names = to_subtree_impl(swc_like, sub)
|
|
146
|
+
return Tree(n_nodes, **ndata, source=source, names=names)
|
|
147
147
|
|
|
148
148
|
|
|
149
149
|
def get_subtree(swc_like: SWCLike, n: int) -> Tree:
|
|
@@ -155,14 +155,8 @@ def get_subtree(swc_like: SWCLike, n: int) -> Tree:
|
|
|
155
155
|
n : int
|
|
156
156
|
Id of the root of the subtree.
|
|
157
157
|
"""
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
traverse(topo, enter=lambda n, _: ids.append(n), root=n)
|
|
161
|
-
|
|
162
|
-
sub_ids = np.array(ids, dtype=np.int32)
|
|
163
|
-
sub_pid = swc_like.pid()[sub_ids]
|
|
164
|
-
sub_pid[0] = -1
|
|
165
|
-
return _to_subtree(swc_like, (sub_ids, sub_pid))
|
|
158
|
+
n_nodes, ndata, source, names = get_subtree_impl(swc_like, n)
|
|
159
|
+
return Tree(n_nodes, **ndata, source=source, names=names)
|
|
166
160
|
|
|
167
161
|
|
|
168
162
|
def redirect_tree(tree: Tree, new_root: int, sort: bool = True) -> Tree:
|
|
@@ -179,9 +173,11 @@ def redirect_tree(tree: Tree, new_root: int, sort: bool = True) -> Tree:
|
|
|
179
173
|
"""
|
|
180
174
|
tree = tree.copy()
|
|
181
175
|
path = [tree.node(new_root)]
|
|
182
|
-
while (p := path[-1]
|
|
176
|
+
while (p := path[-1].parent()) is not None:
|
|
183
177
|
path.append(p)
|
|
184
178
|
|
|
179
|
+
path[0].pid = -1
|
|
180
|
+
path[0].type, path[-1].type = path[-1].type, path[0].type
|
|
185
181
|
for n, p in zip(path[1:], path[:-1]):
|
|
186
182
|
n.pid = p.id
|
|
187
183
|
|
|
@@ -194,11 +190,12 @@ def redirect_tree(tree: Tree, new_root: int, sort: bool = True) -> Tree:
|
|
|
194
190
|
def cat_tree( # pylint: disable=too-many-arguments
|
|
195
191
|
tree1: Tree,
|
|
196
192
|
tree2: Tree,
|
|
197
|
-
node1: int,
|
|
193
|
+
node1: int = 0,
|
|
198
194
|
node2: int = 0,
|
|
199
195
|
*,
|
|
200
|
-
|
|
196
|
+
translate: bool = True,
|
|
201
197
|
names: Optional[SWCNames] = None,
|
|
198
|
+
no_move: Optional[bool] = None, # legacy
|
|
202
199
|
) -> Tree:
|
|
203
200
|
"""Concatenates the second tree onto the first one.
|
|
204
201
|
|
|
@@ -206,31 +203,44 @@ def cat_tree( # pylint: disable=too-many-arguments
|
|
|
206
203
|
---------
|
|
207
204
|
tree1 : Tree
|
|
208
205
|
tree2 : Tree
|
|
209
|
-
node1 : int
|
|
206
|
+
node1 : int, default `0`
|
|
210
207
|
The node id of the tree to be connected.
|
|
211
208
|
node2 : int, default `0`
|
|
212
209
|
The node id of the connection point.
|
|
213
|
-
|
|
214
|
-
|
|
210
|
+
translate : bool, default `True`
|
|
211
|
+
Wheather to translate node_2 to node_1. If False, add link
|
|
212
|
+
between node_1 and node_2 without translate.
|
|
215
213
|
"""
|
|
214
|
+
if no_move is not None:
|
|
215
|
+
warnings.warn(
|
|
216
|
+
"`no_move` has been, it is replaced by `translate` in "
|
|
217
|
+
"v0.12.0, and this will be removed in next version",
|
|
218
|
+
DeprecationWarning,
|
|
219
|
+
)
|
|
220
|
+
translate = not no_move
|
|
221
|
+
|
|
216
222
|
names = get_names(names)
|
|
217
223
|
tree, tree2 = tree1.copy(), tree2.copy()
|
|
218
|
-
if not tree2.node(node2).
|
|
224
|
+
if not tree2.node(node2).is_root():
|
|
219
225
|
tree2 = redirect_tree(tree2, node2, sort=False)
|
|
220
226
|
|
|
221
227
|
c = tree.node(node1)
|
|
222
|
-
if
|
|
228
|
+
if translate:
|
|
223
229
|
tree2.ndata[names.x] -= tree2.node(node2).x - c.x
|
|
224
230
|
tree2.ndata[names.y] -= tree2.node(node2).y - c.y
|
|
225
231
|
tree2.ndata[names.z] -= tree2.node(node2).z - c.z
|
|
226
232
|
|
|
227
|
-
|
|
228
|
-
tree2.ndata[names.pid] += tree.number_of_nodes()
|
|
233
|
+
ns = tree.number_of_nodes()
|
|
229
234
|
if np.linalg.norm(tree2.node(node2).xyz() - c.xyz()) < EPS:
|
|
230
|
-
|
|
231
|
-
|
|
235
|
+
remove = [node2 + ns]
|
|
236
|
+
link_to_root = [n.id + ns for n in tree2.node(node2).children()]
|
|
232
237
|
else:
|
|
233
|
-
|
|
238
|
+
remove = None
|
|
239
|
+
link_to_root = [node2 + ns]
|
|
240
|
+
|
|
241
|
+
# APIs of tree2 are no longer available since we modify the topology
|
|
242
|
+
tree2.ndata[names.id] += ns
|
|
243
|
+
tree2.ndata[names.pid] += ns
|
|
234
244
|
|
|
235
245
|
for k, v in tree.ndata.items(): # only keep keys in tree1
|
|
236
246
|
if k in tree2.ndata:
|
|
@@ -238,7 +248,15 @@ def cat_tree( # pylint: disable=too-many-arguments
|
|
|
238
248
|
else:
|
|
239
249
|
tree.ndata[k] = np.pad(v, (0, tree2.number_of_nodes()))
|
|
240
250
|
|
|
241
|
-
|
|
251
|
+
for n in link_to_root:
|
|
252
|
+
tree.node(n).pid = node1
|
|
253
|
+
|
|
254
|
+
if remove is not None: # TODO: This should be easy to implement during sort
|
|
255
|
+
for k, v in tree.ndata.items():
|
|
256
|
+
tree.ndata[k] = np.delete(v, remove)
|
|
257
|
+
|
|
258
|
+
_sort_tree(tree)
|
|
259
|
+
return tree
|
|
242
260
|
|
|
243
261
|
|
|
244
262
|
def _sort_tree(tree: Tree) -> Tree:
|
|
@@ -247,15 +265,3 @@ def _sort_tree(tree: Tree) -> Tree:
|
|
|
247
265
|
tree.ndata = {k: tree.ndata[k][id_map] for k in tree.ndata}
|
|
248
266
|
tree.ndata.update(id=new_ids, pid=new_pids)
|
|
249
267
|
return tree
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def _to_subtree(swc_like: SWCLike, sub: Topology) -> Tree:
|
|
253
|
-
(new_id, new_pid), id_map = to_sub_topology(sub)
|
|
254
|
-
|
|
255
|
-
n_nodes = new_id.shape[0]
|
|
256
|
-
ndata = {k: swc_like.get_ndata(k)[id_map].copy() for k in swc_like.keys()}
|
|
257
|
-
ndata.update(id=new_id, pid=new_pid)
|
|
258
|
-
|
|
259
|
-
subtree = Tree(n_nodes, **ndata, names=swc_like.names)
|
|
260
|
-
subtree.source = swc_like.source
|
|
261
|
-
return subtree
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""SWC util wrapper for tree, split to avoid circle imports.
|
|
2
|
+
|
|
3
|
+
Notes
|
|
4
|
+
-----
|
|
5
|
+
Do not import `Tree` and keep this file minimized.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, Tuple
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import numpy.typing as npt
|
|
12
|
+
|
|
13
|
+
from swcgeom.core.swc import SWCLike, SWCNames
|
|
14
|
+
from swcgeom.core.swc_utils import Topology, to_sub_topology, traverse
|
|
15
|
+
|
|
16
|
+
__all__ = ["get_subtree_impl", "to_subtree_impl"]
|
|
17
|
+
|
|
18
|
+
TreeArgs = Tuple[int, Dict[str, npt.NDArray[Any]], str, SWCNames]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_subtree_impl(swc_like: SWCLike, n: int) -> TreeArgs:
|
|
22
|
+
ids = []
|
|
23
|
+
topo = (swc_like.id(), swc_like.pid())
|
|
24
|
+
traverse(topo, enter=lambda n, _: ids.append(n), root=n)
|
|
25
|
+
|
|
26
|
+
sub_ids = np.array(ids, dtype=np.int32)
|
|
27
|
+
sub_pid = swc_like.pid()[sub_ids]
|
|
28
|
+
sub_pid[0] = -1
|
|
29
|
+
return to_subtree_impl(swc_like, (sub_ids, sub_pid))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def to_subtree_impl(swc_like: SWCLike, sub: Topology) -> TreeArgs:
|
|
33
|
+
(new_id, new_pid), id_map = to_sub_topology(sub)
|
|
34
|
+
|
|
35
|
+
n_nodes = new_id.shape[0]
|
|
36
|
+
ndata = {k: swc_like.get_ndata(k)[id_map].copy() for k in swc_like.keys()}
|
|
37
|
+
ndata.update(id=new_id, pid=new_pid)
|
|
38
|
+
|
|
39
|
+
return n_nodes, ndata, swc_like.source, swc_like.names
|
swcgeom/images/__init__.py
CHANGED
swcgeom/images/folder.py
CHANGED
|
@@ -9,8 +9,8 @@ import numpy as np
|
|
|
9
9
|
import numpy.typing as npt
|
|
10
10
|
from typing_extensions import Self
|
|
11
11
|
|
|
12
|
-
from
|
|
13
|
-
from .
|
|
12
|
+
from swcgeom.images.io import read_imgs
|
|
13
|
+
from swcgeom.transforms import Identity, Transform
|
|
14
14
|
|
|
15
15
|
__all__ = [
|
|
16
16
|
"ImageStackFolder",
|
swcgeom/images/io.py
CHANGED
|
@@ -6,12 +6,23 @@ import re
|
|
|
6
6
|
import warnings
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
from functools import cache, lru_cache
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import (
|
|
10
|
+
Any,
|
|
11
|
+
Callable,
|
|
12
|
+
Iterable,
|
|
13
|
+
List,
|
|
14
|
+
Literal,
|
|
15
|
+
Optional,
|
|
16
|
+
Tuple,
|
|
17
|
+
cast,
|
|
18
|
+
overload,
|
|
19
|
+
)
|
|
10
20
|
|
|
11
21
|
import nrrd
|
|
12
22
|
import numpy as np
|
|
13
23
|
import numpy.typing as npt
|
|
14
24
|
import tifffile
|
|
25
|
+
from v3dpy.loaders import PBD, Raw
|
|
15
26
|
|
|
16
27
|
__all__ = ["read_imgs", "save_tiff", "read_images"]
|
|
17
28
|
|
|
@@ -20,10 +31,10 @@ RE_TERAFLY_ROOT = re.compile(r"^RES\((\d+)x(\d+)x(\d+)\)$")
|
|
|
20
31
|
RE_TERAFLY_NAME = re.compile(r"^\d+(_\d+)?(_\d+)?")
|
|
21
32
|
|
|
22
33
|
UINT_MAX = {
|
|
23
|
-
np.dtype(np.uint8): (2**8) - 1,
|
|
24
|
-
np.dtype(np.uint16): (2**16) - 1,
|
|
25
|
-
np.dtype(np.uint32): (2**32) - 1,
|
|
26
|
-
np.dtype(np.uint64): (2**64) - 1,
|
|
34
|
+
np.dtype(np.uint8): (2**8) - 1, # type: ignore
|
|
35
|
+
np.dtype(np.uint16): (2**16) - 1, # type: ignore
|
|
36
|
+
np.dtype(np.uint32): (2**32) - 1, # type: ignore
|
|
37
|
+
np.dtype(np.uint64): (2**64) - 1, # type: ignore
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
AXES_ORDER = {
|
|
@@ -92,6 +103,10 @@ def read_imgs(fname: str, **kwargs) -> ImageStack:
|
|
|
92
103
|
return TiffImageStack(fname, **kwargs)
|
|
93
104
|
if ext in [".nrrd"]:
|
|
94
105
|
return NrrdImageStack(fname, **kwargs)
|
|
106
|
+
if ext in [".v3dpbd"]:
|
|
107
|
+
return V3dpbdImageStack(fname, **kwargs)
|
|
108
|
+
if ext in [".v3draw"]:
|
|
109
|
+
return V3drawImageStack(fname, **kwargs)
|
|
95
110
|
if ext in [".npy", ".npz"]:
|
|
96
111
|
return NDArrayImageStack(np.load(fname), **kwargs)
|
|
97
112
|
if TeraflyImageStack.is_root(fname):
|
|
@@ -226,7 +241,7 @@ class NDArrayImageStack(ImageStack):
|
|
|
226
241
|
|
|
227
242
|
@property
|
|
228
243
|
def shape(self) -> Tuple[int, int, int, int]:
|
|
229
|
-
return self.imgs.shape
|
|
244
|
+
return cast(Tuple[int, int, int, int], self.imgs.shape)
|
|
230
245
|
|
|
231
246
|
|
|
232
247
|
class TiffImageStack(NDArrayImageStack):
|
|
@@ -268,6 +283,29 @@ class NrrdImageStack(NDArrayImageStack):
|
|
|
268
283
|
self.header = header
|
|
269
284
|
|
|
270
285
|
|
|
286
|
+
class V3dImageStack(NDArrayImageStack):
|
|
287
|
+
"""v3d image stack."""
|
|
288
|
+
|
|
289
|
+
def __init__(self, fname: str, loader: Raw | PBD, **kwargs) -> None:
|
|
290
|
+
r = loader()
|
|
291
|
+
imgs = r.load(fname)
|
|
292
|
+
super().__init__(imgs, **kwargs)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class V3drawImageStack(V3dImageStack):
|
|
296
|
+
"""v3draw image stack."""
|
|
297
|
+
|
|
298
|
+
def __init__(self, fname: str, **kwargs) -> None:
|
|
299
|
+
super().__init__(fname, loader=Raw, **kwargs)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class V3dpbdImageStack(V3dImageStack):
|
|
303
|
+
"""v3dpbd image stack."""
|
|
304
|
+
|
|
305
|
+
def __init__(self, fname: str, **kwargs) -> None:
|
|
306
|
+
super().__init__(fname, loader=PBD, **kwargs)
|
|
307
|
+
|
|
308
|
+
|
|
271
309
|
class TeraflyImageStack(ImageStack):
|
|
272
310
|
"""TeraFly image stack.
|
|
273
311
|
|
|
@@ -383,7 +421,8 @@ class TeraflyImageStack(ImageStack):
|
|
|
383
421
|
|
|
384
422
|
@property
|
|
385
423
|
def shape(self) -> Tuple[int, int, int, int]:
|
|
386
|
-
|
|
424
|
+
res_max = self.res[-1]
|
|
425
|
+
return res_max[0], res_max[1], res_max[2], 1
|
|
387
426
|
|
|
388
427
|
@classmethod
|
|
389
428
|
def get_resolutions(cls, root: str) -> Tuple[List[Vec3i], List[str], List[Vec3i]]:
|
|
@@ -476,13 +515,13 @@ class TeraflyImageStack(ImageStack):
|
|
|
476
515
|
if shape[1] > lens[1]:
|
|
477
516
|
starts_y = starts + [0, lens[1], 0]
|
|
478
517
|
ends_y = np.array([starts[0], ends[1], ends[2]])
|
|
479
|
-
ends_y += [min(shape[0], lens[0]), 0, 0]
|
|
518
|
+
ends_y += [min(shape[0], lens[0]), 0, 0] # type: ignore
|
|
480
519
|
self._get_range(starts_y, ends_y, res_level, out[:, lens[1] :, :])
|
|
481
520
|
|
|
482
521
|
if shape[2] > lens[2]:
|
|
483
522
|
starts_z = starts + [0, 0, lens[2]]
|
|
484
523
|
ends_z = np.array([starts[0], starts[1], ends[2]])
|
|
485
|
-
ends_z += [min(shape[0], lens[0]), min(shape[1], lens[1]), 0]
|
|
524
|
+
ends_z += [min(shape[0], lens[0]), min(shape[1], lens[1]), 0] # type: ignore
|
|
486
525
|
self._get_range(starts_z, ends_z, res_level, out[:, :, lens[2] :])
|
|
487
526
|
|
|
488
527
|
def _find_correspond_imgs(self, p, res_level):
|
swcgeom/transforms/__init__.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""A series of transformations to compose codes."""
|
|
2
2
|
|
|
3
|
-
from .base import *
|
|
4
|
-
from .branch import *
|
|
5
|
-
from .geometry import *
|
|
6
|
-
from .image_stack import *
|
|
7
|
-
from .images import *
|
|
8
|
-
from .mst import *
|
|
9
|
-
from .
|
|
10
|
-
from .
|
|
3
|
+
from swcgeom.transforms.base import *
|
|
4
|
+
from swcgeom.transforms.branch import *
|
|
5
|
+
from swcgeom.transforms.geometry import *
|
|
6
|
+
from swcgeom.transforms.image_stack import *
|
|
7
|
+
from swcgeom.transforms.images import *
|
|
8
|
+
from swcgeom.transforms.mst import *
|
|
9
|
+
from swcgeom.transforms.path import *
|
|
10
|
+
from swcgeom.transforms.population import *
|
|
11
|
+
from swcgeom.transforms.tree import *
|
|
12
|
+
from swcgeom.transforms.tree_assembler import *
|
swcgeom/transforms/branch.py
CHANGED
|
@@ -7,8 +7,9 @@ import numpy as np
|
|
|
7
7
|
import numpy.typing as npt
|
|
8
8
|
from scipy import signal
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
from
|
|
10
|
+
from swcgeom.core import Branch, DictSWC
|
|
11
|
+
from swcgeom.transforms.base import Transform
|
|
12
|
+
from swcgeom.utils import (
|
|
12
13
|
angle,
|
|
13
14
|
rotate3d_x,
|
|
14
15
|
rotate3d_y,
|
|
@@ -17,7 +18,6 @@ from ..utils import (
|
|
|
17
18
|
to_homogeneous,
|
|
18
19
|
translate3d,
|
|
19
20
|
)
|
|
20
|
-
from .base import Transform
|
|
21
21
|
|
|
22
22
|
__all__ = ["BranchLinearResampler", "BranchConvSmoother", "BranchStandardizer"]
|
|
23
23
|
|
swcgeom/transforms/geometry.py
CHANGED
|
@@ -6,10 +6,17 @@ from typing import Generic, Literal, Optional, TypeVar
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import numpy.typing as npt
|
|
8
8
|
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from .
|
|
9
|
+
from swcgeom.core import DictSWC
|
|
10
|
+
from swcgeom.core.swc_utils import SWCNames
|
|
11
|
+
from swcgeom.transforms.base import Transform
|
|
12
|
+
from swcgeom.utils import (
|
|
13
|
+
rotate3d,
|
|
14
|
+
rotate3d_x,
|
|
15
|
+
rotate3d_y,
|
|
16
|
+
rotate3d_z,
|
|
17
|
+
scale3d,
|
|
18
|
+
translate3d,
|
|
19
|
+
)
|
|
13
20
|
|
|
14
21
|
__all__ = [
|
|
15
22
|
"Normalizer",
|
|
@@ -20,9 +20,9 @@ import numpy as np
|
|
|
20
20
|
import numpy.typing as npt
|
|
21
21
|
import tifffile
|
|
22
22
|
|
|
23
|
-
from
|
|
24
|
-
from
|
|
25
|
-
from .
|
|
23
|
+
from swcgeom.core import Population, Tree
|
|
24
|
+
from swcgeom.transforms.base import Transform
|
|
25
|
+
from swcgeom.utils import SDF, SDFCompose, SDFRoundCone
|
|
26
26
|
|
|
27
27
|
__all__ = ["ToImageStack"]
|
|
28
28
|
|