文章目录
三维空间的刚体运动
三维空间中,刚体的运动可以用两个概念来表示:旋转和平移。平移比较简单一些,一般用一个表示位移的向量来表示。而旋转则有多种表示方法,例如旋转矩阵、旋转向量等等,不同的表示方法各有优劣。
旋转矩阵
设原始向量
p
p
p可以表示为
(
e
1
,
e
2
,
e
3
)
(
a
1
,
a
2
,
a
3
)
T
(e_1,e_2,e_3)(a_1,a_2,a_3)^T
(e1,e2,e3)(a1,a2,a3)T, 旋转以后的向量
p
′
p'
p′表示为
(
e
1
′
,
e
2
′
,
e
3
′
)
(
a
1
′
,
a
2
′
,
a
3
′
)
T
(e'_1,e'_2,e'_3)(a'_1,a'_2,a'_3)^T
(e1′,e2′,e3′)(a1′,a2′,a3′)T。
对于向量
p
p
p,在两坐标系下应该相等,即
(
e
1
,
e
2
,
e
3
)
(
a
1
,
a
2
,
a
3
)
T
=
(
e
1
′
,
e
2
′
,
e
3
′
)
(
a
1
′
,
a
2
′
,
a
3
′
)
T
(e_1,e_2,e_3)(a_1,a_2,a_3)^T=(e'_1,e'_2,e'_3)(a'_1,a'_2,a'_3)^T
(e1,e2,e3)(a1,a2,a3)T=(e1′,e2′,e3′)(a1′,a2′,a3′)T
化简得
(
a
1
,
a
2
,
a
3
)
T
=
(
e
1
T
e
1
′
e
1
T
e
2
′
e
1
T
e
3
′
e
2
T
e
1
′
e
2
T
e
2
′
e
2
T
e
3
′
e
3
T
e
1
′
e
3
T
e
2
′
e
3
T
e
3
′
)
(
a
1
′
,
a
2
′
,
a
3
′
)
T
(a_1,a_2,a_3)^T=\Bigg(\begin{matrix} e_1^Te'_1 & e_1^Te'_2 & e_1^Te'_3\\ e_2^Te'_1 & e_2^Te'_2 & e_2^Te'_3\\ e_3^Te'_1 & e_3^Te'_2 & e_3^Te'_3\end{matrix}\Bigg) (a'_1,a'_2,a'_3)^T
(a1,a2,a3)T=(e1Te1′e2Te1′e3Te1′e1Te2′e2Te2′e3Te2′e1Te3′e2Te3′e3Te3′)(a1′,a2′,a3′)T
我们把中间的矩阵拿出来,定义成一个矩阵R ,它描述了旋转本身,又称为旋转矩阵,是由两组基之间的内积组成的。而且它是一个行列式为1的正交矩阵。它的逆(亦即转置)描述了一个相反的的旋转。
代码
旋转矩阵的表示
class MotionMatrix(Motion):
"""
旋转矩阵
"""
def __init__(self, matrix: np.ndarray):
self.matrix = npa(matrix)
assert self.matrix.shape == (3, 3)
assert np.isclose(np.linalg.det(self.matrix), 1)
旋转向量
旋转矩阵可以很好的描述相机的旋转了,但也有两个问题:
- 一次旋转只有3个自由度,但旋转矩阵却有9个量。显然这冗余了;
- 旋转矩阵自身带有两个约束。在进行估计和优化时,我们显然不想要这些附加的约束。
所以我们想要一个更紧凑的表达,最好就是3个量。而旋转向量就可以。
由于任意旋转都可以用一个旋转轴和一个旋转角来描述。那么,我们可以用一个向量,其方向为旋转轴 n n n 的方向,大小则为旋转角 θ \theta θ,这种向量就是旋转向量,表示为 θ n \theta n θn . 前面说到,外积可以用来表示旋转就是因为外积可以用来表示旋转向量:考虑两个不平行的向量 a a a 和 b b b,在右手法则下,用右手的4个指头从 a a a转向 b b b ,大拇指的朝向就是旋转向量的方向,即 a × b a\times b a×b的方向,而它的大小则由 a a a和 b b b 的夹角决定。
旋转向量和旋转矩阵的转换可以用罗德里格斯公式表示:
R
=
cos
θ
I
+
(
1
−
cos
θ
)
n
n
T
+
sin
θ
n
^
R=\cos\theta I+(1-\cos\theta)nn^T+\sin\theta n\hat{}
R=cosθI+(1−cosθ)nnT+sinθn^
两边同时取迹
t
r
(
R
)
=
cos
θ
t
r
(
I
)
+
(
1
−
cos
θ
)
t
r
(
n
n
T
)
+
sin
θ
t
r
(
n
^
)
=
1
+
2
cos
θ
tr(R)=\cos\theta tr(I)+(1-\cos\theta)tr(nn^T)+\sin\theta tr(n\hat{})=1+2\cos\theta
tr(R)=cosθtr(I)+(1−cosθ)tr(nnT)+sinθtr(n^)=1+2cosθ
得到
θ
=
arccos
(
t
r
(
R
)
−
1
2
)
\theta=\arccos(\frac{tr(R)-1}2)
θ=arccos(2tr(R)−1)
而旋转轴上的向量在旋转后不发生改变,所以
R
n
=
n
Rn=n
Rn=n
说明 n n n是旋转矩阵 R的特征值1所对应的特征向量。求解后并归一化则可得到 n n n .
代码
旋转角的表示
class MotionAxisAngle(Motion):
"""
旋转角
"""
def __init__(self, theta, n):
self.theta, self.n = theta, npa(n)
旋转矩阵与旋转角的相互转化
class MotionMatrix(Motion):
def to_axis_angle(self):
_, n, _ = solve_mat(self.matrix - 1, np.zeros(3)) # 需要导入 astar-math包,求解齐次线性方程组
return MotionAxisAngle(np.trace(np.arccos(self.matrix) / 2 - 0.5), n)
class MotionAxisAngle(Motion):
def to_matrix(self):
cos_theta = np.cos(self.theta)
sin_theta = np.sin(self.theta)
r = np.eye(3) * cos_theta + (1 - cos_theta) * self.n @ self.n.T + sin_theta * skew_symmetric(self.n)
return MotionMatrix(r)
欧拉角
除了旋转向量,我们也可以用欧拉角来紧凑地描述旋转。一个旋转可以分解成3次分别绕X、Y、Z 轴的旋转来表示,在航空摄影测量中,一般用“翻转 - 航偏 - 俯仰”(roll - yaw - pitch),也即XZY来表示。先绕X 轴旋转roll 角度,再绕Y 轴旋转yaw 角度,最后按照Z 轴旋转pitch 角度。这三个旋转矩阵相乘就得到了总的旋转矩阵。
此时,就可以用 ( r , p , y ) T (r,p,y)^T (r,p,y)T这三个量来描述任意旋转,这种表示方式会比其他方式更直观、更易理解。但是出现异常的"万向锁"–一旦选择±90°作为pitch角,就会导致第一次旋转和第三次旋转等价,整个旋转表示系统被限制在只能绕竖直轴旋转,丢失了一个表示维度。
代码
欧拉角表示
class MotionRPY(Motion):
def __init__(self, r, p, y):
self.r, self.p, self.y = r, p, y
旋转矩阵和欧拉角的转化
class MotionMatrix(Motion):
def to_rpy(self):
alpha = np.arctan(self.matrix[2, 1] / self.matrix[2, 2])
beta = np.arctan(-self.matrix[2, 0] / npl.norm(self.matrix[2, 1:]))
gamma = np.arctan(self.matrix[1, 0] / self.matrix[0, 0])
return MotionRPY(alpha, beta, gamma)
四元数
因为欧拉角和旋转向量具有奇异性, 因此我们需要用到四元数,它既是紧凑的,也没有奇异性。
定义:一个四元数包含一个实部和三个虚部。
q
⃗
=
q
0
+
q
1
i
+
q
2
j
+
q
3
k
=
[
s
,
v
⃗
]
\vec{q}=q_0+q_1i+q_2j+q_3k=[s,\vec{v}]
q=q0+q1i+q2j+q3k=[s,v]
其中,后面的等式将四元数表达成一个标量和一个向量,
i
,
j
,
k
i,j,k
i,j,k 表示四元数的三个虚部,满足:
{
i
2
=
j
2
+
k
2
=
−
1
i
k
=
k
,
j
i
=
−
k
j
k
=
i
,
k
j
=
−
i
k
i
=
j
,
i
k
=
−
j
\Bigg\{\begin{matrix}i^2=j^2+k^2=-1\\ik=k,ji=-k\\jk=i, kj=-i\\ki=j,ik=-j\end{matrix}
{i2=j2+k2=−1ik=k,ji=−kjk=i,kj=−iki=j,ik=−j
若一个四元数虚部全为0,则它是一个实四元数;如其实部为零,则称它为虚四元素。而且,一个虚四元数对应一个空间点。
我们能用单位四元数来表示三维空间中的任意一个旋转。我们先考虑下复数。在复数中,乘以 i i i 表示在复平面内旋转90度。但在四元数中,情形却有微妙的不同:乘以i 表示旋转180度,这样才能保证 i j = − k ij=-k ij=−k的性质。而 i 2 = − 1 i^2=-1 i2=−1 ,说明绕i 轴旋转360度后得到一个相反的东西,而要旋转720度(两周)才能得到它原先的样子。
假设某个旋转的旋转向量为 θ n ⃗ \theta \vec{n} θn , 则
q = ( cos θ 2 , n x sin θ 2 , n y sin θ 2 , n z sin θ 2 ) T q=(\cos\frac\theta 2,n_x\sin\frac\theta 2,n_y\sin\frac\theta 2,n_z\sin\frac\theta 2)^T q=(cos2θ,nxsin2θ,nysin2θ,nzsin2θ)T
反之则有
θ = 2 arccos q 0 \theta=2\arccos q_0 θ=2arccosq0 ( n x , n y , n z ) T = ( q 1 , q 2 , q 3 ) T / sin θ 2 (n_x,n_y,n_z)^T=(q_1,q_2,q_3)^T / \sin\frac\theta 2 (nx,ny,nz)T=(q1,q2,q3)T/sin2θ
上式给人一种“转了一半”的感觉。将上式中的 加上 [公式] 后得到一个相同的旋转,但是对应的四元数却变成了 − q -q −q. 所以,在四元数中,任意的旋转都可以由两个互为相反数的四元数表示。
而四元数和旋转矩阵的关系为:
R = ( 1 − 2 q 2 2 − 2 q 3 2 2 q 1 q 2 − 2 q 0 q 3 2 q 1 q 3 + 2 q 0 q 2 2 q 1 q 2 + 2 q 0 q 3 1 − 2 q 1 2 − 2 q 3 2 2 q 2 q 3 − 2 q 0 q 1 w q 1 q 3 − 2 q 0 q 2 2 q 2 q 3 + 2 q 0 q 1 1 − 2 q 1 2 − 2 q 2 2 ) R=\Bigg(\begin{matrix}1-2q_2^2-2q_3^2&2q_1q_2-2q_0q_3&2q_1q_3+2q_0q_2\\2q_1q_2+2q_0q_3&1-2q_1^2-2q_3^2&2q_2q_3-2q_0q_1\\wq_1q_3-2q_0q_2&2q_2q_3+2q_0q_1&1-2q_1^2-2q_2^2\end{matrix}\Bigg) R=(1−2q22−2q322q1q2+2q0q3wq1q3−2q0q22q1q2−2q0q31−2q12−2q322q2q3+2q0q12q1q3+2q0q22q2q3−2q0q11−2q12−2q22)
设矩阵 R = { m i j } , i , j ∈ { 1 , 2 , 3 } R=\{m_{ij}\}, i,j\in\{1,2,3\} R={mij},i,j∈{1,2,3} , 则由上式可以推得:
q 0 = t r ( R ) + 1 2 , q 1 = m 23 − m 32 4 q 0 , q 2 = m 31 − m 13 4 q 0 , q 3 = m 12 − m 21 4 q 0 q_0=\frac{\sqrt{tr(R)}+1}{2},q_1=\frac{m_{23}-m_{32}}{4q_0},q_2=\frac{m_{31}-m_{13}}{4q_0},q_3=\frac{m_{12}-m_{21}}{4q_0} q0=2tr(R)+1,q1=4q0m23−m32,q2=4q0m31−m13,q3=4q0m12−m21
注意: 由于 q q q和 − q -q −q 表示同一个旋转,所以一个旋转矩阵对应的四元数表示并不惟一且存在其他转换公式。在实际中,如果 q 0 q_0 q0 接近于0,会造成其他3个数的解不稳定,应采用其他公式。
四元数的运算
加减法、乘法
现有两个四元数 q a , q b q_a, q_b qa,qb, 向量表示分别为 [ s a , v ⃗ a ] , [ s b , v ⃗ b ] [s_a,\vec v_a], [s_b,\vec v_b] [sa,va],[sb,vb] 。
-
加减法
q a ± q b = [ s a ± s b , v ⃗ a ± v ⃗ b ] q_a \pm q_b = [s_a \pm s_b,\vec v_a \pm \vec v_b] qa±qb=[sa±sb,va±vb] -
乘法
乘法将两个四元数的每一项两两相乘然后相加。注意其满足之前所述的四元数的性质。
q a × q b = [ s a × s b , v ⃗ a ⋅ v ⃗ b ] q_a \times q_b = [s_a \times s_b,\vec v_a · \vec v_b] qa×qb=[sa×sb,va⋅vb]
该乘法定义下,两个实四元数的乘积仍然是实四元数。由于最后一项外积的存在,四元数的乘法不满足交换律,除非 v a v_a va 和 v b v_b vb 在 R 3 R^3 R3 中共线(此时外积为零)。
共轭
共轭即将虚部取为相反数:
q ∗ a = [ s a , − v ⃗ a ] q*_a=[s_a,-\vec v_a] q∗a=[sa,−va]
四元数共轭与其自身相乘可得一个实四元数,实部为模长的平方。
q ∗ q = q q ∗ = [ s a 2 + v T v , 0 ] q* q = qq* = [s^2_a+ v^Tv, 0] q∗q=qq∗=[sa2+vTv,0]
模长
∣
∣
q
a
∣
∣
=
s
a
2
+
x
a
2
+
y
a
2
+
z
a
2
||q_a||=\sqrt{s_a^2+x_a^2+y_a^2+z_a^2}
∣∣qa∣∣=sa2+xa2+ya2+za2
可知,两个四元数乘积的模即为二者模的乘积。所以,单位四元数相乘后仍然得到单位四元数。
∣
∣
q
a
q
b
∣
∣
=
∣
∣
q
a
∣
∣
∣
∣
q
b
∣
∣
||q_aq_b||=||q_a||||q_b||
∣∣qaqb∣∣=∣∣qa∣∣∣∣qb∣∣
逆
q
−
1
=
q
∗
∣
∣
q
∣
∣
q^{-1}=\frac{q*}{||q||}
q−1=∣∣q∣∣q∗
可见,对于单位四元数,其逆和共轭相同。按照定义,四元数和自己逆的乘积为实四元数1 . 最后,四元数乘积的逆有和矩阵相乘相似的性质:
( q a q b ) − 1 = q b − 1 q a − 1 (q_aq_b)^{-1}=q_b^{-1}q_a^{-1} (qaqb)−1=qb−1qa−1
数乘和点乘
四元数的数乘与点乘与向量相同:
k
q
=
[
k
s
,
k
v
]
kq=[ks,kv]
kq=[ks,kv]
q
a
q
b
=
s
a
s
b
+
x
a
x
b
i
+
y
a
y
b
j
+
z
a
z
b
k
q_aq_b=s_as_b+x_ax_bi+y_ay_bj+z_az_bk
qaqb=sasb+xaxbi+yaybj+zazbk
用四元数表示旋转
要用四元数来计算旋转,首先,用一个虚四元数来表示三维空间点:
p
=
[
0
,
x
,
y
,
z
]
T
=
[
0
,
v
]
p=[0,x,y,z]^T=[0,v]
p=[0,x,y,z]T=[0,v]
则旋转后的点
p
′
p'
p′则为
p
′
=
q
p
q
−
1
p'=qpq^{-1}
p′=qpq−1
变换矩阵
解决了旋转问题,就可以考虑平移了。平移可以简单地用一个平移向量
t
t
t来表示。则变换后的坐标为
a
′
=
R
a
+
t
a'=Ra+t
a′=Ra+t
但是,上面这个式子并不是线性的。所以我们引入了齐次坐标。在一个三维向量末尾加上一个1,就得到一个四维向量,成为齐次坐标。上式就可以写成
[
a
′
1
]
=
[
R
t
0
T
1
]
[
a
1
]
=
T
[
a
1
]
\left[\begin{matrix}a'\\1\end{matrix}\right]=\left[\begin{matrix}R&t\\0^T&1\end{matrix}\right]\left[\begin{matrix}a\\1\end{matrix}\right]=T\left[\begin{matrix}a\\1\end{matrix}\right]
[a′1]=[R0Tt1][a1]=T[a1]
S
E
3
=
[
R
t
0
T
1
]
∈
R
4
×
4
∣
R
∈
S
O
3
,
t
∈
R
3
SE_3=\left[\begin{matrix}R&t\\0^T&1\end{matrix}\right] \in R^{4\times4}|R\in SO_3,t\in R^3
SE3=[R0Tt1]∈R4×4∣R∈SO3,t∈R3
代码
四元数的表示
class MotionQuaternion(Motion):
def __init__(self, quaternion=None, s=None, v=None):
if quaternion is not None:
assert len(quaternion) == 4
self.quaternion = npa(quaternion)
elif s is not None and v is not None:
self.quaternion = npa([s, *v])
else:
raise ParameterValueError("error init params")
@property
def s(self):
return self.quaternion[0]
@property
def x(self):
return self.quaternion[1]
@property
def y(self):
return self.quaternion[2]
@property
def z(self):
return self.quaternion[3]
@property
def v(self):
return self.quaternion[1:]
四元数和旋转矩阵的转化
from liepack.domain.liealgebras import so
npa = np.array
def skew_symmetric(a: (np.ndarray, list)):
"""
获取反对称矩阵
:param a:
:return: 反对称矩阵
"""
mat = so(len(a))
mat.set_vector(a)
return mat
class MotionMatrix(Motion):
def to_quaternion(self):
s_4 = np.sqrt(np.trace(self.matrix) + 1) * 2
x = (self.matrix[1, 2] - self.matrix[2, 1]) / s_4
y = (self.matrix[2, 0] - self.matrix[0, 2]) / s_4
z = (self.matrix[0, 1] - self.matrix[1, 0]) / s_4
s = s_4 / 4
return MotionQuaternion([s, x, y, z])
class MotionQuaternion(Motion):
def to_matrix(self):
v_up = skew_symmetric(self.v) # 安装liepack
return MotionMatrix(self.v @ self.v.T + self.s * self.s * np.eye(3) + 2 * self.s * v_up + v_up @ v_up)