swcgeom 0.19.4__cp311-cp311-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.

Files changed (72) hide show
  1. swcgeom/__init__.py +21 -0
  2. swcgeom/analysis/__init__.py +13 -0
  3. swcgeom/analysis/feature_extractor.py +454 -0
  4. swcgeom/analysis/features.py +218 -0
  5. swcgeom/analysis/lmeasure.py +750 -0
  6. swcgeom/analysis/sholl.py +201 -0
  7. swcgeom/analysis/trunk.py +183 -0
  8. swcgeom/analysis/visualization.py +191 -0
  9. swcgeom/analysis/visualization3d.py +81 -0
  10. swcgeom/analysis/volume.py +143 -0
  11. swcgeom/core/__init__.py +19 -0
  12. swcgeom/core/branch.py +129 -0
  13. swcgeom/core/branch_tree.py +65 -0
  14. swcgeom/core/compartment.py +107 -0
  15. swcgeom/core/node.py +130 -0
  16. swcgeom/core/path.py +155 -0
  17. swcgeom/core/population.py +341 -0
  18. swcgeom/core/swc.py +247 -0
  19. swcgeom/core/swc_utils/__init__.py +19 -0
  20. swcgeom/core/swc_utils/assembler.py +35 -0
  21. swcgeom/core/swc_utils/base.py +180 -0
  22. swcgeom/core/swc_utils/checker.py +107 -0
  23. swcgeom/core/swc_utils/io.py +204 -0
  24. swcgeom/core/swc_utils/normalizer.py +163 -0
  25. swcgeom/core/swc_utils/subtree.py +70 -0
  26. swcgeom/core/tree.py +384 -0
  27. swcgeom/core/tree_utils.py +277 -0
  28. swcgeom/core/tree_utils_impl.py +58 -0
  29. swcgeom/images/__init__.py +9 -0
  30. swcgeom/images/augmentation.py +149 -0
  31. swcgeom/images/contrast.py +87 -0
  32. swcgeom/images/folder.py +217 -0
  33. swcgeom/images/io.py +578 -0
  34. swcgeom/images/loaders/__init__.py +8 -0
  35. swcgeom/images/loaders/pbd.cpython-311-x86_64-linux-gnu.so +0 -0
  36. swcgeom/images/loaders/pbd.pyx +523 -0
  37. swcgeom/images/loaders/raw.cpython-311-x86_64-linux-gnu.so +0 -0
  38. swcgeom/images/loaders/raw.pyx +183 -0
  39. swcgeom/transforms/__init__.py +20 -0
  40. swcgeom/transforms/base.py +136 -0
  41. swcgeom/transforms/branch.py +223 -0
  42. swcgeom/transforms/branch_tree.py +74 -0
  43. swcgeom/transforms/geometry.py +270 -0
  44. swcgeom/transforms/image_preprocess.py +107 -0
  45. swcgeom/transforms/image_stack.py +219 -0
  46. swcgeom/transforms/images.py +206 -0
  47. swcgeom/transforms/mst.py +183 -0
  48. swcgeom/transforms/neurolucida_asc.py +498 -0
  49. swcgeom/transforms/path.py +56 -0
  50. swcgeom/transforms/population.py +36 -0
  51. swcgeom/transforms/tree.py +265 -0
  52. swcgeom/transforms/tree_assembler.py +161 -0
  53. swcgeom/utils/__init__.py +18 -0
  54. swcgeom/utils/debug.py +23 -0
  55. swcgeom/utils/download.py +119 -0
  56. swcgeom/utils/dsu.py +58 -0
  57. swcgeom/utils/ellipse.py +131 -0
  58. swcgeom/utils/file.py +90 -0
  59. swcgeom/utils/neuromorpho.py +581 -0
  60. swcgeom/utils/numpy_helper.py +70 -0
  61. swcgeom/utils/plotter_2d.py +134 -0
  62. swcgeom/utils/plotter_3d.py +35 -0
  63. swcgeom/utils/renderer.py +145 -0
  64. swcgeom/utils/sdf.py +324 -0
  65. swcgeom/utils/solid_geometry.py +154 -0
  66. swcgeom/utils/transforms.py +367 -0
  67. swcgeom/utils/volumetric_object.py +483 -0
  68. swcgeom-0.19.4.dist-info/METADATA +86 -0
  69. swcgeom-0.19.4.dist-info/RECORD +72 -0
  70. swcgeom-0.19.4.dist-info/WHEEL +6 -0
  71. swcgeom-0.19.4.dist-info/licenses/LICENSE +201 -0
  72. 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,9 @@
1
+
2
+ # SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """Image Stack Related."""
7
+
8
+ from swcgeom.images.folder import * # noqa: F403
9
+ from swcgeom.images.io import * # noqa: F403
@@ -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()
@@ -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