swcgeom 0.17.0__py3-none-any.whl → 0.17.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of swcgeom might be problematic. Click here for more details.
- swcgeom/_version.py +2 -2
- swcgeom/analysis/feature_extractor.py +13 -12
- swcgeom/analysis/features.py +4 -4
- swcgeom/analysis/lmeasure.py +5 -5
- swcgeom/analysis/sholl.py +4 -4
- 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 +2 -2
- swcgeom/core/path.py +3 -2
- swcgeom/core/population.py +16 -27
- swcgeom/core/swc.py +11 -10
- swcgeom/core/swc_utils/base.py +8 -17
- 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 +22 -34
- swcgeom/core/tree_utils.py +11 -10
- swcgeom/core/tree_utils_impl.py +3 -3
- swcgeom/images/augmentation.py +3 -3
- swcgeom/images/folder.py +10 -16
- swcgeom/images/io.py +19 -30
- swcgeom/transforms/image_stack.py +6 -5
- swcgeom/transforms/images.py +2 -3
- swcgeom/transforms/neurolucida_asc.py +4 -6
- swcgeom/transforms/population.py +1 -3
- swcgeom/transforms/tree.py +8 -7
- swcgeom/transforms/tree_assembler.py +4 -3
- 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 +2 -2
- swcgeom/utils/solid_geometry.py +1 -3
- swcgeom/utils/transforms.py +1 -3
- swcgeom/utils/volumetric_object.py +8 -10
- {swcgeom-0.17.0.dist-info → swcgeom-0.17.1.dist-info}/METADATA +1 -1
- swcgeom-0.17.1.dist-info/RECORD +67 -0
- swcgeom-0.17.0.dist-info/RECORD +0 -65
- {swcgeom-0.17.0.dist-info → swcgeom-0.17.1.dist-info}/LICENSE +0 -0
- {swcgeom-0.17.0.dist-info → swcgeom-0.17.1.dist-info}/WHEEL +0 -0
- {swcgeom-0.17.0.dist-info → swcgeom-0.17.1.dist-info}/top_level.txt +0 -0
swcgeom/images/folder.py
CHANGED
|
@@ -4,18 +4,9 @@ import math
|
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
6
|
import warnings
|
|
7
|
+
from collections.abc import Callable, Iterable
|
|
7
8
|
from dataclasses import dataclass
|
|
8
|
-
from typing import
|
|
9
|
-
Callable,
|
|
10
|
-
Generic,
|
|
11
|
-
Iterable,
|
|
12
|
-
List,
|
|
13
|
-
Literal,
|
|
14
|
-
Optional,
|
|
15
|
-
Tuple,
|
|
16
|
-
TypeVar,
|
|
17
|
-
overload,
|
|
18
|
-
)
|
|
9
|
+
from typing import Generic, Literal, Optional, TypeVar, overload
|
|
19
10
|
|
|
20
11
|
import numpy as np
|
|
21
12
|
import numpy.typing as npt
|
|
@@ -33,7 +24,7 @@ T = TypeVar("T")
|
|
|
33
24
|
class ImageStackFolderBase(Generic[ScalarType, T]):
|
|
34
25
|
"""Image stack folder base."""
|
|
35
26
|
|
|
36
|
-
files:
|
|
27
|
+
files: list[str]
|
|
37
28
|
transform: Transform[npt.NDArray[ScalarType], T]
|
|
38
29
|
|
|
39
30
|
# fmt: off
|
|
@@ -61,7 +52,10 @@ class ImageStackFolderBase(Generic[ScalarType, T]):
|
|
|
61
52
|
return read_imgs(fname, dtype=self.dtype).get_full() # type: ignore
|
|
62
53
|
|
|
63
54
|
@staticmethod
|
|
64
|
-
def scan(root: str, *, pattern: Optional[str] = None) ->
|
|
55
|
+
def scan(root: str, *, pattern: Optional[str] = None) -> list[str]:
|
|
56
|
+
if not os.path.isdir(root):
|
|
57
|
+
raise NotADirectoryError(f"not a directory: {root}")
|
|
58
|
+
|
|
65
59
|
is_valid = re.compile(pattern).match if pattern is not None else truthly
|
|
66
60
|
|
|
67
61
|
fs = []
|
|
@@ -168,13 +162,13 @@ class ImageStackFolder(ImageStackFolderBase[ScalarType, T]):
|
|
|
168
162
|
class LabeledImageStackFolder(ImageStackFolderBase[ScalarType, T]):
|
|
169
163
|
"""Image stack folder with label."""
|
|
170
164
|
|
|
171
|
-
labels:
|
|
165
|
+
labels: list[int]
|
|
172
166
|
|
|
173
167
|
def __init__(self, files: Iterable[str], labels: Iterable[int], **kwargs):
|
|
174
168
|
super().__init__(files, **kwargs)
|
|
175
169
|
self.labels = list(labels)
|
|
176
170
|
|
|
177
|
-
def __getitem__(self, idx: int) ->
|
|
171
|
+
def __getitem__(self, idx: int) -> tuple[T, int]:
|
|
178
172
|
return self._get(self.files[idx]), self.labels[idx]
|
|
179
173
|
|
|
180
174
|
@classmethod
|
|
@@ -205,7 +199,7 @@ class PathImageStackFolder(ImageStackFolderBase[ScalarType, T]):
|
|
|
205
199
|
super().__init__(files, **kwargs)
|
|
206
200
|
self.root = root
|
|
207
201
|
|
|
208
|
-
def __getitem__(self, idx: int) ->
|
|
202
|
+
def __getitem__(self, idx: int) -> tuple[T, str]:
|
|
209
203
|
relpath = os.path.relpath(self.files[idx], self.root)
|
|
210
204
|
return self._get(self.files[idx]), relpath
|
|
211
205
|
|
swcgeom/images/io.py
CHANGED
|
@@ -4,20 +4,9 @@ import os
|
|
|
4
4
|
import re
|
|
5
5
|
import warnings
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import Callable, Iterable
|
|
7
8
|
from functools import cache, lru_cache
|
|
8
|
-
from typing import
|
|
9
|
-
Any,
|
|
10
|
-
Callable,
|
|
11
|
-
Generic,
|
|
12
|
-
Iterable,
|
|
13
|
-
List,
|
|
14
|
-
Literal,
|
|
15
|
-
Optional,
|
|
16
|
-
Tuple,
|
|
17
|
-
TypeVar,
|
|
18
|
-
cast,
|
|
19
|
-
overload,
|
|
20
|
-
)
|
|
9
|
+
from typing import Any, Generic, Literal, Optional, TypeVar, cast, overload
|
|
21
10
|
|
|
22
11
|
import nrrd
|
|
23
12
|
import numpy as np
|
|
@@ -27,7 +16,7 @@ from v3dpy.loaders import PBD, Raw
|
|
|
27
16
|
|
|
28
17
|
__all__ = ["read_imgs", "save_tiff", "read_images"]
|
|
29
18
|
|
|
30
|
-
Vec3i =
|
|
19
|
+
Vec3i = tuple[int, int, int]
|
|
31
20
|
ScalarType = TypeVar("ScalarType", bound=np.generic, covariant=True)
|
|
32
21
|
|
|
33
22
|
RE_TERAFLY_ROOT = re.compile(r"^RES\((\d+)x(\d+)x(\d+)\)$")
|
|
@@ -58,17 +47,17 @@ class ImageStack(ABC, Generic[ScalarType]):
|
|
|
58
47
|
def __getitem__(self, key: int) -> npt.NDArray[ScalarType]: ... # array of shape (Y, Z, C)
|
|
59
48
|
@overload
|
|
60
49
|
@abstractmethod
|
|
61
|
-
def __getitem__(self, key:
|
|
50
|
+
def __getitem__(self, key: tuple[int, int]) -> npt.NDArray[ScalarType]: ... # array of shape (Z, C)
|
|
62
51
|
@overload
|
|
63
52
|
@abstractmethod
|
|
64
|
-
def __getitem__(self, key:
|
|
53
|
+
def __getitem__(self, key: tuple[int, int, int]) -> npt.NDArray[ScalarType]: ... # array of shape (C,)
|
|
65
54
|
@overload
|
|
66
55
|
@abstractmethod
|
|
67
|
-
def __getitem__(self, key:
|
|
56
|
+
def __getitem__(self, key: tuple[int, int, int, int]) -> ScalarType: ... # value
|
|
68
57
|
@overload
|
|
69
58
|
@abstractmethod
|
|
70
59
|
def __getitem__(
|
|
71
|
-
self, key: slice |
|
|
60
|
+
self, key: slice | tuple[slice, slice] | tuple[slice, slice, slice] | tuple[slice, slice, slice, slice],
|
|
72
61
|
) -> npt.NDArray[ScalarType]: ... # array of shape (X, Y, Z, C)
|
|
73
62
|
@overload
|
|
74
63
|
@abstractmethod
|
|
@@ -95,7 +84,7 @@ class ImageStack(ABC, Generic[ScalarType]):
|
|
|
95
84
|
return self[:, :, :, :]
|
|
96
85
|
|
|
97
86
|
@property
|
|
98
|
-
def shape(self) ->
|
|
87
|
+
def shape(self) -> tuple[int, int, int, int]:
|
|
99
88
|
raise NotImplementedError()
|
|
100
89
|
|
|
101
90
|
|
|
@@ -118,7 +107,7 @@ def read_imgs(fname: str, **kwargs): # type: ignore
|
|
|
118
107
|
Casting data to specified dtype. If integer and float
|
|
119
108
|
conversions occur, they will be scaled (assuming floats are
|
|
120
109
|
between 0 and 1).
|
|
121
|
-
**kwargs :
|
|
110
|
+
**kwargs : dict[str, Any]
|
|
122
111
|
Forwarding to the corresponding reader.
|
|
123
112
|
"""
|
|
124
113
|
|
|
@@ -169,7 +158,7 @@ def save_tiff(
|
|
|
169
158
|
Compression algorithm, forwarding to `tifffile.imwrite`. If no
|
|
170
159
|
algorithnm is specify specified, we will use the zlib algorithm
|
|
171
160
|
with compression level 6 by default.
|
|
172
|
-
**kwargs :
|
|
161
|
+
**kwargs : dict[str, Any]
|
|
173
162
|
Forwarding to `tifffile.imwrite`
|
|
174
163
|
"""
|
|
175
164
|
if isinstance(data, ImageStack):
|
|
@@ -245,8 +234,8 @@ class NDArrayImageStack(ImageStack[ScalarType]):
|
|
|
245
234
|
return self.imgs
|
|
246
235
|
|
|
247
236
|
@property
|
|
248
|
-
def shape(self) ->
|
|
249
|
-
return cast(
|
|
237
|
+
def shape(self) -> tuple[int, int, int, int]:
|
|
238
|
+
return cast(tuple[int, int, int, int], self.imgs.shape)
|
|
250
239
|
|
|
251
240
|
|
|
252
241
|
class TiffImageStack(NDArrayImageStack[ScalarType]):
|
|
@@ -324,7 +313,7 @@ class TeraflyImageStack(ImageStack[ScalarType]):
|
|
|
324
313
|
use its coordinate system, remember to FLIP Y-AXIS BACK.
|
|
325
314
|
"""
|
|
326
315
|
|
|
327
|
-
_listdir: Callable[[str],
|
|
316
|
+
_listdir: Callable[[str], list[str]]
|
|
328
317
|
_read_patch: Callable[[str], npt.NDArray]
|
|
329
318
|
|
|
330
319
|
def __init__(
|
|
@@ -350,7 +339,7 @@ class TeraflyImageStack(ImageStack[ScalarType]):
|
|
|
350
339
|
self.res, self.res_dirs, self.res_patch_sizes = self.get_resolutions(root)
|
|
351
340
|
|
|
352
341
|
@cache
|
|
353
|
-
def listdir(path: str) ->
|
|
342
|
+
def listdir(path: str) -> list[str]:
|
|
354
343
|
return os.listdir(path)
|
|
355
344
|
|
|
356
345
|
@lru_cache(maxsize=lru_maxsize)
|
|
@@ -429,19 +418,19 @@ class TeraflyImageStack(ImageStack[ScalarType]):
|
|
|
429
418
|
raise NotImplementedError() # TODO
|
|
430
419
|
|
|
431
420
|
@property
|
|
432
|
-
def shape(self) ->
|
|
421
|
+
def shape(self) -> tuple[int, int, int, int]:
|
|
433
422
|
res_max = self.res[-1]
|
|
434
423
|
return res_max[0], res_max[1], res_max[2], 1
|
|
435
424
|
|
|
436
425
|
@classmethod
|
|
437
|
-
def get_resolutions(cls, root: str) ->
|
|
426
|
+
def get_resolutions(cls, root: str) -> tuple[list[Vec3i], list[str], list[Vec3i]]:
|
|
438
427
|
"""Get all resolutions.
|
|
439
428
|
|
|
440
429
|
Returns
|
|
441
430
|
-------
|
|
442
431
|
resolutions : List of (int, int, int)
|
|
443
432
|
Sequence of sorted resolutions (from small to large).
|
|
444
|
-
roots :
|
|
433
|
+
roots : list[str]
|
|
445
434
|
Sequence of root of resolutions respectively.
|
|
446
435
|
patch_sizes : List of (int, int, int)
|
|
447
436
|
Sequence of patch size of resolutions respectively.
|
|
@@ -581,7 +570,7 @@ class GrayImageStack:
|
|
|
581
570
|
@overload
|
|
582
571
|
def __getitem__(self, key: npt.NDArray[np.integer[Any]]) -> np.float32: ...
|
|
583
572
|
@overload
|
|
584
|
-
def __getitem__(self, key: slice |
|
|
573
|
+
def __getitem__(self, key: slice | tuple[slice, slice] | tuple[slice, slice, slice]) -> npt.NDArray[np.float32]: ...
|
|
585
574
|
# fmt: on
|
|
586
575
|
def __getitem__(self, key):
|
|
587
576
|
"""Get pixel/patch of image stack."""
|
|
@@ -608,7 +597,7 @@ class GrayImageStack:
|
|
|
608
597
|
return self.imgs.get_full()[:, :, :, 0]
|
|
609
598
|
|
|
610
599
|
@property
|
|
611
|
-
def shape(self) ->
|
|
600
|
+
def shape(self) -> tuple[int, int, int]:
|
|
612
601
|
return self.imgs.shape[:-1]
|
|
613
602
|
|
|
614
603
|
|
|
@@ -12,7 +12,8 @@ pip install swcgeom[all]
|
|
|
12
12
|
import os
|
|
13
13
|
import re
|
|
14
14
|
import time
|
|
15
|
-
from
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
from typing import Optional
|
|
16
17
|
|
|
17
18
|
import numpy as np
|
|
18
19
|
import numpy.typing as npt
|
|
@@ -69,7 +70,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
|
|
|
69
70
|
x: Tree,
|
|
70
71
|
verbose: bool = True,
|
|
71
72
|
*,
|
|
72
|
-
ranges: Optional[
|
|
73
|
+
ranges: Optional[tuple[npt.ArrayLike, npt.ArrayLike]] = None,
|
|
73
74
|
) -> Iterable[npt.NDArray[np.uint8]]:
|
|
74
75
|
if verbose:
|
|
75
76
|
print("To image stack: " + x.source)
|
|
@@ -133,7 +134,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
|
|
|
133
134
|
scene = ObjectsScene()
|
|
134
135
|
scene.set_background((0, 0, 0))
|
|
135
136
|
|
|
136
|
-
def leave(n: Tree.Node, children:
|
|
137
|
+
def leave(n: Tree.Node, children: list[Tree.Node]) -> Tree.Node:
|
|
137
138
|
for c in children:
|
|
138
139
|
sdf = RoundCone(_tp3f(n.xyz()), _tp3f(c.xyz()), n.r, c.r).into()
|
|
139
140
|
scene.add_object(SDFObject(sdf, material).into())
|
|
@@ -175,7 +176,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
|
|
|
175
176
|
def save_tif(
|
|
176
177
|
fname: str,
|
|
177
178
|
frames: Iterable[npt.NDArray[np.uint8]],
|
|
178
|
-
resolution:
|
|
179
|
+
resolution: tuple[float, float] = (1, 1),
|
|
179
180
|
) -> None:
|
|
180
181
|
with tifffile.TiffWriter(fname) as tif:
|
|
181
182
|
for frame in frames:
|
|
@@ -191,7 +192,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
|
|
|
191
192
|
)
|
|
192
193
|
|
|
193
194
|
|
|
194
|
-
def _tp3f(x: npt.NDArray) ->
|
|
195
|
+
def _tp3f(x: npt.NDArray) -> tuple[float, float, float]:
|
|
195
196
|
"""Convert to tuple of 3 floats."""
|
|
196
197
|
assert len(x) == 3
|
|
197
198
|
return (float(x[0]), float(x[1]), float(x[2]))
|
swcgeom/transforms/images.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Image stack related transform."""
|
|
2
2
|
|
|
3
3
|
import warnings
|
|
4
|
-
from typing import Tuple
|
|
5
4
|
|
|
6
5
|
import numpy as np
|
|
7
6
|
import numpy.typing as npt
|
|
@@ -28,7 +27,7 @@ NDArrayf32 = npt.NDArray[np.float32]
|
|
|
28
27
|
class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
|
|
29
28
|
"""Get image stack center."""
|
|
30
29
|
|
|
31
|
-
def __init__(self, shape_out: int |
|
|
30
|
+
def __init__(self, shape_out: int | tuple[int, int, int]):
|
|
32
31
|
super().__init__()
|
|
33
32
|
self.shape_out = (
|
|
34
33
|
shape_out
|
|
@@ -53,7 +52,7 @@ class Center(ImagesCenterCrop):
|
|
|
53
52
|
Use :class:`ImagesCenterCrop` instead.
|
|
54
53
|
"""
|
|
55
54
|
|
|
56
|
-
def __init__(self, shape_out: int |
|
|
55
|
+
def __init__(self, shape_out: int | tuple[int, int, int]):
|
|
57
56
|
warnings.warn(
|
|
58
57
|
"`Center` is deprecated, use `ImagesCenterCrop` instead",
|
|
59
58
|
DeprecationWarning,
|
|
@@ -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,7 +1,8 @@
|
|
|
1
1
|
"""Transformation in tree."""
|
|
2
2
|
|
|
3
3
|
import warnings
|
|
4
|
-
from
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Optional
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
7
8
|
|
|
@@ -102,7 +103,7 @@ class CutByType(Transform[Tree, Tree]):
|
|
|
102
103
|
def __call__(self, x: Tree) -> Tree:
|
|
103
104
|
removals = set(x.id()[x.type() != self.type])
|
|
104
105
|
|
|
105
|
-
def leave(n: Tree.Node, keep_children:
|
|
106
|
+
def leave(n: Tree.Node, keep_children: list[bool]) -> bool:
|
|
106
107
|
if n.id in removals and any(keep_children):
|
|
107
108
|
removals.remove(n.id)
|
|
108
109
|
return n.id not in removals
|
|
@@ -145,7 +146,7 @@ class CutByBifurcationOrder(Transform[Tree, Tree]):
|
|
|
145
146
|
def __repr__(self) -> str:
|
|
146
147
|
return f"CutByBifurcationOrder-{self.max_bifurcation_order}"
|
|
147
148
|
|
|
148
|
-
def _enter(self, n: Tree.Node, parent_level: int | None) ->
|
|
149
|
+
def _enter(self, n: Tree.Node, parent_level: int | None) -> tuple[int, bool]:
|
|
149
150
|
if parent_level is None:
|
|
150
151
|
level = 0
|
|
151
152
|
elif n.is_bifurcation():
|
|
@@ -164,7 +165,7 @@ class CutShortTipBranch(Transform[Tree, Tree]):
|
|
|
164
165
|
"""
|
|
165
166
|
|
|
166
167
|
thre: float
|
|
167
|
-
callbacks:
|
|
168
|
+
callbacks: list[Callable[[Tree.Branch], None]]
|
|
168
169
|
|
|
169
170
|
def __init__(
|
|
170
171
|
self, thre: float = 5, callback: Optional[Callable[[Tree.Branch], None]] = None
|
|
@@ -176,7 +177,7 @@ class CutShortTipBranch(Transform[Tree, Tree]):
|
|
|
176
177
|
self.callbacks.append(callback)
|
|
177
178
|
|
|
178
179
|
def __call__(self, x: Tree) -> Tree:
|
|
179
|
-
removals:
|
|
180
|
+
removals: list[int] = []
|
|
180
181
|
self.callbacks.append(lambda br: removals.append(br[1].id))
|
|
181
182
|
x.traverse(leave=self._leave)
|
|
182
183
|
self.callbacks.pop()
|
|
@@ -186,8 +187,8 @@ class CutShortTipBranch(Transform[Tree, Tree]):
|
|
|
186
187
|
return f"threshold={self.thre}"
|
|
187
188
|
|
|
188
189
|
def _leave(
|
|
189
|
-
self, n: Tree.Node, children:
|
|
190
|
-
) ->
|
|
190
|
+
self, n: Tree.Node, children: list[tuple[float, Tree.Node] | None]
|
|
191
|
+
) -> tuple[float, Tree.Node] | None:
|
|
191
192
|
if len(children) == 0: # tip
|
|
192
193
|
return 0, n
|
|
193
194
|
|
|
@@ -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/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)
|