swcgeom 0.17.0__py3-none-any.whl → 0.17.2__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/feature_extractor.py +25 -15
- swcgeom/analysis/features.py +20 -8
- swcgeom/analysis/lmeasure.py +33 -12
- swcgeom/analysis/sholl.py +10 -28
- swcgeom/analysis/trunk.py +12 -11
- swcgeom/analysis/visualization.py +9 -9
- swcgeom/analysis/visualization3d.py +85 -0
- swcgeom/analysis/volume.py +4 -4
- swcgeom/core/branch.py +4 -3
- swcgeom/core/branch_tree.py +3 -4
- swcgeom/core/compartment.py +3 -2
- swcgeom/core/node.py +17 -3
- swcgeom/core/path.py +6 -9
- swcgeom/core/population.py +43 -29
- swcgeom/core/swc.py +11 -10
- swcgeom/core/swc_utils/base.py +8 -17
- swcgeom/core/swc_utils/checker.py +3 -11
- swcgeom/core/swc_utils/io.py +7 -6
- swcgeom/core/swc_utils/normalizer.py +4 -3
- swcgeom/core/swc_utils/subtree.py +2 -2
- swcgeom/core/tree.py +41 -40
- swcgeom/core/tree_utils.py +13 -17
- swcgeom/core/tree_utils_impl.py +3 -3
- swcgeom/images/augmentation.py +3 -3
- swcgeom/images/folder.py +12 -26
- swcgeom/images/io.py +21 -35
- swcgeom/transforms/image_stack.py +20 -8
- swcgeom/transforms/images.py +3 -12
- swcgeom/transforms/neurolucida_asc.py +4 -6
- swcgeom/transforms/population.py +1 -3
- swcgeom/transforms/tree.py +38 -25
- swcgeom/transforms/tree_assembler.py +4 -3
- swcgeom/utils/download.py +44 -21
- swcgeom/utils/ellipse.py +3 -4
- swcgeom/utils/neuromorpho.py +17 -16
- swcgeom/utils/plotter_2d.py +12 -6
- swcgeom/utils/plotter_3d.py +31 -0
- swcgeom/utils/renderer.py +6 -6
- swcgeom/utils/sdf.py +4 -7
- swcgeom/utils/solid_geometry.py +1 -3
- swcgeom/utils/transforms.py +2 -4
- swcgeom/utils/volumetric_object.py +8 -10
- {swcgeom-0.17.0.dist-info → swcgeom-0.17.2.dist-info}/METADATA +19 -19
- swcgeom-0.17.2.dist-info/RECORD +67 -0
- {swcgeom-0.17.0.dist-info → swcgeom-0.17.2.dist-info}/WHEEL +1 -1
- swcgeom-0.17.0.dist-info/RECORD +0 -65
- {swcgeom-0.17.0.dist-info → swcgeom-0.17.2.dist-info}/LICENSE +0 -0
- {swcgeom-0.17.0.dist-info → swcgeom-0.17.2.dist-info}/top_level.txt +0 -0
swcgeom/transforms/images.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
"""Image stack related transform."""
|
|
2
2
|
|
|
3
|
-
import warnings
|
|
4
|
-
from typing import Tuple
|
|
5
|
-
|
|
6
3
|
import numpy as np
|
|
7
4
|
import numpy.typing as npt
|
|
5
|
+
from typing_extensions import deprecated
|
|
8
6
|
|
|
9
7
|
from swcgeom.transforms.base import Identity, Transform
|
|
10
8
|
|
|
@@ -28,7 +26,7 @@ NDArrayf32 = npt.NDArray[np.float32]
|
|
|
28
26
|
class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
|
|
29
27
|
"""Get image stack center."""
|
|
30
28
|
|
|
31
|
-
def __init__(self, shape_out: int |
|
|
29
|
+
def __init__(self, shape_out: int | tuple[int, int, int]):
|
|
32
30
|
super().__init__()
|
|
33
31
|
self.shape_out = (
|
|
34
32
|
shape_out
|
|
@@ -46,6 +44,7 @@ class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
|
|
|
46
44
|
return f"shape_out=({','.join(str(a) for a in self.shape_out)})"
|
|
47
45
|
|
|
48
46
|
|
|
47
|
+
@deprecated("use `ImagesCenterCrop` instead", stacklevel=2)
|
|
49
48
|
class Center(ImagesCenterCrop):
|
|
50
49
|
"""Get image stack center.
|
|
51
50
|
|
|
@@ -53,14 +52,6 @@ class Center(ImagesCenterCrop):
|
|
|
53
52
|
Use :class:`ImagesCenterCrop` instead.
|
|
54
53
|
"""
|
|
55
54
|
|
|
56
|
-
def __init__(self, shape_out: int | Tuple[int, int, int]):
|
|
57
|
-
warnings.warn(
|
|
58
|
-
"`Center` is deprecated, use `ImagesCenterCrop` instead",
|
|
59
|
-
DeprecationWarning,
|
|
60
|
-
stacklevel=2,
|
|
61
|
-
)
|
|
62
|
-
super().__init__(shape_out)
|
|
63
|
-
|
|
64
55
|
|
|
65
56
|
class ImagesScale(Transform[NDArrayf32, NDArrayf32]):
|
|
66
57
|
def __init__(self, scaler: float) -> None:
|
|
@@ -4,9 +4,7 @@ import os
|
|
|
4
4
|
import re
|
|
5
5
|
from enum import Enum, auto
|
|
6
6
|
from io import TextIOBase
|
|
7
|
-
from typing import Any,
|
|
8
|
-
|
|
9
|
-
import numpy as np
|
|
7
|
+
from typing import Any, NamedTuple, Optional, cast
|
|
10
8
|
|
|
11
9
|
from swcgeom.core import Tree
|
|
12
10
|
from swcgeom.core.swc_utils import SWCNames, SWCTypes, get_names, get_types
|
|
@@ -116,8 +114,8 @@ class ASTNode:
|
|
|
116
114
|
self,
|
|
117
115
|
type: ASTType,
|
|
118
116
|
value: Any = None,
|
|
119
|
-
tokens: Optional[
|
|
120
|
-
children: Optional[
|
|
117
|
+
tokens: Optional[list["Token"]] = None,
|
|
118
|
+
children: Optional[list["ASTNode"]] = None,
|
|
121
119
|
):
|
|
122
120
|
self.type = type
|
|
123
121
|
self.value = value
|
|
@@ -149,7 +147,7 @@ class ASTNode:
|
|
|
149
147
|
|
|
150
148
|
|
|
151
149
|
class AST(ASTNode):
|
|
152
|
-
def __init__(self, children: Optional[
|
|
150
|
+
def __init__(self, children: Optional[list[ASTNode]] = None, source: str = ""):
|
|
153
151
|
super().__init__(ASTType.ROOT, children=children)
|
|
154
152
|
self.source = source
|
|
155
153
|
|
swcgeom/transforms/population.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""Transformation in population."""
|
|
2
2
|
|
|
3
|
-
from typing import List
|
|
4
|
-
|
|
5
3
|
from swcgeom.core import Population, Tree
|
|
6
4
|
from swcgeom.transforms.base import Transform
|
|
7
5
|
|
|
@@ -16,7 +14,7 @@ class PopulationTransform(Transform[Population, Population]):
|
|
|
16
14
|
self.transform = transform
|
|
17
15
|
|
|
18
16
|
def __call__(self, population: Population) -> Population:
|
|
19
|
-
trees:
|
|
17
|
+
trees: list[Tree] = []
|
|
20
18
|
for t in population:
|
|
21
19
|
new_t = self.transform(t)
|
|
22
20
|
if new_t.source == "":
|
swcgeom/transforms/tree.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Transformation in tree."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
from typing import
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Optional
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
|
+
from typing_extensions import deprecated
|
|
7
8
|
|
|
8
9
|
from swcgeom.core import BranchTree, DictSWC, Path, Tree, cut_tree, to_subtree
|
|
9
10
|
from swcgeom.core.swc_utils import SWCTypes, get_types
|
|
@@ -19,7 +20,7 @@ __all__ = [
|
|
|
19
20
|
"CutByType",
|
|
20
21
|
"CutAxonTree",
|
|
21
22
|
"CutDendriteTree",
|
|
22
|
-
"
|
|
23
|
+
"CutByFurcationOrder",
|
|
23
24
|
"CutShortTipBranch",
|
|
24
25
|
]
|
|
25
26
|
|
|
@@ -68,6 +69,7 @@ class TreeSmoother(Transform[Tree, Tree]): # pylint: disable=missing-class-docs
|
|
|
68
69
|
return f"n_nodes={self.n_nodes}"
|
|
69
70
|
|
|
70
71
|
|
|
72
|
+
@deprecated("Use `Normalizer` instead")
|
|
71
73
|
class TreeNormalizer(Normalizer[Tree]):
|
|
72
74
|
"""Noramlize coordinates and radius to 0-1.
|
|
73
75
|
|
|
@@ -75,15 +77,6 @@ class TreeNormalizer(Normalizer[Tree]):
|
|
|
75
77
|
Use :cls:`Normalizer` instead.
|
|
76
78
|
"""
|
|
77
79
|
|
|
78
|
-
def __init__(self, *args, **kwargs) -> None:
|
|
79
|
-
warnings.warn(
|
|
80
|
-
"`TreeNormalizer` has been replaced by `Normalizer` since "
|
|
81
|
-
"v0.6.0 beacuse it applies more widely, and this will be "
|
|
82
|
-
"removed in next version",
|
|
83
|
-
DeprecationWarning,
|
|
84
|
-
)
|
|
85
|
-
super().__init__(*args, **kwargs)
|
|
86
|
-
|
|
87
80
|
|
|
88
81
|
class CutByType(Transform[Tree, Tree]):
|
|
89
82
|
"""Cut tree by type.
|
|
@@ -102,7 +95,7 @@ class CutByType(Transform[Tree, Tree]):
|
|
|
102
95
|
def __call__(self, x: Tree) -> Tree:
|
|
103
96
|
removals = set(x.id()[x.type() != self.type])
|
|
104
97
|
|
|
105
|
-
def leave(n: Tree.Node, keep_children:
|
|
98
|
+
def leave(n: Tree.Node, keep_children: list[bool]) -> bool:
|
|
106
99
|
if n.id in removals and any(keep_children):
|
|
107
100
|
removals.remove(n.id)
|
|
108
101
|
return n.id not in removals
|
|
@@ -131,28 +124,48 @@ class CutDendriteTree(CutByType):
|
|
|
131
124
|
super().__init__(type=types.basal_dendrite) # TODO: apical dendrite
|
|
132
125
|
|
|
133
126
|
|
|
134
|
-
class
|
|
135
|
-
"""Cut tree by
|
|
127
|
+
class CutByFurcationOrder(Transform[Tree, Tree]):
|
|
128
|
+
"""Cut tree by furcation order."""
|
|
136
129
|
|
|
137
|
-
|
|
130
|
+
max_furcation_order: int
|
|
138
131
|
|
|
139
132
|
def __init__(self, max_bifurcation_order: int) -> None:
|
|
140
|
-
self.
|
|
133
|
+
self.max_furcation_order = max_bifurcation_order
|
|
141
134
|
|
|
142
135
|
def __call__(self, x: Tree) -> Tree:
|
|
143
136
|
return cut_tree(x, enter=self._enter)
|
|
144
137
|
|
|
145
138
|
def __repr__(self) -> str:
|
|
146
|
-
return f"CutByBifurcationOrder-{self.
|
|
139
|
+
return f"CutByBifurcationOrder-{self.max_furcation_order}"
|
|
147
140
|
|
|
148
|
-
def _enter(self, n: Tree.Node, parent_level: int | None) ->
|
|
141
|
+
def _enter(self, n: Tree.Node, parent_level: int | None) -> tuple[int, bool]:
|
|
149
142
|
if parent_level is None:
|
|
150
143
|
level = 0
|
|
151
|
-
elif n.
|
|
144
|
+
elif n.is_furcation():
|
|
152
145
|
level = parent_level + 1
|
|
153
146
|
else:
|
|
154
147
|
level = parent_level
|
|
155
|
-
return (level, level >= self.
|
|
148
|
+
return (level, level >= self.max_furcation_order)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@deprecated("Use CutByFurcationOrder instead")
|
|
152
|
+
class CutByBifurcationOrder(CutByFurcationOrder):
|
|
153
|
+
"""Cut tree by bifurcation order.
|
|
154
|
+
|
|
155
|
+
Notes
|
|
156
|
+
-----
|
|
157
|
+
Deprecated due to the wrong spelling of furcation. For now, it
|
|
158
|
+
is just an alias of `CutByFurcationOrder` and raise a warning. It
|
|
159
|
+
will be change to raise an error in the future.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
max_furcation_order: int
|
|
163
|
+
|
|
164
|
+
def __init__(self, max_bifurcation_order: int) -> None:
|
|
165
|
+
super().__init__(max_bifurcation_order)
|
|
166
|
+
|
|
167
|
+
def __repr__(self) -> str:
|
|
168
|
+
return f"CutByBifurcationOrder-{self.max_furcation_order}"
|
|
156
169
|
|
|
157
170
|
|
|
158
171
|
class CutShortTipBranch(Transform[Tree, Tree]):
|
|
@@ -164,7 +177,7 @@ class CutShortTipBranch(Transform[Tree, Tree]):
|
|
|
164
177
|
"""
|
|
165
178
|
|
|
166
179
|
thre: float
|
|
167
|
-
callbacks:
|
|
180
|
+
callbacks: list[Callable[[Tree.Branch], None]]
|
|
168
181
|
|
|
169
182
|
def __init__(
|
|
170
183
|
self, thre: float = 5, callback: Optional[Callable[[Tree.Branch], None]] = None
|
|
@@ -176,7 +189,7 @@ class CutShortTipBranch(Transform[Tree, Tree]):
|
|
|
176
189
|
self.callbacks.append(callback)
|
|
177
190
|
|
|
178
191
|
def __call__(self, x: Tree) -> Tree:
|
|
179
|
-
removals:
|
|
192
|
+
removals: list[int] = []
|
|
180
193
|
self.callbacks.append(lambda br: removals.append(br[1].id))
|
|
181
194
|
x.traverse(leave=self._leave)
|
|
182
195
|
self.callbacks.pop()
|
|
@@ -186,8 +199,8 @@ class CutShortTipBranch(Transform[Tree, Tree]):
|
|
|
186
199
|
return f"threshold={self.thre}"
|
|
187
200
|
|
|
188
201
|
def _leave(
|
|
189
|
-
self, n: Tree.Node, children:
|
|
190
|
-
) ->
|
|
202
|
+
self, n: Tree.Node, children: list[tuple[float, Tree.Node] | None]
|
|
203
|
+
) -> tuple[float, Tree.Node] | None:
|
|
191
204
|
if len(children) == 0: # tip
|
|
192
205
|
return 0, n
|
|
193
206
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Assemble a tree."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import Iterable
|
|
3
4
|
from copy import copy
|
|
4
|
-
from typing import
|
|
5
|
+
from typing import Optional
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
7
8
|
import pandas as pd
|
|
@@ -18,7 +19,7 @@ from swcgeom.transforms.base import Transform
|
|
|
18
19
|
EPS = 1e-5
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
class LinesToTree(Transform[
|
|
22
|
+
class LinesToTree(Transform[list[pd.DataFrame], Tree]):
|
|
22
23
|
"""Assemble lines to swc."""
|
|
23
24
|
|
|
24
25
|
def __init__(self, *, thre: float = 0.2, undirected: bool = True):
|
|
@@ -97,7 +98,7 @@ class LinesToTree(Transform[List[pd.DataFrame], Tree]):
|
|
|
97
98
|
undirected: bool = True,
|
|
98
99
|
sort_nodes: bool = True,
|
|
99
100
|
names: Optional[SWCNames] = None,
|
|
100
|
-
) ->
|
|
101
|
+
) -> tuple[pd.DataFrame, list[pd.DataFrame]]:
|
|
101
102
|
"""Trying assemble lines to a tree.
|
|
102
103
|
|
|
103
104
|
Treat the first line as a tree, find a line whose shortest distance
|
swcgeom/utils/download.py
CHANGED
|
@@ -13,6 +13,7 @@ import itertools
|
|
|
13
13
|
import logging
|
|
14
14
|
import multiprocessing
|
|
15
15
|
import os
|
|
16
|
+
from functools import partial
|
|
16
17
|
from urllib.parse import urljoin
|
|
17
18
|
|
|
18
19
|
__all__ = ["download", "fetch_page", "clone_index_page"]
|
|
@@ -26,7 +27,7 @@ def download(dst: str, url: str) -> None:
|
|
|
26
27
|
r = conn.request("GET", url)
|
|
27
28
|
|
|
28
29
|
dirname = os.path.dirname(dst)
|
|
29
|
-
if not os.path.exists(dirname):
|
|
30
|
+
if dirname != "" and not os.path.exists(dirname):
|
|
30
31
|
os.makedirs(dirname)
|
|
31
32
|
|
|
32
33
|
with open(dst, "wb") as file:
|
|
@@ -41,7 +42,7 @@ def fetch_page(url: str):
|
|
|
41
42
|
conn = connection_from_url(url)
|
|
42
43
|
r = conn.request("GET", url)
|
|
43
44
|
data = r.data.decode("utf-8")
|
|
44
|
-
return BeautifulSoup(data)
|
|
45
|
+
return BeautifulSoup(data, features="html.parser")
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
def clone_index_page(
|
|
@@ -62,31 +63,35 @@ def clone_index_page(
|
|
|
62
63
|
multiprocess : int, default `4`
|
|
63
64
|
How many process are available for download.
|
|
64
65
|
"""
|
|
65
|
-
from urllib3.exceptions import HTTPError
|
|
66
|
-
|
|
67
66
|
files = get_urls_in_index_page(index_url)
|
|
68
67
|
logging.info("downloader: search `%s`, found %s files.", index_url, len(files))
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
logging.info("downloader: file `%s` exits, skiped.", dist)
|
|
76
|
-
return
|
|
69
|
+
task = partial(
|
|
70
|
+
_clone_index_page, index_url=index_url, dist_dir=dist_dir, override=override
|
|
71
|
+
)
|
|
72
|
+
with multiprocessing.Pool(multiprocess) as p:
|
|
73
|
+
p.map(task, files)
|
|
77
74
|
|
|
78
|
-
logging.info("downloader: file `%s` exits, deleted.", dist)
|
|
79
|
-
os.remove(filepath)
|
|
80
75
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
download(filepath, url)
|
|
84
|
-
logging.info("downloader: download `%s` to `%s`", url, dist)
|
|
85
|
-
except HTTPError as ex:
|
|
86
|
-
logging.info("downloader: fails to download `%s`, except `%s`", url, ex)
|
|
76
|
+
def _clone_index_page(url: str, index_url: str, dist_dir: str, override: bool) -> None:
|
|
77
|
+
from urllib3.exceptions import HTTPError
|
|
87
78
|
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
filepath = url.removeprefix(index_url)
|
|
80
|
+
dist = os.path.join(dist_dir, filepath)
|
|
81
|
+
if os.path.exists(dist):
|
|
82
|
+
if not override:
|
|
83
|
+
logging.info("downloader: file `%s` exits, skiped.", dist)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
logging.info("downloader: file `%s` exits, deleted.", dist)
|
|
87
|
+
os.remove(dist)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
logging.info("downloader: downloading `%s` to `%s`", url, dist)
|
|
91
|
+
download(dist, url)
|
|
92
|
+
logging.info("downloader: download `%s` to `%s`", url, dist)
|
|
93
|
+
except HTTPError as ex:
|
|
94
|
+
logging.info("downloader: fails to download `%s`, except `%s`", url, ex)
|
|
90
95
|
|
|
91
96
|
|
|
92
97
|
def get_urls_in_index_page(url: str) -> list[str]:
|
|
@@ -97,3 +102,21 @@ def get_urls_in_index_page(url: str) -> list[str]:
|
|
|
97
102
|
dirs = [urljoin(url, a) for a in links if a != "../" and a.endswith("/")]
|
|
98
103
|
files.extend(itertools.chain(*[get_urls_in_index_page(dir) for dir in dirs]))
|
|
99
104
|
return files
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
import argparse
|
|
109
|
+
|
|
110
|
+
parser = argparse.ArgumentParser(description="Download files from index page.")
|
|
111
|
+
parser.add_argument("url", type=str, help="URL of index page.")
|
|
112
|
+
parser.add_argument("dist", type=str, help="Directory of dist.")
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--override", type=bool, default=False, help="Override existing file."
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--multiprocess", type=int, default=4, help="How many process are available."
|
|
118
|
+
)
|
|
119
|
+
args = parser.parse_args()
|
|
120
|
+
|
|
121
|
+
logging.basicConfig(level=logging.INFO)
|
|
122
|
+
clone_index_page(args.url, args.dist, args.override, args.multiprocess)
|
swcgeom/utils/ellipse.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
# pylint: disable=invalid-name
|
|
4
4
|
|
|
5
5
|
from functools import cached_property
|
|
6
|
-
from typing import Tuple
|
|
7
6
|
|
|
8
7
|
import numpy as np
|
|
9
8
|
import numpy.linalg as la
|
|
@@ -22,7 +21,7 @@ class Ellipse:
|
|
|
22
21
|
self.centroid = centroid
|
|
23
22
|
|
|
24
23
|
@property
|
|
25
|
-
def radii(self) ->
|
|
24
|
+
def radii(self) -> tuple[float, float]:
|
|
26
25
|
# x, y radii.
|
|
27
26
|
_U, D, _V = self.svd
|
|
28
27
|
rx, ry = 1.0 / np.sqrt(D)
|
|
@@ -39,7 +38,7 @@ class Ellipse:
|
|
|
39
38
|
return b
|
|
40
39
|
|
|
41
40
|
@property
|
|
42
|
-
def axes(self) ->
|
|
41
|
+
def axes(self) -> tuple[float, float]:
|
|
43
42
|
# Major and minor semi-axis of the ellipse.
|
|
44
43
|
rx, ry = self.radii
|
|
45
44
|
dx, dy = 2 * rx, 2 * ry
|
|
@@ -77,7 +76,7 @@ def mvee(points: npt.NDArray[np.floating], tol: float = 1e-3) -> Ellipse:
|
|
|
77
76
|
|
|
78
77
|
def _mvee(
|
|
79
78
|
points: npt.NDArray[np.floating], tol: float = 1e-3
|
|
80
|
-
) ->
|
|
79
|
+
) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
|
|
81
80
|
"""Finds the Minimum Volume Enclosing Ellipsoid.
|
|
82
81
|
|
|
83
82
|
Returns
|
swcgeom/utils/neuromorpho.py
CHANGED
|
@@ -81,7 +81,8 @@ import logging
|
|
|
81
81
|
import math
|
|
82
82
|
import os
|
|
83
83
|
import urllib.parse
|
|
84
|
-
from
|
|
84
|
+
from collections.abc import Callable, Iterable
|
|
85
|
+
from typing import Any, Literal, Optional
|
|
85
86
|
|
|
86
87
|
from tqdm import tqdm
|
|
87
88
|
|
|
@@ -116,7 +117,7 @@ SIZE_METADATA = 2 * GB
|
|
|
116
117
|
SIZE_DATA = 20 * GB
|
|
117
118
|
|
|
118
119
|
RESOURCES = Literal["morpho_cng", "morpho_source", "log_cng", "log_source"]
|
|
119
|
-
DOWNLOAD_CONFIGS:
|
|
120
|
+
DOWNLOAD_CONFIGS: dict[RESOURCES, tuple[str, int]] = {
|
|
120
121
|
# name/path: (url, size)
|
|
121
122
|
"morpho_cng": (URL_MORPHO_CNG, 20 * GB),
|
|
122
123
|
"morpho_source": (URL_LOG_CNG, 512 * GB),
|
|
@@ -145,7 +146,7 @@ invalid_ids = [
|
|
|
145
146
|
# fmt: on
|
|
146
147
|
|
|
147
148
|
|
|
148
|
-
def neuromorpho_is_valid(metadata:
|
|
149
|
+
def neuromorpho_is_valid(metadata: dict[str, Any]) -> bool:
|
|
149
150
|
return metadata["neuron_id"] not in invalid_ids
|
|
150
151
|
|
|
151
152
|
|
|
@@ -209,7 +210,7 @@ class NeuroMorpho:
|
|
|
209
210
|
self._info("skip download metadata")
|
|
210
211
|
|
|
211
212
|
# file
|
|
212
|
-
def dumps(keys:
|
|
213
|
+
def dumps(keys: list[bytes]) -> str:
|
|
213
214
|
return json.dumps([i.decode("utf-8") for i in keys])
|
|
214
215
|
|
|
215
216
|
for name in resources:
|
|
@@ -238,8 +239,8 @@ class NeuroMorpho:
|
|
|
238
239
|
self,
|
|
239
240
|
dest: Optional[str] = None,
|
|
240
241
|
*,
|
|
241
|
-
group_by: Optional[str | Callable[[
|
|
242
|
-
where: Optional[Callable[[
|
|
242
|
+
group_by: Optional[str | Callable[[dict[str, Any]], str | None]] = None,
|
|
243
|
+
where: Optional[Callable[[dict[str, Any]], bool]] = None,
|
|
243
244
|
encoding: str | None = "utf-8",
|
|
244
245
|
) -> None:
|
|
245
246
|
r"""Convert lmdb format to SWCs.
|
|
@@ -249,11 +250,11 @@ class NeuroMorpho:
|
|
|
249
250
|
path : str
|
|
250
251
|
dest : str, optional
|
|
251
252
|
If None, use `path/swc`.
|
|
252
|
-
group_by : str | (metadata:
|
|
253
|
+
group_by : str | (metadata: dict[str, Any]) -> str | None, optional
|
|
253
254
|
Group neurons by metadata. If a None is returned then no
|
|
254
255
|
grouping. If a string is entered, use it as a metadata
|
|
255
256
|
attribute name for grouping, e.g.: `archive`, `species`.
|
|
256
|
-
where : (metadata:
|
|
257
|
+
where : (metadata: dict[str, Any]) -> bool, optional
|
|
257
258
|
Filter neurons by metadata.
|
|
258
259
|
encoding : str | None, default to `utf-8`
|
|
259
260
|
Change swc encoding, part of the original data is not utf-8
|
|
@@ -346,14 +347,14 @@ class NeuroMorpho:
|
|
|
346
347
|
pages: Optional[Iterable[int]] = None,
|
|
347
348
|
page_size: int = API_PAGE_SIZE_MAX,
|
|
348
349
|
**kwargs,
|
|
349
|
-
) ->
|
|
350
|
+
) -> list[int]:
|
|
350
351
|
r"""Download all neuron metadata.
|
|
351
352
|
|
|
352
353
|
Parameters
|
|
353
354
|
----------
|
|
354
355
|
path : str
|
|
355
356
|
Path to save data.
|
|
356
|
-
pages :
|
|
357
|
+
pages : List of int, optional
|
|
357
358
|
If is None, download all pages.
|
|
358
359
|
verbose : bool, default False
|
|
359
360
|
Show verbose log.
|
|
@@ -362,7 +363,7 @@ class NeuroMorpho:
|
|
|
362
363
|
|
|
363
364
|
Returns
|
|
364
365
|
-------
|
|
365
|
-
err_pages :
|
|
366
|
+
err_pages : List of int
|
|
366
367
|
Failed pages.
|
|
367
368
|
"""
|
|
368
369
|
|
|
@@ -402,7 +403,7 @@ class NeuroMorpho:
|
|
|
402
403
|
override: bool = False,
|
|
403
404
|
map_size: int = 512 * GB,
|
|
404
405
|
**kwargs,
|
|
405
|
-
) ->
|
|
406
|
+
) -> list[bytes]:
|
|
406
407
|
"""Download files.
|
|
407
408
|
|
|
408
409
|
Parameters
|
|
@@ -412,7 +413,7 @@ class NeuroMorpho:
|
|
|
412
413
|
Path to save data.
|
|
413
414
|
path_metadata : str
|
|
414
415
|
Path to lmdb of metadata.
|
|
415
|
-
keys :
|
|
416
|
+
keys : List of bytes, optional
|
|
416
417
|
If exist, ignore `override` option. If None, download all key.
|
|
417
418
|
override : bool, default False
|
|
418
419
|
Override even exists.
|
|
@@ -422,7 +423,7 @@ class NeuroMorpho:
|
|
|
422
423
|
|
|
423
424
|
Returns
|
|
424
425
|
-------
|
|
425
|
-
err_keys :
|
|
426
|
+
err_keys : List of str
|
|
426
427
|
Failed keys.
|
|
427
428
|
"""
|
|
428
429
|
|
|
@@ -459,7 +460,7 @@ class NeuroMorpho:
|
|
|
459
460
|
|
|
460
461
|
def _get_metadata(
|
|
461
462
|
self, page: int, page_size: int = API_PAGE_SIZE_MAX, **kwargs
|
|
462
|
-
) ->
|
|
463
|
+
) -> dict[str, Any]:
|
|
463
464
|
params = {
|
|
464
465
|
"page": page,
|
|
465
466
|
"size": page_size,
|
|
@@ -470,7 +471,7 @@ class NeuroMorpho:
|
|
|
470
471
|
resp = self._get(url, **kwargs)
|
|
471
472
|
return json.loads(resp)
|
|
472
473
|
|
|
473
|
-
def _get_file(self, url: str, metadata:
|
|
474
|
+
def _get_file(self, url: str, metadata: dict[str, Any], **kwargs) -> bytes:
|
|
474
475
|
"""Get file.
|
|
475
476
|
|
|
476
477
|
Returns
|
swcgeom/utils/plotter_2d.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""2D Plotting utils."""
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import Optional
|
|
4
4
|
|
|
5
5
|
import matplotlib.pyplot as plt
|
|
6
6
|
import numpy as np
|
|
@@ -19,7 +19,12 @@ __all__ = ["draw_lines", "draw_direction_indicator", "draw_circles", "get_fig_ax
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def draw_lines(
|
|
22
|
-
ax: Axes,
|
|
22
|
+
ax: Axes,
|
|
23
|
+
lines: npt.NDArray[np.floating],
|
|
24
|
+
camera: Camera,
|
|
25
|
+
joinstyle="round",
|
|
26
|
+
capstyle="round",
|
|
27
|
+
**kwargs,
|
|
23
28
|
) -> LineCollection:
|
|
24
29
|
"""Draw lines.
|
|
25
30
|
|
|
@@ -43,11 +48,12 @@ def draw_lines(
|
|
|
43
48
|
starts, ends = np.dot(T, starts.T).T[:, 0:2], np.dot(T, ends.T).T[:, 0:2]
|
|
44
49
|
|
|
45
50
|
edges = np.stack([starts, ends], axis=1)
|
|
46
|
-
|
|
51
|
+
collection = LineCollection(edges, joinstyle=joinstyle, capstyle=capstyle, **kwargs) # type: ignore
|
|
52
|
+
return ax.add_collection(collection) # type: ignore
|
|
47
53
|
|
|
48
54
|
|
|
49
55
|
def draw_direction_indicator(
|
|
50
|
-
ax: Axes, camera: Camera, loc:
|
|
56
|
+
ax: Axes, camera: Camera, loc: tuple[float, float]
|
|
51
57
|
) -> None:
|
|
52
58
|
x, y = loc
|
|
53
59
|
direction = camera.MV.dot(
|
|
@@ -120,7 +126,7 @@ def draw_circles(
|
|
|
120
126
|
|
|
121
127
|
def get_fig_ax(
|
|
122
128
|
fig: Optional[Figure] = None, ax: Optional[Axes] = None
|
|
123
|
-
) ->
|
|
129
|
+
) -> tuple[Figure, Axes]:
|
|
124
130
|
if fig is None and ax is not None:
|
|
125
131
|
fig = ax.get_figure()
|
|
126
132
|
assert fig is not None, "expecting a figure from the axes"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""3D Plotting utils."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import numpy.typing as npt
|
|
5
|
+
from mpl_toolkits.mplot3d import Axes3D
|
|
6
|
+
from mpl_toolkits.mplot3d.art3d import Line3DCollection
|
|
7
|
+
|
|
8
|
+
__all__ = ["draw_lines_3d"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def draw_lines_3d(
|
|
12
|
+
ax: Axes3D,
|
|
13
|
+
lines: npt.NDArray[np.floating],
|
|
14
|
+
joinstyle="round",
|
|
15
|
+
capstyle="round",
|
|
16
|
+
**kwargs,
|
|
17
|
+
):
|
|
18
|
+
"""Draw lines.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
ax : ~matplotlib.axes.Axes
|
|
23
|
+
lines : A collection of coords of lines
|
|
24
|
+
Excepting a ndarray of shape (N, 2, 3), the axis-2 holds two points,
|
|
25
|
+
and the axis-3 holds the coordinates (x, y, z).
|
|
26
|
+
**kwargs : dict[str, Unknown]
|
|
27
|
+
Forwarded to `~mpl_toolkits.mplot3d.art3d.Line3DCollection`.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
line_collection = Line3DCollection(lines, joinstyle=joinstyle, capstyle=capstyle, **kwargs) # type: ignore
|
|
31
|
+
return ax.add_collection3d(line_collection)
|
swcgeom/utils/renderer.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Rendering related utils."""
|
|
2
2
|
|
|
3
3
|
from functools import cached_property
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Literal, cast
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import numpy.typing as npt
|
|
@@ -15,9 +15,9 @@ from swcgeom.utils.transforms import (
|
|
|
15
15
|
|
|
16
16
|
__all__ = ["CameraOptions", "Camera", "SimpleCamera", "palette"]
|
|
17
17
|
|
|
18
|
-
CameraOption = Vec3f |
|
|
18
|
+
CameraOption = Vec3f | tuple[Vec3f, Vec3f] | tuple[Vec3f, Vec3f, Vec3f]
|
|
19
19
|
CameraPreset = Literal["xy", "yz", "zx", "yx", "zy", "xz"]
|
|
20
|
-
CameraPresets:
|
|
20
|
+
CameraPresets: dict[CameraPreset, tuple[Vec3f, Vec3f, Vec3f]] = {
|
|
21
21
|
"xy": ((0.0, 0.0, 0.0), (+0.0, +0.0, -1.0), (+0.0, +1.0, +0.0)),
|
|
22
22
|
"yz": ((0.0, 0.0, 0.0), (-1.0, +0.0, +0.0), (+0.0, +0.0, +1.0)),
|
|
23
23
|
"zx": ((0.0, 0.0, 0.0), (+0.0, -1.0, +0.0), (+1.0, +0.0, +0.0)),
|
|
@@ -77,7 +77,7 @@ class SimpleCamera(Camera):
|
|
|
77
77
|
if isinstance(camera[0], tuple):
|
|
78
78
|
return cls((0, 0, 0), cast(Vec3f, camera), (0, 1, 0))
|
|
79
79
|
|
|
80
|
-
return cls(*cast(
|
|
80
|
+
return cls(*cast(tuple[Vec3f, Vec3f, Vec3f], camera))
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
class Palette:
|
|
@@ -85,8 +85,8 @@ class Palette:
|
|
|
85
85
|
|
|
86
86
|
# pylint: disable=too-few-public-methods
|
|
87
87
|
|
|
88
|
-
default:
|
|
89
|
-
vaa3d:
|
|
88
|
+
default: dict[int, str]
|
|
89
|
+
vaa3d: dict[int, str]
|
|
90
90
|
|
|
91
91
|
def __init__(self):
|
|
92
92
|
default = [
|
swcgeom/utils/sdf.py
CHANGED
|
@@ -10,10 +10,11 @@ the future, use `sdflit` instead.
|
|
|
10
10
|
|
|
11
11
|
import warnings
|
|
12
12
|
from abc import ABC, abstractmethod
|
|
13
|
-
from
|
|
13
|
+
from collections.abc import Iterable
|
|
14
14
|
|
|
15
15
|
import numpy as np
|
|
16
16
|
import numpy.typing as npt
|
|
17
|
+
from typing_extensions import deprecated
|
|
17
18
|
|
|
18
19
|
from swcgeom.utils.solid_geometry import project_vector_on_plane
|
|
19
20
|
|
|
@@ -29,7 +30,7 @@ __all__ = [
|
|
|
29
30
|
]
|
|
30
31
|
|
|
31
32
|
# Axis-aligned bounding box, tuple of array of shape (3,)
|
|
32
|
-
AABB =
|
|
33
|
+
AABB = tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class SDF(ABC):
|
|
@@ -173,6 +174,7 @@ class SDFDifference(SDF):
|
|
|
173
174
|
return flags
|
|
174
175
|
|
|
175
176
|
|
|
177
|
+
@deprecated("Use `SDFUnion` instead")
|
|
176
178
|
class SDFCompose(SDFUnion):
|
|
177
179
|
"""Compose multiple SDFs.
|
|
178
180
|
|
|
@@ -181,11 +183,6 @@ class SDFCompose(SDFUnion):
|
|
|
181
183
|
"""
|
|
182
184
|
|
|
183
185
|
def __init__(self, sdfs: Iterable[SDF]) -> None:
|
|
184
|
-
warnings.warn(
|
|
185
|
-
"`SDFCompose` has been replace by `SDFUnion` since v0.14.0, "
|
|
186
|
-
"and will be removed in next version",
|
|
187
|
-
DeprecationWarning,
|
|
188
|
-
)
|
|
189
186
|
sdfs = list(sdfs)
|
|
190
187
|
if len(sdfs) == 1:
|
|
191
188
|
warnings.warn("compose only one SDF, use SDFCompose.compose instead")
|