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.

Files changed (72) hide show
  1. swcgeom/__init__.py +26 -1
  2. swcgeom/analysis/__init__.py +21 -8
  3. swcgeom/analysis/feature_extractor.py +43 -18
  4. swcgeom/analysis/features.py +250 -0
  5. swcgeom/analysis/lmeasure.py +857 -0
  6. swcgeom/analysis/sholl.py +55 -29
  7. swcgeom/analysis/trunk.py +27 -11
  8. swcgeom/analysis/visualization.py +24 -9
  9. swcgeom/analysis/visualization3d.py +100 -0
  10. swcgeom/analysis/volume.py +19 -4
  11. swcgeom/core/__init__.py +32 -9
  12. swcgeom/core/branch.py +28 -7
  13. swcgeom/core/branch_tree.py +18 -4
  14. swcgeom/core/{segment.py → compartment.py} +31 -10
  15. swcgeom/core/node.py +31 -10
  16. swcgeom/core/path.py +37 -10
  17. swcgeom/core/population.py +103 -34
  18. swcgeom/core/swc.py +26 -10
  19. swcgeom/core/swc_utils/__init__.py +21 -7
  20. swcgeom/core/swc_utils/assembler.py +27 -1
  21. swcgeom/core/swc_utils/base.py +25 -12
  22. swcgeom/core/swc_utils/checker.py +31 -14
  23. swcgeom/core/swc_utils/io.py +24 -7
  24. swcgeom/core/swc_utils/normalizer.py +20 -4
  25. swcgeom/core/swc_utils/subtree.py +17 -2
  26. swcgeom/core/tree.py +85 -72
  27. swcgeom/core/tree_utils.py +31 -16
  28. swcgeom/core/tree_utils_impl.py +18 -3
  29. swcgeom/images/__init__.py +17 -2
  30. swcgeom/images/augmentation.py +24 -4
  31. swcgeom/images/contrast.py +122 -0
  32. swcgeom/images/folder.py +97 -39
  33. swcgeom/images/io.py +108 -121
  34. swcgeom/transforms/__init__.py +28 -10
  35. swcgeom/transforms/base.py +17 -2
  36. swcgeom/transforms/branch.py +74 -8
  37. swcgeom/transforms/branch_tree.py +82 -0
  38. swcgeom/transforms/geometry.py +22 -7
  39. swcgeom/transforms/image_preprocess.py +115 -0
  40. swcgeom/transforms/image_stack.py +37 -13
  41. swcgeom/transforms/images.py +184 -7
  42. swcgeom/transforms/mst.py +20 -5
  43. swcgeom/transforms/neurolucida_asc.py +508 -0
  44. swcgeom/transforms/path.py +15 -0
  45. swcgeom/transforms/population.py +16 -3
  46. swcgeom/transforms/tree.py +89 -31
  47. swcgeom/transforms/tree_assembler.py +23 -7
  48. swcgeom/utils/__init__.py +27 -11
  49. swcgeom/utils/debug.py +15 -0
  50. swcgeom/utils/download.py +59 -21
  51. swcgeom/utils/dsu.py +15 -0
  52. swcgeom/utils/ellipse.py +18 -4
  53. swcgeom/utils/file.py +15 -0
  54. swcgeom/utils/neuromorpho.py +439 -302
  55. swcgeom/utils/numpy_helper.py +29 -4
  56. swcgeom/utils/plotter_2d.py +151 -0
  57. swcgeom/utils/plotter_3d.py +48 -0
  58. swcgeom/utils/renderer.py +49 -145
  59. swcgeom/utils/sdf.py +24 -8
  60. swcgeom/utils/solid_geometry.py +16 -3
  61. swcgeom/utils/transforms.py +17 -4
  62. swcgeom/utils/volumetric_object.py +23 -10
  63. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/LICENSE +1 -1
  64. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/METADATA +28 -24
  65. swcgeom-0.18.3.dist-info/RECORD +67 -0
  66. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/WHEEL +1 -1
  67. swcgeom/_version.py +0 -16
  68. swcgeom/analysis/branch_features.py +0 -67
  69. swcgeom/analysis/node_features.py +0 -121
  70. swcgeom/analysis/path_features.py +0 -37
  71. swcgeom-0.15.0.dist-info/RECORD +0 -62
  72. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/top_level.txt +0 -0
@@ -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 typing import Callable, Dict, Iterable, List, Optional, Tuple, TypeVar, overload
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], Tuple[T, bool]]) -> Tree: ...
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, List[K]], Tuple[K, bool]]) -> Tree: ...
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: List[int] = []
80
+ removals: list[int] = []
64
81
 
65
82
  if enter:
66
83
 
67
- def _enter(n: Tree.Node, parent: Tuple[T, bool] | None) -> Tuple[T, bool]:
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: List[K]) -> K:
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
- def to_sub_tree(swc_like: SWCLike, sub: Topology) -> Tuple[Tree, Dict[int, int]]:
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 : Dict[int, int]
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 Dict[int, int], optional
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 Dict[int, int], optional
184
+ out_mapping: List of int or dict[int, int], optional
170
185
  Map new id to old id.
171
186
  """
172
187
 
@@ -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, Dict, List, Optional, Tuple
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 = Dict[int, int] | List[int]
19
- TreeArgs = Tuple[int, Dict[str, npt.NDArray[Any]], str, SWCNames]
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(
@@ -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
@@ -1,7 +1,27 @@
1
- """Play augment in image stack."""
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 List, Literal, Optional
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: List[Literal["xy", "xz", "yz"]] = ["xy", "xz", "yz"]
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: List[Literal["xy", "xz", "yz"]] = ["xy", "xz", "yz"]
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 warnings
6
- from abc import ABC, abstractmethod
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 typing_extensions import Self
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], ABC):
38
+ class ImageStackFolderBase(Generic[ScalarType, T]):
36
39
  """Image stack folder base."""
37
40
 
38
- files: List[str]
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) -> List[str]:
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
- warnings.warn(
82
- "`ImageStackFolderBase.read_imgs` serves as a "
83
- "straightforward wrapper for `~swcgeom.images.io.read_imgs(fname).get_full()`. "
84
- "However, as it is not utilized within our internal "
85
- "processes, it is scheduled for removal in the "
86
- "forthcoming version.",
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: List[int]
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) -> Tuple[npt.NDArray[np.float32], int]:
122
- return self.read_imgs(self.files[idx]), self.labels[idx]
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(ImageStackFolder[ScalarType, T]):
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) -> Tuple[T, str]:
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