swcgeom 0.19.4__cp312-cp312-macosx_14_0_arm64.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-312-darwin.so +0 -0
  36. swcgeom/images/loaders/pbd.pyx +523 -0
  37. swcgeom/images/loaders/raw.cpython-312-darwin.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 +5 -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,154 @@
1
+ # SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Solid Geometry."""
6
+
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+
10
+ __all__ = [
11
+ "find_unit_vector_on_plane",
12
+ "find_sphere_line_intersection",
13
+ "project_point_on_line",
14
+ "project_vector_on_vector",
15
+ "project_vector_on_plane",
16
+ ]
17
+
18
+
19
+ def find_unit_vector_on_plane(normal_vec3: npt.NDArray) -> npt.NDArray:
20
+ """Find a random unit vector on the plane defined by the normal vector.
21
+
22
+ >>> normal = np.array([0, 0, 1])
23
+ >>> u = find_unit_vector_on_plane(normal)
24
+ >>> np.allclose(np.dot(u, normal), 0) # Should be perpendicular
25
+ True
26
+ >>> np.allclose(np.linalg.norm(u), 1) # Should be unit length
27
+ True
28
+ """
29
+ r = np.random.rand(3)
30
+ r /= np.linalg.norm(r)
31
+ while np.allclose(r, normal_vec3) or np.allclose(r, -normal_vec3):
32
+ r = np.random.rand(3)
33
+ r /= np.linalg.norm(r)
34
+
35
+ u = np.cross(r, normal_vec3)
36
+ u /= np.linalg.norm(u)
37
+ return u
38
+
39
+
40
+ def find_sphere_line_intersection(
41
+ sphere_center: npt.NDArray,
42
+ sphere_radius: float,
43
+ line_point_a: npt.NDArray,
44
+ line_point_b: npt.NDArray,
45
+ ) -> list[tuple[float, npt.NDArray[np.float64]]]:
46
+ """Find intersection points between a sphere and a line.
47
+
48
+ >>> center = np.array([0, 0, 0])
49
+ >>> radius = 1.0
50
+ >>> p1 = np.array([-2, 0, 0])
51
+ >>> p2 = np.array([2, 0, 0])
52
+ >>> intersections = find_sphere_line_intersection(center, radius, p1, p2)
53
+ >>> len(intersections)
54
+ 2
55
+ >>> np.allclose(intersections[0][1], [-1, 0, 0])
56
+ True
57
+ >>> np.allclose(intersections[1][1], [1, 0, 0])
58
+ True
59
+ """
60
+
61
+ A = np.array(line_point_a)
62
+ B = np.array(line_point_b)
63
+ C = np.array(sphere_center)
64
+
65
+ D = B - A # line direction vector
66
+ f = A - C # line to sphere center
67
+
68
+ # solve: a*t^2 + b*t + c = 0
69
+ a = np.dot(D, D)
70
+ b = 2 * np.dot(f, D)
71
+ c = np.dot(f, f) - sphere_radius**2
72
+ discriminant = b**2 - 4 * a * c
73
+
74
+ if discriminant < 0:
75
+ return [] # no intersection
76
+
77
+ if discriminant == 0:
78
+ t = -b / (2 * a)
79
+ p = A + t * D
80
+ return [(t, p)] # single intersection, a tangent
81
+
82
+ t1 = (-b - np.sqrt(discriminant)) / (2 * a)
83
+ t2 = (-b + np.sqrt(discriminant)) / (2 * a)
84
+ p1 = A + t1 * D
85
+ p2 = A + t2 * D
86
+ return [(t1, p1), (t2, p2)]
87
+
88
+
89
+ def project_point_on_line(
90
+ point_a: npt.ArrayLike,
91
+ direction_vector: npt.ArrayLike,
92
+ point_p: npt.ArrayLike,
93
+ ) -> npt.NDArray:
94
+ """Project a point onto a line defined by a point and direction vector.
95
+
96
+ >>> a = np.array([0, 0, 0])
97
+ >>> d = np.array([1, 0, 0])
98
+ >>> p = np.array([1, 1, 0])
99
+ >>> projection = project_point_on_line(a, d, p)
100
+ >>> np.allclose(projection, [1, 0, 0])
101
+ True
102
+ """
103
+
104
+ A = np.array(point_a)
105
+ n = np.array(direction_vector)
106
+ P = np.array(point_p)
107
+
108
+ AP = P - A
109
+ projection = A + np.dot(AP, n) / np.dot(n, n) * n
110
+ return projection
111
+
112
+
113
+ def project_vector_on_vector(vec: npt.ArrayLike, target: npt.ArrayLike) -> npt.NDArray:
114
+ """Project one vector onto another.
115
+
116
+ >>> v = np.array([1, 1, 0])
117
+ >>> t = np.array([1, 0, 0])
118
+ >>> proj = project_vector_on_vector(v, t)
119
+ >>> np.allclose(proj, [1, 0, 0])
120
+ True
121
+ """
122
+ v = np.array(vec)
123
+ n = np.array(target)
124
+
125
+ n_normalized = n / np.linalg.norm(n)
126
+ projection_on_n = np.dot(v, n_normalized) * n_normalized
127
+ return projection_on_n
128
+
129
+
130
+ def project_vector_on_plane(
131
+ vec: npt.ArrayLike,
132
+ plane_normal_vec: npt.ArrayLike,
133
+ ) -> npt.NDArray:
134
+ """Project a vector onto a plane defined by its normal vector.
135
+
136
+ >>> v = np.array([1, 1, 1])
137
+ >>> n = np.array([0, 0, 1])
138
+ >>> proj = project_vector_on_plane(v, n)
139
+ >>> np.allclose(proj, [1, 1, 0]) # Z component removed
140
+ True
141
+ >>> np.allclose(np.dot(proj, n), 0) # Should be perpendicular to normal
142
+ True
143
+ """
144
+
145
+ v = np.array(vec)
146
+ n = np.array(plane_normal_vec)
147
+
148
+ # project v to n
149
+ projection_on_n = project_vector_on_vector(vec, n)
150
+
151
+ # project v to plane
152
+ projection_on_plane = v - projection_on_n
153
+
154
+ return projection_on_plane
@@ -0,0 +1,367 @@
1
+
2
+ # SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """3D geometry transformations."""
7
+
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+
11
+ __all__ = [
12
+ "Vec3f",
13
+ "angle",
14
+ "scale3d",
15
+ "translate3d",
16
+ "rotate3d",
17
+ "rotate3d_x",
18
+ "rotate3d_y",
19
+ "rotate3d_z",
20
+ "to_homogeneous",
21
+ "model_view_transformation",
22
+ "orthographic_projection_simple",
23
+ ]
24
+
25
+ Vec3f = tuple[float, float, float]
26
+
27
+
28
+ def angle(a: npt.ArrayLike, b: npt.ArrayLike) -> float:
29
+ """Get the signed angle between two vectors.
30
+
31
+ The angle is positive if the rotation from a to b is counter-clockwise, and
32
+ negative if clockwise.
33
+
34
+ >>> angle([1, 0, 0], [1, 0, 0]) # identical
35
+ 0.0
36
+ >>> angle([1, 0, 0], [0, 1, 0]) # 90 degrees counter-clockwise
37
+ 1.5707963267948966
38
+ >>> angle([1, 0, 0], [0, -1, 0]) # 90 degrees clockwise
39
+ -1.5707963267948966
40
+
41
+ Returns:
42
+ angle: Angle in radians between -π and π.
43
+ """
44
+
45
+ a = np.asarray(a)
46
+ b = np.asarray(b)
47
+
48
+ # Normalize vectors
49
+ a_norm = a / np.linalg.norm(a)
50
+ b_norm = b / np.linalg.norm(b)
51
+
52
+ # Calculate cosine of angle
53
+ cos_theta = np.dot(a_norm, b_norm)
54
+ cos_theta = np.clip(cos_theta, -1.0, 1.0) # Ensure within valid range
55
+ theta = np.arccos(cos_theta)
56
+
57
+ # Determine sign using cross product
58
+ cross = np.cross(a_norm, b_norm)
59
+ sign = np.sign(cross[2]) # Use z-component for 3D
60
+ return float(sign * theta)
61
+
62
+
63
+ def scale3d(sx: float, sy: float, sz: float) -> npt.NDArray[np.float32]:
64
+ """Get the 3D scale transformation matrix.
65
+
66
+ >>> np.allclose(
67
+ ... scale3d(2, 3, 4),
68
+ ... [
69
+ ... [2.0, 0.0, 0.0, 0.0],
70
+ ... [0.0, 3.0, 0.0, 0.0],
71
+ ... [0.0, 0.0, 4.0, 0.0],
72
+ ... [0.0, 0.0, 0.0, 1.0],
73
+ ... ],
74
+ ... )
75
+ True
76
+
77
+ Returns:
78
+ T: The homogeneous transformation matrix, shape (4, 4).
79
+ """
80
+ return np.array(
81
+ [
82
+ [sx, 0, 0, 0],
83
+ [0, sy, 0, 0],
84
+ [0, 0, sz, 0],
85
+ [0, 0, 0, 1],
86
+ ],
87
+ dtype=np.float32,
88
+ )
89
+
90
+
91
+ def translate3d(tx: float, ty: float, tz: float) -> npt.NDArray[np.float32]:
92
+ """Get the 3D translate transformation matrix.
93
+
94
+ >>> np.allclose(
95
+ ... translate3d(1, 2, 3),
96
+ ... [
97
+ ... [1.0, 0.0, 0.0, 1.0],
98
+ ... [0.0, 1.0, 0.0, 2.0],
99
+ ... [0.0, 0.0, 1.0, 3.0],
100
+ ... [0.0, 0.0, 0.0, 1.0],
101
+ ... ],
102
+ ... )
103
+ True
104
+
105
+ Returns:
106
+ T: The homogeneous transformation matrix, shape (4, 4).
107
+ """
108
+ return np.array(
109
+ [
110
+ [1, 0, 0, tx],
111
+ [0, 1, 0, ty],
112
+ [0, 0, 1, tz],
113
+ [0, 0, 0, 1],
114
+ ],
115
+ dtype=np.float32,
116
+ )
117
+
118
+
119
+ def rotate3d(n: npt.ArrayLike, theta: float) -> npt.NDArray[np.float32]:
120
+ r"""Get the 3D rotation transformation matrix.
121
+
122
+ Rotate v with axis n by an angle theta according to the right hand rule, follow
123
+ rodrigues' rotation formula.
124
+
125
+ .. math::
126
+
127
+ R(\mathbf{n}, \alpha) = \cos{\alpha} \cdot \mathbf{I}
128
+ + (1-\cos{\alpha}) \cdot \mathbf{n} \cdot \mathbf{n^T}
129
+ + \sin{\alpha} \cdot \mathbf{N} \\
130
+
131
+ N = \begin{pmatrix}
132
+ 0 & -n_z & n_y \\
133
+ n_z & 0 & -n_x \\
134
+ -n_y & n_x & 0
135
+ \end{pmatrix}
136
+
137
+ Args:
138
+ n: Rotation axis.
139
+ theta: Rotation angle in radians.
140
+
141
+ Returns:
142
+ T: The homogeneous transformation matrix, shape (4, 4).
143
+ """
144
+
145
+ n = np.array(n)
146
+ nx, ny, nz = n[0:3]
147
+ # pylint: disable-next=invalid-name
148
+ N = np.array(
149
+ [
150
+ [0, -nz, ny],
151
+ [nz, 0, -nx],
152
+ [-ny, nx, 0],
153
+ ],
154
+ dtype=np.float32,
155
+ )
156
+
157
+ return (
158
+ np.cos(theta) * np.identity(4)
159
+ + (1 - np.cos(theta)) * n * n[:, None]
160
+ + np.sin(theta) * N
161
+ )
162
+
163
+
164
+ def rotate3d_x(theta: float) -> npt.NDArray[np.float32]:
165
+ """Get the 3D rotation transformation matrix.
166
+
167
+ Rotate 3D vector `v` with `x`-axis by an angle theta according to the right
168
+ hand rule.
169
+
170
+ >>> np.allclose(
171
+ ... rotate3d_x(np.pi / 2), # 90 degree rotation
172
+ ... [
173
+ ... [+1.0, +0.0, +0.0, +0.0],
174
+ ... [+0.0, +0.0, -1.0, +0.0],
175
+ ... [+0.0, +1.0, +0.0, +0.0],
176
+ ... [+0.0, +0.0, +0.0, +1.0],
177
+ ... ],
178
+ ... )
179
+ True
180
+
181
+ Args:
182
+ theta: float
183
+ Rotation angle in radians.
184
+
185
+ Returns:
186
+ T: The homogeneous transformation matrix, shape (4, 4).
187
+ """
188
+
189
+ return np.array(
190
+ [
191
+ [1, 0, 0, 0],
192
+ [0, np.cos(theta), -np.sin(theta), 0],
193
+ [0, np.sin(theta), np.cos(theta), 0],
194
+ [0, 0, 0, 1],
195
+ ],
196
+ dtype=np.float32,
197
+ )
198
+
199
+
200
+ def rotate3d_y(theta: float) -> npt.NDArray[np.float32]:
201
+ """Get the 3D rotation transformation matrix.
202
+
203
+ Rotate 3D vector `v` with `y`-axis by an angle theta according to the right
204
+ hand rule.
205
+
206
+ >>> np.allclose(
207
+ ... rotate3d_y(np.pi / 2), # 90 degree rotation
208
+ ... [
209
+ ... [+0.0, +0.0, +1.0, +0.0],
210
+ ... [+0.0, +1.0, +0.0, +0.0],
211
+ ... [-1.0, +0.0, +0.0, +0.0],
212
+ ... [+0.0, +0.0, +0.0, +1.0],
213
+ ... ],
214
+ ... )
215
+ True
216
+
217
+ Args:
218
+ theta: Rotation angle in radians.
219
+
220
+ Returns:
221
+ T: The homogeneous transformation matrix, shape (4, 4).
222
+ """
223
+ return np.array(
224
+ [
225
+ [np.cos(theta), 0, np.sin(theta), 0],
226
+ [0, 1, 0, 0],
227
+ [-np.sin(theta), 0, np.cos(theta), 0],
228
+ [0, 0, 0, 1],
229
+ ],
230
+ dtype=np.float32,
231
+ )
232
+
233
+
234
+ def rotate3d_z(theta: float) -> npt.NDArray[np.float32]:
235
+ """Get the 3D rotation transformation matrix.
236
+
237
+ Rotate 3D vector `v` with `z`-axis by an angle theta according to the right hand
238
+ rule.
239
+
240
+ >>> np.allclose(
241
+ ... rotate3d_z(np.pi / 2), # 90 degree rotation
242
+ ... [
243
+ ... [+0.0, -1.0, +0.0, +0.0],
244
+ ... [+1.0, +0.0, +0.0, +0.0],
245
+ ... [+0.0, +0.0, +1.0, +0.0],
246
+ ... [+0.0, +0.0, +0.0, +1.0],
247
+ ... ],
248
+ ... )
249
+ True
250
+
251
+ Args:
252
+ theta: float
253
+ Rotation angle in radians.
254
+
255
+ Returns:
256
+ T: np.NDArray
257
+ The homogeneous transformation matrix, shape (4, 4).
258
+ """
259
+ return np.array(
260
+ [
261
+ [np.cos(theta), -np.sin(theta), 0, 0],
262
+ [np.sin(theta), np.cos(theta), 0, 0],
263
+ [0, 0, 1, 0],
264
+ [0, 0, 0, 1],
265
+ ],
266
+ dtype=np.float32,
267
+ )
268
+
269
+
270
+ def to_homogeneous(xyz: npt.ArrayLike, w: float) -> npt.NDArray[np.float32]:
271
+ """Fill xyz to homogeneous coordinates.
272
+
273
+ >>> np.allclose(to_homogeneous([1, 2, 3], 1), [1.0, 2.0, 3.0, 1.0])
274
+ True
275
+ >>> np.allclose(
276
+ ... to_homogeneous([[1, 2, 3], [4, 5, 6]], 0),
277
+ ... [[1.0, 2.0, 3.0, 0.0], [4.0, 5.0, 6.0, 0.0]],
278
+ ... )
279
+ True
280
+
281
+ Args:
282
+ xyz: Coordinate of shape (..., 3)
283
+ w: w of homogeneous coordinate, 1 for dot, 0 for vector.
284
+
285
+ Returns:
286
+ xyz4: Array of shape (..., 4)
287
+ """
288
+ xyz = np.array(xyz)
289
+ if xyz.ndim == 1:
290
+ return _to_homogeneous(xyz[None, ...], w)[0]
291
+
292
+ shape = xyz.shape[:-1]
293
+ xyz = xyz.reshape(-1, xyz.shape[-1])
294
+ xyz4 = _to_homogeneous(xyz, w).reshape(*shape, 4)
295
+ return xyz4
296
+
297
+
298
+ def _to_homogeneous(xyz: npt.NDArray, w: float) -> npt.NDArray[np.float32]:
299
+ """Fill xyz to homogeneous coordinates.
300
+
301
+ Args:
302
+ xyz: Coordinate of shape (N, 3)
303
+ w: w of homogeneous coordinate, 1 for dot, 0 for vector.
304
+
305
+ Returns:
306
+ xyz4: Array of shape (N, 4)
307
+ """
308
+ if xyz.shape[1] == 4:
309
+ return xyz
310
+
311
+ assert xyz.shape[1] == 3
312
+ filled = np.full((xyz.shape[0], 1), fill_value=w)
313
+ xyz4 = np.concatenate([xyz, filled], axis=1)
314
+ return xyz4
315
+
316
+
317
+ def model_view_transformation(
318
+ position: Vec3f, look_at: Vec3f, up: Vec3f
319
+ ) -> npt.NDArray[np.float32]:
320
+ r"""Play model/view transformation.
321
+
322
+ Args:
323
+ position: Camera position \vec{e}.
324
+ look_at: Camera look-at \vec{g}.
325
+ up: Camera up direction \vec{t}.
326
+ """
327
+
328
+ e = np.array(position, dtype=np.float32)
329
+ g = np.array(look_at, dtype=np.float32) / np.linalg.norm(look_at)
330
+ t = np.array(up, dtype=np.float32) / np.linalg.norm(up)
331
+
332
+ t_view = translate3d(*(-1 * e))
333
+ r_view = np.array(
334
+ [
335
+ [*np.cross(g, t), 0],
336
+ [*t, 0],
337
+ [*(-1 * g), 0],
338
+ [0, 0, 0, 1],
339
+ ],
340
+ dtype=np.float32,
341
+ )
342
+ return np.dot(r_view, t_view)
343
+
344
+
345
+ def orthographic_projection_simple() -> npt.NDArray[np.float32]:
346
+ """Simple orthographic projection by drop z-axis
347
+
348
+ >>> np.allclose(
349
+ ... orthographic_projection_simple(),
350
+ ... [
351
+ ... [1.0, 0.0, 0.0, 0.0],
352
+ ... [0.0, 1.0, 0.0, 0.0],
353
+ ... [0.0, 0.0, 0.0, 0.0],
354
+ ... [0.0, 0.0, 0.0, 0.0],
355
+ ... ],
356
+ ... )
357
+ True
358
+ """
359
+ return np.array(
360
+ [
361
+ [1, 0, 0, 0],
362
+ [0, 1, 0, 0],
363
+ [0, 0, 0, 0], # drop z-axis
364
+ [0, 0, 0, 0], # drop w
365
+ ],
366
+ dtype=np.float32,
367
+ )