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.
Files changed (90) hide show
  1. cgmath/__init__.py +7 -0
  2. cgmath/constraints/__init__.py +5 -0
  3. cgmath/constraints/procrustes.py +253 -0
  4. cgmath/formats/__init__.py +1 -0
  5. cgmath/formats/fbx.py +2555 -0
  6. cgmath/formats/glb.py +497 -0
  7. cgmath/formats/usd/__init__.py +1 -0
  8. cgmath/formats/usd/prim.py +460 -0
  9. cgmath/formats/usd/stage.py +237 -0
  10. cgmath/geometry/__init__.py +20 -0
  11. cgmath/geometry/_base.py +890 -0
  12. cgmath/geometry/_cdt.py +598 -0
  13. cgmath/geometry/_saddle_surface.py +356 -0
  14. cgmath/geometry/bspline.py +1225 -0
  15. cgmath/geometry/bspline_patch.py +1916 -0
  16. cgmath/geometry/camera.py +38 -0
  17. cgmath/geometry/deform/__init__.py +9 -0
  18. cgmath/geometry/deform/delta_mush.py +587 -0
  19. cgmath/geometry/deform/ffd.py +401 -0
  20. cgmath/geometry/deform/patch_relax.py +377 -0
  21. cgmath/geometry/deform/skin_deform.py +508 -0
  22. cgmath/geometry/deform/wrap.py +305 -0
  23. cgmath/geometry/delta_mush.py +29 -0
  24. cgmath/geometry/ffd.py +29 -0
  25. cgmath/geometry/map.py +234 -0
  26. cgmath/geometry/mesh.py +5220 -0
  27. cgmath/geometry/morph_target.py +494 -0
  28. cgmath/geometry/pack.py +332 -0
  29. cgmath/geometry/patch_relax.py +29 -0
  30. cgmath/geometry/raytracer.py +38 -0
  31. cgmath/geometry/resample.py +393 -0
  32. cgmath/geometry/robust_skinweights_transfer_bilinear.py +154 -0
  33. cgmath/geometry/sdf.py +1315 -0
  34. cgmath/geometry/skin_weights.py +1155 -0
  35. cgmath/geometry/surface_plotting.py +1077 -0
  36. cgmath/geometry/texture.py +29 -0
  37. cgmath/geometry/utils/__init__.py +61 -0
  38. cgmath/geometry/utils/_numba/__init__.py +12 -0
  39. cgmath/geometry/utils/_numba/_bilinear.py +2001 -0
  40. cgmath/geometry/utils/_numba/_blur.py +408 -0
  41. cgmath/geometry/utils/_numba/_bspline.py +2350 -0
  42. cgmath/geometry/utils/_numba/_bvh.py +154 -0
  43. cgmath/geometry/utils/_numba/_cdt.py +117 -0
  44. cgmath/geometry/utils/_numba/_connectivity.py +559 -0
  45. cgmath/geometry/utils/_numba/_delta_mush.py +1192 -0
  46. cgmath/geometry/utils/_numba/_ffd.py +289 -0
  47. cgmath/geometry/utils/_numba/_main.py +857 -0
  48. cgmath/geometry/utils/_numba/_normals.py +194 -0
  49. cgmath/geometry/utils/_numba/_pack.py +148 -0
  50. cgmath/geometry/utils/_numba/_patch_relax.py +600 -0
  51. cgmath/geometry/utils/_numba/_rasterize.py +437 -0
  52. cgmath/geometry/utils/_numba/_sdf.py +454 -0
  53. cgmath/geometry/utils/_numba/_skin_deform.py +535 -0
  54. cgmath/geometry/utils/_numba/_skin_weights.py +757 -0
  55. cgmath/geometry/utils/_numba/_subdivide.py +466 -0
  56. cgmath/geometry/utils/_numba/_subdivision.py +468 -0
  57. cgmath/geometry/utils/_numba/_tangent_space.py +77 -0
  58. cgmath/geometry/utils/_numba/_topology.py +351 -0
  59. cgmath/geometry/utils/_numba/_wrap.py +22 -0
  60. cgmath/geometry/utils/main.py +2053 -0
  61. cgmath/hierarchy/__init__.py +18 -0
  62. cgmath/hierarchy/hierarchy.py +3354 -0
  63. cgmath/rbf/__init__.py +7 -0
  64. cgmath/rbf/_kernels.py +780 -0
  65. cgmath/rbf/_numba/__init__.py +12 -0
  66. cgmath/rbf/_numba/_kernels.py +362 -0
  67. cgmath/rbf/_numba/_lu.py +198 -0
  68. cgmath/render/__init__.py +33 -0
  69. cgmath/render/_numba/__init__.py +12 -0
  70. cgmath/render/_numba/_texture.py +62 -0
  71. cgmath/render/camera.py +266 -0
  72. cgmath/render/frame.py +1103 -0
  73. cgmath/render/raytracer.py +1023 -0
  74. cgmath/render/scene.py +3687 -0
  75. cgmath/render/texture.py +75 -0
  76. cgmath/transforms/__init__.py +85 -0
  77. cgmath/transforms/_numba/__init__.py +15 -0
  78. cgmath/transforms/_numba/_axis.py +71 -0
  79. cgmath/transforms/_numba/_euler.py +156 -0
  80. cgmath/transforms/_numba/_matrix.py +386 -0
  81. cgmath/transforms/_numba/_quaternion.py +286 -0
  82. cgmath/transforms/_numba/_vector.py +266 -0
  83. cgmath/transforms/main.py +1190 -0
  84. cgmath/transforms/utils.py +270 -0
  85. cgmath/utils.py +687 -0
  86. cgmath-1.0.0.dist-info/METADATA +417 -0
  87. cgmath-1.0.0.dist-info/RECORD +90 -0
  88. cgmath-1.0.0.dist-info/WHEEL +5 -0
  89. cgmath-1.0.0.dist-info/licenses/LICENSE +31 -0
  90. cgmath-1.0.0.dist-info/top_level.txt +1 -0
cgmath/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Geometry, deformation, RBF and rendering tools.
2
+
3
+ The underlying matrix, quaternion, euler and vector math lives in
4
+ :mod:`cgmath.transforms`.
5
+ """
6
+
7
+ __version__ = "1.0.0"
@@ -0,0 +1,5 @@
1
+ """
2
+ Move all data types into the same namespace
3
+ """
4
+
5
+ from cgmath.constraints.procrustes import ProcrustesData
@@ -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."""