【原创】SLAM学习笔记(一)三维刚体运动旋转

三维空间的刚体运动

三维空间中,刚体的运动可以用两个概念来表示:旋转平移。平移比较简单一些,一般用一个表示位移的向量来表示。而旋转则有多种表示方法,例如旋转矩阵、旋转向量等等,不同的表示方法各有优劣。

旋转矩阵

设原始向量 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=(e1Te1e2Te1e3Te1e1Te2e2Te2e3Te2e1Te3e2Te3e3Te3)(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+(1cosθ)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)+(1cosθ)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=(12q222q322q1q2+2q0q3wq1q32q0q22q1q22q0q312q122q322q2q3+2q0q12q1q3+2q0q22q2q32q0q112q122q22)

设矩阵 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=4q0m23m32,q2=4q0m31m13,q3=4q0m12m21

注意: 由于 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,v a],[sb,v b]

  • 加减法
    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,v a±v b]

  • 乘法
    乘法将两个四元数的每一项两两相乘然后相加。注意其满足之前所述的四元数的性质。
    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,v av b]

该乘法定义下,两个实四元数的乘积仍然是实四元数。由于最后一项外积的存在,四元数的乘法不满足交换律,除非 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] qa=[sa,v a]

四元数共轭与其自身相乘可得一个实四元数,实部为模长的平方。

q ∗ q = q q ∗ = [ s a 2 + v T v , 0 ] q* q = qq* = [s^2_a+ v^Tv, 0] qq=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=qaqb

q − 1 = q ∗ ∣ ∣ q ∣ ∣ q^{-1}=\frac{q*}{||q||} q1=qq
可见,对于单位四元数,其逆和共轭相同。按照定义,四元数和自己逆的乘积为实四元数1 . 最后,四元数乘积的逆有和矩阵相乘相似的性质:

( q a q b ) − 1 = q b − 1 q a − 1 (q_aq_b)^{-1}=q_b^{-1}q_a^{-1} (qaqb)1=qb1qa1

数乘和点乘

四元数的数乘与点乘与向量相同:
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=qpq1

变换矩阵

解决了旋转问题,就可以考虑平移了。平移可以简单地用一个平移向量 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] [a1]=[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×4RSO3,tR3

代码

四元数的表示
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)

完整代码

代码完整版在这里

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

A.Star

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值