swcgeom 0.19.4__cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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 +21 -0
- swcgeom/analysis/__init__.py +13 -0
- swcgeom/analysis/feature_extractor.py +454 -0
- swcgeom/analysis/features.py +218 -0
- swcgeom/analysis/lmeasure.py +750 -0
- swcgeom/analysis/sholl.py +201 -0
- swcgeom/analysis/trunk.py +183 -0
- swcgeom/analysis/visualization.py +191 -0
- swcgeom/analysis/visualization3d.py +81 -0
- swcgeom/analysis/volume.py +143 -0
- swcgeom/core/__init__.py +19 -0
- swcgeom/core/branch.py +129 -0
- swcgeom/core/branch_tree.py +65 -0
- swcgeom/core/compartment.py +107 -0
- swcgeom/core/node.py +130 -0
- swcgeom/core/path.py +155 -0
- swcgeom/core/population.py +341 -0
- swcgeom/core/swc.py +247 -0
- swcgeom/core/swc_utils/__init__.py +19 -0
- swcgeom/core/swc_utils/assembler.py +35 -0
- swcgeom/core/swc_utils/base.py +180 -0
- swcgeom/core/swc_utils/checker.py +107 -0
- swcgeom/core/swc_utils/io.py +204 -0
- swcgeom/core/swc_utils/normalizer.py +163 -0
- swcgeom/core/swc_utils/subtree.py +70 -0
- swcgeom/core/tree.py +384 -0
- swcgeom/core/tree_utils.py +277 -0
- swcgeom/core/tree_utils_impl.py +58 -0
- swcgeom/images/__init__.py +9 -0
- swcgeom/images/augmentation.py +149 -0
- swcgeom/images/contrast.py +87 -0
- swcgeom/images/folder.py +217 -0
- swcgeom/images/io.py +578 -0
- swcgeom/images/loaders/__init__.py +8 -0
- swcgeom/images/loaders/pbd.cpython-313-x86_64-linux-gnu.so +0 -0
- swcgeom/images/loaders/pbd.pyx +523 -0
- swcgeom/images/loaders/raw.cpython-313-x86_64-linux-gnu.so +0 -0
- swcgeom/images/loaders/raw.pyx +183 -0
- swcgeom/transforms/__init__.py +20 -0
- swcgeom/transforms/base.py +136 -0
- swcgeom/transforms/branch.py +223 -0
- swcgeom/transforms/branch_tree.py +74 -0
- swcgeom/transforms/geometry.py +270 -0
- swcgeom/transforms/image_preprocess.py +107 -0
- swcgeom/transforms/image_stack.py +219 -0
- swcgeom/transforms/images.py +206 -0
- swcgeom/transforms/mst.py +183 -0
- swcgeom/transforms/neurolucida_asc.py +498 -0
- swcgeom/transforms/path.py +56 -0
- swcgeom/transforms/population.py +36 -0
- swcgeom/transforms/tree.py +265 -0
- swcgeom/transforms/tree_assembler.py +161 -0
- swcgeom/utils/__init__.py +18 -0
- swcgeom/utils/debug.py +23 -0
- swcgeom/utils/download.py +119 -0
- swcgeom/utils/dsu.py +58 -0
- swcgeom/utils/ellipse.py +131 -0
- swcgeom/utils/file.py +90 -0
- swcgeom/utils/neuromorpho.py +581 -0
- swcgeom/utils/numpy_helper.py +70 -0
- swcgeom/utils/plotter_2d.py +134 -0
- swcgeom/utils/plotter_3d.py +35 -0
- swcgeom/utils/renderer.py +145 -0
- swcgeom/utils/sdf.py +324 -0
- swcgeom/utils/solid_geometry.py +154 -0
- swcgeom/utils/transforms.py +367 -0
- swcgeom/utils/volumetric_object.py +483 -0
- swcgeom-0.19.4.dist-info/METADATA +86 -0
- swcgeom-0.19.4.dist-info/RECORD +72 -0
- swcgeom-0.19.4.dist-info/WHEEL +6 -0
- swcgeom-0.19.4.dist-info/licenses/LICENSE +201 -0
- swcgeom-0.19.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""SWC util wrapper for tree, split to avoid circle imports.
|
|
7
|
+
|
|
8
|
+
NOTE: Do not import `Tree` and keep this file minimized.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import numpy.typing as npt
|
|
15
|
+
|
|
16
|
+
from swcgeom.core.swc import SWCLike, SWCNames
|
|
17
|
+
from swcgeom.core.swc_utils import Topology, to_sub_topology, traverse
|
|
18
|
+
|
|
19
|
+
__all__ = ["get_subtree_impl", "to_subtree_impl"]
|
|
20
|
+
|
|
21
|
+
Mapping = dict[int, int] | list[int]
|
|
22
|
+
TreeArgs = tuple[int, dict[str, npt.NDArray[Any]], str, SWCNames]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_subtree_impl(
|
|
26
|
+
swc_like: SWCLike, n: int, *, out_mapping: Mapping | None = None
|
|
27
|
+
) -> TreeArgs:
|
|
28
|
+
ids = []
|
|
29
|
+
topo = (swc_like.id(), swc_like.pid())
|
|
30
|
+
traverse(topo, enter=lambda n, _: ids.append(n), root=n)
|
|
31
|
+
|
|
32
|
+
sub_ids = np.array(ids, dtype=np.int32)
|
|
33
|
+
sub_pid = swc_like.pid()[sub_ids]
|
|
34
|
+
sub_pid[0] = -1
|
|
35
|
+
return to_subtree_impl(swc_like, (sub_ids, sub_pid), out_mapping=out_mapping)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def to_subtree_impl(
|
|
39
|
+
swc_like: SWCLike,
|
|
40
|
+
sub: Topology,
|
|
41
|
+
*,
|
|
42
|
+
out_mapping: Mapping | None = None,
|
|
43
|
+
) -> TreeArgs:
|
|
44
|
+
(new_id, new_pid), mapping = to_sub_topology(sub)
|
|
45
|
+
|
|
46
|
+
n_nodes = new_id.shape[0]
|
|
47
|
+
ndata = {k: swc_like.get_ndata(k)[mapping].copy() for k in swc_like.keys()}
|
|
48
|
+
ndata.update(id=new_id, pid=new_pid)
|
|
49
|
+
|
|
50
|
+
if isinstance(out_mapping, list):
|
|
51
|
+
out_mapping.clear()
|
|
52
|
+
out_mapping.extend(mapping)
|
|
53
|
+
elif isinstance(out_mapping, dict):
|
|
54
|
+
out_mapping.clear()
|
|
55
|
+
for new_id, old_id in enumerate(mapping):
|
|
56
|
+
out_mapping[new_id] = old_id # returning a dict may leads to bad perf
|
|
57
|
+
|
|
58
|
+
return n_nodes, ndata, swc_like.source, swc_like.names
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""Play augment in image stack.
|
|
7
|
+
|
|
8
|
+
NOTE: This is expremental code, and the API is subject to change.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import random
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import numpy.typing as npt
|
|
16
|
+
|
|
17
|
+
__all__ = ["play_augment", "random_augmentations"]
|
|
18
|
+
|
|
19
|
+
NDArrf32 = npt.NDArray[np.float32]
|
|
20
|
+
|
|
21
|
+
# Augmentation = Literal[
|
|
22
|
+
# "swap_xy",
|
|
23
|
+
# "swap_xz",
|
|
24
|
+
# "swap_yz",
|
|
25
|
+
# "flip_x",
|
|
26
|
+
# "flip_y",
|
|
27
|
+
# "flip_z",
|
|
28
|
+
# "rot90_xy",
|
|
29
|
+
# "rot90_xz",
|
|
30
|
+
# "rot90_yz",
|
|
31
|
+
# ]
|
|
32
|
+
|
|
33
|
+
IDENTITY = -1
|
|
34
|
+
|
|
35
|
+
augs = {
|
|
36
|
+
# swaps
|
|
37
|
+
"swap_xy": lambda x: np.swapaxes(x, 0, 1),
|
|
38
|
+
"swap_xz": lambda x: np.swapaxes(x, 0, 2),
|
|
39
|
+
"swap_yz": lambda x: np.swapaxes(x, 1, 2),
|
|
40
|
+
# flips
|
|
41
|
+
"flip_x": lambda x: np.flip(x, axis=[0]),
|
|
42
|
+
"flip_y": lambda x: np.flip(x, axis=[1]),
|
|
43
|
+
"flip_z": lambda x: np.flip(x, axis=[2]),
|
|
44
|
+
# rotations
|
|
45
|
+
"rot90_xy": lambda x: np.rot90(x, k=1, axes=(0, 1)),
|
|
46
|
+
"rot90_xz": lambda x: np.rot90(x, k=1, axes=(0, 2)),
|
|
47
|
+
"rot90_yz": lambda x: np.rot90(x, k=1, axes=(1, 2)),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Augmentation:
|
|
52
|
+
"""Play augmentation."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, *, seed: int | None) -> None:
|
|
55
|
+
self.seed = seed
|
|
56
|
+
self.rand = random.Random(seed)
|
|
57
|
+
|
|
58
|
+
def swapaxes(self, x, mode: Literal["xy", "xz", "yz"] | None = None) -> NDArrf32:
|
|
59
|
+
if mode is None:
|
|
60
|
+
modes: list[Literal["xy", "xz", "yz"]] = ["xy", "xz", "yz"]
|
|
61
|
+
mode = modes[self.rand.randint(0, 2)]
|
|
62
|
+
|
|
63
|
+
match mode:
|
|
64
|
+
case "xy":
|
|
65
|
+
return np.swapaxes(x, 0, 1)
|
|
66
|
+
case "xz":
|
|
67
|
+
return np.swapaxes(x, 0, 2)
|
|
68
|
+
case "yz":
|
|
69
|
+
return np.swapaxes(x, 1, 2)
|
|
70
|
+
case _:
|
|
71
|
+
raise ValueError(f"invalid mode: {mode}")
|
|
72
|
+
|
|
73
|
+
def flip(self, x, mode: Literal["xy", "xz", "yz"] | None = None) -> NDArrf32:
|
|
74
|
+
if mode is None:
|
|
75
|
+
modes: list[Literal["xy", "xz", "yz"]] = ["xy", "xz", "yz"]
|
|
76
|
+
mode = modes[random.randint(0, 2)]
|
|
77
|
+
|
|
78
|
+
match mode:
|
|
79
|
+
case "xy":
|
|
80
|
+
return np.flip(x, axis=0)
|
|
81
|
+
case "xz":
|
|
82
|
+
return np.flip(x, axis=1)
|
|
83
|
+
case "yz":
|
|
84
|
+
return np.flip(x, axis=2)
|
|
85
|
+
case _:
|
|
86
|
+
raise ValueError(f"invalid mode: {mode}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
fns = list(augs.keys())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def play_augment(x: NDArrf32, method: Augmentation | int | None = None) -> NDArrf32:
|
|
93
|
+
"""Play augment in x.
|
|
94
|
+
|
|
95
|
+
Args
|
|
96
|
+
x: Array of shape (X, Y, Z, C)
|
|
97
|
+
method: Augmentation method index / name.
|
|
98
|
+
If not provided, a random augment will be apply.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
if isinstance(method, str):
|
|
102
|
+
key = method
|
|
103
|
+
elif isinstance(method, int):
|
|
104
|
+
key = fns[method]
|
|
105
|
+
elif method is None:
|
|
106
|
+
key = fns[random.randint(0, len(augs))]
|
|
107
|
+
elif method == IDENTITY:
|
|
108
|
+
return x
|
|
109
|
+
else:
|
|
110
|
+
raise ValueError("invalid augment method")
|
|
111
|
+
|
|
112
|
+
return augs[key](x)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def random_augmentations(
|
|
116
|
+
n: int, k: int, *, seed: int | None = None, include_identity: bool = True
|
|
117
|
+
) -> npt.NDArray[np.int64]:
|
|
118
|
+
"""Generate a sequence of augmentations.
|
|
119
|
+
|
|
120
|
+
>>> xs = os.listdir("path_to_imgs") # doctest: +SKIP
|
|
121
|
+
>>> augs = generate_random_augmentations(len(xs), 5) # doctest: +SKIP
|
|
122
|
+
>>> for i, j in range(augs): # doctest: +SKIP
|
|
123
|
+
... x = play_augment(read_imgs(os.path.join("path_to_imgs", xs[i])), j)
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
n: Size of image stacks.
|
|
127
|
+
k: Each image stack augmented to K image stack.
|
|
128
|
+
seed: Random seed, forwarding to `random.Random`
|
|
129
|
+
include_identity: Include identity transform.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
augmentations: List of (int, int)
|
|
133
|
+
Sequence of length N * K, contains index of image and augmentation method.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
rand = random.Random(seed)
|
|
137
|
+
seq = list(range(len(augs)))
|
|
138
|
+
if include_identity:
|
|
139
|
+
seq.append(IDENTITY)
|
|
140
|
+
|
|
141
|
+
assert 0 < k < len(seq), "too large augment specify."
|
|
142
|
+
|
|
143
|
+
augmentations = []
|
|
144
|
+
for _ in range(n):
|
|
145
|
+
rand.shuffle(seq)
|
|
146
|
+
augmentations.extend(seq[:k])
|
|
147
|
+
|
|
148
|
+
xs = np.stack([np.repeat(np.arange(n), k), augmentations])
|
|
149
|
+
return xs
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""The contrast of an image.
|
|
6
|
+
|
|
7
|
+
NOTE: This is expremental code, and the API is subject to change.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import overload
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import numpy.typing as npt
|
|
14
|
+
|
|
15
|
+
__all__ = ["contrast_std", "contrast_michelson", "contrast_rms", "contrast_weber"]
|
|
16
|
+
|
|
17
|
+
Array3D = npt.NDArray[np.float32]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@overload
|
|
21
|
+
def contrast_std(image: Array3D) -> float:
|
|
22
|
+
"""Get the std contrast of an image stack.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
imgs: ndarray
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
contrast
|
|
29
|
+
"""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@overload
|
|
34
|
+
def contrast_std(image: Array3D, contrast: float) -> Array3D:
|
|
35
|
+
"""Adjust the contrast of an image stack.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
imgs: ndarray
|
|
39
|
+
contrast: The contrast adjustment factor. 1.0 leaves the image unchanged.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
imgs: The adjusted image.
|
|
43
|
+
"""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def contrast_std(image: Array3D, contrast: float | None = None):
|
|
48
|
+
if contrast is None:
|
|
49
|
+
return np.std(image).item()
|
|
50
|
+
else:
|
|
51
|
+
return np.clip(contrast * image, 0, 1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def contrast_michelson(image: Array3D) -> float:
|
|
55
|
+
"""Get the Michelson contrast of an image stack.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
contrast: float
|
|
59
|
+
"""
|
|
60
|
+
vmax = np.max(image)
|
|
61
|
+
vmin = np.min(image)
|
|
62
|
+
return ((vmax - vmin) / (vmax + vmin)).item()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def contrast_rms(imgs: npt.NDArray[np.float32]) -> float:
|
|
66
|
+
"""Get the RMS contrast of an image stack.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
contrast
|
|
70
|
+
"""
|
|
71
|
+
return np.sqrt(np.mean(imgs**2)).item()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def contrast_weber(imgs: Array3D, mask: npt.NDArray[np.bool_]) -> float:
|
|
75
|
+
"""Get the Weber contrast of an image stack.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
imgs: ndarray
|
|
79
|
+
mask: The mask to segment the foreground and background.
|
|
80
|
+
1 for foreground, 0 for background.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
contrast
|
|
84
|
+
"""
|
|
85
|
+
l_foreground = np.mean(imgs, where=mask)
|
|
86
|
+
l_background = np.mean(imgs, where=np.logical_not(mask))
|
|
87
|
+
return ((l_foreground - l_background) / l_background).item()
|
swcgeom/images/folder.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""Image stack folder."""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from collections.abc import Callable, Iterable
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Generic, Literal, TypeVar, overload
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import numpy.typing as npt
|
|
17
|
+
from tqdm import tqdm
|
|
18
|
+
from typing_extensions import Self, deprecated
|
|
19
|
+
|
|
20
|
+
from swcgeom.images.io import ScalarType, read_imgs
|
|
21
|
+
from swcgeom.transforms import Identity, Transform
|
|
22
|
+
|
|
23
|
+
__all__ = ["ImageStackFolder", "LabeledImageStackFolder", "PathImageStackFolder"]
|
|
24
|
+
|
|
25
|
+
T = TypeVar("T")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ImageStackFolderBase(Generic[ScalarType, T]):
|
|
29
|
+
"""Image stack folder base."""
|
|
30
|
+
|
|
31
|
+
files: list[str]
|
|
32
|
+
transform: Transform[npt.NDArray[ScalarType], T]
|
|
33
|
+
|
|
34
|
+
@overload
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
files: Iterable[str],
|
|
38
|
+
*,
|
|
39
|
+
transform: Transform[npt.NDArray[np.float32], T] | None = ...,
|
|
40
|
+
) -> None: ...
|
|
41
|
+
@overload
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
files: Iterable[str],
|
|
45
|
+
*,
|
|
46
|
+
dtype: ScalarType,
|
|
47
|
+
transform: Transform[npt.NDArray[ScalarType], T] | None = ...,
|
|
48
|
+
) -> None: ...
|
|
49
|
+
def __init__(self, files: Iterable[str], *, dtype=None, transform=None) -> None:
|
|
50
|
+
super().__init__()
|
|
51
|
+
self.files = list(files)
|
|
52
|
+
self.dtype = dtype or np.float32
|
|
53
|
+
self.transform = transform or Identity() # type: ignore
|
|
54
|
+
|
|
55
|
+
def __len__(self) -> int:
|
|
56
|
+
return len(self.files)
|
|
57
|
+
|
|
58
|
+
def _get(self, fname: str) -> T:
|
|
59
|
+
imgs = self._read(fname)
|
|
60
|
+
imgs = self.transform(imgs)
|
|
61
|
+
return imgs
|
|
62
|
+
|
|
63
|
+
def _read(self, fname: str) -> npt.NDArray[ScalarType]:
|
|
64
|
+
return read_imgs(fname, dtype=self.dtype).get_full() # type: ignore
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def scan(root: str, *, pattern: str | None = None) -> list[str]:
|
|
68
|
+
if not os.path.isdir(root):
|
|
69
|
+
raise NotADirectoryError(f"not a directory: {root}")
|
|
70
|
+
|
|
71
|
+
is_valid = re.compile(pattern).match if pattern is not None else truthly
|
|
72
|
+
|
|
73
|
+
fs = []
|
|
74
|
+
for d, _, files in os.walk(root):
|
|
75
|
+
fs.extend(os.path.join(d, f) for f in files if is_valid(f))
|
|
76
|
+
|
|
77
|
+
return fs
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
@deprecated("Use `~swcgeom.images.io.read_imgs(fname).get_full()` instead")
|
|
81
|
+
def read_imgs(fname: str) -> npt.NDArray[np.float32]:
|
|
82
|
+
"""Read images.
|
|
83
|
+
|
|
84
|
+
.. deprecated:: 0.16.0
|
|
85
|
+
Use :meth:`~swcgeom.images.io.read_imgs(fname).get_full()` instead.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
return read_imgs(fname).get_full()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class Statistics:
|
|
93
|
+
count: int = 0
|
|
94
|
+
minimum: float = math.nan
|
|
95
|
+
maximum: float = math.nan
|
|
96
|
+
mean: float = 0
|
|
97
|
+
variance: float = 0
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ImageStackFolder(ImageStackFolderBase[ScalarType, T]):
|
|
101
|
+
"""Image stack folder."""
|
|
102
|
+
|
|
103
|
+
def __getitem__(self, idx: int, /) -> T:
|
|
104
|
+
return self._get(self.files[idx])
|
|
105
|
+
|
|
106
|
+
def stat(self, *, transform: bool = False, verbose: bool = False) -> Statistics:
|
|
107
|
+
"""Statistics of folder.
|
|
108
|
+
|
|
109
|
+
NOTE: We are asserting that the images are of the same shape.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
transform: Apply transform to the images.
|
|
113
|
+
If True, you need to make sure the transformed data is a ndarray.
|
|
114
|
+
verbose: Show progress bar.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
vmin, vmax = math.inf, -math.inf
|
|
118
|
+
n, mean, M2 = 0, None, None
|
|
119
|
+
|
|
120
|
+
for idx in tqdm(range(len(self))) if verbose else range(len(self)):
|
|
121
|
+
imgs = self[idx] if transform else self._read(self.files[idx])
|
|
122
|
+
|
|
123
|
+
vmin = min(vmin, np.min(imgs)) # type: ignore
|
|
124
|
+
vmax = max(vmax, np.max(imgs)) # type: ignore
|
|
125
|
+
# Welford algorithm to calculate mean and variance
|
|
126
|
+
if mean is None:
|
|
127
|
+
mean = np.zeros_like(imgs)
|
|
128
|
+
M2 = np.zeros_like(imgs)
|
|
129
|
+
|
|
130
|
+
n += 1
|
|
131
|
+
delta = imgs - mean
|
|
132
|
+
mean += delta / n
|
|
133
|
+
delta2 = imgs - mean
|
|
134
|
+
M2 += delta * delta2
|
|
135
|
+
|
|
136
|
+
if mean is None or M2 is None: # n = 0
|
|
137
|
+
raise ValueError("empty folder")
|
|
138
|
+
|
|
139
|
+
variance = M2 / (n - 1) if n > 1 else np.zeros_like(mean)
|
|
140
|
+
return Statistics(
|
|
141
|
+
count=len(self),
|
|
142
|
+
maximum=vmax,
|
|
143
|
+
minimum=vmin,
|
|
144
|
+
mean=np.mean(mean).item(),
|
|
145
|
+
variance=np.mean(variance).item(),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def from_dir(cls, root: str, *, pattern: str | None = None, **kwargs) -> Self:
|
|
150
|
+
"""
|
|
151
|
+
Args:
|
|
152
|
+
root: str
|
|
153
|
+
pattern: Filter files by pattern.
|
|
154
|
+
**kwargs: Pass to `cls.__init__`
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
return cls(cls.scan(root, pattern=pattern), **kwargs)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class LabeledImageStackFolder(ImageStackFolderBase[ScalarType, T]):
|
|
161
|
+
"""Image stack folder with label."""
|
|
162
|
+
|
|
163
|
+
labels: list[int]
|
|
164
|
+
|
|
165
|
+
def __init__(self, files: Iterable[str], labels: Iterable[int], **kwargs):
|
|
166
|
+
super().__init__(files, **kwargs)
|
|
167
|
+
self.labels = list(labels)
|
|
168
|
+
|
|
169
|
+
def __getitem__(self, idx: int) -> tuple[T, int]:
|
|
170
|
+
return self._get(self.files[idx]), self.labels[idx]
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def from_dir(
|
|
174
|
+
cls,
|
|
175
|
+
root: str,
|
|
176
|
+
label: int | Callable[[str], int],
|
|
177
|
+
*,
|
|
178
|
+
pattern: str | None = None,
|
|
179
|
+
**kwargs,
|
|
180
|
+
) -> Self:
|
|
181
|
+
files = cls.scan(root, pattern=pattern)
|
|
182
|
+
if callable(label):
|
|
183
|
+
labels = [label(f) for f in files]
|
|
184
|
+
elif isinstance(label, int):
|
|
185
|
+
labels = [label for _ in files]
|
|
186
|
+
else:
|
|
187
|
+
raise ValueError("invalid label")
|
|
188
|
+
return cls(files, labels, **kwargs)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class PathImageStackFolder(ImageStackFolderBase[ScalarType, T]):
|
|
192
|
+
"""Image stack folder with relpath."""
|
|
193
|
+
|
|
194
|
+
root: str
|
|
195
|
+
|
|
196
|
+
def __init__(self, files: Iterable[str], *, root: str, **kwargs):
|
|
197
|
+
super().__init__(files, **kwargs)
|
|
198
|
+
self.root = root
|
|
199
|
+
|
|
200
|
+
def __getitem__(self, idx: int) -> tuple[T, str]:
|
|
201
|
+
relpath = os.path.relpath(self.files[idx], self.root)
|
|
202
|
+
return self._get(self.files[idx]), relpath
|
|
203
|
+
|
|
204
|
+
@classmethod
|
|
205
|
+
def from_dir(cls, root: str, *, pattern: str | None = None, **kwargs) -> Self:
|
|
206
|
+
"""
|
|
207
|
+
Args:
|
|
208
|
+
root: str
|
|
209
|
+
pattern: Filter files by pattern.
|
|
210
|
+
**kwargs: Pass to `cls.__init__`
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
return cls(cls.scan(root, pattern=pattern), root=root, **kwargs)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def truthly(*args, **kwargs) -> Literal[True]: # pylint: disable=unused-argument
|
|
217
|
+
return True
|