swcgeom 0.15.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 +857 -0
- swcgeom/analysis/sholl.py +55 -29
- 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 +32 -9
- swcgeom/core/branch.py +28 -7
- swcgeom/core/branch_tree.py +18 -4
- swcgeom/core/{segment.py → compartment.py} +31 -10
- swcgeom/core/node.py +31 -10
- swcgeom/core/path.py +37 -10
- swcgeom/core/population.py +103 -34
- swcgeom/core/swc.py +26 -10
- swcgeom/core/swc_utils/__init__.py +21 -7
- swcgeom/core/swc_utils/assembler.py +27 -1
- swcgeom/core/swc_utils/base.py +25 -12
- swcgeom/core/swc_utils/checker.py +31 -14
- 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 +85 -72
- swcgeom/core/tree_utils.py +31 -16
- swcgeom/core/tree_utils_impl.py +18 -3
- swcgeom/images/__init__.py +17 -2
- swcgeom/images/augmentation.py +24 -4
- swcgeom/images/contrast.py +122 -0
- swcgeom/images/folder.py +97 -39
- swcgeom/images/io.py +108 -121
- swcgeom/transforms/__init__.py +28 -10
- 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 +115 -0
- swcgeom/transforms/image_stack.py +37 -13
- swcgeom/transforms/images.py +184 -7
- swcgeom/transforms/mst.py +20 -5
- swcgeom/transforms/neurolucida_asc.py +508 -0
- swcgeom/transforms/path.py +15 -0
- swcgeom/transforms/population.py +16 -3
- swcgeom/transforms/tree.py +89 -31
- swcgeom/transforms/tree_assembler.py +23 -7
- swcgeom/utils/__init__.py +27 -11
- 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 +439 -302
- swcgeom/utils/numpy_helper.py +29 -4
- swcgeom/utils/plotter_2d.py +151 -0
- swcgeom/utils/plotter_3d.py +48 -0
- swcgeom/utils/renderer.py +49 -145
- swcgeom/utils/sdf.py +24 -8
- swcgeom/utils/solid_geometry.py +16 -3
- swcgeom/utils/transforms.py +17 -4
- swcgeom/utils/volumetric_object.py +23 -10
- {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/LICENSE +1 -1
- {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/METADATA +28 -24
- swcgeom-0.18.3.dist-info/RECORD +67 -0
- {swcgeom-0.15.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.15.0.dist-info/RECORD +0 -62
- {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/top_level.txt +0 -0
swcgeom/core/tree_utils.py
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
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
|
"""SWC util wrapper for tree."""
|
|
2
17
|
|
|
3
18
|
import warnings
|
|
4
|
-
from
|
|
19
|
+
from collections.abc import Callable, Iterable
|
|
20
|
+
from typing import Optional, TypeVar, overload
|
|
5
21
|
|
|
6
22
|
import numpy as np
|
|
23
|
+
from typing_extensions import deprecated
|
|
7
24
|
|
|
8
25
|
from swcgeom.core.swc import SWCLike
|
|
9
26
|
from swcgeom.core.swc_utils import (
|
|
@@ -50,9 +67,9 @@ def sort_tree(tree: Tree) -> Tree:
|
|
|
50
67
|
|
|
51
68
|
# fmt:off
|
|
52
69
|
@overload
|
|
53
|
-
def cut_tree(tree: Tree, *, enter: Callable[[Tree.Node, T | None],
|
|
70
|
+
def cut_tree(tree: Tree, *, enter: Callable[[Tree.Node, T | None], tuple[T, bool]]) -> Tree: ...
|
|
54
71
|
@overload
|
|
55
|
-
def cut_tree(tree: Tree, *, leave: Callable[[Tree.Node,
|
|
72
|
+
def cut_tree(tree: Tree, *, leave: Callable[[Tree.Node, list[K]], tuple[K, bool]]) -> Tree: ...
|
|
56
73
|
# fmt:on
|
|
57
74
|
def cut_tree(tree: Tree, *, enter=None, leave=None):
|
|
58
75
|
"""Traverse and cut the tree.
|
|
@@ -60,11 +77,11 @@ def cut_tree(tree: Tree, *, enter=None, leave=None):
|
|
|
60
77
|
Returning a `True` can delete the current node and its children.
|
|
61
78
|
"""
|
|
62
79
|
|
|
63
|
-
removals:
|
|
80
|
+
removals: list[int] = []
|
|
64
81
|
|
|
65
82
|
if enter:
|
|
66
83
|
|
|
67
|
-
def _enter(n: Tree.Node, parent:
|
|
84
|
+
def _enter(n: Tree.Node, parent: tuple[T, bool] | None) -> tuple[T, bool]:
|
|
68
85
|
if parent is not None and parent[1]:
|
|
69
86
|
removals.append(n.id)
|
|
70
87
|
return parent
|
|
@@ -79,7 +96,7 @@ def cut_tree(tree: Tree, *, enter=None, leave=None):
|
|
|
79
96
|
|
|
80
97
|
elif leave:
|
|
81
98
|
|
|
82
|
-
def _leave(n: Tree.Node, children:
|
|
99
|
+
def _leave(n: Tree.Node, children: list[K]) -> K:
|
|
83
100
|
res, removal = leave(n, children)
|
|
84
101
|
if removal:
|
|
85
102
|
removals.append(n.id)
|
|
@@ -94,24 +111,22 @@ def cut_tree(tree: Tree, *, enter=None, leave=None):
|
|
|
94
111
|
return to_subtree(tree, removals)
|
|
95
112
|
|
|
96
113
|
|
|
97
|
-
|
|
114
|
+
@deprecated("Use `to_subtree` instead")
|
|
115
|
+
def to_sub_tree(swc_like: SWCLike, sub: Topology) -> tuple[Tree, dict[int, int]]:
|
|
98
116
|
"""Create subtree from origin tree.
|
|
99
117
|
|
|
100
118
|
You can directly mark the node for removal, and we will remove it,
|
|
101
119
|
but if the node you remove is not a leaf node, you need to use
|
|
102
120
|
`propagate_remove` to remove all children.
|
|
103
121
|
|
|
122
|
+
.. deprecated:: 0.6.0
|
|
123
|
+
Use :meth:`to_subtree` instead.
|
|
124
|
+
|
|
104
125
|
Returns
|
|
105
126
|
-------
|
|
106
127
|
tree : Tree
|
|
107
|
-
id_map :
|
|
128
|
+
id_map : dict[int, int]
|
|
108
129
|
"""
|
|
109
|
-
warnings.warn(
|
|
110
|
-
"`to_sub_tree` will be removed in v0.6.0, it is replaced by "
|
|
111
|
-
"`to_subtree` beacuse it is easy to use, and this will be "
|
|
112
|
-
"removed in next version",
|
|
113
|
-
DeprecationWarning,
|
|
114
|
-
)
|
|
115
130
|
|
|
116
131
|
sub = propagate_removal(sub)
|
|
117
132
|
(new_id, new_pid), id_map_arr = to_sub_topology(sub)
|
|
@@ -141,7 +156,7 @@ def to_subtree(
|
|
|
141
156
|
swc_like : SWCLike
|
|
142
157
|
removals : List of int
|
|
143
158
|
A list of id of nodes to be removed.
|
|
144
|
-
out_mapping: List of int or
|
|
159
|
+
out_mapping: List of int or dict[int, int], optional
|
|
145
160
|
Map new id to old id.
|
|
146
161
|
"""
|
|
147
162
|
|
|
@@ -166,7 +181,7 @@ def get_subtree(
|
|
|
166
181
|
swc_like : SWCLike
|
|
167
182
|
n : int
|
|
168
183
|
Id of the root of the subtree.
|
|
169
|
-
out_mapping: List of int or
|
|
184
|
+
out_mapping: List of int or dict[int, int], optional
|
|
170
185
|
Map new id to old id.
|
|
171
186
|
"""
|
|
172
187
|
|
swcgeom/core/tree_utils_impl.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
|
"""SWC util wrapper for tree, split to avoid circle imports.
|
|
2
17
|
|
|
3
18
|
Notes
|
|
@@ -5,7 +20,7 @@ Notes
|
|
|
5
20
|
Do not import `Tree` and keep this file minimized.
|
|
6
21
|
"""
|
|
7
22
|
|
|
8
|
-
from typing import Any,
|
|
23
|
+
from typing import Any, Optional
|
|
9
24
|
|
|
10
25
|
import numpy as np
|
|
11
26
|
import numpy.typing as npt
|
|
@@ -15,8 +30,8 @@ from swcgeom.core.swc_utils import Topology, to_sub_topology, traverse
|
|
|
15
30
|
|
|
16
31
|
__all__ = ["get_subtree_impl", "to_subtree_impl"]
|
|
17
32
|
|
|
18
|
-
Mapping =
|
|
19
|
-
TreeArgs =
|
|
33
|
+
Mapping = dict[int, int] | list[int]
|
|
34
|
+
TreeArgs = tuple[int, dict[str, npt.NDArray[Any]], str, SWCNames]
|
|
20
35
|
|
|
21
36
|
|
|
22
37
|
def get_subtree_impl(
|
swcgeom/images/__init__.py
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
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
|
"""Image Stack Related."""
|
|
2
17
|
|
|
3
|
-
from swcgeom.images.folder import *
|
|
4
|
-
from swcgeom.images.io import *
|
|
18
|
+
from swcgeom.images.folder import * # noqa: F403
|
|
19
|
+
from swcgeom.images.io import * # noqa: F403
|
swcgeom/images/augmentation.py
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
|
-
|
|
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
|
+
|
|
16
|
+
"""Play augment in image stack.
|
|
17
|
+
|
|
18
|
+
Notes
|
|
19
|
+
-----
|
|
20
|
+
This is expremental code, and the API is subject to change.
|
|
21
|
+
"""
|
|
2
22
|
|
|
3
23
|
import random
|
|
4
|
-
from typing import
|
|
24
|
+
from typing import Literal, Optional
|
|
5
25
|
|
|
6
26
|
import numpy as np
|
|
7
27
|
import numpy.typing as npt
|
|
@@ -49,7 +69,7 @@ class Augmentation:
|
|
|
49
69
|
|
|
50
70
|
def swapaxes(self, x, mode: Optional[Literal["xy", "xz", "yz"]] = None) -> NDArrf32:
|
|
51
71
|
if mode is None:
|
|
52
|
-
modes:
|
|
72
|
+
modes: list[Literal["xy", "xz", "yz"]] = ["xy", "xz", "yz"]
|
|
53
73
|
mode = modes[self.rand.randint(0, 2)]
|
|
54
74
|
|
|
55
75
|
match mode:
|
|
@@ -64,7 +84,7 @@ class Augmentation:
|
|
|
64
84
|
|
|
65
85
|
def flip(self, x, mode: Optional[Literal["xy", "xz", "yz"]] = None) -> NDArrf32:
|
|
66
86
|
if mode is None:
|
|
67
|
-
modes:
|
|
87
|
+
modes: list[Literal["xy", "xz", "yz"]] = ["xy", "xz", "yz"]
|
|
68
88
|
mode = modes[random.randint(0, 2)]
|
|
69
89
|
|
|
70
90
|
match mode:
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
|
|
16
|
+
"""The contrast of an image.
|
|
17
|
+
|
|
18
|
+
Notes
|
|
19
|
+
-----
|
|
20
|
+
This is expremental code, and the API is subject to change.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from typing import Optional, overload
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
import numpy.typing as npt
|
|
27
|
+
|
|
28
|
+
__all__ = ["contrast_std", "contrast_michelson", "contrast_rms", "contrast_weber"]
|
|
29
|
+
|
|
30
|
+
Array3D = npt.NDArray[np.float32]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@overload
|
|
34
|
+
def contrast_std(image: Array3D) -> float:
|
|
35
|
+
"""Get the std contrast of an image stack.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
imgs : ndarray
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
contrast : float
|
|
44
|
+
"""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@overload
|
|
49
|
+
def contrast_std(image: Array3D, contrast: float) -> Array3D:
|
|
50
|
+
"""Adjust the contrast of an image stack.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
imgs : ndarray
|
|
55
|
+
constrast : float
|
|
56
|
+
The contrast adjustment factor. 1.0 leaves the image unchanged.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
imgs : ndarray
|
|
61
|
+
The adjusted image.
|
|
62
|
+
"""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def contrast_std(image: Array3D, contrast: Optional[float] = None):
|
|
67
|
+
if contrast is None:
|
|
68
|
+
return np.std(image).item()
|
|
69
|
+
else:
|
|
70
|
+
return np.clip(contrast * image, 0, 1)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def contrast_michelson(image: Array3D) -> float:
|
|
74
|
+
"""Get the Michelson contrast of an image stack.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
imgs : ndarray
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
contrast : float
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
vmax = np.max(image)
|
|
86
|
+
vmin = np.min(image)
|
|
87
|
+
return ((vmax - vmin) / (vmax + vmin)).item()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def contrast_rms(imgs: npt.NDArray[np.float32]) -> float:
|
|
91
|
+
"""Get the RMS contrast of an image stack.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
imgs : ndarray
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
contrast : float
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
return np.sqrt(np.mean(imgs**2)).item()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def contrast_weber(imgs: Array3D, mask: npt.NDArray[np.bool_]) -> float:
|
|
106
|
+
"""Get the Weber contrast of an image stack.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
imgs : ndarray
|
|
111
|
+
mask : ndarray of bool
|
|
112
|
+
The mask to segment the foreground and background. 1 for
|
|
113
|
+
foreground, 0 for background.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
contrast : float
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
l_foreground = np.mean(imgs, where=mask)
|
|
121
|
+
l_background = np.mean(imgs, where=np.logical_not(mask))
|
|
122
|
+
return ((l_foreground - l_background) / l_background).item()
|
swcgeom/images/folder.py
CHANGED
|
@@ -1,41 +1,44 @@
|
|
|
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
|
"""Image stack folder."""
|
|
2
17
|
|
|
18
|
+
import math
|
|
3
19
|
import os
|
|
4
20
|
import re
|
|
5
|
-
import
|
|
6
|
-
from
|
|
7
|
-
from typing import
|
|
8
|
-
Callable,
|
|
9
|
-
Generic,
|
|
10
|
-
Iterable,
|
|
11
|
-
List,
|
|
12
|
-
Literal,
|
|
13
|
-
Optional,
|
|
14
|
-
Tuple,
|
|
15
|
-
TypeVar,
|
|
16
|
-
overload,
|
|
17
|
-
)
|
|
21
|
+
from collections.abc import Callable, Iterable
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Generic, Literal, Optional, TypeVar, overload
|
|
18
24
|
|
|
19
25
|
import numpy as np
|
|
20
26
|
import numpy.typing as npt
|
|
21
|
-
from
|
|
27
|
+
from tqdm import tqdm
|
|
28
|
+
from typing_extensions import Self, deprecated
|
|
22
29
|
|
|
23
30
|
from swcgeom.images.io import ScalarType, read_imgs
|
|
24
31
|
from swcgeom.transforms import Identity, Transform
|
|
25
32
|
|
|
26
|
-
__all__ = [
|
|
27
|
-
"ImageStackFolder",
|
|
28
|
-
"LabeledImageStackFolder",
|
|
29
|
-
"PathImageStackFolder",
|
|
30
|
-
]
|
|
33
|
+
__all__ = ["ImageStackFolder", "LabeledImageStackFolder", "PathImageStackFolder"]
|
|
31
34
|
|
|
32
35
|
T = TypeVar("T")
|
|
33
36
|
|
|
34
37
|
|
|
35
|
-
class ImageStackFolderBase(Generic[ScalarType, T]
|
|
38
|
+
class ImageStackFolderBase(Generic[ScalarType, T]):
|
|
36
39
|
"""Image stack folder base."""
|
|
37
40
|
|
|
38
|
-
files:
|
|
41
|
+
files: list[str]
|
|
39
42
|
transform: Transform[npt.NDArray[ScalarType], T]
|
|
40
43
|
|
|
41
44
|
# fmt: off
|
|
@@ -51,10 +54,6 @@ class ImageStackFolderBase(Generic[ScalarType, T], ABC):
|
|
|
51
54
|
self.dtype = dtype or np.float32
|
|
52
55
|
self.transform = transform or Identity() # type: ignore
|
|
53
56
|
|
|
54
|
-
@abstractmethod
|
|
55
|
-
def __getitem__(self, key: str, /) -> T:
|
|
56
|
-
raise NotImplementedError()
|
|
57
|
-
|
|
58
57
|
def __len__(self) -> int:
|
|
59
58
|
return len(self.files)
|
|
60
59
|
|
|
@@ -67,7 +66,10 @@ class ImageStackFolderBase(Generic[ScalarType, T], ABC):
|
|
|
67
66
|
return read_imgs(fname, dtype=self.dtype).get_full() # type: ignore
|
|
68
67
|
|
|
69
68
|
@staticmethod
|
|
70
|
-
def scan(root: str, *, pattern: Optional[str] = None) ->
|
|
69
|
+
def scan(root: str, *, pattern: Optional[str] = None) -> list[str]:
|
|
70
|
+
if not os.path.isdir(root):
|
|
71
|
+
raise NotADirectoryError(f"not a directory: {root}")
|
|
72
|
+
|
|
71
73
|
is_valid = re.compile(pattern).match if pattern is not None else truthly
|
|
72
74
|
|
|
73
75
|
fs = []
|
|
@@ -77,24 +79,78 @@ class ImageStackFolderBase(Generic[ScalarType, T], ABC):
|
|
|
77
79
|
return fs
|
|
78
80
|
|
|
79
81
|
@staticmethod
|
|
82
|
+
@deprecated("Use `~swcgeom.images.io.read_imgs(fname).get_full()` instead")
|
|
80
83
|
def read_imgs(fname: str) -> npt.NDArray[np.float32]:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
DeprecationWarning,
|
|
88
|
-
)
|
|
84
|
+
"""Read images.
|
|
85
|
+
|
|
86
|
+
.. deprecated:: 0.16.0
|
|
87
|
+
Use :meth:`~swcgeom.images.io.read_imgs(fname).get_full()` instead.
|
|
88
|
+
"""
|
|
89
|
+
|
|
89
90
|
return read_imgs(fname).get_full()
|
|
90
91
|
|
|
91
92
|
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class Statistics:
|
|
95
|
+
count: int = 0
|
|
96
|
+
minimum: float = math.nan
|
|
97
|
+
maximum: float = math.nan
|
|
98
|
+
mean: float = 0
|
|
99
|
+
variance: float = 0
|
|
100
|
+
|
|
101
|
+
|
|
92
102
|
class ImageStackFolder(ImageStackFolderBase[ScalarType, T]):
|
|
93
103
|
"""Image stack folder."""
|
|
94
104
|
|
|
95
105
|
def __getitem__(self, idx: int, /) -> T:
|
|
96
106
|
return self._get(self.files[idx])
|
|
97
107
|
|
|
108
|
+
def stat(self, *, transform: bool = False, verbose: bool = False) -> Statistics:
|
|
109
|
+
"""Statistics of folder.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
transform : bool, default to False
|
|
114
|
+
Apply transform to the images. If True, you need to make
|
|
115
|
+
sure the transformed data is a ndarray.
|
|
116
|
+
verbose : bool, optional
|
|
117
|
+
|
|
118
|
+
Notes
|
|
119
|
+
-----
|
|
120
|
+
We are asserting that the images are of the same shape.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
vmin, vmax = math.inf, -math.inf
|
|
124
|
+
n, mean, M2 = 0, None, None
|
|
125
|
+
|
|
126
|
+
for idx in tqdm(range(len(self))) if verbose else range(len(self)):
|
|
127
|
+
imgs = self[idx] if transform else self._read(self.files[idx])
|
|
128
|
+
|
|
129
|
+
vmin = min(vmin, np.min(imgs)) # type: ignore
|
|
130
|
+
vmax = max(vmax, np.max(imgs)) # type: ignore
|
|
131
|
+
# Welford algorithm to calculate mean and variance
|
|
132
|
+
if mean is None:
|
|
133
|
+
mean = np.zeros_like(imgs)
|
|
134
|
+
M2 = np.zeros_like(imgs)
|
|
135
|
+
|
|
136
|
+
n += 1
|
|
137
|
+
delta = imgs - mean # type: ignore
|
|
138
|
+
mean += delta / n
|
|
139
|
+
delta2 = imgs - mean
|
|
140
|
+
M2 += delta * delta2
|
|
141
|
+
|
|
142
|
+
if mean is None or M2 is None: # n = 0
|
|
143
|
+
raise ValueError("empty folder")
|
|
144
|
+
|
|
145
|
+
variance = M2 / (n - 1) if n > 1 else np.zeros_like(mean)
|
|
146
|
+
return Statistics(
|
|
147
|
+
count=len(self),
|
|
148
|
+
maximum=vmax,
|
|
149
|
+
minimum=vmin,
|
|
150
|
+
mean=np.mean(mean).item(),
|
|
151
|
+
variance=np.mean(variance).item(),
|
|
152
|
+
)
|
|
153
|
+
|
|
98
154
|
@classmethod
|
|
99
155
|
def from_dir(cls, root: str, *, pattern: Optional[str] = None, **kwargs) -> Self:
|
|
100
156
|
"""
|
|
@@ -106,20 +162,21 @@ class ImageStackFolder(ImageStackFolderBase[ScalarType, T]):
|
|
|
106
162
|
**kwargs
|
|
107
163
|
Pass to `cls.__init__`
|
|
108
164
|
"""
|
|
165
|
+
|
|
109
166
|
return cls(cls.scan(root, pattern=pattern), **kwargs)
|
|
110
167
|
|
|
111
168
|
|
|
112
169
|
class LabeledImageStackFolder(ImageStackFolderBase[ScalarType, T]):
|
|
113
170
|
"""Image stack folder with label."""
|
|
114
171
|
|
|
115
|
-
labels:
|
|
172
|
+
labels: list[int]
|
|
116
173
|
|
|
117
174
|
def __init__(self, files: Iterable[str], labels: Iterable[int], **kwargs):
|
|
118
175
|
super().__init__(files, **kwargs)
|
|
119
176
|
self.labels = list(labels)
|
|
120
177
|
|
|
121
|
-
def __getitem__(self, idx: int) ->
|
|
122
|
-
return self.
|
|
178
|
+
def __getitem__(self, idx: int) -> tuple[T, int]:
|
|
179
|
+
return self._get(self.files[idx]), self.labels[idx]
|
|
123
180
|
|
|
124
181
|
@classmethod
|
|
125
182
|
def from_dir(
|
|
@@ -140,7 +197,7 @@ class LabeledImageStackFolder(ImageStackFolderBase[ScalarType, T]):
|
|
|
140
197
|
return cls(files, labels, **kwargs)
|
|
141
198
|
|
|
142
199
|
|
|
143
|
-
class PathImageStackFolder(
|
|
200
|
+
class PathImageStackFolder(ImageStackFolderBase[ScalarType, T]):
|
|
144
201
|
"""Image stack folder with relpath."""
|
|
145
202
|
|
|
146
203
|
root: str
|
|
@@ -149,7 +206,7 @@ class PathImageStackFolder(ImageStackFolder[ScalarType, T]):
|
|
|
149
206
|
super().__init__(files, **kwargs)
|
|
150
207
|
self.root = root
|
|
151
208
|
|
|
152
|
-
def __getitem__(self, idx: int) ->
|
|
209
|
+
def __getitem__(self, idx: int) -> tuple[T, str]:
|
|
153
210
|
relpath = os.path.relpath(self.files[idx], self.root)
|
|
154
211
|
return self._get(self.files[idx]), relpath
|
|
155
212
|
|
|
@@ -164,6 +221,7 @@ class PathImageStackFolder(ImageStackFolder[ScalarType, T]):
|
|
|
164
221
|
**kwargs
|
|
165
222
|
Pass to `cls.__init__`
|
|
166
223
|
"""
|
|
224
|
+
|
|
167
225
|
return cls(cls.scan(root, pattern=pattern), root=root, **kwargs)
|
|
168
226
|
|
|
169
227
|
|