三维空间中,刚体的运动可以用两个概念来表示:旋转和平移。平移比较简单一些,一般用一个表示位移的向量来表示。而旋转则有多种表示方法,例如旋转矩阵、旋转向量等等,不同的表示方法各有优劣。
设原始向量 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)
旋转矩阵可以很好的描述相机的旋转了,但也有两个问题:
由于任意旋转都可以用一个旋转轴和一个旋转角来描述。那么,我们可以用一个向量,其方向为旋转轴 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)
代码完整版在这里