swcgeom 0.19.4__cp311-cp311-win_amd64.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.cp311-win_amd64.pyd +0 -0
- swcgeom/images/loaders/pbd.pyx +523 -0
- swcgeom/images/loaders/raw.cp311-win_amd64.pyd +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 +5 -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,483 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""Volumetric object.
|
|
7
|
+
|
|
8
|
+
This library implements the calculation of volumes for any shape
|
|
9
|
+
generated through boolean operations, employing Signed Distance
|
|
10
|
+
Function (SDF) and Monte Carlo algorithms.
|
|
11
|
+
|
|
12
|
+
However, this approach is computationally demanding. To address this,
|
|
13
|
+
we have specialized certain operations to accelerate the computation
|
|
14
|
+
process.
|
|
15
|
+
|
|
16
|
+
If you wish to use these methods, please review our implementation.
|
|
17
|
+
Additionally, consider specializing some subclasses that can utilize
|
|
18
|
+
formula-based calculations for further optimization of your
|
|
19
|
+
computations.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import warnings
|
|
23
|
+
from abc import ABC, abstractmethod
|
|
24
|
+
from typing import Generic, TypeVar
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
import numpy.typing as npt
|
|
28
|
+
from sdflit import SDF, FrustumCone, Sphere, intersect, merge, subtract
|
|
29
|
+
|
|
30
|
+
from swcgeom.utils.solid_geometry import (
|
|
31
|
+
find_sphere_line_intersection,
|
|
32
|
+
find_unit_vector_on_plane,
|
|
33
|
+
project_point_on_line,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = ["VolObject", "VolMCObject", "VolSDFObject", "VolSphere", "VolFrustumCone"]
|
|
37
|
+
|
|
38
|
+
eps = 1e-6
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class VolObject(ABC):
|
|
42
|
+
"""Volumetric object."""
|
|
43
|
+
|
|
44
|
+
volume = None
|
|
45
|
+
|
|
46
|
+
def get_volume(self, **kwargs) -> float:
|
|
47
|
+
"""Get volume."""
|
|
48
|
+
if len(kwargs) != 0:
|
|
49
|
+
# not cached
|
|
50
|
+
return self._get_volume(**kwargs)
|
|
51
|
+
|
|
52
|
+
if self.volume is None:
|
|
53
|
+
self.volume = self._get_volume()
|
|
54
|
+
return self.volume
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def _get_volume(self) -> float:
|
|
58
|
+
"""Get volume."""
|
|
59
|
+
raise NotImplementedError()
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def union(self, obj: "VolObject") -> "VolObject":
|
|
63
|
+
"""Union with another volume object."""
|
|
64
|
+
classname = obj.__class__.__name__
|
|
65
|
+
raise NotImplementedError(f"unable to union with {classname}")
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def intersect(self, obj: "VolObject") -> "VolObject":
|
|
69
|
+
"""Intersect with another volume object."""
|
|
70
|
+
classname = obj.__class__.__name__
|
|
71
|
+
raise NotImplementedError(f"unable to intersect with {classname}")
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def subtract(self, obj: "VolObject") -> "VolObject":
|
|
75
|
+
"""Subtract another volume object."""
|
|
76
|
+
classname = obj.__class__.__name__
|
|
77
|
+
raise NotImplementedError(f"unable to diff with {classname}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class VolMCObject(VolObject, ABC):
|
|
81
|
+
"""Volumetric Monte Carlo Object.
|
|
82
|
+
|
|
83
|
+
The volume of the object is calculated by Monte Carlo integration.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
n_samples: int | None = None
|
|
87
|
+
|
|
88
|
+
cache_volume: float | None = None
|
|
89
|
+
cache_volume_n_samples: int = 0
|
|
90
|
+
|
|
91
|
+
def __init__(self, *, n_samples: int | None = None) -> None:
|
|
92
|
+
super().__init__()
|
|
93
|
+
if n_samples is not None:
|
|
94
|
+
warnings.warn(
|
|
95
|
+
"`VolMCObject(n_samples=...)` has been move to since "
|
|
96
|
+
"v0.14.0 and will be removed in next version, use "
|
|
97
|
+
"`VolMCObject().get_volume(n_samples=...)` instead",
|
|
98
|
+
DeprecationWarning,
|
|
99
|
+
)
|
|
100
|
+
self.n_samples = n_samples
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
def sample(self, n: int) -> tuple[npt.NDArray[np.float32], float]:
|
|
104
|
+
"""Sample points.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
n: Number of points to sample.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
points: Sampled points.
|
|
111
|
+
volume: Volume of the sample range.
|
|
112
|
+
"""
|
|
113
|
+
raise NotImplementedError()
|
|
114
|
+
|
|
115
|
+
@abstractmethod
|
|
116
|
+
def inside(self, p: npt.NDArray[np.float32]) -> bool:
|
|
117
|
+
"""Is p in the object."""
|
|
118
|
+
raise NotImplementedError()
|
|
119
|
+
|
|
120
|
+
def is_in(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.bool_]:
|
|
121
|
+
"""Is p in the object.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
is_in: Array of shape (N,).
|
|
125
|
+
If bounding box is `None`, `True` will be returned.
|
|
126
|
+
"""
|
|
127
|
+
return np.array([self.inside(pp) for pp in p])
|
|
128
|
+
|
|
129
|
+
def _get_volume(self, *, n_samples: int | None = None) -> float:
|
|
130
|
+
"""Get volume by Monte Carlo integration.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
n_samples: Number of samples, default 1_000_000
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
# legacy
|
|
137
|
+
DEFAULT_N_SAMPLES = 1_000_000
|
|
138
|
+
if n_samples is None:
|
|
139
|
+
n_samples = self.n_samples or DEFAULT_N_SAMPLES
|
|
140
|
+
|
|
141
|
+
# cache volume
|
|
142
|
+
if self.cache_volume is not None and n_samples <= self.cache_volume_n_samples:
|
|
143
|
+
return self.cache_volume
|
|
144
|
+
|
|
145
|
+
p, v = self.sample(n_samples)
|
|
146
|
+
hits = sum(self.inside(pp) for pp in p)
|
|
147
|
+
volume = hits / n_samples * v
|
|
148
|
+
|
|
149
|
+
# update cache
|
|
150
|
+
self.cache_volume = volume
|
|
151
|
+
self.cache_volume_n_samples = n_samples
|
|
152
|
+
|
|
153
|
+
return volume
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# Volumetric SDF Objects
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class VolSDFObject(VolMCObject):
|
|
160
|
+
"""Volumetric SDF Object.
|
|
161
|
+
|
|
162
|
+
NOTE: SDF must has a bounding box.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def __init__(self, sdf: SDF, **kwargs) -> None:
|
|
166
|
+
super().__init__(**kwargs)
|
|
167
|
+
self.sdf = sdf
|
|
168
|
+
|
|
169
|
+
def sample(self, n: int) -> tuple[npt.NDArray[np.float32], float]:
|
|
170
|
+
(min_x, min_y, min_z), (max_x, max_y, max_z) = self.sdf.bounding_box()
|
|
171
|
+
samples = np.random.uniform(
|
|
172
|
+
(min_x, min_y, min_z), (max_x, max_y, max_z), size=(n, 3)
|
|
173
|
+
).astype(np.float32)
|
|
174
|
+
v = (max_x - min_x) * (max_y - min_y) * (max_z - min_z)
|
|
175
|
+
return samples, v
|
|
176
|
+
|
|
177
|
+
def inside(self, p: npt.NDArray[np.float32]) -> bool:
|
|
178
|
+
return self.sdf.inside(_tp3f(p))
|
|
179
|
+
|
|
180
|
+
def union(self, obj: VolObject) -> VolObject:
|
|
181
|
+
if isinstance(obj, VolSDFObject):
|
|
182
|
+
return VolSDFUnion(self, obj)
|
|
183
|
+
raise NotImplementedError()
|
|
184
|
+
|
|
185
|
+
def intersect(self, obj: VolObject) -> VolObject:
|
|
186
|
+
if isinstance(obj, VolSDFObject):
|
|
187
|
+
return VolSDFIntersection(self, obj)
|
|
188
|
+
raise NotImplementedError()
|
|
189
|
+
|
|
190
|
+
def subtract(self, obj: VolObject) -> VolObject:
|
|
191
|
+
if isinstance(obj, VolSDFObject):
|
|
192
|
+
return VolSDFDifference(self, obj)
|
|
193
|
+
raise NotImplementedError()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
T = TypeVar("T", bound=VolSDFObject)
|
|
197
|
+
K = TypeVar("K", bound=VolSDFObject)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class VolSDFIntersection(VolSDFObject, ABC, Generic[T, K]):
|
|
201
|
+
"""Intersection of two volumetric sdf objects."""
|
|
202
|
+
|
|
203
|
+
def __init__(self, obj1: T, obj2: K, **kwargs) -> None:
|
|
204
|
+
obj = intersect(obj1.sdf, obj2.sdf)
|
|
205
|
+
super().__init__(obj, **kwargs)
|
|
206
|
+
self.obj1 = obj1
|
|
207
|
+
self.obj2 = obj2
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class VolSDFUnion(VolSDFObject, ABC, Generic[T, K]):
|
|
211
|
+
"""Union of two volumetric sdf objects."""
|
|
212
|
+
|
|
213
|
+
def __init__(self, obj1: T, obj2: K, **kwargs) -> None:
|
|
214
|
+
obj = merge(obj1.sdf, obj2.sdf)
|
|
215
|
+
super().__init__(obj, **kwargs)
|
|
216
|
+
self.obj1 = obj1
|
|
217
|
+
self.obj2 = obj2
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class VolSDFDifference(VolSDFObject, ABC, Generic[T, K]):
|
|
221
|
+
"""Difference of volumetric sdf object and another object."""
|
|
222
|
+
|
|
223
|
+
def __init__(self, obj1: T, obj2: K, **kwargs) -> None:
|
|
224
|
+
obj = subtract(obj1.sdf, obj2.sdf)
|
|
225
|
+
super().__init__(obj, **kwargs)
|
|
226
|
+
self.obj1 = obj1
|
|
227
|
+
self.obj2 = obj2
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# Primitive Volumetric Objects
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class VolSphere(VolSDFObject):
|
|
234
|
+
"""Volumetric Sphere."""
|
|
235
|
+
|
|
236
|
+
def __init__(self, center: npt.ArrayLike, radius: float):
|
|
237
|
+
center = np.array(center)
|
|
238
|
+
sdf = Sphere(_tp3f(center), radius)
|
|
239
|
+
super().__init__(sdf.into())
|
|
240
|
+
|
|
241
|
+
self.center = center
|
|
242
|
+
self.radius = radius
|
|
243
|
+
|
|
244
|
+
def _get_volume(self) -> float:
|
|
245
|
+
return self.calc_volume(self.radius)
|
|
246
|
+
|
|
247
|
+
def get_volume_spherical_cap(self, h: float) -> float:
|
|
248
|
+
return self.calc_volume_spherical_cap(self.radius, h)
|
|
249
|
+
|
|
250
|
+
def union(self, obj: VolObject) -> VolObject:
|
|
251
|
+
if isinstance(obj, VolSphere):
|
|
252
|
+
return VolSphere2Union(self, obj)
|
|
253
|
+
|
|
254
|
+
if isinstance(obj, VolFrustumCone):
|
|
255
|
+
return VolSphereFrustumConeUnion(self, obj)
|
|
256
|
+
|
|
257
|
+
return super().union(obj)
|
|
258
|
+
|
|
259
|
+
def intersect(self, obj: VolObject) -> VolObject:
|
|
260
|
+
if isinstance(obj, VolSphere):
|
|
261
|
+
return VolSphere2Intersection(self, obj)
|
|
262
|
+
|
|
263
|
+
if isinstance(obj, VolFrustumCone):
|
|
264
|
+
return VolSphereFrustumConeIntersection(self, obj)
|
|
265
|
+
|
|
266
|
+
return super().intersect(obj)
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def calc_volume(radius: float) -> float:
|
|
270
|
+
r"""Calculate volume of sphere.
|
|
271
|
+
|
|
272
|
+
\being{equation}
|
|
273
|
+
V = \frac{4}{3} * π * r^3
|
|
274
|
+
\end{equation}
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
volume: volume of sphere.
|
|
278
|
+
"""
|
|
279
|
+
return 4 / 3 * np.pi * radius**3
|
|
280
|
+
|
|
281
|
+
@staticmethod
|
|
282
|
+
def calc_volume_spherical_cap(r: float, h: float) -> float:
|
|
283
|
+
r"""Calculate the volume of a spherical cap.
|
|
284
|
+
|
|
285
|
+
\being{equation}
|
|
286
|
+
V = π * h^2 * (3r - h) / 3
|
|
287
|
+
\end{equation}
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
r: radius of the sphere
|
|
291
|
+
h: height of the spherical cap
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
volume: volume of the spherical cap
|
|
295
|
+
"""
|
|
296
|
+
return np.pi * h**2 * (3 * r - h) / 3
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class VolFrustumCone(VolSDFObject):
|
|
300
|
+
"""Volumetric Frustum."""
|
|
301
|
+
|
|
302
|
+
def __init__(self, c1: npt.ArrayLike, r1: float, c2: npt.ArrayLike, r2: float):
|
|
303
|
+
c1, c2 = np.array(c1), np.array(c2)
|
|
304
|
+
sdf = FrustumCone(_tp3f(c1), _tp3f(c2), r1, r2)
|
|
305
|
+
super().__init__(sdf.into())
|
|
306
|
+
|
|
307
|
+
self.c1 = c1
|
|
308
|
+
self.c2 = c2
|
|
309
|
+
self.r1 = r1
|
|
310
|
+
self.r2 = r2
|
|
311
|
+
|
|
312
|
+
def height(self) -> float:
|
|
313
|
+
"""Get height of frustum."""
|
|
314
|
+
return np.linalg.norm(self.c1 - self.c2).item()
|
|
315
|
+
|
|
316
|
+
def _get_volume(self) -> float:
|
|
317
|
+
return self.calc_volume(self.r1, self.r2, self.height())
|
|
318
|
+
|
|
319
|
+
def union(self, obj: VolObject) -> VolObject:
|
|
320
|
+
if isinstance(obj, VolSphere):
|
|
321
|
+
return VolSphereFrustumConeUnion(obj, self)
|
|
322
|
+
|
|
323
|
+
return super().union(obj)
|
|
324
|
+
|
|
325
|
+
def intersect(self, obj: VolObject) -> VolObject:
|
|
326
|
+
return super().intersect(obj)
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def calc_volume(r1: float, r2: float, height: float) -> float:
|
|
330
|
+
r"""Calculate volume of frustum.
|
|
331
|
+
|
|
332
|
+
\being{equation}
|
|
333
|
+
V = \frac{1}{3} * π * h * (r^2 + r * R + R^2)
|
|
334
|
+
\end{equation}
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
volume: volume of frustum.
|
|
338
|
+
"""
|
|
339
|
+
return (1 / 3) * np.pi * height * (r1**2 + r1 * r2 + r2**2)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# Composite sphere and sphere
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class VolSphere2Intersection(VolSDFIntersection[VolSphere, VolSphere]):
|
|
346
|
+
"""Intersection of two spheres."""
|
|
347
|
+
|
|
348
|
+
def _get_volume(self) -> float:
|
|
349
|
+
return self.calc_intersect_volume(self.obj1, self.obj2)
|
|
350
|
+
|
|
351
|
+
@staticmethod
|
|
352
|
+
def calc_intersect_volume(obj1: VolSphere, obj2: VolSphere) -> float:
|
|
353
|
+
r"""Calculate intersect volume of two spheres.
|
|
354
|
+
|
|
355
|
+
\being{equation}
|
|
356
|
+
V = \frac{\pi}{12d} * (r_1 + r_2 - d)^2 (d^2 + 2d r_1 - 3r_1^2 + 2d r_2 - 3r_2^2 + 6 r_1r_2)
|
|
357
|
+
\end{equation}
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
volume: Intersect volume.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
r1, r2 = obj1.radius, obj2.radius
|
|
364
|
+
d = np.linalg.norm(obj1.center - obj2.center).item()
|
|
365
|
+
if d > r1 + r2:
|
|
366
|
+
return 0
|
|
367
|
+
|
|
368
|
+
if d <= abs(r1 - r2):
|
|
369
|
+
return VolSphere.calc_volume(min(r1, r2))
|
|
370
|
+
|
|
371
|
+
part1 = (np.pi / (12 * d)) * (r1 + r2 - d) ** 2
|
|
372
|
+
part2 = d**2 + 2 * d * r1 - 3 * r1**2 + 2 * d * r2 - 3 * r2**2 + 6 * r1 * r2
|
|
373
|
+
return part1 * part2
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class VolSphere2Union(VolSDFUnion[VolSphere, VolSphere]):
|
|
377
|
+
"""Union of two spheres."""
|
|
378
|
+
|
|
379
|
+
def _get_volume(self) -> float:
|
|
380
|
+
return (
|
|
381
|
+
self.obj1.get_volume()
|
|
382
|
+
+ self.obj2.get_volume()
|
|
383
|
+
- VolSphere2Intersection.calc_intersect_volume(self.obj1, self.obj2)
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# Composite sphere and frustum cone
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class VolSphereFrustumConeIntersection(VolSDFIntersection[VolSphere, VolFrustumCone]):
|
|
391
|
+
"""Intersection of sphere and frustum cone."""
|
|
392
|
+
|
|
393
|
+
def _get_volume(self) -> float:
|
|
394
|
+
if (
|
|
395
|
+
np.allclose(self.obj1.center, self.obj2.c1)
|
|
396
|
+
and np.allclose(self.obj1.radius, self.obj2.r1)
|
|
397
|
+
) or (
|
|
398
|
+
np.allclose(self.obj1.center, self.obj2.c2)
|
|
399
|
+
and np.allclose(self.obj1.radius, self.obj2.r2)
|
|
400
|
+
):
|
|
401
|
+
return self.calc_concentric_intersect_volume(self.obj1, self.obj2)
|
|
402
|
+
|
|
403
|
+
return super()._get_volume()
|
|
404
|
+
|
|
405
|
+
@staticmethod
|
|
406
|
+
def calc_concentric_intersect_volume(
|
|
407
|
+
sphere: VolSphere, frustum_cone: VolFrustumCone
|
|
408
|
+
) -> float:
|
|
409
|
+
r"""Calculate intersect volume of sphere and frustum cone.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
volume: Intersect volume.
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
h = frustum_cone.height()
|
|
416
|
+
c1, r1 = sphere.center, sphere.radius
|
|
417
|
+
if np.allclose(c1, frustum_cone.c1) and np.allclose(r1, frustum_cone.r1):
|
|
418
|
+
c2, r2 = frustum_cone.c2, frustum_cone.r2
|
|
419
|
+
elif np.allclose(c1, frustum_cone.c2) and np.allclose(r1, frustum_cone.r2):
|
|
420
|
+
c2, r2 = frustum_cone.c1, frustum_cone.r1
|
|
421
|
+
else:
|
|
422
|
+
raise ValueError("sphere and frustum cone is not concentric")
|
|
423
|
+
|
|
424
|
+
# Fast-Path: The surface of the frustum concentric with the sphere
|
|
425
|
+
# is the surface with smaller radius
|
|
426
|
+
if r2 - r1 >= -eps: # r2 >= r1:
|
|
427
|
+
v_himisphere = VolSphere.calc_volume_spherical_cap(r1, r1)
|
|
428
|
+
if h >= r1:
|
|
429
|
+
# The hemisphere is completely inside the frustum cone
|
|
430
|
+
return v_himisphere
|
|
431
|
+
|
|
432
|
+
# The frustum cone is lower than the hemisphere
|
|
433
|
+
v_cap = VolSphere.calc_volume_spherical_cap(r1, r1 - h)
|
|
434
|
+
return v_himisphere - v_cap
|
|
435
|
+
|
|
436
|
+
up = (c2 - c1) / np.linalg.norm(c2 - c1)
|
|
437
|
+
v = find_unit_vector_on_plane(up)
|
|
438
|
+
|
|
439
|
+
intersections = find_sphere_line_intersection(c1, r1, c1 + r1 * v, c2 + r2 * v)
|
|
440
|
+
if len(intersections) == 0:
|
|
441
|
+
# Tricky case: Since the intersection point not found with
|
|
442
|
+
# numerical precision, we can simply assume that there are two
|
|
443
|
+
# intersection points and at the same position
|
|
444
|
+
intersections = [(0, c1 + r1 * v), (0, c1 + r1 * v)]
|
|
445
|
+
assert len(intersections) == 2
|
|
446
|
+
t, p = max(intersections, key=lambda x: x[0])
|
|
447
|
+
|
|
448
|
+
# Fast-Path: The frustum cone is completely inside the sphere
|
|
449
|
+
if t > 1 + eps:
|
|
450
|
+
return frustum_cone.get_volume()
|
|
451
|
+
|
|
452
|
+
M = project_point_on_line(c1, up, p)
|
|
453
|
+
h1 = np.linalg.norm(M - c1).item()
|
|
454
|
+
r3 = np.linalg.norm(M - p).item()
|
|
455
|
+
v_cap1 = VolSphere.calc_volume_spherical_cap(r1, r1 - h1)
|
|
456
|
+
v_frustum = VolFrustumCone.calc_volume(r1, r3, h1)
|
|
457
|
+
|
|
458
|
+
# Fast-Path: The frustum cone is higher than the sphere
|
|
459
|
+
if h >= r1:
|
|
460
|
+
return v_cap1 + v_frustum
|
|
461
|
+
|
|
462
|
+
v_cap2 = VolSphere.calc_volume_spherical_cap(r1, r1 - h)
|
|
463
|
+
return v_cap1 + v_frustum - v_cap2
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class VolSphereFrustumConeUnion(VolSDFUnion[VolSphere, VolFrustumCone]):
|
|
467
|
+
"""Union of sphere and frustum cone."""
|
|
468
|
+
|
|
469
|
+
def _get_volume(self) -> float:
|
|
470
|
+
return (
|
|
471
|
+
self.obj1.get_volume()
|
|
472
|
+
+ self.obj2.get_volume()
|
|
473
|
+
- VolSphereFrustumConeIntersection.calc_concentric_intersect_volume(
|
|
474
|
+
self.obj1, self.obj2
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _tp3f(x: npt.NDArray) -> tuple[float, float, float]:
|
|
480
|
+
"""Convert to tuple of 3 floats."""
|
|
481
|
+
|
|
482
|
+
assert len(x) == 3
|
|
483
|
+
return (float(x[0]), float(x[1]), float(x[2]))
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: swcgeom
|
|
3
|
+
Version: 0.19.4
|
|
4
|
+
Summary: Neuron geometry library for swc format
|
|
5
|
+
Author-email: yzx9 <pypi@yzx9.xyz>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: repository, https://github.com/yzx9/swcgeom
|
|
8
|
+
Keywords: neuroscience,neuron,neuroanatomy,neuron-morphology
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: imagecodecs>=2023.3.16
|
|
24
|
+
Requires-Dist: matplotlib>=3.5.2
|
|
25
|
+
Requires-Dist: numpy>=1.22.3
|
|
26
|
+
Requires-Dist: pandas>=1.4.2
|
|
27
|
+
Requires-Dist: pynrrd>=1.1.0
|
|
28
|
+
Requires-Dist: scipy>=1.9.1
|
|
29
|
+
Requires-Dist: sdflit>=0.2.6
|
|
30
|
+
Requires-Dist: seaborn>=0.12.0
|
|
31
|
+
Requires-Dist: tifffile>=2022.8.12
|
|
32
|
+
Requires-Dist: typing-extensions>=4.4.0
|
|
33
|
+
Requires-Dist: tqdm>=4.46.1
|
|
34
|
+
Provides-Extra: all
|
|
35
|
+
Requires-Dist: beautifulsoup4>=4.11.1; extra == "all"
|
|
36
|
+
Requires-Dist: certifi>=2023.5.7; extra == "all"
|
|
37
|
+
Requires-Dist: chardet>=5.2.0; extra == "all"
|
|
38
|
+
Requires-Dist: lmdb>=1.4.1; extra == "all"
|
|
39
|
+
Requires-Dist: requests>=2.0.0; extra == "all"
|
|
40
|
+
Requires-Dist: urllib3>=1.26.0; extra == "all"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# SWCGEOM
|
|
44
|
+
|
|
45
|
+
[](https://github.com/yzx9/swcgeom/actions/workflows/test.yml)
|
|
46
|
+
[](https://github.com/yzx9/swcgeom/releases)
|
|
47
|
+
[](https://pypi.org/project/swcgeom/)
|
|
48
|
+
|
|
49
|
+
A neuron geometry library for swc format.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
See examples for details.
|
|
54
|
+
|
|
55
|
+
## Development
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# clone repo
|
|
59
|
+
git clone git@github.com:yzx9/swcgeom.git
|
|
60
|
+
cd swcgeom
|
|
61
|
+
|
|
62
|
+
# install dependencies
|
|
63
|
+
python -m pip install --upgrade pip
|
|
64
|
+
pip install build
|
|
65
|
+
|
|
66
|
+
# install editable version
|
|
67
|
+
pip install --editable .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Static analysis don't support import hook used in editable install for
|
|
71
|
+
[PEP660](https://peps.python.org/pep-0660/) since upgrade to setuptools v64+,
|
|
72
|
+
detail information at [setuptools#3518](https://github.com/pypa/setuptools/issues/3518),
|
|
73
|
+
a workaround for vscode with pylance:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"python.analysis.extraPaths": ["/path/to/this/project"]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## LICENSE
|
|
82
|
+
|
|
83
|
+
This work is licensed under a
|
|
84
|
+
<a rel="license" href="https://www.apache.org/licenses/">Apache-2.0</a>.
|
|
85
|
+
|
|
86
|
+
Copyright (c) 2022-present, Zexin Yuan
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
swcgeom/__init__.py,sha256=kqxv-GpypsmuViVtQ2zmVihoEMVUEFR031F4E1KB_rE,486
|
|
2
|
+
swcgeom/analysis/__init__.py,sha256=OxF3lbhbX-S7jvvxpIV2ioi539e6WnWE2NbogVxpmmo,484
|
|
3
|
+
swcgeom/analysis/feature_extractor.py,sha256=SR72042CGCGfAwRp78AFlbJaTBXHN_mSFceiRCupxCM,14766
|
|
4
|
+
swcgeom/analysis/features.py,sha256=v9dC4ejXBR9MJV5KYuv6qMVo0DjxpJjOJchG4c5cMYc,6329
|
|
5
|
+
swcgeom/analysis/lmeasure.py,sha256=NiDPrq6ke5gWSKJtZqwaPYlN0aKNN4UWWid-yjxNp6g,28115
|
|
6
|
+
swcgeom/analysis/sholl.py,sha256=D_becIODn_T3gzZZLI4c0d6h3juEov0qHFvS0yksmUQ,6770
|
|
7
|
+
swcgeom/analysis/trunk.py,sha256=7XcGhUc8QmFMzZ_kMuoGPuKu7BlAe9cCCODbnfqo81o,5653
|
|
8
|
+
swcgeom/analysis/visualization.py,sha256=jeW9YVVgCI-Qs4w6Ds1n-AjwDEDBc8um5OWPCtYxkig,6293
|
|
9
|
+
swcgeom/analysis/visualization3d.py,sha256=5f7QK2kkHGgnT8URyE6Dy9FDRVtJkaWFWmmOKZRo7js,2539
|
|
10
|
+
swcgeom/analysis/volume.py,sha256=SN7VBTXTXxCkNYZ-9KGxak_jnPDpg4Eo_GC3WFAGQg0,4743
|
|
11
|
+
swcgeom/core/__init__.py,sha256=CkJIzhQC9VzFvMa8JQ6g7WwNE_edwNt6xDYieQMB_YI,697
|
|
12
|
+
swcgeom/core/branch.py,sha256=HUe_Yo4YZBYuKMr8jFU4kJ8nosrbhCzuph7eRPQLBE0,4306
|
|
13
|
+
swcgeom/core/branch_tree.py,sha256=6doQaarCB-nEcrMuNElwmPqgvE15yn5fxy3JePBcaTs,2034
|
|
14
|
+
swcgeom/core/compartment.py,sha256=gWzklvRH9nXBPezr8QnIEKxta3uZHk3Um9hLC7DT_2I,3484
|
|
15
|
+
swcgeom/core/node.py,sha256=XkMjlQ-1E_WaCCK6ghArQ9lXKuxPvzylLJyf6hnSzAE,3958
|
|
16
|
+
swcgeom/core/path.py,sha256=5ZARke8KAnM2bO99OgDTvYDdsqxdJ6Oa2lZlfSI4Mco,4704
|
|
17
|
+
swcgeom/core/population.py,sha256=rRkWh0Aik24EGBCo3jAOrvF-x2chpdJJCSA82caU4no,10563
|
|
18
|
+
swcgeom/core/swc.py,sha256=Wlz4kV44dJpsYgiOEGTi8mL56KjvLAVuz_F_ip_JtaU,7189
|
|
19
|
+
swcgeom/core/tree.py,sha256=TdApE-ZANdyh6U8YoYmI-LsoaMzsyDQMvcKrHR6yuSE,13068
|
|
20
|
+
swcgeom/core/tree_utils.py,sha256=4eFHQ8EajJG4fGO3_in6LKLYW1e-dw1cP8ZhqBJA0b8,7662
|
|
21
|
+
swcgeom/core/tree_utils_impl.py,sha256=mhgQqrtnGwei8B2jY2JvVgPLM-KU6PsKy8t-thdxxh8,1740
|
|
22
|
+
swcgeom/core/swc_utils/__init__.py,sha256=afVBWtkRFqnZUZeHf194JiycdbvGi1TLP_jf6FN7XLw,778
|
|
23
|
+
swcgeom/core/swc_utils/assembler.py,sha256=_VlOPnJvmveB8jSnnOhqY0MchnYq7F1dXCWJdvbC44c,1001
|
|
24
|
+
swcgeom/core/swc_utils/base.py,sha256=TVT4e6XoKuyBWbu2sFyzslnTobIXSXLqAiHEbOILi_I,4979
|
|
25
|
+
swcgeom/core/swc_utils/checker.py,sha256=RBFFgzw_LtwK_o2oXzrg54DClf0m3POn27lFwM5Wans,2774
|
|
26
|
+
swcgeom/core/swc_utils/io.py,sha256=pI0NbD4b9KCv9btYiP4OCtMJchcp_GVZobVOlEMSmqY,6421
|
|
27
|
+
swcgeom/core/swc_utils/normalizer.py,sha256=b0RxQroVyPmfKvV4x8FneEGgce8G4MI2xTsv34FADZA,5265
|
|
28
|
+
swcgeom/core/swc_utils/subtree.py,sha256=ri3Sh_uTi1mrfcA-rqTD88rW_L0cxZPRuON4zzADwNA,2126
|
|
29
|
+
swcgeom/images/__init__.py,sha256=3BLzhVKRbVtdsjaPTXX0SMO8JVxGwGvwVc4D0uyZ2nA,240
|
|
30
|
+
swcgeom/images/augmentation.py,sha256=vjUC9v6QC7SyVot4D7ErH00og_S9I2A_SW2syRuMgCM,4261
|
|
31
|
+
swcgeom/images/contrast.py,sha256=2Z38wV2laTCFAjZ3mT4vQlDtwHIZ-GmQQi81JDEzgO0,2074
|
|
32
|
+
swcgeom/images/folder.py,sha256=veQnUP5W9eXtOsBB0YtwEs3GlsylREJt9yth1FMTyAA,6590
|
|
33
|
+
swcgeom/images/io.py,sha256=mY6rklHn3xuSNvd_g4YGwLHP8Y3u-hEb6bV2jFVraso,20424
|
|
34
|
+
swcgeom/images/loaders/__init__.py,sha256=a4XSH2Bn6v5YUGb_lKwdjAGD1i4SXBTXGVh08mct7pg,254
|
|
35
|
+
swcgeom/images/loaders/pbd.cp311-win_amd64.pyd,sha256=9L-R4uAosyXzR4_HcukpAVbmKBvBUfB0Vw-yC4ykJXU,194560
|
|
36
|
+
swcgeom/images/loaders/pbd.pyx,sha256=14uWjLpdxLhdPVUoAjthEnRVbhV0AdKGBipXY_BZeWU,23092
|
|
37
|
+
swcgeom/images/loaders/raw.cp311-win_amd64.pyd,sha256=t3ZpbD3jwGXd7IZdqW_ogw_EKpi6fWElNKNoTb3NgCo,93184
|
|
38
|
+
swcgeom/images/loaders/raw.pyx,sha256=Zd6CIoy97aaGs6bxi9CzoraPp261dpuWMK_ZujElfYU,6947
|
|
39
|
+
swcgeom/transforms/__init__.py,sha256=DSE21HbgD6WczilWtONVq_m3zUgZXP-nsJjOCE_vKKU,916
|
|
40
|
+
swcgeom/transforms/base.py,sha256=oaOSZGrZKVbauypp9G-3G2f_WIx2gEJIft60jMg5rBc,4612
|
|
41
|
+
swcgeom/transforms/branch.py,sha256=OPx8D36neNmgePXPTw9qOXTzGfskCL5Og-k_s5GXV5g,7309
|
|
42
|
+
swcgeom/transforms/branch_tree.py,sha256=ZhV5KNPprtcOc_jbqO36sNaQaPibyTKkblcyFYKAOgs,2482
|
|
43
|
+
swcgeom/transforms/geometry.py,sha256=j6IXyM88W64FDEosdiN1KY1A793vCJrvibk4gZWWF8U,7936
|
|
44
|
+
swcgeom/transforms/image_preprocess.py,sha256=UDHzXPeGhVpQ7yTAUGMJb8P0tFHZoRAY3JI5IYNEoKU,3929
|
|
45
|
+
swcgeom/transforms/image_stack.py,sha256=pFisQ2x_H-3Bn-slVwlsiterv8p9E05zWv6-jm5x3_4,7098
|
|
46
|
+
swcgeom/transforms/images.py,sha256=Xg7nAnAuDbjxCwmXJqshGDi4EDxO8MlNBOCNQsdiEBk,5996
|
|
47
|
+
swcgeom/transforms/mst.py,sha256=QS2yO40thu-FfGkjY7yYZqnK1QOF7mkgSex6QQSZHcY,6104
|
|
48
|
+
swcgeom/transforms/neurolucida_asc.py,sha256=QV9K6Q0lUandF0r2Fyd999VyiI8zdS4rYNZ94vIMl4g,14699
|
|
49
|
+
swcgeom/transforms/path.py,sha256=JMjPKm3eYB_nr6j2-BBF-zUqPRTTIDMInM8TkCoSafE,1319
|
|
50
|
+
swcgeom/transforms/population.py,sha256=305NHfplPu_VJvVNUm1_8-me0Cd6TsGXAQTi9AYfSTE,1019
|
|
51
|
+
swcgeom/transforms/tree.py,sha256=ykmEP9626z4SBAwl0Vgw3Z7VZwRqiO2GxPsWtTha5wQ,8028
|
|
52
|
+
swcgeom/transforms/tree_assembler.py,sha256=42bAOBzzSlMMYdqlFu8ZoNQrS2Qi-MJje5_kAkWqdmA,5060
|
|
53
|
+
swcgeom/utils/__init__.py,sha256=k74fxbYGZfizmqHbWTqavtONOiqs5mndrBSC8SzDMQE,758
|
|
54
|
+
swcgeom/utils/debug.py,sha256=vURuhDf1Mx67-p8EMSoOTQbzZRAQd1Q5OQzD4OR6PaA,594
|
|
55
|
+
swcgeom/utils/download.py,sha256=bU2Be9fX_zNLaIe04fCaZFDHaLd_KoJMwnUvyUG_qUI,3789
|
|
56
|
+
swcgeom/utils/dsu.py,sha256=hNEsAO0HtlyI-SeLqx0F7sEE4YVCSuR9nZda6gwo_9E,1850
|
|
57
|
+
swcgeom/utils/ellipse.py,sha256=xTw1y6fNVcHIbY5c_MN2YLBju5DfHH3j7OP4m0zhGro,3883
|
|
58
|
+
swcgeom/utils/file.py,sha256=aPGv7peQUDqbQPdBvo7OW6G8_wqHCQhaQPx4P7b3yDI,2695
|
|
59
|
+
swcgeom/utils/neuromorpho.py,sha256=ANAsp7sIGOwjYu52WNaGHMmd7Wjgj5o_wzfEecF0FOg,19400
|
|
60
|
+
swcgeom/utils/numpy_helper.py,sha256=iNf4-mbv22l21VuaA2ulUC4vFRlx4827OSKP0_TkFvc,1778
|
|
61
|
+
swcgeom/utils/plotter_2d.py,sha256=9FG-b3Hj85tYKEBbjCldXRQ45-7odglMQsfaejt42D8,4018
|
|
62
|
+
swcgeom/utils/plotter_3d.py,sha256=x2U6ogarUNHMbqiVEShe4SABVimXqhvWrKHvyeBGWVM,965
|
|
63
|
+
swcgeom/utils/renderer.py,sha256=iFeKJIVAQfqehloRZHtb5vIf4e6zDMIdXazsIkyFxGI,4469
|
|
64
|
+
swcgeom/utils/sdf.py,sha256=WxPwGd4DJRM8sIA2t8RP5_6fypom3PxMtg03sSNqKrM,10816
|
|
65
|
+
swcgeom/utils/solid_geometry.py,sha256=ma30775813e8bFC6ycGLNdY1N2v96iPmECeHZc2YnPA,4257
|
|
66
|
+
swcgeom/utils/transforms.py,sha256=hX6QgHKcaWmRwc5RlbKwVBGqCdXJbuQh-CPr02Lme0Q,9469
|
|
67
|
+
swcgeom/utils/volumetric_object.py,sha256=C6k73avzKJUIsvaP6Bb43K2oENjv0mRub2caNHz4qGI,15316
|
|
68
|
+
swcgeom-0.19.4.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
|
69
|
+
swcgeom-0.19.4.dist-info/METADATA,sha256=fG08-VedmB_7VuWAAt_sxQXK9D9pBOinkr7nXWfSVvs,2951
|
|
70
|
+
swcgeom-0.19.4.dist-info/WHEEL,sha256=ge07nwgZNrHM7F-h6ntB4NDhu5ar8XLYRt0vb6q09YM,101
|
|
71
|
+
swcgeom-0.19.4.dist-info/top_level.txt,sha256=hmLyUXWS61Gxl07haswFEKKefYPBVJYlUlol8ghNkjY,8
|
|
72
|
+
swcgeom-0.19.4.dist-info/RECORD,,
|