-
Notifications
You must be signed in to change notification settings - Fork 8
/
rigid_motion.py
129 lines (93 loc) · 3.38 KB
/
rigid_motion.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
from numpy.testing import assert_array_almost_equal
import numpy as np
def random_rotation_matrix_3d():
"""Generate a random matrix in :math:`\mathbb{SO}(3)`"""
A = np.random.uniform(-1, 1, (3, 3)) # Generate random matrix A
Q = np.dot(A, A.T) # Q is a symmetric matrix
# Q is decomposed into RDR^T, where R is a rotation matrix
R = np.linalg.svd(Q)[0]
return R
def random_vector_3d(scale=1.0):
"""
Generate a random 3D vector :math:`\mathbf{v}` such that
:math:`\mathbf{v}_{i} \in [-s, s),\,i=1,2,3`
"""
v = np.random.uniform(-1, 1, size=3)
v = v / np.linalg.norm(v)
return scale * v
def calculate_rotation(X, Y):
S = np.dot(X.T, Y)
U, _, VT = np.linalg.svd(S) # S = U * Sigma * VT
V = VT.T
return np.dot(V, U.T)
def calculate_scaling(X, Y, R):
n = np.sum(np.dot(np.dot(y, R), x) for x, y in zip(X, Y))
d = np.sum(X * X) # equivalent to sum([dot(x, x) for x in X])
return n / d
def calculate_translation(s, R, p, q):
return q - s * np.dot(R, p)
class LeastSquaresRigidMotion(object):
"""
For each element in :math:`P = \{\mathbf{p}_i\}`
and the corresponding element in :math:`Q = \{\mathbf{q}_i\}`,
calculate the transformation which minimizes the error
.. math::
E(P, Q) = \sum_{i} ||s R \mathbf{p}_i + \mathbf{t} - \mathbf{q}_i||^2
where :math:`s`, :math:`R`, :math:`\mathbf{t}` are scaling factor,
rotation matrix and translation vector respectively.
Examples:
>>> s, R, t = LeastSquaresRigidMotion(P, Q).solve()
>>> P = transform(s, R, t, P)
See :cite:`zinsser2005point` for the detailed method.
.. bibliography:: refs.bib
"""
def __init__(self, P: np.ndarray, Q: np.ndarray):
"""
Args:
P: Set of points of shape (n_image_points, n_channels)
to be transformed
Q: Set of points of shape (n_image_points, n_channels)
to be used as a reference
"""
if P.shape != Q.shape:
raise ValueError("P and Q must be the same shape")
self.n_features = P.shape[1]
self.P = P
self.Q = Q
def solve(self):
"""
Calculate (:math:`s`, :math:`R`, :math:`\mathbf{t}`)
Returns:
tuple: (s, R, t) where
- :math:`s`: Scaling coefficient
- :math:`R`: Rotation matrix
- :math:`\mathbf{t}`: translation vector
"""
mean_p = np.mean(self.P, axis=0)
mean_q = np.mean(self.Q, axis=0)
X = self.P - mean_p
Y = self.Q - mean_q
R = calculate_rotation(X, Y)
s = calculate_scaling(X, Y, R)
t = calculate_translation(s, R, mean_p, mean_q)
return s, R, t
def transform(s: float, R: np.ndarray,
t: np.ndarray, P: np.ndarray) -> np.ndarray:
"""
Transform each point :math:`\mathbf{p} \in P` into
:math:`\mathbf{q} = sR\mathbf{p} + \mathbf{t}` where
- :math:`s`: Scaling factor
- :math:`R`: Rotation matrix
- :math:`\mathbf{t}`: Translation vector
Args:
s: Scaling factor
R: Rotation matrix
t: Translation vector
P: Points to be transformed of shape (n_image_points, n_channels)
Returns:
Transformed vector
"""
if np.ndim(t) == 1:
t = t.reshape(-1, 1) # align the dimension
P = s * np.dot(R, P.T) + t
return P.T