swcgeom 0.16.0__py3-none-any.whl → 0.18.3__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 +26 -1
- swcgeom/analysis/__init__.py +21 -8
- swcgeom/analysis/feature_extractor.py +43 -18
- swcgeom/analysis/features.py +250 -0
- swcgeom/analysis/lmeasure.py +48 -12
- swcgeom/analysis/sholl.py +25 -28
- swcgeom/analysis/trunk.py +27 -11
- swcgeom/analysis/visualization.py +24 -9
- swcgeom/analysis/visualization3d.py +100 -0
- swcgeom/analysis/volume.py +19 -4
- swcgeom/core/__init__.py +31 -12
- swcgeom/core/branch.py +19 -3
- swcgeom/core/branch_tree.py +18 -4
- swcgeom/core/compartment.py +18 -2
- swcgeom/core/node.py +32 -3
- swcgeom/core/path.py +21 -9
- swcgeom/core/population.py +58 -29
- swcgeom/core/swc.py +26 -10
- swcgeom/core/swc_utils/__init__.py +21 -7
- swcgeom/core/swc_utils/assembler.py +15 -0
- swcgeom/core/swc_utils/base.py +23 -17
- swcgeom/core/swc_utils/checker.py +19 -12
- swcgeom/core/swc_utils/io.py +24 -7
- swcgeom/core/swc_utils/normalizer.py +20 -4
- swcgeom/core/swc_utils/subtree.py +17 -2
- swcgeom/core/tree.py +56 -40
- swcgeom/core/tree_utils.py +28 -17
- swcgeom/core/tree_utils_impl.py +18 -3
- swcgeom/images/__init__.py +17 -2
- swcgeom/images/augmentation.py +18 -3
- swcgeom/images/contrast.py +15 -0
- swcgeom/images/folder.py +27 -26
- swcgeom/images/io.py +94 -117
- swcgeom/transforms/__init__.py +28 -12
- swcgeom/transforms/base.py +17 -2
- swcgeom/transforms/branch.py +74 -8
- swcgeom/transforms/branch_tree.py +82 -0
- swcgeom/transforms/geometry.py +22 -7
- swcgeom/transforms/image_preprocess.py +15 -0
- swcgeom/transforms/image_stack.py +36 -9
- swcgeom/transforms/images.py +121 -14
- swcgeom/transforms/mst.py +15 -0
- swcgeom/transforms/neurolucida_asc.py +20 -7
- swcgeom/transforms/path.py +15 -0
- swcgeom/transforms/population.py +16 -3
- swcgeom/transforms/tree.py +84 -30
- swcgeom/transforms/tree_assembler.py +23 -7
- swcgeom/utils/__init__.py +27 -12
- swcgeom/utils/debug.py +15 -0
- swcgeom/utils/download.py +59 -21
- swcgeom/utils/dsu.py +15 -0
- swcgeom/utils/ellipse.py +18 -4
- swcgeom/utils/file.py +15 -0
- swcgeom/utils/neuromorpho.py +35 -23
- swcgeom/utils/numpy_helper.py +15 -0
- swcgeom/utils/plotter_2d.py +27 -6
- swcgeom/utils/plotter_3d.py +48 -0
- swcgeom/utils/renderer.py +21 -6
- swcgeom/utils/sdf.py +19 -7
- swcgeom/utils/solid_geometry.py +16 -3
- swcgeom/utils/transforms.py +17 -4
- swcgeom/utils/volumetric_object.py +23 -10
- {swcgeom-0.16.0.dist-info → swcgeom-0.18.3.dist-info}/LICENSE +1 -1
- {swcgeom-0.16.0.dist-info → swcgeom-0.18.3.dist-info}/METADATA +28 -24
- swcgeom-0.18.3.dist-info/RECORD +67 -0
- {swcgeom-0.16.0.dist-info → swcgeom-0.18.3.dist-info}/WHEEL +1 -1
- swcgeom/_version.py +0 -16
- swcgeom/analysis/branch_features.py +0 -67
- swcgeom/analysis/node_features.py +0 -121
- swcgeom/analysis/path_features.py +0 -37
- swcgeom-0.16.0.dist-info/RECORD +0 -67
- {swcgeom-0.16.0.dist-info → swcgeom-0.18.3.dist-info}/top_level.txt +0 -0
swcgeom/transforms/tree.py
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""Transformation in tree."""
|
|
2
17
|
|
|
3
|
-
import
|
|
4
|
-
from typing import
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from typing import Optional
|
|
5
20
|
|
|
6
21
|
import numpy as np
|
|
22
|
+
from typing_extensions import deprecated
|
|
7
23
|
|
|
8
|
-
from swcgeom.core import BranchTree, DictSWC, Path, Tree, cut_tree, to_subtree
|
|
24
|
+
from swcgeom.core import Branch, BranchTree, DictSWC, Path, Tree, cut_tree, to_subtree
|
|
9
25
|
from swcgeom.core.swc_utils import SWCTypes, get_types
|
|
10
26
|
from swcgeom.transforms.base import Transform
|
|
11
|
-
from swcgeom.transforms.branch import BranchConvSmoother
|
|
27
|
+
from swcgeom.transforms.branch import BranchConvSmoother, BranchIsometricResampler
|
|
28
|
+
from swcgeom.transforms.branch_tree import BranchTreeAssembler
|
|
12
29
|
from swcgeom.transforms.geometry import Normalizer
|
|
13
30
|
|
|
14
31
|
__all__ = [
|
|
@@ -19,8 +36,9 @@ __all__ = [
|
|
|
19
36
|
"CutByType",
|
|
20
37
|
"CutAxonTree",
|
|
21
38
|
"CutDendriteTree",
|
|
22
|
-
"
|
|
39
|
+
"CutByFurcationOrder",
|
|
23
40
|
"CutShortTipBranch",
|
|
41
|
+
"IsometricResampler",
|
|
24
42
|
]
|
|
25
43
|
|
|
26
44
|
|
|
@@ -64,10 +82,11 @@ class TreeSmoother(Transform[Tree, Tree]): # pylint: disable=missing-class-docs
|
|
|
64
82
|
|
|
65
83
|
return x
|
|
66
84
|
|
|
67
|
-
def extra_repr(self):
|
|
85
|
+
def extra_repr(self) -> str:
|
|
68
86
|
return f"n_nodes={self.n_nodes}"
|
|
69
87
|
|
|
70
88
|
|
|
89
|
+
@deprecated("Use `Normalizer` instead")
|
|
71
90
|
class TreeNormalizer(Normalizer[Tree]):
|
|
72
91
|
"""Noramlize coordinates and radius to 0-1.
|
|
73
92
|
|
|
@@ -75,15 +94,6 @@ class TreeNormalizer(Normalizer[Tree]):
|
|
|
75
94
|
Use :cls:`Normalizer` instead.
|
|
76
95
|
"""
|
|
77
96
|
|
|
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
97
|
|
|
88
98
|
class CutByType(Transform[Tree, Tree]):
|
|
89
99
|
"""Cut tree by type.
|
|
@@ -102,7 +112,7 @@ class CutByType(Transform[Tree, Tree]):
|
|
|
102
112
|
def __call__(self, x: Tree) -> Tree:
|
|
103
113
|
removals = set(x.id()[x.type() != self.type])
|
|
104
114
|
|
|
105
|
-
def leave(n: Tree.Node, keep_children:
|
|
115
|
+
def leave(n: Tree.Node, keep_children: list[bool]) -> bool:
|
|
106
116
|
if n.id in removals and any(keep_children):
|
|
107
117
|
removals.remove(n.id)
|
|
108
118
|
return n.id not in removals
|
|
@@ -111,7 +121,7 @@ class CutByType(Transform[Tree, Tree]):
|
|
|
111
121
|
y = to_subtree(x, removals)
|
|
112
122
|
return y
|
|
113
123
|
|
|
114
|
-
def extra_repr(self):
|
|
124
|
+
def extra_repr(self) -> str:
|
|
115
125
|
return f"type={self.type}"
|
|
116
126
|
|
|
117
127
|
|
|
@@ -131,28 +141,48 @@ class CutDendriteTree(CutByType):
|
|
|
131
141
|
super().__init__(type=types.basal_dendrite) # TODO: apical dendrite
|
|
132
142
|
|
|
133
143
|
|
|
134
|
-
class
|
|
135
|
-
"""Cut tree by
|
|
144
|
+
class CutByFurcationOrder(Transform[Tree, Tree]):
|
|
145
|
+
"""Cut tree by furcation order."""
|
|
136
146
|
|
|
137
|
-
|
|
147
|
+
max_furcation_order: int
|
|
138
148
|
|
|
139
149
|
def __init__(self, max_bifurcation_order: int) -> None:
|
|
140
|
-
self.
|
|
150
|
+
self.max_furcation_order = max_bifurcation_order
|
|
141
151
|
|
|
142
152
|
def __call__(self, x: Tree) -> Tree:
|
|
143
153
|
return cut_tree(x, enter=self._enter)
|
|
144
154
|
|
|
145
155
|
def __repr__(self) -> str:
|
|
146
|
-
return f"CutByBifurcationOrder-{self.
|
|
156
|
+
return f"CutByBifurcationOrder-{self.max_furcation_order}"
|
|
147
157
|
|
|
148
|
-
def _enter(self, n: Tree.Node, parent_level: int | None) ->
|
|
158
|
+
def _enter(self, n: Tree.Node, parent_level: int | None) -> tuple[int, bool]:
|
|
149
159
|
if parent_level is None:
|
|
150
160
|
level = 0
|
|
151
|
-
elif n.
|
|
161
|
+
elif n.is_furcation():
|
|
152
162
|
level = parent_level + 1
|
|
153
163
|
else:
|
|
154
164
|
level = parent_level
|
|
155
|
-
return (level, level >= self.
|
|
165
|
+
return (level, level >= self.max_furcation_order)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@deprecated("Use CutByFurcationOrder instead")
|
|
169
|
+
class CutByBifurcationOrder(CutByFurcationOrder):
|
|
170
|
+
"""Cut tree by bifurcation order.
|
|
171
|
+
|
|
172
|
+
Notes
|
|
173
|
+
-----
|
|
174
|
+
Deprecated due to the wrong spelling of furcation. For now, it
|
|
175
|
+
is just an alias of `CutByFurcationOrder` and raise a warning. It
|
|
176
|
+
will be change to raise an error in the future.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
max_furcation_order: int
|
|
180
|
+
|
|
181
|
+
def __init__(self, max_bifurcation_order: int) -> None:
|
|
182
|
+
super().__init__(max_bifurcation_order)
|
|
183
|
+
|
|
184
|
+
def __repr__(self) -> str:
|
|
185
|
+
return f"CutByBifurcationOrder-{self.max_furcation_order}"
|
|
156
186
|
|
|
157
187
|
|
|
158
188
|
class CutShortTipBranch(Transform[Tree, Tree]):
|
|
@@ -164,7 +194,7 @@ class CutShortTipBranch(Transform[Tree, Tree]):
|
|
|
164
194
|
"""
|
|
165
195
|
|
|
166
196
|
thre: float
|
|
167
|
-
callbacks:
|
|
197
|
+
callbacks: list[Callable[[Tree.Branch], None]]
|
|
168
198
|
|
|
169
199
|
def __init__(
|
|
170
200
|
self, thre: float = 5, callback: Optional[Callable[[Tree.Branch], None]] = None
|
|
@@ -176,18 +206,18 @@ class CutShortTipBranch(Transform[Tree, Tree]):
|
|
|
176
206
|
self.callbacks.append(callback)
|
|
177
207
|
|
|
178
208
|
def __call__(self, x: Tree) -> Tree:
|
|
179
|
-
removals:
|
|
209
|
+
removals: list[int] = []
|
|
180
210
|
self.callbacks.append(lambda br: removals.append(br[1].id))
|
|
181
211
|
x.traverse(leave=self._leave)
|
|
182
212
|
self.callbacks.pop()
|
|
183
213
|
return to_subtree(x, removals)
|
|
184
214
|
|
|
185
|
-
def extra_repr(self):
|
|
215
|
+
def extra_repr(self) -> str:
|
|
186
216
|
return f"threshold={self.thre}"
|
|
187
217
|
|
|
188
218
|
def _leave(
|
|
189
|
-
self, n: Tree.Node, children:
|
|
190
|
-
) ->
|
|
219
|
+
self, n: Tree.Node, children: list[tuple[float, Tree.Node] | None]
|
|
220
|
+
) -> tuple[float, Tree.Node] | None:
|
|
191
221
|
if len(children) == 0: # tip
|
|
192
222
|
return 0, n
|
|
193
223
|
|
|
@@ -214,3 +244,27 @@ class CutShortTipBranch(Transform[Tree, Tree]):
|
|
|
214
244
|
cb(br)
|
|
215
245
|
|
|
216
246
|
return None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class Resampler(Transform[Tree, Tree]):
|
|
250
|
+
def __init__(self, branch_resampler: Transform[Branch, Branch]) -> None:
|
|
251
|
+
super().__init__()
|
|
252
|
+
self.resampler = branch_resampler
|
|
253
|
+
self.assembler = BranchTreeAssembler()
|
|
254
|
+
|
|
255
|
+
def __call__(self, x: Tree) -> Tree:
|
|
256
|
+
t = BranchTree.from_tree(x)
|
|
257
|
+
t.branches = {
|
|
258
|
+
k: [self.resampler(br) for br in brs] for k, brs in t.branches.items()
|
|
259
|
+
}
|
|
260
|
+
return self.assembler(t)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class IsometricResampler(Resampler):
|
|
264
|
+
def __init__(
|
|
265
|
+
self, distance: float, *, adjust_last_gap: bool = True, **kwargs
|
|
266
|
+
) -> None:
|
|
267
|
+
branch_resampler = BranchIsometricResampler(
|
|
268
|
+
distance, adjust_last_gap=adjust_last_gap, **kwargs
|
|
269
|
+
)
|
|
270
|
+
super().__init__(branch_resampler)
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""Assemble a tree."""
|
|
2
17
|
|
|
18
|
+
from collections.abc import Iterable
|
|
3
19
|
from copy import copy
|
|
4
|
-
from typing import
|
|
20
|
+
from typing import Optional
|
|
5
21
|
|
|
6
22
|
import numpy as np
|
|
7
23
|
import pandas as pd
|
|
@@ -18,7 +34,7 @@ from swcgeom.transforms.base import Transform
|
|
|
18
34
|
EPS = 1e-5
|
|
19
35
|
|
|
20
36
|
|
|
21
|
-
class LinesToTree(Transform[
|
|
37
|
+
class LinesToTree(Transform[list[pd.DataFrame], Tree]):
|
|
22
38
|
"""Assemble lines to swc."""
|
|
23
39
|
|
|
24
40
|
def __init__(self, *, thre: float = 0.2, undirected: bool = True):
|
|
@@ -37,7 +53,7 @@ class LinesToTree(Transform[List[pd.DataFrame], Tree]):
|
|
|
37
53
|
|
|
38
54
|
def __call__(
|
|
39
55
|
self, lines: Iterable[pd.DataFrame], *, names: Optional[SWCNames] = None
|
|
40
|
-
):
|
|
56
|
+
): # TODO check this
|
|
41
57
|
return self.assemble(lines, names=names)
|
|
42
58
|
|
|
43
59
|
def assemble(
|
|
@@ -55,8 +71,8 @@ class LinesToTree(Transform[List[pd.DataFrame], Tree]):
|
|
|
55
71
|
Parameters
|
|
56
72
|
----------
|
|
57
73
|
lines : List of ~pd.DataFrame
|
|
58
|
-
An array of tables containing a line, columns should
|
|
59
|
-
the swc.
|
|
74
|
+
An array of tables containing a line, columns should
|
|
75
|
+
following the swc.
|
|
60
76
|
undirected : bool, default `True`
|
|
61
77
|
Forwarding to `self.try_assemble`.
|
|
62
78
|
names : SWCNames, optional
|
|
@@ -97,7 +113,7 @@ class LinesToTree(Transform[List[pd.DataFrame], Tree]):
|
|
|
97
113
|
undirected: bool = True,
|
|
98
114
|
sort_nodes: bool = True,
|
|
99
115
|
names: Optional[SWCNames] = None,
|
|
100
|
-
) ->
|
|
116
|
+
) -> tuple[pd.DataFrame, list[pd.DataFrame]]:
|
|
101
117
|
"""Trying assemble lines to a tree.
|
|
102
118
|
|
|
103
119
|
Treat the first line as a tree, find a line whose shortest distance
|
|
@@ -169,5 +185,5 @@ class LinesToTree(Transform[List[pd.DataFrame], Tree]):
|
|
|
169
185
|
|
|
170
186
|
return tree, lines
|
|
171
187
|
|
|
172
|
-
def extra_repr(self):
|
|
188
|
+
def extra_repr(self) -> str:
|
|
173
189
|
return f"thre={self.thre}, undirected={self.undirected}"
|
swcgeom/utils/__init__.py
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""Utils."""
|
|
2
17
|
|
|
3
|
-
from swcgeom.utils.debug import *
|
|
4
|
-
from swcgeom.utils.dsu import *
|
|
5
|
-
from swcgeom.utils.ellipse import *
|
|
6
|
-
from swcgeom.utils.file import *
|
|
7
|
-
from swcgeom.utils.neuromorpho import *
|
|
8
|
-
from swcgeom.utils.numpy_helper import *
|
|
9
|
-
from swcgeom.utils.plotter_2d import *
|
|
10
|
-
from swcgeom.utils.renderer import *
|
|
11
|
-
from swcgeom.utils.sdf import *
|
|
12
|
-
from swcgeom.utils.solid_geometry import *
|
|
13
|
-
from swcgeom.utils.transforms import *
|
|
14
|
-
from swcgeom.utils.volumetric_object import *
|
|
18
|
+
from swcgeom.utils.debug import * # noqa: F403
|
|
19
|
+
from swcgeom.utils.dsu import * # noqa: F403
|
|
20
|
+
from swcgeom.utils.ellipse import * # noqa: F403
|
|
21
|
+
from swcgeom.utils.file import * # noqa: F403
|
|
22
|
+
from swcgeom.utils.neuromorpho import * # noqa: F403
|
|
23
|
+
from swcgeom.utils.numpy_helper import * # noqa: F403
|
|
24
|
+
from swcgeom.utils.plotter_2d import * # noqa: F403
|
|
25
|
+
from swcgeom.utils.renderer import * # noqa: F403
|
|
26
|
+
from swcgeom.utils.sdf import * # noqa: F403
|
|
27
|
+
from swcgeom.utils.solid_geometry import * # noqa: F403
|
|
28
|
+
from swcgeom.utils.transforms import * # noqa: F403
|
|
29
|
+
from swcgeom.utils.volumetric_object import * # noqa: F403
|
swcgeom/utils/debug.py
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""Debug helpers"""
|
|
2
17
|
|
|
3
18
|
import time
|
swcgeom/utils/download.py
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""Download helpers.
|
|
2
17
|
|
|
3
18
|
Notes
|
|
@@ -13,6 +28,7 @@ import itertools
|
|
|
13
28
|
import logging
|
|
14
29
|
import multiprocessing
|
|
15
30
|
import os
|
|
31
|
+
from functools import partial
|
|
16
32
|
from urllib.parse import urljoin
|
|
17
33
|
|
|
18
34
|
__all__ = ["download", "fetch_page", "clone_index_page"]
|
|
@@ -26,7 +42,7 @@ def download(dst: str, url: str) -> None:
|
|
|
26
42
|
r = conn.request("GET", url)
|
|
27
43
|
|
|
28
44
|
dirname = os.path.dirname(dst)
|
|
29
|
-
if not os.path.exists(dirname):
|
|
45
|
+
if dirname != "" and not os.path.exists(dirname):
|
|
30
46
|
os.makedirs(dirname)
|
|
31
47
|
|
|
32
48
|
with open(dst, "wb") as file:
|
|
@@ -41,7 +57,7 @@ def fetch_page(url: str):
|
|
|
41
57
|
conn = connection_from_url(url)
|
|
42
58
|
r = conn.request("GET", url)
|
|
43
59
|
data = r.data.decode("utf-8")
|
|
44
|
-
return BeautifulSoup(data)
|
|
60
|
+
return BeautifulSoup(data, features="html.parser")
|
|
45
61
|
|
|
46
62
|
|
|
47
63
|
def clone_index_page(
|
|
@@ -62,31 +78,35 @@ def clone_index_page(
|
|
|
62
78
|
multiprocess : int, default `4`
|
|
63
79
|
How many process are available for download.
|
|
64
80
|
"""
|
|
65
|
-
from urllib3.exceptions import HTTPError
|
|
66
|
-
|
|
67
81
|
files = get_urls_in_index_page(index_url)
|
|
68
82
|
logging.info("downloader: search `%s`, found %s files.", index_url, len(files))
|
|
69
83
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
logging.info("downloader: file `%s` exits, skiped.", dist)
|
|
76
|
-
return
|
|
84
|
+
task = partial(
|
|
85
|
+
_clone_index_page, index_url=index_url, dist_dir=dist_dir, override=override
|
|
86
|
+
)
|
|
87
|
+
with multiprocessing.Pool(multiprocess) as p:
|
|
88
|
+
p.map(task, files)
|
|
77
89
|
|
|
78
|
-
logging.info("downloader: file `%s` exits, deleted.", dist)
|
|
79
|
-
os.remove(filepath)
|
|
80
90
|
|
|
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)
|
|
91
|
+
def _clone_index_page(url: str, index_url: str, dist_dir: str, override: bool) -> None:
|
|
92
|
+
from urllib3.exceptions import HTTPError
|
|
87
93
|
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
filepath = url.removeprefix(index_url)
|
|
95
|
+
dist = os.path.join(dist_dir, filepath)
|
|
96
|
+
if os.path.exists(dist):
|
|
97
|
+
if not override:
|
|
98
|
+
logging.info("downloader: file `%s` exits, skiped.", dist)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
logging.info("downloader: file `%s` exits, deleted.", dist)
|
|
102
|
+
os.remove(dist)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
logging.info("downloader: downloading `%s` to `%s`", url, dist)
|
|
106
|
+
download(dist, url)
|
|
107
|
+
logging.info("downloader: download `%s` to `%s`", url, dist)
|
|
108
|
+
except HTTPError as ex:
|
|
109
|
+
logging.info("downloader: fails to download `%s`, except `%s`", url, ex)
|
|
90
110
|
|
|
91
111
|
|
|
92
112
|
def get_urls_in_index_page(url: str) -> list[str]:
|
|
@@ -97,3 +117,21 @@ def get_urls_in_index_page(url: str) -> list[str]:
|
|
|
97
117
|
dirs = [urljoin(url, a) for a in links if a != "../" and a.endswith("/")]
|
|
98
118
|
files.extend(itertools.chain(*[get_urls_in_index_page(dir) for dir in dirs]))
|
|
99
119
|
return files
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
import argparse
|
|
124
|
+
|
|
125
|
+
parser = argparse.ArgumentParser(description="Download files from index page.")
|
|
126
|
+
parser.add_argument("url", type=str, help="URL of index page.")
|
|
127
|
+
parser.add_argument("dist", type=str, help="Directory of dist.")
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"--override", type=bool, default=False, help="Override existing file."
|
|
130
|
+
)
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
"--multiprocess", type=int, default=4, help="How many process are available."
|
|
133
|
+
)
|
|
134
|
+
args = parser.parse_args()
|
|
135
|
+
|
|
136
|
+
logging.basicConfig(level=logging.INFO)
|
|
137
|
+
clone_index_page(args.url, args.dist, args.override, args.multiprocess)
|
swcgeom/utils/dsu.py
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""Disjoint Set Union Impl."""
|
|
2
17
|
|
|
3
18
|
__all__ = ["DisjointSetUnion"]
|
swcgeom/utils/ellipse.py
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""Finds the Minimum Volume Enclosing Ellipsoid."""
|
|
2
17
|
|
|
3
18
|
# pylint: disable=invalid-name
|
|
4
19
|
|
|
5
20
|
from functools import cached_property
|
|
6
|
-
from typing import Tuple
|
|
7
21
|
|
|
8
22
|
import numpy as np
|
|
9
23
|
import numpy.linalg as la
|
|
@@ -22,7 +36,7 @@ class Ellipse:
|
|
|
22
36
|
self.centroid = centroid
|
|
23
37
|
|
|
24
38
|
@property
|
|
25
|
-
def radii(self) ->
|
|
39
|
+
def radii(self) -> tuple[float, float]:
|
|
26
40
|
# x, y radii.
|
|
27
41
|
_U, D, _V = self.svd
|
|
28
42
|
rx, ry = 1.0 / np.sqrt(D)
|
|
@@ -39,7 +53,7 @@ class Ellipse:
|
|
|
39
53
|
return b
|
|
40
54
|
|
|
41
55
|
@property
|
|
42
|
-
def axes(self) ->
|
|
56
|
+
def axes(self) -> tuple[float, float]:
|
|
43
57
|
# Major and minor semi-axis of the ellipse.
|
|
44
58
|
rx, ry = self.radii
|
|
45
59
|
dx, dy = 2 * rx, 2 * ry
|
|
@@ -77,7 +91,7 @@ def mvee(points: npt.NDArray[np.floating], tol: float = 1e-3) -> Ellipse:
|
|
|
77
91
|
|
|
78
92
|
def _mvee(
|
|
79
93
|
points: npt.NDArray[np.floating], tol: float = 1e-3
|
|
80
|
-
) ->
|
|
94
|
+
) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
|
|
81
95
|
"""Finds the Minimum Volume Enclosing Ellipsoid.
|
|
82
96
|
|
|
83
97
|
Returns
|
swcgeom/utils/file.py
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""File related utils.
|
|
2
17
|
|
|
3
18
|
Notes
|