Coverage for src / sdynpy / signal_processing / sdynpy_rotation.py: 27%
91 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-11 16:22 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-11 16:22 +0000
1# -*- coding: utf-8 -*-
2"""
3Functions for dealing with the geometry of rotations.
4"""
5"""
6Copyright 2022 National Technology & Engineering Solutions of Sandia,
7LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S.
8Government retains certain rights in this software.
10This program is free software: you can redistribute it and/or modify
11it under the terms of the GNU General Public License as published by
12the Free Software Foundation, either version 3 of the License, or
13(at your option) any later version.
15This program is distributed in the hope that it will be useful,
16but WITHOUT ANY WARRANTY; without even the implied warranty of
17MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18GNU General Public License for more details.
20You should have received a copy of the GNU General Public License
21along with this program. If not, see <https://www.gnu.org/licenses/>.
22"""
24import numpy as np
25from scipy.optimize import NonlinearConstraint
28def cross_mat(v):
29 """
30 Return matrix representation of the cross product of the given vector
32 Parameters
33 ----------
34 v : np.ndarray
35 3-vector that will be assembled into the matrix.
37 Returns
38 -------
39 cross_matrix : np.ndarray
40 Matrix representing the cross product. np.cross(a,b) is equivalent to
41 cross_mat(a)@b
43 """
44 v = np.array(v).flatten()
45 return np.array([[0, -v[2], v[1]],
46 [v[2], 0, -v[0]],
47 [-v[1], v[0], 0]])
50def R(axis, angle, degrees=False):
51 """
52 Create a rotation matrix consisting of a rotation about an axis
54 Parameters
55 ----------
56 axis : index or np.ndarray
57 The axis of rotation. Specifying 0, 1, or 2 will construct a rotation
58 about the X, Y, or Z axes, respectively. Alternatively, the axis of
59 rotation can be specficied as a 3-vector.
60 angle : float
61 The angle of rotation
62 degrees : bool, optional
63 True if the angle value is specified in degrees, false if radians.
64 The default is False.
66 Raises
67 ------
68 ValueError
69 If an improper axis is specified.
71 Returns
72 -------
73 rotmat : np.ndarray
74 A 3x3 rotation matrix.
76 """
77 rotmat = None
78 if degrees:
79 angle *= np.pi / 180
80 s = np.sin(angle)
81 c = np.cos(angle)
82 try:
83 if axis == 0:
84 rotmat = np.array([[1, 0, 0],
85 [0, c, -s],
86 [0, s, c]])
87 elif axis == 1:
88 rotmat = np.array([[c, 0, s],
89 [0, 1, 0],
90 [-s, 0, c]])
91 elif axis == 2:
92 rotmat = np.array([[c, -s, 0],
93 [s, c, 0],
94 [0, 0, 1]])
95 else:
96 axis = np.reshape(axis, (3, 1)) / np.linalg.norm(axis)
97 rotmat = np.eye(3) * c + s * cross_mat(axis) + (1 - c) * (axis @ axis.T)
98 except ValueError:
99 axis = np.reshape(axis, (3, 1)) / np.linalg.norm(axis)
100 rotmat = np.eye(3) * c + s * cross_mat(axis) + (1 - c) * (axis @ axis.T)
101 if rotmat is None:
102 raise ValueError('Invalid Axis {:}'.format(axis))
103 return rotmat
106def quaternion_to_rotation_matrix(quat):
107 output = np.zeros(quat.shape[:-1] + (3, 3))
108 q0 = quat[..., 0]
109 q1 = quat[..., 1]
110 q2 = quat[..., 2]
111 q3 = quat[..., 3]
113 # From https://automaticaddison.com/how-to-convert-a-quaternion-to-a-rotation-matrix/
114 # First row of the rotation matrix
115 output[..., 0, 0] = 2 * (q0 * q0 + q1 * q1) - 1
116 output[..., 0, 1] = 2 * (q1 * q2 - q0 * q3)
117 output[..., 0, 2] = 2 * (q1 * q3 + q0 * q2)
118 # Second row of the rotation matrix
119 output[..., 1, 0] = 2 * (q1 * q2 + q0 * q3)
120 output[..., 1, 1] = 2 * (q0 * q0 + q2 * q2) - 1
121 output[..., 1, 2] = 2 * (q2 * q3 - q0 * q1)
122 # Third row of the rotation matrix
123 output[..., 2, 0] = 2 * (q1 * q3 - q0 * q2)
124 output[..., 2, 1] = 2 * (q2 * q3 + q0 * q1)
125 output[..., 2, 2] = 2 * (q0 * q0 + q3 * q3) - 1
127 return output
130unit_magnitude_constraint = NonlinearConstraint(np.linalg.norm, 1, 1)
133def lstsq_rigid_transform(x, y, w=None):
134 """ Computes the Transform between two point sets such that
135 y = Rx + t
137 This is a least squares methods as descibed in
138 "Least-Squares Rigid Motion Using SVD", O. Sorkine-Hornung, M.
139 Rabinovich, Department of Computer Science, ETH Zurich, Jan 16, 2017
140 Note found online at https://igl.ethz.ch/projects/ARAP/svd_rot.pdf
142 INPUT
143 x,y = the two point sets as [x,y,z] column vectors
144 sizes are (3,n) for n points
145 w = weighting vector for each point, wi>0
146 default is uniform weighting of wi=1
147 size is (1,n) for n points
149 OUTPUT
150 R,t = [(3,3),(3,1)] transformation parameters such that
151 y = Rx + t
152 """
153 if w is None:
154 w = np.ones((x.shape[-1], 1)).T
155 # default weighting is uniform w=1
157 W = np.diag(w[0])
159 # Find the weighted centroids (for w=1, this is the mean)
160 xbar = np.sum(w * x, axis=-1) / np.sum(w, axis=1)
161 ybar = np.sum(w * y, axis=-1) / np.sum(w, axis=1)
163 # Center the points
164 X = x - xbar[..., np.newaxis]
165 Y = y - ybar[..., np.newaxis]
167 # Calculate the Covariance
168 Cov = X @ W @ np.moveaxis(Y, -1, -2)
170 # Take the SVD of the Covariance matrix
171 U, S, VH = np.linalg.svd(Cov)
172 V = np.moveaxis(VH, -1, -2) # numpy's SVD gives you back V', not V like Matlab
173 det = np.linalg.det(V @ np.moveaxis(U, -1, -2))
174 D = np.broadcast_to(np.eye(3), det.shape+(3, 3)).copy()
175 D[..., -1, -1] = det
177 R = V @ D @ np.moveaxis(U, -1, -2)
178 t = ybar[..., np.newaxis] - R @ xbar[..., np.newaxis]
180 return R, t
183def cross_mat_vectorized(rvec):
184 zero = np.zeros(rvec.shape[:-1])
185 return np.moveaxis(np.moveaxis(
186 np.array([
187 [zero, -rvec[..., 2], rvec[..., 1]],
188 [rvec[..., 2], zero, -rvec[..., 0]],
189 [-rvec[..., 1], rvec[..., 0], zero]
190 ]), 0, -1), 0, -1)
193def rodrigues_to_matrix(rvec, threshold=0.000001):
194 theta = np.linalg.norm(rvec, axis=-1, keepdims=True)
195 rvec = rvec / theta
196 R = (
197 np.cos(theta[..., np.newaxis]) * np.eye(3)
198 + (1 - np.cos(theta[..., np.newaxis])) * rvec[..., np.newaxis] @ rvec[..., np.newaxis, :]
199 + np.sin(theta[..., np.newaxis]) * cross_mat_vectorized(rvec))
200 R[theta[..., 0] < threshold] = np.eye(3)
201 return R
204def matrix_to_rodrigues(R, threshold=0.000001):
205 A = (R - np.moveaxis(R, -2, -1)) / 2
206 rho = np.moveaxis(np.array((A[..., 2, 1], A[..., 0, 2], A[..., 1, 0])), 0, -1)
207 s = np.linalg.norm(rho, axis=-1)
208 c = (R[..., 0, 0] + R[..., 1, 1] + R[..., 2, 2] - 1) / 2
209 u = np.zeros(rho.shape)
210 u[s >= threshold] = rho[s >= threshold] / s[s >= threshold][:, np.newaxis]
211 u[(s < threshold) & (c > 1 - threshold)] = 0
212 theta = np.arctan2(s, c)
213 rvec = u * theta[..., np.newaxis]
214 # Now handle the cases where s = 0 and c = -1
215 special_cases = (s < threshold) & (c < -1 + threshold)
216 for index in zip(*np.where(special_cases)):
217 thisR = R[index]
218 RpI = thisR + np.eye(3)
219 best_column = np.linalg.norm(RpI, axis=0).argmax()
220 v = RpI[:, best_column]
221 thisu = v / np.linalg.norm(v)
222 if ((abs(thisu[0]) < threshold and abs(thisu[1]) < threshold and thisu[2] < 0) or
223 (abs(thisu[0] < threshold and thisu[1] < 0)) or
224 (thisu[0] < 0)):
225 rvec[index] = -thisu * np.pi
226 else:
227 rvec[index] = thisu * np.pi
228 return rvec