swcgeom 0.12.0__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/_version.py +2 -2
- swcgeom/analysis/feature_extractor.py +15 -6
- swcgeom/analysis/sholl.py +7 -3
- swcgeom/analysis/volume.py +80 -0
- swcgeom/core/swc_utils/checker.py +29 -5
- swcgeom/core/swc_utils/io.py +2 -2
- swcgeom/utils/__init__.py +2 -0
- swcgeom/utils/dsu.py +42 -0
- swcgeom/utils/file.py +7 -6
- swcgeom/utils/geometry_object.py +299 -0
- swcgeom/utils/neuromorpho.py +7 -2
- swcgeom/utils/renderer.py +4 -3
- swcgeom/utils/transforms.py +26 -1
- {swcgeom-0.12.0.dist-info → swcgeom-0.13.0.dist-info}/METADATA +2 -1
- {swcgeom-0.12.0.dist-info → swcgeom-0.13.0.dist-info}/RECORD +18 -15
- {swcgeom-0.12.0.dist-info → swcgeom-0.13.0.dist-info}/WHEEL +1 -1
- {swcgeom-0.12.0.dist-info → swcgeom-0.13.0.dist-info}/LICENSE +0 -0
- {swcgeom-0.12.0.dist-info → swcgeom-0.13.0.dist-info}/top_level.txt +0 -0
swcgeom/_version.py
CHANGED
|
@@ -25,6 +25,7 @@ from swcgeom.analysis.node_features import (
|
|
|
25
25
|
)
|
|
26
26
|
from swcgeom.analysis.path_features import PathFeatures
|
|
27
27
|
from swcgeom.analysis.sholl import Sholl
|
|
28
|
+
from swcgeom.analysis.volume import get_volume
|
|
28
29
|
from swcgeom.core import Population, Populations, Tree
|
|
29
30
|
from swcgeom.utils import padding1d
|
|
30
31
|
|
|
@@ -32,6 +33,7 @@ __all__ = ["Feature", "extract_feature"]
|
|
|
32
33
|
|
|
33
34
|
Feature = Literal[
|
|
34
35
|
"length",
|
|
36
|
+
"volume",
|
|
35
37
|
"sholl",
|
|
36
38
|
# node
|
|
37
39
|
"node_count",
|
|
@@ -54,7 +56,7 @@ Feature = Literal[
|
|
|
54
56
|
NDArrayf32 = npt.NDArray[np.float32]
|
|
55
57
|
FeatAndKwargs = Feature | Tuple[Feature, Dict[str, Any]]
|
|
56
58
|
|
|
57
|
-
Feature1D = set(["length", "node_count", "bifurcation_count", "tip_count"])
|
|
59
|
+
Feature1D = set(["length", "volume", "node_count", "bifurcation_count", "tip_count"])
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
class Features:
|
|
@@ -105,6 +107,9 @@ class Features:
|
|
|
105
107
|
def get_length(self, **kwargs) -> NDArrayf32:
|
|
106
108
|
return np.array([self.tree.length(**kwargs)], dtype=np.float32)
|
|
107
109
|
|
|
110
|
+
def get_volume(self, **kwargs) -> NDArrayf32:
|
|
111
|
+
return np.array([get_volume(self.tree, **kwargs)], dtype=np.float32)
|
|
112
|
+
|
|
108
113
|
def get_sholl(self, **kwargs) -> NDArrayf32:
|
|
109
114
|
return self.sholl.get(**kwargs).astype(np.float32)
|
|
110
115
|
|
|
@@ -288,9 +293,11 @@ class PopulationFeatureExtractor(FeatureExtractor):
|
|
|
288
293
|
v = np.stack([padding1d(len_max, v, dtype=np.float32) for v in vals])
|
|
289
294
|
return v
|
|
290
295
|
|
|
291
|
-
def _get_sholl_impl(
|
|
296
|
+
def _get_sholl_impl(
|
|
297
|
+
self, steps: int = 20, **kwargs
|
|
298
|
+
) -> Tuple[NDArrayf32, NDArrayf32]:
|
|
292
299
|
rmax = max(t.sholl.rmax for t in self._features)
|
|
293
|
-
rs = Sholl.get_rs(rmax=rmax, steps=
|
|
300
|
+
rs = Sholl.get_rs(rmax=rmax, steps=steps)
|
|
294
301
|
vals = self._get_impl("sholl", steps=rs, **kwargs)
|
|
295
302
|
return vals, rs
|
|
296
303
|
|
|
@@ -360,10 +367,12 @@ class PopulationsFeatureExtractor(FeatureExtractor):
|
|
|
360
367
|
|
|
361
368
|
return out
|
|
362
369
|
|
|
363
|
-
def _get_sholl_impl(
|
|
370
|
+
def _get_sholl_impl(
|
|
371
|
+
self, steps: int = 20, **kwargs
|
|
372
|
+
) -> Tuple[NDArrayf32, NDArrayf32]:
|
|
364
373
|
rmaxs = chain.from_iterable((t.sholl.rmax for t in p) for p in self._features)
|
|
365
374
|
rmax = max(rmaxs)
|
|
366
|
-
rs = Sholl.get_rs(rmax=rmax, steps=
|
|
375
|
+
rs = Sholl.get_rs(rmax=rmax, steps=steps)
|
|
367
376
|
vals = self._get_impl("sholl", steps=rs, **kwargs)
|
|
368
377
|
return vals, rs
|
|
369
378
|
|
|
@@ -408,7 +417,7 @@ class PopulationsFeatureExtractor(FeatureExtractor):
|
|
|
408
417
|
return ax
|
|
409
418
|
|
|
410
419
|
|
|
411
|
-
def extract_feature(obj: Tree | Population) -> FeatureExtractor:
|
|
420
|
+
def extract_feature(obj: Tree | Population | Populations) -> FeatureExtractor:
|
|
412
421
|
if isinstance(obj, Tree):
|
|
413
422
|
return TreeFeatureExtractor(obj)
|
|
414
423
|
|
swcgeom/analysis/sholl.py
CHANGED
|
@@ -42,9 +42,12 @@ class Sholl:
|
|
|
42
42
|
step: Optional[float] = None,
|
|
43
43
|
) -> None:
|
|
44
44
|
tree = Tree.from_swc(tree) if isinstance(tree, str) else tree
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
try:
|
|
46
|
+
self.tree = TranslateOrigin.transform(tree) # shift
|
|
47
|
+
self.rs = np.linalg.norm(self.tree.get_segments().xyz(), axis=2)
|
|
48
|
+
self.rmax = self.rs.max()
|
|
49
|
+
except Exception as e:
|
|
50
|
+
raise ValueError(f"invalid tree: {tree.source or ''}") from e
|
|
48
51
|
|
|
49
52
|
if step is not None:
|
|
50
53
|
warnings.warn(
|
|
@@ -96,6 +99,7 @@ class Sholl:
|
|
|
96
99
|
**kwargs :
|
|
97
100
|
Forwarding to plot method.
|
|
98
101
|
"""
|
|
102
|
+
|
|
99
103
|
if plot_type is not None:
|
|
100
104
|
warnings.warn(
|
|
101
105
|
"`plot_type` has been renamed to `kind` since v0.5.0, "
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Analysis of volume of a SWC tree."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from swcgeom.core import Tree
|
|
6
|
+
from swcgeom.utils import GeomFrustumCone, GeomSphere
|
|
7
|
+
|
|
8
|
+
__all__ = ["get_volume"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_volume(tree: Tree):
|
|
12
|
+
"""Get the volume of the tree.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
tree : Tree
|
|
17
|
+
SWC tree.
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
volume : float
|
|
22
|
+
Volume of the tree.
|
|
23
|
+
|
|
24
|
+
Notes
|
|
25
|
+
-----
|
|
26
|
+
The SWC format is a method for representing neurons, which includes
|
|
27
|
+
both the radius of individual points and their interconnectivity.
|
|
28
|
+
Consequently, there are multiple distinct approaches to
|
|
29
|
+
representation within this framework.
|
|
30
|
+
|
|
31
|
+
Currently, we support a standard approach to volume calculation.
|
|
32
|
+
This method involves treating each node as a sphere and
|
|
33
|
+
representing the connections between them as truncated cone-like
|
|
34
|
+
structures, or frustums, with varying radii at their top and bottom
|
|
35
|
+
surfaces.
|
|
36
|
+
|
|
37
|
+
More representation methods will be supported in the future.
|
|
38
|
+
"""
|
|
39
|
+
volume = 0.0
|
|
40
|
+
|
|
41
|
+
def leave(node: Tree.Node, children: List[GeomSphere]) -> GeomSphere:
|
|
42
|
+
sphere = GeomSphere(node.xyz(), node.r)
|
|
43
|
+
frustum_cones = [
|
|
44
|
+
GeomFrustumCone(node.xyz(), node.r, c.center, c.radius) for c in children
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
v = sphere.get_volume()
|
|
48
|
+
v += sum(fc.get_volume() for fc in frustum_cones)
|
|
49
|
+
v -= sum(sphere.get_intersect_volume(fc) for fc in frustum_cones)
|
|
50
|
+
v -= sum(s.get_intersect_volume(fc) for s, fc in zip(children, frustum_cones))
|
|
51
|
+
|
|
52
|
+
# TODO
|
|
53
|
+
# remove volume of intersection between frustum cones
|
|
54
|
+
# v -= sum(
|
|
55
|
+
# fc1.get_intersect_volume(fc2)
|
|
56
|
+
# for fc1 in frustum_cones
|
|
57
|
+
# for fc2 in frustum_cones
|
|
58
|
+
# if fc1 != fc2
|
|
59
|
+
# )
|
|
60
|
+
|
|
61
|
+
nonlocal volume
|
|
62
|
+
volume += v
|
|
63
|
+
return sphere
|
|
64
|
+
|
|
65
|
+
tree.traverse(leave=leave)
|
|
66
|
+
return volume
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
from io import StringIO
|
|
71
|
+
|
|
72
|
+
swc = """
|
|
73
|
+
1 1 0 0 0 1 -1
|
|
74
|
+
2 1 2 0 0 1 1
|
|
75
|
+
3 1 4 0 0 1 2
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
tree = Tree.from_swc(StringIO(swc))
|
|
79
|
+
volume = get_volume(tree)
|
|
80
|
+
print(volume)
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
"""Check common """
|
|
2
2
|
|
|
3
3
|
import warnings
|
|
4
|
+
from collections import defaultdict
|
|
4
5
|
from typing import Optional
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
7
8
|
import pandas as pd
|
|
8
9
|
|
|
9
10
|
from swcgeom.core.swc_utils.base import SWCNames, Topology, get_dsu, get_names, traverse
|
|
11
|
+
from swcgeom.utils import DisjointSetUnion
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
__all__ = [
|
|
12
15
|
"is_single_root",
|
|
13
16
|
"is_bifurcate",
|
|
14
17
|
"is_sorted",
|
|
18
|
+
"has_cyclic",
|
|
19
|
+
# legacy
|
|
15
20
|
"is_binary_tree",
|
|
16
21
|
"check_single_root",
|
|
17
22
|
]
|
|
@@ -25,13 +30,11 @@ def is_single_root(df: pd.DataFrame, *, names: Optional[SWCNames] = None) -> boo
|
|
|
25
30
|
def is_bifurcate(topology: Topology, *, exclude_root: bool = True) -> bool:
|
|
26
31
|
"""Check is it a bifurcate topology."""
|
|
27
32
|
|
|
28
|
-
children =
|
|
33
|
+
children = defaultdict(list)
|
|
29
34
|
for idx, pid in zip(*topology):
|
|
30
|
-
|
|
31
|
-
s.append(idx)
|
|
32
|
-
children[pid] = s
|
|
35
|
+
children[pid].append(idx)
|
|
33
36
|
|
|
34
|
-
root = children
|
|
37
|
+
root = children[-1]
|
|
35
38
|
for k, v in children.items():
|
|
36
39
|
if len(v) > 1 and (not exclude_root or k in root):
|
|
37
40
|
return False
|
|
@@ -58,6 +61,27 @@ def is_sorted(topology: Topology) -> bool:
|
|
|
58
61
|
return flag
|
|
59
62
|
|
|
60
63
|
|
|
64
|
+
def has_cyclic(topology: Topology) -> bool:
|
|
65
|
+
"""Has cyclic in topology."""
|
|
66
|
+
node_num = len(topology[0])
|
|
67
|
+
dsu = DisjointSetUnion(node_number=node_num)
|
|
68
|
+
|
|
69
|
+
for i in range(node_num):
|
|
70
|
+
node_a = topology[0][i]
|
|
71
|
+
node_b = topology[1][i]
|
|
72
|
+
# skip the root node
|
|
73
|
+
if node_b == -1:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
# check whether it is circle
|
|
77
|
+
if dsu.is_same_set(node_a, node_b):
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
dsu.union_sets(node_a, node_b)
|
|
81
|
+
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
61
85
|
def check_single_root(*args, **kwargs) -> bool:
|
|
62
86
|
warnings.warn(
|
|
63
87
|
"`check_single_root` has been renamed to `is_single_root` since"
|
swcgeom/core/swc_utils/io.py
CHANGED
|
@@ -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.
|
|
@@ -142,7 +142,7 @@ def parse_swc(
|
|
|
142
142
|
|
|
143
143
|
Parameters
|
|
144
144
|
----------
|
|
145
|
-
fname :
|
|
145
|
+
fname : PathOrIO
|
|
146
146
|
names : SWCNames
|
|
147
147
|
extra_cols : list of str, optional
|
|
148
148
|
encoding : str | 'detect', default `utf-8`
|
swcgeom/utils/__init__.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""Utils."""
|
|
2
2
|
|
|
3
3
|
from swcgeom.utils.debug import *
|
|
4
|
+
from swcgeom.utils.dsu import *
|
|
4
5
|
from swcgeom.utils.ellipse import *
|
|
5
6
|
from swcgeom.utils.file import *
|
|
7
|
+
from swcgeom.utils.geometry_object import *
|
|
6
8
|
from swcgeom.utils.neuromorpho import *
|
|
7
9
|
from swcgeom.utils.numpy_helper import *
|
|
8
10
|
from swcgeom.utils.renderer import *
|
swcgeom/utils/dsu.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Disjoint Set Union Impl."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["DisjointSetUnion"]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DisjointSetUnion:
|
|
7
|
+
"""Disjoint Set Union.
|
|
8
|
+
|
|
9
|
+
DSU with path compression and union by rank.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, node_number: int):
|
|
13
|
+
self.element_parent = [i for i in range(node_number)]
|
|
14
|
+
self.rank = [0 for _ in range(node_number)]
|
|
15
|
+
|
|
16
|
+
def find_parent(self, node_id: int) -> int:
|
|
17
|
+
if node_id != self.element_parent[node_id]:
|
|
18
|
+
self.element_parent[node_id] = self.find_parent(
|
|
19
|
+
self.element_parent[node_id]
|
|
20
|
+
)
|
|
21
|
+
return self.element_parent[node_id]
|
|
22
|
+
|
|
23
|
+
def union_sets(self, node_a: int, node_b: int) -> None:
|
|
24
|
+
assert self.validate_node(node_a) and self.validate_node(node_b)
|
|
25
|
+
|
|
26
|
+
root_a = self.find_parent(node_a)
|
|
27
|
+
root_b = self.find_parent(node_b)
|
|
28
|
+
if root_a != root_b:
|
|
29
|
+
# union by rank
|
|
30
|
+
if self.rank[root_a] < self.rank[root_b]:
|
|
31
|
+
self.element_parent[root_a] = root_b
|
|
32
|
+
elif self.rank[root_a] > self.rank[root_b]:
|
|
33
|
+
self.element_parent[root_b] = root_a
|
|
34
|
+
else:
|
|
35
|
+
self.element_parent[root_b] = root_a
|
|
36
|
+
self.rank[root_a] += 1
|
|
37
|
+
|
|
38
|
+
def is_same_set(self, node_a: int, node_b: int) -> bool:
|
|
39
|
+
return self.find_parent(node_a) == self.find_parent(node_b)
|
|
40
|
+
|
|
41
|
+
def validate_node(self, node_id: int) -> bool:
|
|
42
|
+
return 0 <= node_id < len(self.element_parent)
|
swcgeom/utils/file.py
CHANGED
|
@@ -11,12 +11,12 @@ pip install swcgeom[all]
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import warnings
|
|
14
|
-
from io import BytesIO, TextIOWrapper
|
|
14
|
+
from io import BytesIO, TextIOBase, TextIOWrapper
|
|
15
15
|
from typing import Literal
|
|
16
16
|
|
|
17
17
|
__all__ = ["FileReader", "PathOrIO"]
|
|
18
18
|
|
|
19
|
-
PathOrIO = int | str | bytes | BytesIO |
|
|
19
|
+
PathOrIO = int | str | bytes | BytesIO | TextIOBase
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class FileReader:
|
|
@@ -32,7 +32,7 @@ class FileReader:
|
|
|
32
32
|
|
|
33
33
|
Parameters
|
|
34
34
|
----------
|
|
35
|
-
fname :
|
|
35
|
+
fname : PathOrIO
|
|
36
36
|
encoding : str | 'detect', default `utf-8`
|
|
37
37
|
The name of the encoding used to decode the file. If is
|
|
38
38
|
`detect`, we will try to detect the character encoding.
|
|
@@ -40,8 +40,9 @@ class FileReader:
|
|
|
40
40
|
Used for detect character endocing, raising warning when
|
|
41
41
|
parsing with low confidence.
|
|
42
42
|
"""
|
|
43
|
+
# TODO: support StringIO
|
|
43
44
|
self.fname, self.fb, self.f = "", None, None
|
|
44
|
-
if isinstance(fname,
|
|
45
|
+
if isinstance(fname, TextIOBase):
|
|
45
46
|
self.f = fname
|
|
46
47
|
encoding = fname.encoding # skip detect
|
|
47
48
|
elif isinstance(fname, BytesIO):
|
|
@@ -54,7 +55,7 @@ class FileReader:
|
|
|
54
55
|
self.encoding = encoding
|
|
55
56
|
self.kwargs = kwargs
|
|
56
57
|
|
|
57
|
-
def __enter__(self) ->
|
|
58
|
+
def __enter__(self) -> TextIOBase:
|
|
58
59
|
if isinstance(self.fb, BytesIO):
|
|
59
60
|
self.f = TextIOWrapper(self.fb, encoding=self.encoding)
|
|
60
61
|
elif self.f is None:
|
|
@@ -71,7 +72,7 @@ class FileReader:
|
|
|
71
72
|
def detect_encoding(fname: PathOrIO, *, low_confidence: float = 0.9) -> str:
|
|
72
73
|
import chardet
|
|
73
74
|
|
|
74
|
-
if isinstance(fname,
|
|
75
|
+
if isinstance(fname, TextIOBase):
|
|
75
76
|
return fname.encoding
|
|
76
77
|
elif isinstance(fname, BytesIO):
|
|
77
78
|
data = fname.read()
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Geometry object."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import List, Tuple
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import numpy.typing as npt
|
|
9
|
+
from sympy import Eq, solve, symbols
|
|
10
|
+
|
|
11
|
+
__all__ = ["GeomObject", "GeomSphere", "GeomFrustumCone"]
|
|
12
|
+
|
|
13
|
+
eps = 1e-6
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GeomObject(ABC):
|
|
17
|
+
"""Geometry object."""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def get_volume(self) -> float:
|
|
21
|
+
"""Get volume."""
|
|
22
|
+
raise NotImplementedError()
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def get_intersect_volume(self, obj: "GeomObject") -> float:
|
|
26
|
+
"""Get intersect volume.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
obj : GeometryObject
|
|
31
|
+
Another geometry object.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
volume : float
|
|
36
|
+
Intersect volume.
|
|
37
|
+
"""
|
|
38
|
+
raise NotImplementedError()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class GeomSphere(GeomObject):
|
|
42
|
+
"""Geometry Sphere."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, center: npt.ArrayLike, radius: float):
|
|
45
|
+
super().__init__()
|
|
46
|
+
|
|
47
|
+
self.center = np.array(center)
|
|
48
|
+
assert len(self.center) == 3
|
|
49
|
+
|
|
50
|
+
self.radius = radius
|
|
51
|
+
|
|
52
|
+
def get_volume(self) -> float:
|
|
53
|
+
return self.calc_volume(self.radius)
|
|
54
|
+
|
|
55
|
+
def get_volume_spherical_cap(self, h: float) -> float:
|
|
56
|
+
return self.calc_volume_spherical_cap(self.radius, h)
|
|
57
|
+
|
|
58
|
+
def get_intersect_volume_sphere(self, obj: "GeomSphere") -> float:
|
|
59
|
+
return self.calc_intersect_volume_sphere(self, obj)
|
|
60
|
+
|
|
61
|
+
def get_intersect_volume_sphere_frustum_cone(
|
|
62
|
+
self, frustum_cone: "GeomFrustumCone"
|
|
63
|
+
) -> float:
|
|
64
|
+
return calc_intersect_volume_sphere_frustum_cone(self, frustum_cone)
|
|
65
|
+
|
|
66
|
+
def get_intersect_volume(self, obj: GeomObject) -> float:
|
|
67
|
+
if isinstance(obj, GeomSphere):
|
|
68
|
+
return self.get_intersect_volume_sphere(obj)
|
|
69
|
+
|
|
70
|
+
if isinstance(obj, GeomFrustumCone):
|
|
71
|
+
return self.get_intersect_volume_sphere_frustum_cone(obj)
|
|
72
|
+
|
|
73
|
+
classname = obj.__class__.__name__
|
|
74
|
+
raise NotImplementedError(f"unsupported geometry object: {classname}")
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def calc_volume(radius: float) -> float:
|
|
78
|
+
r"""Calculate volume of sphere.
|
|
79
|
+
|
|
80
|
+
\being{equation}
|
|
81
|
+
V = \frac{4}{3} * π * r^3
|
|
82
|
+
\end{equation}
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
volume : float
|
|
87
|
+
Volume.
|
|
88
|
+
"""
|
|
89
|
+
return 4 / 3 * np.pi * radius**3
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def calc_volume_spherical_cap(r: float, h: float) -> float:
|
|
93
|
+
r"""Calculate the volume of a spherical cap.
|
|
94
|
+
|
|
95
|
+
\being{equation}
|
|
96
|
+
V = π * h^2 * (3r - h) / 3
|
|
97
|
+
\end{equation}
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
r : float
|
|
102
|
+
radius of the sphere
|
|
103
|
+
h : float
|
|
104
|
+
height of the spherical cap
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
volume : float
|
|
109
|
+
volume of the spherical cap
|
|
110
|
+
"""
|
|
111
|
+
return np.pi * h**2 * (3 * r - h) / 3
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def calc_intersect_volume_sphere(
|
|
115
|
+
cls, obj1: "GeomSphere", obj2: "GeomSphere"
|
|
116
|
+
) -> float:
|
|
117
|
+
r"""Calculate intersect volume of two spheres.
|
|
118
|
+
|
|
119
|
+
\being{equation}
|
|
120
|
+
V = \frac{\pi}{12d} * (r_1 + r_2 - d)^2 (d^2 + 2d r_1 - 3r_1^2 + 2d r_2 - 3r_2^2 + 6 r_1r_2)
|
|
121
|
+
\end{equation}
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
volume : float
|
|
126
|
+
Intersect volume.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
r1, r2 = obj1.radius, obj2.radius
|
|
130
|
+
d = np.linalg.norm(obj1.center - obj2.center).item()
|
|
131
|
+
if d > r1 + r2:
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
if d <= abs(r1 - r2):
|
|
135
|
+
return cls.calc_volume(min(r1, r2))
|
|
136
|
+
|
|
137
|
+
part1 = (np.pi / (12 * d)) * (r1 + r2 - d) ** 2
|
|
138
|
+
part2 = (
|
|
139
|
+
d**2 + 2 * d * r1 - 3 * r1**2 + 2 * d * r2 - 3 * r2**2 + 6 * r1 * r2
|
|
140
|
+
)
|
|
141
|
+
return part1 * part2
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class GeomFrustumCone(GeomObject):
|
|
145
|
+
"""Geometry Frustum."""
|
|
146
|
+
|
|
147
|
+
def __init__(self, c1: npt.ArrayLike, r1: float, c2: npt.ArrayLike, r2: float):
|
|
148
|
+
super().__init__()
|
|
149
|
+
|
|
150
|
+
self.c1 = np.array(c1)
|
|
151
|
+
assert len(self.c1) == 3
|
|
152
|
+
|
|
153
|
+
self.c2 = np.array(c2)
|
|
154
|
+
assert len(self.c2) == 3
|
|
155
|
+
|
|
156
|
+
self.r1 = r1
|
|
157
|
+
self.r2 = r2
|
|
158
|
+
|
|
159
|
+
def height(self) -> float:
|
|
160
|
+
"""Get height of frustum."""
|
|
161
|
+
return np.linalg.norm(self.c1 - self.c2).item()
|
|
162
|
+
|
|
163
|
+
def get_volume(self) -> float:
|
|
164
|
+
return self.calc_volume(self.r1, self.r2, self.height())
|
|
165
|
+
|
|
166
|
+
def get_intersect_volume_sphere(self, sphere: GeomSphere) -> float:
|
|
167
|
+
return calc_intersect_volume_sphere_frustum_cone(sphere, self)
|
|
168
|
+
|
|
169
|
+
def get_intersect_volume(self, obj: GeomObject) -> float:
|
|
170
|
+
if isinstance(obj, GeomSphere):
|
|
171
|
+
return self.get_intersect_volume_sphere(obj)
|
|
172
|
+
|
|
173
|
+
classname = obj.__class__.__name__
|
|
174
|
+
raise NotImplementedError(f"unsupported geometry object: {classname}")
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def calc_volume(r1: float, r2: float, height: float) -> float:
|
|
178
|
+
r"""Calculate volume of frustum.
|
|
179
|
+
|
|
180
|
+
\being{equation}
|
|
181
|
+
V = \frac{1}{3} * π * h * (r^2 + r * R + R^2)
|
|
182
|
+
\end{equation}
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
volume : float
|
|
187
|
+
Volume.
|
|
188
|
+
"""
|
|
189
|
+
return (1 / 3) * np.pi * height * (r1**2 + r1 * r2 + r2**2)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@lru_cache
|
|
193
|
+
def calc_intersect_volume_sphere_frustum_cone(
|
|
194
|
+
sphere: GeomSphere, frustum_cone: GeomFrustumCone
|
|
195
|
+
) -> float:
|
|
196
|
+
r"""Calculate intersect volume of sphere and frustum cone.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
volume : float
|
|
201
|
+
Intersect volume.
|
|
202
|
+
"""
|
|
203
|
+
h = frustum_cone.height()
|
|
204
|
+
c1, r1 = sphere.center, sphere.radius
|
|
205
|
+
if np.allclose(c1, frustum_cone.c1) and np.allclose(r1, frustum_cone.r1):
|
|
206
|
+
c2, r2 = frustum_cone.c2, frustum_cone.r2
|
|
207
|
+
elif np.allclose(c1, frustum_cone.c2) and np.allclose(r1, frustum_cone.r2):
|
|
208
|
+
c2, r2 = frustum_cone.c1, frustum_cone.r1
|
|
209
|
+
else:
|
|
210
|
+
raise NotImplementedError("unsupported to calculate intersect volume")
|
|
211
|
+
|
|
212
|
+
# Fast-Path: The surface of the frustum concentric with the sphere
|
|
213
|
+
# is the surface with smaller radius
|
|
214
|
+
if r2 >= r1:
|
|
215
|
+
if h >= r1:
|
|
216
|
+
# The hemisphere is completely inside the frustum cone
|
|
217
|
+
return GeomSphere.calc_volume_spherical_cap(r1, r1)
|
|
218
|
+
|
|
219
|
+
# The frustum cone is lower than the hemisphere
|
|
220
|
+
v_himisphere = GeomSphere.calc_volume_spherical_cap(r1, r1)
|
|
221
|
+
v_cap = GeomSphere.calc_volume_spherical_cap(r1, r1 - h)
|
|
222
|
+
return v_himisphere - v_cap
|
|
223
|
+
|
|
224
|
+
up = (c2 - c1) / np.linalg.norm(c2 - c1)
|
|
225
|
+
v = _find_unit_vector_on_plane(up)
|
|
226
|
+
|
|
227
|
+
intersections = _find_sphere_line_intersection(c1, r1, c1 + r1 * v, c2 + r2 * v)
|
|
228
|
+
assert len(intersections) == 2
|
|
229
|
+
t, p = max(intersections, key=lambda x: x[0])
|
|
230
|
+
|
|
231
|
+
# Fast-Path: The frustum cone is completely inside the sphere
|
|
232
|
+
if t > 1 + eps:
|
|
233
|
+
return frustum_cone.get_volume()
|
|
234
|
+
|
|
235
|
+
M = _project_point_on_line(c1, up, p)
|
|
236
|
+
h1 = np.linalg.norm(M - c1).item()
|
|
237
|
+
r3 = np.linalg.norm(M - p).item()
|
|
238
|
+
v_cap1 = GeomSphere.calc_volume_spherical_cap(r1, r1 - h1)
|
|
239
|
+
v_frustum = GeomFrustumCone.calc_volume(r1, r3, h1)
|
|
240
|
+
|
|
241
|
+
# Fast-Path: The frustum cone is higher than the sphere
|
|
242
|
+
if h >= r1:
|
|
243
|
+
return v_cap1 + v_frustum
|
|
244
|
+
|
|
245
|
+
v_cap2 = GeomSphere.calc_volume_spherical_cap(r1, r1 - h)
|
|
246
|
+
return v_cap1 + v_frustum - v_cap2
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _find_unit_vector_on_plane(normal_vec3: npt.NDArray) -> npt.NDArray:
|
|
250
|
+
r = np.random.rand(3)
|
|
251
|
+
while np.allclose(r, normal_vec3) or np.allclose(r, -normal_vec3):
|
|
252
|
+
r = np.random.rand(3)
|
|
253
|
+
|
|
254
|
+
u = np.cross(r, normal_vec3)
|
|
255
|
+
unit_vector = u / np.linalg.norm(u)
|
|
256
|
+
return unit_vector
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _find_sphere_line_intersection(
|
|
260
|
+
sphere_center: npt.NDArray,
|
|
261
|
+
sphere_radius: float,
|
|
262
|
+
line_point_a: npt.NDArray,
|
|
263
|
+
line_point_b: npt.NDArray,
|
|
264
|
+
) -> List[Tuple[float, npt.NDArray[np.float64]]]:
|
|
265
|
+
x1, y1, z1 = sphere_center
|
|
266
|
+
x2, y2, z2 = line_point_a
|
|
267
|
+
x3, y3, z3 = line_point_b
|
|
268
|
+
t = symbols("t")
|
|
269
|
+
|
|
270
|
+
# line
|
|
271
|
+
x = x2 + t * (x3 - x2)
|
|
272
|
+
y = y2 + t * (y3 - y2)
|
|
273
|
+
z = z2 + t * (z3 - z2)
|
|
274
|
+
|
|
275
|
+
# sphere
|
|
276
|
+
sphere_eq = Eq((x - x1) ** 2 + (y - y1) ** 2 + (z - z1) ** 2, sphere_radius**2)
|
|
277
|
+
|
|
278
|
+
# solve
|
|
279
|
+
t_values = solve(sphere_eq, t)
|
|
280
|
+
intersections = [
|
|
281
|
+
np.array(
|
|
282
|
+
[float(x.subs(t, t_val)), float(y.subs(t, t_val)), float(z.subs(t, t_val))]
|
|
283
|
+
)
|
|
284
|
+
for t_val in t_values
|
|
285
|
+
]
|
|
286
|
+
return list(zip(t_values, intersections))
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _project_point_on_line(
|
|
290
|
+
point_a: npt.ArrayLike, direction_vector: npt.ArrayLike, point_p: npt.ArrayLike
|
|
291
|
+
) -> npt.NDArray:
|
|
292
|
+
A = np.array(point_a)
|
|
293
|
+
n = np.array(direction_vector)
|
|
294
|
+
P = np.array(point_p)
|
|
295
|
+
print(A.dtype, n.dtype, P.dtype)
|
|
296
|
+
|
|
297
|
+
AP = P - A
|
|
298
|
+
projection = A + np.dot(AP, n) / np.dot(n, n) * n
|
|
299
|
+
return projection
|
swcgeom/utils/neuromorpho.py
CHANGED
|
@@ -97,14 +97,19 @@ URL_CNG_VERSION = (
|
|
|
97
97
|
)
|
|
98
98
|
API_NEURON_MAX_SIZE = 500
|
|
99
99
|
|
|
100
|
-
# about 1.1 GB and 18 GB in version 8.5.25 released in 2023-08-01
|
|
101
100
|
KB = 1024
|
|
102
101
|
MB = 1024 * KB
|
|
103
102
|
GB = 1024 * MB
|
|
103
|
+
|
|
104
|
+
# Test version: 8.5.25 (2023-08-01)
|
|
105
|
+
# About 1.1 GB and 18 GB
|
|
106
|
+
# No ETAs for future version
|
|
104
107
|
SIZE_METADATA = 2 * GB
|
|
105
108
|
SIZE_DATA = 20 * GB
|
|
106
109
|
|
|
107
110
|
# fmt:off
|
|
111
|
+
# Test version: 8.5.25 (2023-08-01)
|
|
112
|
+
# No ETAs for future version
|
|
108
113
|
invalid_ids = [
|
|
109
114
|
# bad file
|
|
110
115
|
81062, 86970, 79791,
|
|
@@ -171,7 +176,7 @@ def neuromorpho_convert_lmdb_to_swc(
|
|
|
171
176
|
See Also
|
|
172
177
|
--------
|
|
173
178
|
neuromorpho_is_valid :
|
|
174
|
-
|
|
179
|
+
Recommended filter function, try `where=neuromorpho_is_valid`
|
|
175
180
|
"""
|
|
176
181
|
import lmdb
|
|
177
182
|
from tqdm import tqdm
|
swcgeom/utils/renderer.py
CHANGED
|
@@ -9,7 +9,7 @@ import numpy.typing as npt
|
|
|
9
9
|
from matplotlib import cm
|
|
10
10
|
from matplotlib.axes import Axes
|
|
11
11
|
from matplotlib.collections import LineCollection, PatchCollection
|
|
12
|
-
from matplotlib.colors import Normalize
|
|
12
|
+
from matplotlib.colors import Colormap, Normalize
|
|
13
13
|
from matplotlib.figure import Figure
|
|
14
14
|
from matplotlib.patches import Circle
|
|
15
15
|
from typing_extensions import Self
|
|
@@ -208,14 +208,15 @@ def draw_circles(
|
|
|
208
208
|
*,
|
|
209
209
|
y_min: Optional[float] = None,
|
|
210
210
|
y_max: Optional[float] = None,
|
|
211
|
-
cmap: str = "viridis",
|
|
211
|
+
cmap: str | Colormap = "viridis",
|
|
212
212
|
) -> PatchCollection:
|
|
213
213
|
"""Draw a sequential of circles."""
|
|
214
|
+
|
|
214
215
|
y_min = y.min() if y_min is None else y_min
|
|
215
216
|
y_max = y.max() if y_max is None else y_max
|
|
216
217
|
norm = Normalize(y_min, y_max)
|
|
217
218
|
|
|
218
|
-
color_map = cm.get_cmap(name=cmap)
|
|
219
|
+
color_map = cmap if isinstance(cmap, Colormap) else cm.get_cmap(name=cmap)
|
|
219
220
|
colors = color_map(norm(y))
|
|
220
221
|
|
|
221
222
|
circles = [
|
swcgeom/utils/transforms.py
CHANGED
|
@@ -212,6 +212,31 @@ def to_homogeneous(xyz: npt.ArrayLike, w: float) -> npt.NDArray[np.float32]:
|
|
|
212
212
|
Parameters
|
|
213
213
|
----------
|
|
214
214
|
xyz : ArrayLike
|
|
215
|
+
Coordinate of shape (..., 3)
|
|
216
|
+
w : float
|
|
217
|
+
w of homogeneous coordinate, 1 for dot, 0 for vector.
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
xyz4 : npt.NDArray[np.float32]
|
|
222
|
+
Array of shape (..., 4)
|
|
223
|
+
"""
|
|
224
|
+
xyz = np.array(xyz)
|
|
225
|
+
if xyz.ndim == 1:
|
|
226
|
+
return _to_homogeneous(xyz[None, ...], w)[0]
|
|
227
|
+
|
|
228
|
+
shape = xyz.shape[:-1]
|
|
229
|
+
xyz = xyz.reshape(-1, xyz.shape[-1])
|
|
230
|
+
xyz4 = _to_homogeneous(xyz, w).reshape(*shape, 4)
|
|
231
|
+
return xyz4
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _to_homogeneous(xyz: npt.NDArray, w: float) -> npt.NDArray[np.float32]:
|
|
235
|
+
"""Fill xyz to homogeneous coordinates.
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
xyz : npt.NDArray
|
|
215
240
|
Coordinate of shape (N, 3)
|
|
216
241
|
w : float
|
|
217
242
|
w of homogeneous coordinate, 1 for dot, 0 for vector.
|
|
@@ -221,10 +246,10 @@ def to_homogeneous(xyz: npt.ArrayLike, w: float) -> npt.NDArray[np.float32]:
|
|
|
221
246
|
xyz4 : npt.NDArray[np.float32]
|
|
222
247
|
Array of shape (N, 4)
|
|
223
248
|
"""
|
|
224
|
-
xyz = np.array(xyz)
|
|
225
249
|
if xyz.shape[1] == 4:
|
|
226
250
|
return xyz
|
|
227
251
|
|
|
252
|
+
assert xyz.shape[1] == 3
|
|
228
253
|
filled = np.full((xyz.shape[0], 1), fill_value=w)
|
|
229
254
|
xyz4 = np.concatenate([xyz, filled], axis=1)
|
|
230
255
|
return xyz4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: swcgeom
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Neuron geometry library for swc format
|
|
5
5
|
Author-email: yzx9 <yuan.zx@outlook.com>
|
|
6
6
|
License: CC4.0 BY-NC-SA
|
|
@@ -16,6 +16,7 @@ Requires-Dist: pandas >=1.4.2
|
|
|
16
16
|
Requires-Dist: pynrrd >=1.0.0
|
|
17
17
|
Requires-Dist: scipy >=1.9.1
|
|
18
18
|
Requires-Dist: seaborn >=0.12.0
|
|
19
|
+
Requires-Dist: sympy >=1.12
|
|
19
20
|
Requires-Dist: tifffile >=2022.8.12
|
|
20
21
|
Requires-Dist: typing-extensions >=4.4.0
|
|
21
22
|
Requires-Dist: v3d-py-helper-0.1.0
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
swcgeom/__init__.py,sha256=z88Zwcjv-ii7c7dYd9QPg9XrUVorQjtrgGbQCsEnQhc,265
|
|
2
|
-
swcgeom/_version.py,sha256=
|
|
2
|
+
swcgeom/_version.py,sha256=Y4u7iBqF7QJAbpBNSFA2tk5t2mrMGrI-nonxUAhVkPU,413
|
|
3
3
|
swcgeom/analysis/__init__.py,sha256=c8X_oMxaOijEVjHexFseYs49WMalG_mSxmlQZr5CZeI,339
|
|
4
4
|
swcgeom/analysis/branch_features.py,sha256=s6PTMwwvxrVtZXRZlQbUSIw4M9-1IG63kf-Nxc0tMB0,1958
|
|
5
|
-
swcgeom/analysis/feature_extractor.py,sha256=
|
|
5
|
+
swcgeom/analysis/feature_extractor.py,sha256=coe07_bJau96BkimcnXzuf4KqjY5_QRLwqaumFsu_tQ,14031
|
|
6
6
|
swcgeom/analysis/node_features.py,sha256=fevnyrF-t4PX39ifLypiDW6EUWB8i-a3PpBnQZU3VOc,3407
|
|
7
7
|
swcgeom/analysis/path_features.py,sha256=iE21HBxAoGLxk_qK7MBwQhyUOBqNPcnk4urVHr9SVqk,889
|
|
8
|
-
swcgeom/analysis/sholl.py,sha256=
|
|
8
|
+
swcgeom/analysis/sholl.py,sha256=9hSW8rZm1gvSIgcEZg8IVPT8kzBgBfwqbwP4E8R7L44,6390
|
|
9
9
|
swcgeom/analysis/trunk.py,sha256=L2tjUIUmrRQpah_W3ZETGWd16bDXJ5F8Sk2XBNGms0Q,5558
|
|
10
10
|
swcgeom/analysis/visualization.py,sha256=mKOpzTPkLpr1ggGL1MZPZRTG92GEg4idLT4eN5z5KOs,5654
|
|
11
|
+
swcgeom/analysis/volume.py,sha256=bMQ9N9Ow1-h27SLQ9AzUm9dyF_jhoTNBMNVqQWDcHMw,2103
|
|
11
12
|
swcgeom/core/__init__.py,sha256=ZUudZavxAIUU6Q0lBHrQ4ybmL5lBfvzyYsTtpuih9wg,332
|
|
12
13
|
swcgeom/core/branch.py,sha256=uuJCxaByRu-OdDZVWEffSFcmZWY-6ZWUhHN1M2Awj1s,3980
|
|
13
14
|
swcgeom/core/branch_tree.py,sha256=sN0viBVg5A4r9dMCkGNAaVttrdR4bEoZZBbHZFKdXj4,1892
|
|
@@ -22,8 +23,8 @@ swcgeom/core/tree_utils_impl.py,sha256=5Cb63ziVVLADnkjfuq1T-ePw2TQQ5TKk4gcPZR6f_
|
|
|
22
23
|
swcgeom/core/swc_utils/__init__.py,sha256=qghRxjtzvq5KKfN4HhvLpZNsGPfZQu-Jj2x62_5-TbQ,575
|
|
23
24
|
swcgeom/core/swc_utils/assembler.py,sha256=RoMJ3RjLC4O7mk62QxXVTQ5SUHagrFmpEw3nOnnqeJo,4563
|
|
24
25
|
swcgeom/core/swc_utils/base.py,sha256=huVxjuMLlTHbEb-KSEFDLgU0Ss3723t2Gr4Z_gQtl00,4737
|
|
25
|
-
swcgeom/core/swc_utils/checker.py,sha256=
|
|
26
|
-
swcgeom/core/swc_utils/io.py,sha256
|
|
26
|
+
swcgeom/core/swc_utils/checker.py,sha256=E72GtLZ_1IqQQ7aWQGs0dZ3Z609__bw3EYQqeWrk-EI,2657
|
|
27
|
+
swcgeom/core/swc_utils/io.py,sha256=6_--Qoe8kDja4PWsjwqRAvPJZNMFILFgauHaeWeGikU,6444
|
|
27
28
|
swcgeom/core/swc_utils/normalizer.py,sha256=_Ysi8bSJ2JBnIGB8o6BvAg2mcz6xuJp9rgNLZqpLuR8,5083
|
|
28
29
|
swcgeom/core/swc_utils/subtree.py,sha256=bd4XOLmRDfQSn_ktfQM3Hn8ONpCuZ_TdTWhE9-7QXW4,1999
|
|
29
30
|
swcgeom/images/__init__.py,sha256=QBP1ZGGo2nWAcV7Krz-vbvW_jN4ChqXrrpoScXcUURs,96
|
|
@@ -41,18 +42,20 @@ swcgeom/transforms/path.py,sha256=Gk2iunGQMX7vE83bdo8xoDO-KAT1Vvep0iZs7oFLzFQ,10
|
|
|
41
42
|
swcgeom/transforms/population.py,sha256=ZrKfMAMx4l729f-JLgw0dnGIPtPUoV0ZZoNNyA5cBw8,826
|
|
42
43
|
swcgeom/transforms/tree.py,sha256=Q5OnSti0ZeTNb-WpA_UZsDN7dhLN8YweEF_Siyrj66c,6420
|
|
43
44
|
swcgeom/transforms/tree_assembler.py,sha256=UZ9OUg1bQNecYIY_7ippg3S8gpuoi617ZUE0jg6BrQE,3177
|
|
44
|
-
swcgeom/utils/__init__.py,sha256=
|
|
45
|
+
swcgeom/utils/__init__.py,sha256=Ys04ObDfoNOX0mft2s4PGTpgelkdgwuM0qfeZRSx6VM,382
|
|
45
46
|
swcgeom/utils/debug.py,sha256=qay2qJpViLX82mzxdndxQFn-pi1vaEj9CbLGuGt8Y9k,465
|
|
46
47
|
swcgeom/utils/download.py,sha256=By2qZezo6h1Ke_4YpSIhDgcisOrpjVqRmNzbhynC2xs,2834
|
|
48
|
+
swcgeom/utils/dsu.py,sha256=3aCbtpnl_D0OXnowTS8-kuwnCS4BKBYL5ECiFQ1fUW8,1435
|
|
47
49
|
swcgeom/utils/ellipse.py,sha256=LB3q5CIy75GEUdTauIpKySwIHaDrwXzzkBhOCnjJ8Vw,3259
|
|
48
|
-
swcgeom/utils/file.py,sha256=
|
|
49
|
-
swcgeom/utils/
|
|
50
|
+
swcgeom/utils/file.py,sha256=1hchQDsPgn-i-Vz5OQtcogxav_ajCQ_OaEZCLmqczRg,2515
|
|
51
|
+
swcgeom/utils/geometry_object.py,sha256=y3Ikg0ywYQ6c_KKZOb0oq5ovHhuFiwI_VH-f4tAWZAI,8520
|
|
52
|
+
swcgeom/utils/neuromorpho.py,sha256=CDK2tUM2pNwHv_lEserHhQs_VlY3Rn557-jtV63EFlk,14420
|
|
50
53
|
swcgeom/utils/numpy_helper.py,sha256=A-F-eFdGktCHVAQ_HcXiFB3Y1YhhSNfAmtOl8483Dvo,1292
|
|
51
|
-
swcgeom/utils/renderer.py,sha256=
|
|
54
|
+
swcgeom/utils/renderer.py,sha256=xHVZ06Z1MeKBPC3nKzuwA1HryzR0ga79y6johZA4-q0,7290
|
|
52
55
|
swcgeom/utils/sdf.py,sha256=cmkHwdnh33u1gAXlzol5u9ZI3_SdMoMiY4pIkOgEcK0,5206
|
|
53
|
-
swcgeom/utils/transforms.py,sha256=
|
|
54
|
-
swcgeom-0.
|
|
55
|
-
swcgeom-0.
|
|
56
|
-
swcgeom-0.
|
|
57
|
-
swcgeom-0.
|
|
58
|
-
swcgeom-0.
|
|
56
|
+
swcgeom/utils/transforms.py,sha256=PmP5fL_iVguq4GR2aqXhM0TeCsiFVnrPZMZG6zLohrE,6983
|
|
57
|
+
swcgeom-0.13.0.dist-info/LICENSE,sha256=aiTJ_1to1Xx6PaByy-pSXg42VYzE4FvF6GIt69WSDDI,202
|
|
58
|
+
swcgeom-0.13.0.dist-info/METADATA,sha256=vsgLGIGz97A-V8n1DZsAhGAc-uVJuX_CdFbB2BCQZ5g,2605
|
|
59
|
+
swcgeom-0.13.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
60
|
+
swcgeom-0.13.0.dist-info/top_level.txt,sha256=hmLyUXWS61Gxl07haswFEKKefYPBVJYlUlol8ghNkjY,8
|
|
61
|
+
swcgeom-0.13.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|