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.
- 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.cpython-313-x86_64-linux-gnu.so +0 -0
- swcgeom/images/loaders/pbd.pyx +523 -0
- swcgeom/images/loaders/raw.cpython-313-x86_64-linux-gnu.so +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 +6 -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,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
|
+
)
|