cgmath 1.0.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.
- cgmath/__init__.py +7 -0
- cgmath/constraints/__init__.py +5 -0
- cgmath/constraints/procrustes.py +253 -0
- cgmath/formats/__init__.py +1 -0
- cgmath/formats/fbx.py +2555 -0
- cgmath/formats/glb.py +497 -0
- cgmath/formats/usd/__init__.py +1 -0
- cgmath/formats/usd/prim.py +460 -0
- cgmath/formats/usd/stage.py +237 -0
- cgmath/geometry/__init__.py +20 -0
- cgmath/geometry/_base.py +890 -0
- cgmath/geometry/_cdt.py +598 -0
- cgmath/geometry/_saddle_surface.py +356 -0
- cgmath/geometry/bspline.py +1225 -0
- cgmath/geometry/bspline_patch.py +1916 -0
- cgmath/geometry/camera.py +38 -0
- cgmath/geometry/deform/__init__.py +9 -0
- cgmath/geometry/deform/delta_mush.py +587 -0
- cgmath/geometry/deform/ffd.py +401 -0
- cgmath/geometry/deform/patch_relax.py +377 -0
- cgmath/geometry/deform/skin_deform.py +508 -0
- cgmath/geometry/deform/wrap.py +305 -0
- cgmath/geometry/delta_mush.py +29 -0
- cgmath/geometry/ffd.py +29 -0
- cgmath/geometry/map.py +234 -0
- cgmath/geometry/mesh.py +5220 -0
- cgmath/geometry/morph_target.py +494 -0
- cgmath/geometry/pack.py +332 -0
- cgmath/geometry/patch_relax.py +29 -0
- cgmath/geometry/raytracer.py +38 -0
- cgmath/geometry/resample.py +393 -0
- cgmath/geometry/robust_skinweights_transfer_bilinear.py +154 -0
- cgmath/geometry/sdf.py +1315 -0
- cgmath/geometry/skin_weights.py +1155 -0
- cgmath/geometry/surface_plotting.py +1077 -0
- cgmath/geometry/texture.py +29 -0
- cgmath/geometry/utils/__init__.py +61 -0
- cgmath/geometry/utils/_numba/__init__.py +12 -0
- cgmath/geometry/utils/_numba/_bilinear.py +2001 -0
- cgmath/geometry/utils/_numba/_blur.py +408 -0
- cgmath/geometry/utils/_numba/_bspline.py +2350 -0
- cgmath/geometry/utils/_numba/_bvh.py +154 -0
- cgmath/geometry/utils/_numba/_cdt.py +117 -0
- cgmath/geometry/utils/_numba/_connectivity.py +559 -0
- cgmath/geometry/utils/_numba/_delta_mush.py +1192 -0
- cgmath/geometry/utils/_numba/_ffd.py +289 -0
- cgmath/geometry/utils/_numba/_main.py +857 -0
- cgmath/geometry/utils/_numba/_normals.py +194 -0
- cgmath/geometry/utils/_numba/_pack.py +148 -0
- cgmath/geometry/utils/_numba/_patch_relax.py +600 -0
- cgmath/geometry/utils/_numba/_rasterize.py +437 -0
- cgmath/geometry/utils/_numba/_sdf.py +454 -0
- cgmath/geometry/utils/_numba/_skin_deform.py +535 -0
- cgmath/geometry/utils/_numba/_skin_weights.py +757 -0
- cgmath/geometry/utils/_numba/_subdivide.py +466 -0
- cgmath/geometry/utils/_numba/_subdivision.py +468 -0
- cgmath/geometry/utils/_numba/_tangent_space.py +77 -0
- cgmath/geometry/utils/_numba/_topology.py +351 -0
- cgmath/geometry/utils/_numba/_wrap.py +22 -0
- cgmath/geometry/utils/main.py +2053 -0
- cgmath/hierarchy/__init__.py +18 -0
- cgmath/hierarchy/hierarchy.py +3354 -0
- cgmath/rbf/__init__.py +7 -0
- cgmath/rbf/_kernels.py +780 -0
- cgmath/rbf/_numba/__init__.py +12 -0
- cgmath/rbf/_numba/_kernels.py +362 -0
- cgmath/rbf/_numba/_lu.py +198 -0
- cgmath/render/__init__.py +33 -0
- cgmath/render/_numba/__init__.py +12 -0
- cgmath/render/_numba/_texture.py +62 -0
- cgmath/render/camera.py +266 -0
- cgmath/render/frame.py +1103 -0
- cgmath/render/raytracer.py +1023 -0
- cgmath/render/scene.py +3687 -0
- cgmath/render/texture.py +75 -0
- cgmath/transforms/__init__.py +85 -0
- cgmath/transforms/_numba/__init__.py +15 -0
- cgmath/transforms/_numba/_axis.py +71 -0
- cgmath/transforms/_numba/_euler.py +156 -0
- cgmath/transforms/_numba/_matrix.py +386 -0
- cgmath/transforms/_numba/_quaternion.py +286 -0
- cgmath/transforms/_numba/_vector.py +266 -0
- cgmath/transforms/main.py +1190 -0
- cgmath/transforms/utils.py +270 -0
- cgmath/utils.py +687 -0
- cgmath-1.0.0.dist-info/METADATA +417 -0
- cgmath-1.0.0.dist-info/RECORD +90 -0
- cgmath-1.0.0.dist-info/WHEEL +5 -0
- cgmath-1.0.0.dist-info/licenses/LICENSE +31 -0
- cgmath-1.0.0.dist-info/top_level.txt +1 -0
cgmath/__init__.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from cgmath.geometry._base import Data
|
|
8
|
+
from cgmath.transforms import matrix_inverse, matrix_multiply, matrix_to_euler
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(repr=False, eq=False)
|
|
12
|
+
class ProcrustesData(Data):
|
|
13
|
+
points: np.ndarray = None
|
|
14
|
+
transforms: np.ndarray = None
|
|
15
|
+
clusters: np.ndarray = None
|
|
16
|
+
|
|
17
|
+
# --- cached attributes --- #
|
|
18
|
+
_scale = None # scale factors
|
|
19
|
+
_rotate = None # euler angles in radians
|
|
20
|
+
_translate = None # translation vectors
|
|
21
|
+
_matrix = None # output matrices
|
|
22
|
+
_points = None # the updated points
|
|
23
|
+
_scale_offset = None # whether to apply scale to offset matrix
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def scale_offset(self):
|
|
27
|
+
if self._scale_offset is None:
|
|
28
|
+
self._scale_offset = True
|
|
29
|
+
return self._scale_offset
|
|
30
|
+
|
|
31
|
+
@scale_offset.setter
|
|
32
|
+
def scale_offset(self, state: bool):
|
|
33
|
+
if self._scale_offset != state:
|
|
34
|
+
self._scale_offset = state
|
|
35
|
+
self.compute()
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def scale(self):
|
|
39
|
+
return np.ones((self._scale.size, 3)) * self._scale[:, None]
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def rotate(self):
|
|
43
|
+
return self._rotate
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def translate(self):
|
|
47
|
+
return self._translate
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def matrix(self):
|
|
51
|
+
return self._matrix
|
|
52
|
+
|
|
53
|
+
def __init__(self, target: np.ndarray):
|
|
54
|
+
"""Initialize the procrustes data using a target mesh"""
|
|
55
|
+
|
|
56
|
+
# be nice and support native dataclasses (eg: MeshData)
|
|
57
|
+
if not isinstance(target, np.ndarray) and hasattr(target, "points"):
|
|
58
|
+
self.points = target.points.copy()
|
|
59
|
+
else:
|
|
60
|
+
self.points = np.asarray(target)
|
|
61
|
+
|
|
62
|
+
def attach(self, transform: np.ndarray, indices: np.ndarray | None = None):
|
|
63
|
+
"""Attach a 4x4 transform matrix to a specified cluster of point indices"""
|
|
64
|
+
|
|
65
|
+
# if indices is None, then use all points
|
|
66
|
+
if indices is None:
|
|
67
|
+
indices = np.arange(self.points.shape[0])
|
|
68
|
+
|
|
69
|
+
# make sure indices are unique
|
|
70
|
+
else:
|
|
71
|
+
indices = np.unique(indices).astype(np.intp)
|
|
72
|
+
indices = indices[indices >= 0] # get rid of -1's
|
|
73
|
+
|
|
74
|
+
# store inside self.matrix
|
|
75
|
+
if self.transforms is None:
|
|
76
|
+
self.transforms = transform[None]
|
|
77
|
+
else:
|
|
78
|
+
self.transforms = np.concatenate((self.transforms, [transform]))
|
|
79
|
+
|
|
80
|
+
# rebuild neighborhood matrix
|
|
81
|
+
if self.clusters is None:
|
|
82
|
+
self.clusters = indices[None]
|
|
83
|
+
else:
|
|
84
|
+
max_length = self.clusters.shape[1]
|
|
85
|
+
if indices.size > self.clusters.shape[1]:
|
|
86
|
+
max_length = indices.size
|
|
87
|
+
|
|
88
|
+
clusters = np.ones((self.clusters.shape[0] + 1, max_length), dtype=int) * -1
|
|
89
|
+
clusters[: self.clusters.shape[0], : self.clusters.shape[1]] = self.clusters
|
|
90
|
+
clusters[-1, : indices.size] = indices
|
|
91
|
+
self.clusters = clusters
|
|
92
|
+
|
|
93
|
+
def update(self, target: np.ndarray) -> None:
|
|
94
|
+
"""updates procrustes points and triggers compute"""
|
|
95
|
+
|
|
96
|
+
# be nice and support native dataclasses (eg: MeshData)
|
|
97
|
+
if not isinstance(target, np.ndarray) and hasattr(target, "points"):
|
|
98
|
+
self._points = target.points
|
|
99
|
+
else:
|
|
100
|
+
self._points = np.asarray(target)
|
|
101
|
+
|
|
102
|
+
self.compute()
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def valid(self):
|
|
106
|
+
"""a valid constraint has the same count of clusters and transforms"""
|
|
107
|
+
if self.clusters is not None and self.transforms is not None:
|
|
108
|
+
return self.clusters.shape[0] == self.transforms.shape[0]
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
def compute(self) -> None:
|
|
112
|
+
"""compute procrustes"""
|
|
113
|
+
|
|
114
|
+
# compute only if
|
|
115
|
+
if self.valid:
|
|
116
|
+
clusters = self.clusters
|
|
117
|
+
points0 = self.points
|
|
118
|
+
points1 = self._points
|
|
119
|
+
if points1 is None:
|
|
120
|
+
points1 = points0
|
|
121
|
+
|
|
122
|
+
# pad 0's at end of matrix stack to handle -1 neighbor elements
|
|
123
|
+
points0_ = np.zeros((points0.shape[0] + 1, points0.shape[1]))
|
|
124
|
+
points0_[:-1] = points0
|
|
125
|
+
points0_ = points0_[clusters]
|
|
126
|
+
|
|
127
|
+
points1_ = np.zeros((points1.shape[0] + 1, points1.shape[1]))
|
|
128
|
+
points1_[:-1] = points1
|
|
129
|
+
points1_ = points1_[clusters]
|
|
130
|
+
|
|
131
|
+
# get the actual count of each clusters
|
|
132
|
+
counts = (clusters > -1).sum(axis=1)
|
|
133
|
+
|
|
134
|
+
# compute the centroids using the clusters counts
|
|
135
|
+
centroid0 = np.sum(points0_, axis=1) / counts[:, None]
|
|
136
|
+
centroid1 = np.sum(points1_, axis=1) / counts[:, None]
|
|
137
|
+
|
|
138
|
+
vectors0 = points0_ - centroid0[:, None]
|
|
139
|
+
vectors0[clusters == -1] = 0 # so outer product ignores the -1's
|
|
140
|
+
vectors1 = points1_ - centroid1[:, None]
|
|
141
|
+
vectors1[clusters == -1] = 0 # so outer product ignores the -1's
|
|
142
|
+
|
|
143
|
+
# sum the vectorized outer products
|
|
144
|
+
H = np.einsum("bji,bjk->bik", vectors1, vectors0)
|
|
145
|
+
|
|
146
|
+
# rotation factor of H -- try the parallel numba polar
|
|
147
|
+
# decomposition first (much faster for many small clusters
|
|
148
|
+
# because it skips per-call np.linalg.svd dispatch),
|
|
149
|
+
# fall back to vectorized SVD if the kernel is unavailable.
|
|
150
|
+
R = self._batch_rotation(H)
|
|
151
|
+
|
|
152
|
+
# compute scale
|
|
153
|
+
sx = np.sqrt(np.einsum("...ij,...ij", vectors0, vectors0)) / counts
|
|
154
|
+
sy = np.sqrt(np.einsum("...ij,...ij", vectors1, vectors1)) / counts
|
|
155
|
+
S = sy / sx
|
|
156
|
+
|
|
157
|
+
# apply scale to matrix if desired
|
|
158
|
+
if self.scale_offset:
|
|
159
|
+
R *= S[:, None, None]
|
|
160
|
+
|
|
161
|
+
# compute new position using the scaled matrices
|
|
162
|
+
T = np.einsum("ijk,ki->ji", -R, centroid0.T).T + centroid1
|
|
163
|
+
p = np.einsum("...ij,...j->...i", R, self.transforms[:, 3, :3])
|
|
164
|
+
p = p + T
|
|
165
|
+
|
|
166
|
+
# Embed the 3x3 rotation into a 4x4 homogeneous matrix
|
|
167
|
+
R4 = np.zeros((R.shape[0], 4, 4))
|
|
168
|
+
R4[:, :3, :3] = R
|
|
169
|
+
R4[:, 3, 3] = 1.0
|
|
170
|
+
|
|
171
|
+
# compute new transform orientations
|
|
172
|
+
R4 = matrix_inverse(R4)
|
|
173
|
+
M = matrix_multiply(self.transforms, R4)
|
|
174
|
+
M[:, 3, :3] = p
|
|
175
|
+
|
|
176
|
+
# set internals
|
|
177
|
+
self._scale = S
|
|
178
|
+
self._rotate = matrix_to_euler(M)
|
|
179
|
+
self._translate = p
|
|
180
|
+
self._matrix = M
|
|
181
|
+
|
|
182
|
+
else:
|
|
183
|
+
raise RuntimeError("Cannot compute invalid ProcrustesData object")
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def _determinants(H: np.ndarray) -> np.ndarray:
|
|
187
|
+
"""Determinants of a batch of 3x3 matrices, by cofactor expansion.
|
|
188
|
+
|
|
189
|
+
``np.linalg.det`` goes through LAPACK and costs ~11 ms on 100k 3x3
|
|
190
|
+
matrices against ~3 ms here, which matters because this runs on every
|
|
191
|
+
batch purely to decide which rows may take the fast path.
|
|
192
|
+
"""
|
|
193
|
+
return (
|
|
194
|
+
H[:, 0, 0] * (H[:, 1, 1] * H[:, 2, 2] - H[:, 1, 2] * H[:, 2, 1])
|
|
195
|
+
- H[:, 0, 1] * (H[:, 1, 0] * H[:, 2, 2] - H[:, 1, 2] * H[:, 2, 0])
|
|
196
|
+
+ H[:, 0, 2] * (H[:, 1, 0] * H[:, 2, 1] - H[:, 1, 1] * H[:, 2, 0])
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _svd_rotation(H: np.ndarray) -> np.ndarray:
|
|
201
|
+
"""Kabsch rotation: maximises ``trace(R.T @ H)``, reflections included.
|
|
202
|
+
|
|
203
|
+
For a reflection (``det(H) < 0``) the unconstrained optimum is itself
|
|
204
|
+
a reflection, so the smallest singular direction is flipped; the
|
|
205
|
+
resulting objective is ``s0 + s1 - s2``.
|
|
206
|
+
"""
|
|
207
|
+
U, S, V = np.linalg.svd(H)
|
|
208
|
+
R = np.einsum("bji,bkj->bki", V, U)
|
|
209
|
+
|
|
210
|
+
refl = np.where(np.linalg.det(R) < 0)[0]
|
|
211
|
+
if refl.size:
|
|
212
|
+
V = V.copy() # np.linalg.svd's output is not ours to mutate
|
|
213
|
+
V[refl, 2, :] *= -1
|
|
214
|
+
R[refl] = np.einsum("...ji,...kj->...ki", V[refl], U[refl])
|
|
215
|
+
return R
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def _batch_rotation(H: np.ndarray) -> np.ndarray:
|
|
219
|
+
"""Per-cluster rotation factor of a batch of 3x3 covariance matrices.
|
|
220
|
+
|
|
221
|
+
Clusters with ``det(H) >= 0`` go through the parallel numba polar
|
|
222
|
+
decomposition in ``cgmath.geometry.utils.main``, which avoids the
|
|
223
|
+
per-call ``np.linalg.svd`` dispatch overhead. Measured on 100k
|
|
224
|
+
clusters: 5.0 ms against 164 ms for pure Kabsch when nothing is
|
|
225
|
+
reflected (33x), falling to 1.8x on a batch that is half reflected
|
|
226
|
+
because those rows still take the SVD branch.
|
|
227
|
+
|
|
228
|
+
The polar factor is the Procrustes optimum only when the covariance
|
|
229
|
+
is not a reflection, so ``det(H) < 0`` clusters go through Kabsch
|
|
230
|
+
instead. On those the polar iteration converges to a rotation that
|
|
231
|
+
is orthonormal with ``det == +1`` -- so nothing downstream can tell
|
|
232
|
+
it apart -- but does not maximise ``trace(R.T @ H)``; on random
|
|
233
|
+
reflected covariances it averages 1.41 against Kabsch's 3.46.
|
|
234
|
+
|
|
235
|
+
Falls back to Kabsch for the whole batch when numba is unavailable.
|
|
236
|
+
"""
|
|
237
|
+
H = np.ascontiguousarray(np.asarray(H, dtype=np.float64))
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
from cgmath.geometry.utils.main import batch_procrustes_rotations
|
|
241
|
+
except ImportError:
|
|
242
|
+
return ProcrustesData._svd_rotation(H)
|
|
243
|
+
|
|
244
|
+
reflected = ProcrustesData._determinants(H) < 0.0
|
|
245
|
+
if not reflected.any():
|
|
246
|
+
return batch_procrustes_rotations(H, max_iter=16)
|
|
247
|
+
if reflected.all():
|
|
248
|
+
return ProcrustesData._svd_rotation(H)
|
|
249
|
+
|
|
250
|
+
R = np.empty_like(H)
|
|
251
|
+
R[~reflected] = batch_procrustes_rotations(H[~reflected], max_iter=16)
|
|
252
|
+
R[reflected] = ProcrustesData._svd_rotation(H[reflected])
|
|
253
|
+
return R
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Readers and writers for external asset formats."""
|