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.

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-313-x86_64-linux-gnu.so +0 -0
  36. swcgeom/images/loaders/pbd.pyx +523 -0
  37. swcgeom/images/loaders/raw.cpython-313-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,270 @@
1
+
2
+ # SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """SWC geometry operations."""
7
+
8
+ import warnings
9
+ from typing import Generic, Literal, TypeVar
10
+
11
+ import numpy as np
12
+ import numpy.typing as npt
13
+ from typing_extensions import override
14
+
15
+ from swcgeom.core import DictSWC
16
+ from swcgeom.core.swc_utils import SWCNames
17
+ from swcgeom.transforms.base import Transform
18
+ from swcgeom.utils import (
19
+ rotate3d,
20
+ rotate3d_x,
21
+ rotate3d_y,
22
+ rotate3d_z,
23
+ scale3d,
24
+ translate3d,
25
+ )
26
+
27
+ __all__ = [
28
+ "Normalizer",
29
+ "RadiusReseter",
30
+ "AffineTransform",
31
+ "Translate",
32
+ "TranslateOrigin",
33
+ "Scale",
34
+ "Rotate",
35
+ "RotateX",
36
+ "RotateY",
37
+ "RotateZ",
38
+ ]
39
+
40
+ T = TypeVar("T", bound=DictSWC)
41
+ Center = Literal["root", "soma", "origin"]
42
+
43
+
44
+ # pylint: disable=too-few-public-methods
45
+ class Normalizer(Generic[T], Transform[T, T]):
46
+ """Noramlize coordinates and radius to 0-1."""
47
+
48
+ def __init__(self, *, names: SWCNames | None = None) -> None:
49
+ super().__init__()
50
+ if names is not None:
51
+ warnings.warn(
52
+ "`name` parameter is no longer needed, now use the "
53
+ "built-in names table, you can directly remove it.",
54
+ DeprecationWarning,
55
+ )
56
+
57
+ @override
58
+ def __call__(self, x: T) -> T:
59
+ """Scale the `x`, `y`, `z`, `r` of nodes to 0-1."""
60
+ new_tree = x.copy()
61
+ xyzr = [x.names.x, x.names.y, x.names.z, x.names.r]
62
+ for key in xyzr: # TODO: does r is the same?
63
+ vs = new_tree.ndata[key]
64
+ new_tree.ndata[key] = (vs - np.min(vs)) / np.max(vs)
65
+
66
+ return new_tree
67
+
68
+
69
+ class RadiusReseter(Generic[T], Transform[T, T]):
70
+ """Reset radius to fixed value."""
71
+
72
+ def __init__(self, r: float) -> None:
73
+ super().__init__()
74
+ self.r = r
75
+
76
+ def __call__(self, x: T) -> T:
77
+ r = np.full_like(x.r(), fill_value=self.r)
78
+ new_tree = x.copy()
79
+ new_tree.ndata[new_tree.names.r] = r
80
+ return new_tree
81
+
82
+ @override
83
+ def extra_repr(self) -> str:
84
+ return f"r={self.r:.4f}"
85
+
86
+
87
+ class AffineTransform(Generic[T], Transform[T, T]):
88
+ """Apply affine matrix."""
89
+
90
+ tm: npt.NDArray[np.float32]
91
+ center: Center
92
+ fmt: str
93
+
94
+ def __init__(
95
+ self,
96
+ tm: npt.NDArray[np.float32],
97
+ center: Center = "origin",
98
+ *,
99
+ fmt: str | None = None,
100
+ names: SWCNames | None = None,
101
+ ) -> None:
102
+ self.tm, self.center = tm, center
103
+
104
+ if fmt is not None:
105
+ warnings.warn(
106
+ "`fmt` parameter is no longer needed, now use the "
107
+ "extra_repr(), you can directly remove it.",
108
+ DeprecationWarning,
109
+ )
110
+
111
+ if names is not None:
112
+ warnings.warn(
113
+ "`name` parameter is no longer needed, now use the "
114
+ "built-in names table, you can directly remove it.",
115
+ DeprecationWarning,
116
+ )
117
+
118
+ @override
119
+ def __call__(self, x: T) -> T:
120
+ match self.center:
121
+ case "root" | "soma":
122
+ idx = np.nonzero(x.ndata[x.names.pid] == -1)[0][0].item()
123
+ xyz = x.xyz()[idx]
124
+ tm = (
125
+ translate3d(-xyz[0], -xyz[1], -xyz[2])
126
+ .dot(self.tm)
127
+ .dot(translate3d(xyz[0], xyz[1], xyz[2]))
128
+ )
129
+ case _:
130
+ tm = self.tm
131
+
132
+ return self.apply(x, tm)
133
+
134
+ @staticmethod
135
+ def apply(x: T, tm: npt.NDArray[np.float32]) -> T:
136
+ xyzw = x.xyzw().dot(tm.T).T
137
+ xyzw /= xyzw[3]
138
+
139
+ y = x.copy()
140
+ y.ndata[x.names.x] = xyzw[0]
141
+ y.ndata[x.names.y] = xyzw[1]
142
+ y.ndata[x.names.z] = xyzw[2]
143
+ return y
144
+
145
+
146
+ class Translate(Generic[T], AffineTransform[T]):
147
+ """Translate SWC."""
148
+
149
+ def __init__(self, tx: float, ty: float, tz: float, **kwargs) -> None:
150
+ super().__init__(translate3d(tx, ty, tz), **kwargs)
151
+ self.tx, self.ty, self.tz = tx, ty, tz
152
+
153
+ @override
154
+ def extra_repr(self) -> str:
155
+ return f"tx={self.tx:.4f}, ty={self.ty:.4f}, tz={self.tz:.4f}"
156
+
157
+ @classmethod
158
+ def transform(cls, x: T, tx: float, ty: float, tz: float, **kwargs) -> T:
159
+ return cls(tx, ty, tz, **kwargs)(x)
160
+
161
+
162
+ class TranslateOrigin(Generic[T], Transform[T, T]):
163
+ """Translate root of SWC to origin point."""
164
+
165
+ def __call__(self, x: T) -> T:
166
+ return self.transform(x)
167
+
168
+ @classmethod
169
+ def transform(cls, x: T) -> T:
170
+ pid = np.nonzero(x.ndata[x.names.pid] == -1)[0][0].item()
171
+ xyzw = x.xyzw()
172
+ tm = translate3d(-xyzw[pid, 0], -xyzw[pid, 1], -xyzw[pid, 2])
173
+ return AffineTransform.apply(x, tm)
174
+
175
+
176
+ class Scale(Generic[T], AffineTransform[T]):
177
+ """Scale SWC."""
178
+
179
+ def __init__(
180
+ self, sx: float, sy: float, sz: float, center: Center = "root", **kwargs
181
+ ) -> None:
182
+ super().__init__(scale3d(sx, sy, sz), center=center, **kwargs)
183
+
184
+ @classmethod
185
+ def transform( # pylint: disable=too-many-arguments
186
+ cls, x: T, sx: float, sy: float, sz: float, center: Center = "root", **kwargs
187
+ ) -> T:
188
+ return cls(sx, sy, sz, center=center, **kwargs)(x)
189
+
190
+
191
+ class Rotate(Generic[T], AffineTransform[T]):
192
+ """Rotate SWC."""
193
+
194
+ def __init__(
195
+ self,
196
+ n: npt.NDArray[np.float32],
197
+ theta: float,
198
+ center: Center = "root",
199
+ **kwargs,
200
+ ) -> None:
201
+ fmt = f"Rotate-{n[0]}-{n[1]}-{n[2]}-{theta:.4f}"
202
+ super().__init__(rotate3d(n, theta), center=center, fmt=fmt, **kwargs)
203
+ self.n = n
204
+ self.theta = theta
205
+ self.center = center
206
+
207
+ @override
208
+ def extra_repr(self) -> str:
209
+ return f"n={self.n}, theta={self.theta:.4f}, center={self.center}" # TODO: improve format of n
210
+
211
+ @classmethod
212
+ def transform(
213
+ cls,
214
+ x: T,
215
+ n: npt.NDArray[np.float32],
216
+ theta: float,
217
+ center: Center = "root",
218
+ **kwargs,
219
+ ) -> T:
220
+ return cls(n, theta, center=center, **kwargs)(x)
221
+
222
+
223
+ class RotateX(Generic[T], AffineTransform[T]):
224
+ """Rotate SWC with x-axis."""
225
+
226
+ def __init__(self, theta: float, center: Center = "root", **kwargs) -> None:
227
+ super().__init__(rotate3d_x(theta), center=center, **kwargs)
228
+ self.theta = theta
229
+
230
+ @override
231
+ def extra_repr(self) -> str:
232
+ return f"center={self.center}, theta={self.theta:.4f}"
233
+
234
+ @classmethod
235
+ def transform(cls, x: T, theta: float, center: Center = "root", **kwargs) -> T:
236
+ return cls(theta, center=center, **kwargs)(x)
237
+
238
+
239
+ class RotateY(Generic[T], AffineTransform[T]):
240
+ """Rotate SWC with y-axis."""
241
+
242
+ def __init__(self, theta: float, center: Center = "root", **kwargs) -> None:
243
+ super().__init__(rotate3d_y(theta), center=center, **kwargs)
244
+ self.theta = theta
245
+ self.center = center
246
+
247
+ @override
248
+ def extra_repr(self) -> str:
249
+ return f"theta={self.theta:.4f}, center={self.center}"
250
+
251
+ @classmethod
252
+ def transform(cls, x: T, theta: float, center: Center = "root", **kwargs) -> T:
253
+ return cls(theta, center=center, **kwargs)(x)
254
+
255
+
256
+ class RotateZ(Generic[T], AffineTransform[T]):
257
+ """Rotate SWC with z-axis."""
258
+
259
+ def __init__(self, theta: float, center: Center = "root", **kwargs) -> None:
260
+ super().__init__(rotate3d_z(theta), center=center, **kwargs)
261
+ self.theta = theta
262
+ self.center = center
263
+
264
+ @override
265
+ def extra_repr(self) -> str:
266
+ return f"theta={self.theta:.4f}, center={self.center}"
267
+
268
+ @classmethod
269
+ def transform(cls, x: T, theta: float, center: Center = "root", **kwargs) -> T:
270
+ return cls(theta, center=center, **kwargs)(x)
@@ -0,0 +1,107 @@
1
+
2
+ # SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """Image stack pre-processing."""
7
+
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+ from scipy.fftpack import fftn, fftshift, ifftn
11
+ from scipy.ndimage import gaussian_filter, minimum_filter
12
+ from typing_extensions import override
13
+
14
+ from swcgeom.transforms.base import Transform
15
+
16
+ __all__ = ["SGuoImPreProcess"]
17
+
18
+
19
+ class SGuoImPreProcess(Transform[npt.NDArray[np.uint8], npt.NDArray[np.uint8]]):
20
+ """Single-Neuron Image Enhancement.
21
+
22
+ Implementation of the image enhancement method described in the paper:
23
+
24
+ Shuxia Guo, Xuan Zhao, Shengdian Jiang, Liya Ding, Hanchuan Peng,
25
+ Image enhancement to leverage the 3D morphological reconstruction
26
+ of single-cell neurons, Bioinformatics, Volume 38, Issue 2,
27
+ January 2022, Pages 503–512, https://doi.org/10.1093/bioinformatics/btab638
28
+ """
29
+
30
+ @override
31
+ def __call__(self, x: npt.NDArray[np.uint8]) -> npt.NDArray[np.uint8]:
32
+ # TODO: support np.float32
33
+ assert x.dtype == np.uint8, "Image must be in uint8 format"
34
+ x = self.sigmoid_adjustment(x)
35
+ x = self.subtract_min_along_z(x)
36
+ x = self.bilateral_filter_3d(x)
37
+ x = self.high_pass_fft(x)
38
+ return x
39
+
40
+ @staticmethod
41
+ def sigmoid_adjustment(
42
+ image: npt.NDArray[np.uint8], sigma: float = 3, percentile: float = 25
43
+ ) -> npt.NDArray[np.uint8]:
44
+ image_normalized = image / 255.0
45
+ u = np.percentile(image_normalized, percentile)
46
+ adjusted = 1 / (1 + np.exp(-sigma * (image_normalized - u)))
47
+ adjusted_rescaled = (adjusted * 255).astype(np.uint8)
48
+ return adjusted_rescaled
49
+
50
+ @staticmethod
51
+ def subtract_min_along_z(image: npt.NDArray[np.uint8]) -> npt.NDArray[np.uint8]:
52
+ min_along_z = minimum_filter(
53
+ image,
54
+ size=(1, 1, image.shape[2], 1),
55
+ mode="constant",
56
+ cval=np.max(image).item(),
57
+ )
58
+ subtracted = image - min_along_z
59
+ return subtracted
60
+
61
+ @staticmethod
62
+ def bilateral_filter_3d(
63
+ image: npt.NDArray[np.uint8], spatial_sigma=(1, 1, 0.33), range_sigma=35
64
+ ) -> npt.NDArray[np.uint8]:
65
+ # initialize the output image
66
+ filtered_image = np.zeros_like(image)
67
+
68
+ spatial_gaussian = gaussian_filter(image, spatial_sigma)
69
+
70
+ # traverse each pixel to perform bilateral filtering
71
+ # TODO: optimization is needed
72
+ for z in range(image.shape[2]):
73
+ for y in range(image.shape[1]):
74
+ for x in range(image.shape[0]):
75
+ value = image[x, y, z]
76
+ range_weight = np.exp(
77
+ -((image - value) ** 2) / (2 * range_sigma**2)
78
+ )
79
+ weights = spatial_gaussian * range_weight
80
+ filtered_image[x, y, z] = np.sum(image * weights) / np.sum(weights)
81
+
82
+ return filtered_image
83
+
84
+ @staticmethod
85
+ def high_pass_fft(image: npt.NDArray[np.uint8]) -> npt.NDArray[np.uint8]:
86
+ # fft
87
+ fft_image = fftn(image)
88
+ fft_shifted = fftshift(fft_image)
89
+
90
+ # create a high-pass filter
91
+ h, w, d, _ = image.shape
92
+ x, y, z = np.ogrid[:h, :w, :d]
93
+ center = (h / 2, w / 2, d / 2)
94
+ distance = np.sqrt(
95
+ (x - center[0]) ** 2 + (y - center[1]) ** 2 + (z - center[2]) ** 2
96
+ )
97
+ # adjust this threshold to control the filtering strength
98
+ high_pass_mask = distance > (d // 4)
99
+ # apply the high-pass filter
100
+ fft_shifted *= high_pass_mask
101
+
102
+ # inverse fft
103
+ fft_unshifted = np.fft.ifftshift(fft_shifted)
104
+ filtered_image = np.real(ifftn(fft_unshifted))
105
+
106
+ filtered_rescaled = np.clip(filtered_image, 0, 255).astype(np.uint8)
107
+ return filtered_rescaled
@@ -0,0 +1,219 @@
1
+
2
+ # SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """Create image stack from morphology.
7
+
8
+ NOTE: All denpendencies need to be installed, try:
9
+
10
+ ```sh
11
+ pip install swcgeom[all]
12
+ ```
13
+ """
14
+
15
+ import os
16
+ import re
17
+ import time
18
+ from collections.abc import Iterable
19
+ from typing import Sequence
20
+
21
+ import numpy as np
22
+ import numpy.typing as npt
23
+ import tifffile
24
+ from sdflit import (
25
+ ColoredMaterial,
26
+ ObjectsScene,
27
+ RangeSampler,
28
+ RoundCone,
29
+ Scene,
30
+ SDFObject,
31
+ )
32
+ from tqdm import tqdm
33
+ from typing_extensions import deprecated, override
34
+
35
+ from swcgeom.core import Population, Tree
36
+ from swcgeom.transforms.base import Transform
37
+
38
+ __all__ = ["ToImageStack"]
39
+
40
+
41
+ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
42
+ r"""Transform tree to image stack."""
43
+
44
+ resolution: npt.NDArray[np.float32]
45
+
46
+ def __init__(self, resolution: int | float | npt.ArrayLike = 1) -> None:
47
+ """Transform tree to image stack.
48
+
49
+ Args:
50
+ resolution: Resolution of image stack.
51
+ If a scalar, it will be broadcasted to a vector of 3d.
52
+ """
53
+ if isinstance(resolution, (int, float, np.integer, np.floating)):
54
+ resolution = [resolution, resolution, resolution] # type: ignore
55
+
56
+ self.resolution = np.array(resolution, dtype=np.float32)
57
+ assert len(self.resolution) == 3, "resolution should be vector of 3d."
58
+
59
+ @override
60
+ def __call__(self, x: Tree) -> npt.NDArray[np.uint8]:
61
+ """Transform tree to image stack.
62
+
63
+ NOTE: This method loads the entire image stack into memory, so it ONLY works
64
+ for small image stacks, use :meth`transform_and_save` for big image stack.
65
+ """
66
+ return np.stack(list(self.transform(x, verbose=False)), axis=0)
67
+
68
+ def transform(
69
+ self,
70
+ x: Tree | Sequence[Tree],
71
+ verbose: bool = True,
72
+ *,
73
+ ranges: tuple[npt.ArrayLike, npt.ArrayLike] | None = None,
74
+ ) -> Iterable[npt.NDArray[np.uint8]]:
75
+ trees = [x] if isinstance(x, Tree) else x
76
+ if not trees:
77
+ return iter([]) # Return empty iterator if sequence is empty
78
+
79
+ time_start = None
80
+ if verbose:
81
+ sources = ", ".join(t.source for t in trees if t.source)
82
+ print(f"To image stack: {sources if sources else 'unnamed trees'}")
83
+ time_start = time.time()
84
+
85
+ scene = self._get_scene(trees)
86
+
87
+ if ranges is None:
88
+ all_xyz = np.concatenate([t.xyz() for t in trees], axis=0)
89
+ all_r = np.concatenate([t.r() for t in trees], axis=0).reshape(-1, 1)
90
+ if all_xyz.size == 0: # Handle empty trees
91
+ coord_min = np.zeros(3, dtype=np.float32)
92
+ coord_max = np.zeros(3, dtype=np.float32)
93
+ else:
94
+ coord_min = np.floor(np.min(all_xyz - all_r, axis=0))
95
+ coord_max = np.ceil(np.max(all_xyz + all_r, axis=0))
96
+ else:
97
+ assert len(ranges) == 2
98
+ coord_min = np.array(ranges[0])
99
+ coord_max = np.array(ranges[1])
100
+ assert len(coord_min) == len(coord_max) == 3
101
+
102
+ samplers = self._get_samplers(coord_min, coord_max)
103
+
104
+ if verbose and time_start is not None:
105
+ total = (coord_max[2] - coord_min[2]) / self.resolution[2]
106
+ samplers = tqdm(samplers, total=total.astype(np.int64).item())
107
+
108
+ time_end = time.time()
109
+ print("Prepare in: ", time_end - time_start, "s")
110
+
111
+ for sampler in samplers:
112
+ voxel = sampler.sample(scene) # should be shape of (x, y, z, 3) and z = 1
113
+ frame = (255 * voxel[..., 0, 0]).astype(np.uint8)
114
+ yield frame
115
+
116
+ @deprecated("Use transform instead")
117
+ def transfrom(
118
+ self,
119
+ x: Tree,
120
+ verbose: bool = True,
121
+ *,
122
+ ranges: tuple[npt.ArrayLike, npt.ArrayLike] | None = None,
123
+ ) -> Iterable[npt.NDArray[np.uint8]]:
124
+ return self.transform(x, verbose, ranges=ranges)
125
+
126
+ def transform_and_save(
127
+ self, fname: str, x: Tree | Sequence[Tree], verbose: bool = True, **kwargs
128
+ ) -> None:
129
+ self.save_tif(fname, self.transform(x, verbose=verbose, **kwargs))
130
+
131
+ def transform_population(
132
+ self, population: Population | str, verbose: bool = True
133
+ ) -> None:
134
+ trees = (
135
+ Population.from_swc(population)
136
+ if isinstance(population, str)
137
+ else population
138
+ )
139
+
140
+ if verbose:
141
+ trees = tqdm(trees)
142
+
143
+ # TODO: multiprocess
144
+ for tree in trees:
145
+ tif = re.sub(r".swc$", ".tif", tree.source)
146
+ if not os.path.isfile(tif):
147
+ self.transform_and_save(tif, tree, verbose=False)
148
+
149
+ @override
150
+ def extra_repr(self) -> str:
151
+ res = ",".join(f"{a:.4f}" for a in self.resolution)
152
+ return f"resolution=({res})"
153
+
154
+ def _get_scene(self, trees: Sequence[Tree]) -> Scene:
155
+ material = ColoredMaterial((1, 0, 0)).into()
156
+ scene = ObjectsScene()
157
+ scene.set_background((0, 0, 0))
158
+
159
+ def leave(n: Tree.Node, children: list[Tree.Node]) -> Tree.Node:
160
+ for c in children:
161
+ sdf = RoundCone(_tp3f(n.xyz()), _tp3f(c.xyz()), n.r, c.r).into()
162
+ scene.add_object(SDFObject(sdf, material).into())
163
+ return n
164
+
165
+ for tree in trees:
166
+ tree.traverse(leave=leave)
167
+
168
+ scene.build_bvh()
169
+ return scene.into()
170
+
171
+ def _get_samplers(
172
+ self,
173
+ coord_min: npt.NDArray,
174
+ coord_max: npt.NDArray,
175
+ offset: npt.NDArray | None = None,
176
+ ) -> Iterable[RangeSampler]:
177
+ """Get Samplers.
178
+
179
+ Args:
180
+ coord_min, coord_max: Coordinates array of shape (3,).
181
+ """
182
+
183
+ eps = 1e-6
184
+ stride = self.resolution
185
+ offset = offset or (stride / 2)
186
+
187
+ xmin, ymin, zmin = _tp3f(coord_min + offset)
188
+ xmax, ymax, zmax = _tp3f(coord_max)
189
+ z = zmin
190
+ while z < zmax:
191
+ yield RangeSampler(
192
+ (xmin, ymin, z), (xmax, ymax, z + stride[2] - eps), _tp3f(stride)
193
+ )
194
+ z += stride[2]
195
+
196
+ @staticmethod
197
+ def save_tif(
198
+ fname: str,
199
+ frames: Iterable[npt.NDArray[np.uint8]],
200
+ resolution: tuple[float, float] = (1, 1),
201
+ ) -> None:
202
+ with tifffile.TiffWriter(fname) as tif:
203
+ for frame in frames:
204
+ tif.write(
205
+ frame,
206
+ contiguous=True,
207
+ photometric="minisblack",
208
+ resolution=resolution,
209
+ metadata={
210
+ "unit": "um",
211
+ "axes": "ZXY",
212
+ },
213
+ )
214
+
215
+
216
+ def _tp3f(x: npt.NDArray) -> tuple[float, float, float]:
217
+ """Convert to tuple of 3 floats."""
218
+ assert len(x) == 3
219
+ return (float(x[0]), float(x[1]), float(x[2]))