swcgeom 0.13.2__py3-none-any.whl → 0.15.0__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.
- swcgeom/_version.py +2 -2
- swcgeom/analysis/volume.py +105 -20
- swcgeom/core/branch_tree.py +2 -3
- swcgeom/core/population.py +6 -7
- swcgeom/core/swc_utils/assembler.py +12 -141
- swcgeom/core/swc_utils/subtree.py +2 -2
- swcgeom/core/tree.py +19 -12
- swcgeom/core/tree_utils.py +23 -5
- swcgeom/core/tree_utils_impl.py +22 -6
- swcgeom/images/folder.py +42 -17
- swcgeom/images/io.py +65 -36
- swcgeom/transforms/base.py +41 -21
- swcgeom/transforms/branch.py +5 -5
- swcgeom/transforms/geometry.py +42 -18
- swcgeom/transforms/image_stack.py +104 -124
- swcgeom/transforms/images.py +2 -2
- swcgeom/transforms/mst.py +5 -13
- swcgeom/transforms/population.py +2 -2
- swcgeom/transforms/tree.py +7 -13
- swcgeom/transforms/tree_assembler.py +85 -19
- swcgeom/utils/__init__.py +1 -1
- swcgeom/utils/sdf.py +167 -10
- swcgeom/utils/solid_geometry.py +26 -0
- swcgeom/utils/volumetric_object.py +504 -0
- swcgeom-0.15.0.dist-info/LICENSE +201 -0
- {swcgeom-0.13.2.dist-info → swcgeom-0.15.0.dist-info}/METADATA +6 -5
- {swcgeom-0.13.2.dist-info → swcgeom-0.15.0.dist-info}/RECORD +29 -29
- swcgeom/utils/geometry_object.py +0 -255
- swcgeom-0.13.2.dist-info/LICENSE +0 -3
- {swcgeom-0.13.2.dist-info → swcgeom-0.15.0.dist-info}/WHEEL +0 -0
- {swcgeom-0.13.2.dist-info → swcgeom-0.15.0.dist-info}/top_level.txt +0 -0
swcgeom/utils/sdf.py
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"""Signed distance functions.
|
|
2
2
|
|
|
3
3
|
Refs: https://iquilezles.org/articles/distfunctions/
|
|
4
|
+
|
|
5
|
+
Note
|
|
6
|
+
----
|
|
7
|
+
This module has been deprecated since v0.14.0, and will be removed in
|
|
8
|
+
the future, use `sdflit` instead.
|
|
4
9
|
"""
|
|
5
10
|
|
|
6
11
|
import warnings
|
|
@@ -10,7 +15,18 @@ from typing import Iterable, Tuple
|
|
|
10
15
|
import numpy as np
|
|
11
16
|
import numpy.typing as npt
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
from swcgeom.utils.solid_geometry import project_vector_on_plane
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"SDF",
|
|
22
|
+
"SDFUnion",
|
|
23
|
+
"SDFIntersection",
|
|
24
|
+
"SDFDifference",
|
|
25
|
+
"SDFCompose",
|
|
26
|
+
"SDFSphere",
|
|
27
|
+
"SDFFrustumCone",
|
|
28
|
+
"SDFRoundCone",
|
|
29
|
+
]
|
|
14
30
|
|
|
15
31
|
# Axis-aligned bounding box, tuple of array of shape (3,)
|
|
16
32
|
AABB = Tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]
|
|
@@ -26,7 +42,7 @@ class SDF(ABC):
|
|
|
26
42
|
|
|
27
43
|
@abstractmethod
|
|
28
44
|
def distance(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
|
|
29
|
-
"""
|
|
45
|
+
"""Calculate signed distance.
|
|
30
46
|
|
|
31
47
|
Parmeters
|
|
32
48
|
---------
|
|
@@ -69,15 +85,12 @@ class SDF(ABC):
|
|
|
69
85
|
return is_in
|
|
70
86
|
|
|
71
87
|
|
|
72
|
-
class
|
|
73
|
-
"""
|
|
88
|
+
class SDFUnion(SDF):
|
|
89
|
+
"""Union multiple SDFs."""
|
|
74
90
|
|
|
75
|
-
def __init__(self, sdfs:
|
|
76
|
-
sdfs = list(sdfs)
|
|
91
|
+
def __init__(self, *sdfs: SDF) -> None:
|
|
77
92
|
assert len(sdfs) != 0, "must combine at least one SDF"
|
|
78
|
-
|
|
79
|
-
if len(sdfs) == 1:
|
|
80
|
-
warnings.warn("compose only one SDF, use SDFCompose.compose instead")
|
|
93
|
+
super().__init__()
|
|
81
94
|
|
|
82
95
|
self.sdfs = sdfs
|
|
83
96
|
|
|
@@ -92,18 +105,162 @@ class SDFCompose(SDF):
|
|
|
92
105
|
return np.min([sdf(p) for sdf in self.sdfs], axis=0)
|
|
93
106
|
|
|
94
107
|
def is_in(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.bool_]:
|
|
108
|
+
p = np.array(p, dtype=np.float32)
|
|
109
|
+
assert p.ndim == 2 and p.shape[1] == 3, "p should be array of shape (N, 3)"
|
|
110
|
+
|
|
95
111
|
in_box = self.is_in_bounding_box(p)
|
|
96
|
-
|
|
112
|
+
p_in_box = p[in_box]
|
|
113
|
+
is_in = np.stack([sdf.is_in(p_in_box) for sdf in self.sdfs])
|
|
97
114
|
flags = np.full_like(in_box, False, dtype=np.bool_)
|
|
98
115
|
flags[in_box] = np.any(is_in, axis=0)
|
|
99
116
|
return flags
|
|
100
117
|
|
|
118
|
+
|
|
119
|
+
class SDFIntersection(SDF):
|
|
120
|
+
def __init__(self, *sdfs: SDF) -> None:
|
|
121
|
+
assert len(sdfs) != 0, "must intersect at least one SDF"
|
|
122
|
+
super().__init__()
|
|
123
|
+
self.sdfs = sdfs
|
|
124
|
+
|
|
125
|
+
bounding_boxes = [sdf.bounding_box for sdf in self.sdfs if sdf.bounding_box]
|
|
126
|
+
if len(bounding_boxes) == len(self.sdfs):
|
|
127
|
+
self.bounding_box = (
|
|
128
|
+
np.max(np.stack([box[0] for box in bounding_boxes]).T, axis=1),
|
|
129
|
+
np.min(np.stack([box[1] for box in bounding_boxes]).T, axis=1),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def distance(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
|
|
133
|
+
distances = np.stack([sdf.distance(p) for sdf in self.sdfs])
|
|
134
|
+
return np.max(distances, axis=1)
|
|
135
|
+
|
|
136
|
+
def is_in(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.bool_]:
|
|
137
|
+
p = np.array(p, dtype=np.float32)
|
|
138
|
+
assert p.ndim == 2 and p.shape[1] == 3, "p should be array of shape (N, 3)"
|
|
139
|
+
|
|
140
|
+
in_box = self.is_in_bounding_box(p)
|
|
141
|
+
p_in_box = p[in_box]
|
|
142
|
+
is_in = np.stack([sdf.is_in(p_in_box) for sdf in self.sdfs])
|
|
143
|
+
flags = np.full_like(in_box, False, dtype=np.bool_)
|
|
144
|
+
flags[in_box] = np.all(is_in, axis=0)
|
|
145
|
+
return flags
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class SDFDifference(SDF):
|
|
149
|
+
"""Difference of two SDFs A-B."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, sdf_a: SDF, sdf_b: SDF) -> None:
|
|
152
|
+
super().__init__()
|
|
153
|
+
self.sdf_a = sdf_a
|
|
154
|
+
self.sdf_b = sdf_b
|
|
155
|
+
|
|
156
|
+
self.bounding_box = sdf_a.bounding_box
|
|
157
|
+
|
|
158
|
+
def distance(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
|
|
159
|
+
da = self.sdf_a.distance(p)
|
|
160
|
+
db = self.sdf_b.distance(p)
|
|
161
|
+
return np.maximum(da, -db)
|
|
162
|
+
|
|
163
|
+
def is_in(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.bool_]:
|
|
164
|
+
p = np.array(p, dtype=np.float32)
|
|
165
|
+
assert p.ndim == 2 and p.shape[1] == 3, "p should be array of shape (N, 3)"
|
|
166
|
+
|
|
167
|
+
in_box = self.is_in_bounding_box(p)
|
|
168
|
+
p_in_box = p[in_box]
|
|
169
|
+
is_in_a = self.sdf_a.is_in(p_in_box)
|
|
170
|
+
is_in_b = self.sdf_b.is_in(p_in_box)
|
|
171
|
+
flags = np.full_like(in_box, False, dtype=np.bool_)
|
|
172
|
+
flags[in_box] = np.logical_and(is_in_a, np.logical_not(is_in_b))
|
|
173
|
+
return flags
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class SDFCompose(SDFUnion):
|
|
177
|
+
"""Compose multiple SDFs."""
|
|
178
|
+
|
|
179
|
+
def __init__(self, sdfs: Iterable[SDF]) -> None:
|
|
180
|
+
warnings.warn(
|
|
181
|
+
"`SDFCompose` has been replace by `SDFUnion` since v0.14.0, "
|
|
182
|
+
"and will be removed in next version",
|
|
183
|
+
DeprecationWarning,
|
|
184
|
+
)
|
|
185
|
+
sdfs = list(sdfs)
|
|
186
|
+
if len(sdfs) == 1:
|
|
187
|
+
warnings.warn("compose only one SDF, use SDFCompose.compose instead")
|
|
188
|
+
|
|
189
|
+
super().__init__(*sdfs)
|
|
190
|
+
|
|
101
191
|
@staticmethod
|
|
102
192
|
def compose(sdfs: Iterable[SDF]) -> SDF:
|
|
103
193
|
sdfs = list(sdfs)
|
|
104
194
|
return SDFCompose(sdfs) if len(sdfs) != 1 else sdfs[0]
|
|
105
195
|
|
|
106
196
|
|
|
197
|
+
class SDFSphere(SDF):
|
|
198
|
+
"""SDF of sphere."""
|
|
199
|
+
|
|
200
|
+
def __init__(self, center: npt.ArrayLike, radius: float) -> None:
|
|
201
|
+
super().__init__()
|
|
202
|
+
self.center = np.array(center)
|
|
203
|
+
self.radius = radius
|
|
204
|
+
assert tuple(self.center.shape) == (3,), "center should be vector of 3d"
|
|
205
|
+
|
|
206
|
+
self.bounding_box = (self.center - self.radius, self.center + self.radius)
|
|
207
|
+
|
|
208
|
+
def distance(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
|
|
209
|
+
return np.linalg.norm(p - self.center, axis=1) - self.radius
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class SDFFrustumCone(SDF):
|
|
213
|
+
"""SDF of frustum cone."""
|
|
214
|
+
|
|
215
|
+
def __init__(
|
|
216
|
+
self, a: npt.ArrayLike, b: npt.ArrayLike, ra: float, rb: float
|
|
217
|
+
) -> None:
|
|
218
|
+
super().__init__()
|
|
219
|
+
self.a = np.array(a)
|
|
220
|
+
self.b = np.array(b)
|
|
221
|
+
self.ra = ra
|
|
222
|
+
self.rb = rb
|
|
223
|
+
assert tuple(self.a.shape) == (3,), "a should be vector of 3d"
|
|
224
|
+
assert tuple(self.b.shape) == (3,), "b should be vector of 3d"
|
|
225
|
+
|
|
226
|
+
self.bounding_box = self.get_bounding_box()
|
|
227
|
+
|
|
228
|
+
def distance(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
|
|
229
|
+
a, b, ra, rb = self.a, self.b, self.ra, self.rb
|
|
230
|
+
|
|
231
|
+
rba = rb - ra
|
|
232
|
+
baba = np.dot(b - a, b - a)
|
|
233
|
+
papa = np.einsum("ij,ij->i", p - a, p - a)
|
|
234
|
+
paba = np.dot(p - a, b - a) / baba
|
|
235
|
+
# maybe negative due to numerical error
|
|
236
|
+
x = np.sqrt(np.maximum(papa - paba * paba * baba, 0))
|
|
237
|
+
cax = np.maximum(0.0, x - np.where(paba < 0.5, ra, rb))
|
|
238
|
+
cay = np.abs(paba - 0.5) - 0.5
|
|
239
|
+
k = rba * rba + baba
|
|
240
|
+
f = np.clip((rba * (x - ra) + paba * baba) / k, 0.0, 1.0)
|
|
241
|
+
cbx = x - ra - f * rba
|
|
242
|
+
cby = paba - f
|
|
243
|
+
s = np.where(np.logical_and(cbx < 0.0, cay < 0.0), -1.0, 1.0)
|
|
244
|
+
return s * np.sqrt(
|
|
245
|
+
np.minimum(cax * cax + cay * cay * baba, cbx * cbx + cby * cby * baba)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def get_bounding_box(self) -> AABB | None:
|
|
249
|
+
a, b, ra, rb = self.a, self.b, self.ra, self.rb
|
|
250
|
+
up = a - b
|
|
251
|
+
vx = project_vector_on_plane((1, 0, 0), up)
|
|
252
|
+
vy = project_vector_on_plane((0, 1, 0), up)
|
|
253
|
+
vz = project_vector_on_plane((0, 0, 1), up)
|
|
254
|
+
a1 = a - ra * vx - ra * vy - ra * vz
|
|
255
|
+
a2 = a + ra * vx + ra * vy + ra * vz
|
|
256
|
+
b1 = b - rb * vx - rb * vy - rb * vz
|
|
257
|
+
b2 = b + rb * vx + rb * vy + rb * vz
|
|
258
|
+
return (
|
|
259
|
+
np.minimum(a1, b1).astype(np.float32),
|
|
260
|
+
np.maximum(a2, b2).astype(np.float32),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
107
264
|
class SDFRoundCone(SDF):
|
|
108
265
|
"""Round cone is made up of two balls and a cylinder."""
|
|
109
266
|
|
swcgeom/utils/solid_geometry.py
CHANGED
|
@@ -9,6 +9,8 @@ __all__ = [
|
|
|
9
9
|
"find_unit_vector_on_plane",
|
|
10
10
|
"find_sphere_line_intersection",
|
|
11
11
|
"project_point_on_line",
|
|
12
|
+
"project_vector_on_vector",
|
|
13
|
+
"project_vector_on_plane",
|
|
12
14
|
]
|
|
13
15
|
|
|
14
16
|
|
|
@@ -68,3 +70,27 @@ def project_point_on_line(
|
|
|
68
70
|
AP = P - A
|
|
69
71
|
projection = A + np.dot(AP, n) / np.dot(n, n) * n
|
|
70
72
|
return projection
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def project_vector_on_vector(vec: npt.ArrayLike, target: npt.ArrayLike) -> npt.NDArray:
|
|
76
|
+
v = np.array(vec)
|
|
77
|
+
n = np.array(target)
|
|
78
|
+
|
|
79
|
+
n_normalized = n / np.linalg.norm(n)
|
|
80
|
+
projection_on_n = np.dot(v, n_normalized) * n_normalized
|
|
81
|
+
return projection_on_n
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def project_vector_on_plane(
|
|
85
|
+
vec: npt.ArrayLike, plane_normal_vec: npt.ArrayLike
|
|
86
|
+
) -> npt.NDArray:
|
|
87
|
+
v = np.array(vec)
|
|
88
|
+
n = np.array(plane_normal_vec)
|
|
89
|
+
|
|
90
|
+
# project v to n
|
|
91
|
+
projection_on_n = project_vector_on_vector(vec, n)
|
|
92
|
+
|
|
93
|
+
# project v to plane
|
|
94
|
+
projection_on_plane = v - projection_on_n
|
|
95
|
+
|
|
96
|
+
return projection_on_plane
|