MTK数学库源码剖析:SO(3)与SE(3)的李群-李代数转换精讲

📘 深入理解流形上的微积分,从基础数学到工业级实现


📌 引言

在激光SLAM、视觉惯性里程计(VIO)以及卫星导航等领域,状态估计是核心问题。我们通常要估计机器人的姿态(旋转)和位姿(旋转+平移),这些量生活在流形上——它们对加法不封闭,不能直接用欧氏空间的微积分工具。

为了解决这一难题,李群与李代数理论应运而生。通过将流形上的元素映射到其切空间(李代数),我们可以把非线性优化问题转化为线性最小二乘问题,从而借助高斯-牛顿、LM等经典算法高效求解。

MTK(Manifold Toolkit) 是一个轻量级的 C++ 头文件库,提供了流形运算的基础数学工具。本文将结合其核心头文件 mtkmath.hpp,详细剖析 SO(3)SO(3)SO(3)SE(3)SE(3)SE(3) 的李群-李代数转换,并深入解析代码实现中的数值稳定技巧。


一、为什么需要李群和李代数?

1.1 旋转矩阵的"加法"困境

三维旋转矩阵 R∈SO(3)R \in SO(3)RSO(3) 满足 RR⊤=IRR^\top = IRR=I,行列式为 111

两个旋转矩阵相乘仍然是旋转矩阵(群乘法),但两个旋转矩阵相加一般不再是旋转矩阵。

然而,在优化算法中,我们需要对变量进行"加增量"操作(即 x←x+Δxx \leftarrow x + \Delta xxx+Δx),并求目标函数对增量的导数。如果直接在矩阵空间上进行加法,会破坏约束,导致结果不合法。

1.2 切空间的魔力

李代数 so(3)\mathfrak{so}(3)so(3) 是三维向量空间,它与 SO(3)SO(3)SO(3) 在单位元处的切空间同构。旋转向量(轴角)就是 so(3)\mathfrak{so}(3)so(3) 的元素:

  • 方向 → 旋转轴
  • 模长 → 旋转角度

在切空间中,我们可以自由做加减法、求梯度,然后通过指数映射将切空间中的增量"卷回"到流形上,保证结果始终处于流形内。

同理,SE(3)SE(3)SE(3) 的切空间 se(3)\mathfrak{se}(3)se(3) 是六维向量,涵盖了旋转和平移的增量。

💡 一句话总结:李代数负责"微积分",李群负责"几何约束"。二者的桥梁就是指数映射(exp⁡\expexp)和对数映射(log⁡\loglog)。


二、SO(3) 与 so(3) 的转换细节

2.1 定义与表示

李群:

SO(3)={R∈R3×3 | RR⊤=I, det⁡(R)=1} SO(3) = \left\{ R \in \mathbb{R}^{3\times3} \ \middle|\ RR^\top = I,\ \det(R) = 1 \right\} SO(3)={RR3×3  RR=I, det(R)=1}

  • 维数 = 3,由绕三个轴的旋转角度参数化。

李代数:

so(3)={ϕ∈R3} \mathfrak{so}(3) = \left\{ \phi \in \mathbb{R}^3 \right\} so(3)={ϕR3}

通常记 ϕ=θu\phi = \theta \mathbf{u}ϕ=θu,其中 u\mathbf{u}u 是单位轴,θ\thetaθ 是旋转角度。其反对称矩阵表示为:

ϕ∧=[0−ϕ3ϕ2ϕ30−ϕ1−ϕ2ϕ10] \phi^\wedge = \begin{bmatrix} 0 & -\phi_3 & \phi_2 \\ \phi_3 & 0 & -\phi_1 \\ -\phi_2 & \phi_1 & 0 \end{bmatrix} ϕ=0ϕ3ϕ2ϕ30ϕ1ϕ2ϕ10

2.2 指数映射(so(3) → SO(3))

这就是著名的罗德里格斯公式(Rodrigues’ Formula)

exp⁡(ϕ∧)=I+sin⁡θθϕ∧+1−cos⁡θθ2(ϕ∧)2 \exp(\phi^\wedge) = I + \frac{\sin\theta}{\theta}\phi^\wedge + \frac{1-\cos\theta}{\theta^2}(\phi^\wedge)^2 exp(ϕ)=I+θsinθϕ+θ21cosθ(ϕ)2

其中 θ=∥ϕ∥\theta = \|\phi\|θ=ϕ

⚠️ 当 θ\thetaθ 接近 0 时,直接用该公式会面临除零风险,故需要泰勒展开:

exp⁡(ϕ∧)≈I+ϕ∧+12(ϕ∧)2+⋯ \exp(\phi^\wedge) \approx I + \phi^\wedge + \frac{1}{2}(\phi^\wedge)^2 + \cdots exp(ϕ)I+ϕ+21(ϕ)2+

💻 代码对应:

  • hat(v) 生成 ϕ∧\phi^\wedgeϕ
  • cos_sinc_sqrt(x2) 输入 θ2\theta^2θ2,返回 first = cosθsecond = sinθ/θ(即 sinc\text{sinc}sinc);
  • A_matrix(v) 计算的是公式中 1−cos⁡θθ2\dfrac{1-\cos\theta}{\theta^2}θ21cosθθ−sin⁡θθ3\dfrac{\theta-\sin\theta}{\theta^3}θ3θsinθ 的组合系数。

2.3 对数映射(SO(3) → so(3))

给定旋转矩阵 RRR,提取旋转向量 ϕ\phiϕ

θ=arccos⁡(tr(R)−12),ϕ=θ2sin⁡θ[R32−R23R13−R31R21−R12] \theta = \arccos\left(\frac{\text{tr}(R) - 1}{2}\right), \qquad \phi = \frac{\theta}{2\sin\theta} \begin{bmatrix} R_{32} - R_{23} \\ R_{13} - R_{31} \\ R_{21} - R_{12} \end{bmatrix} θ=arccos(2tr(R)1),ϕ=2sinθθR32R23R13R31R21R12

(当 θ\thetaθ 极小时,使用泰勒展开避免除零。)

💻 代码对应: mtkmath.hpp 中的 log() 函数模板提供了泛型支持。对于纯旋转,w 是标量部分(单位四元数的实部),vec 是向量部分(虚部),它利用 atan2 提取出角度和轴。


三、SE(3) 与 se(3) 的转换细节

3.1 定义与表示

李群:

SE(3)={T=[Rt0⊤1] | R∈SO(3), t∈R3} SE(3) = \left\{ T = \begin{bmatrix} R & t \\ 0^\top & 1 \end{bmatrix} \ \middle|\ R \in SO(3),\ t \in \mathbb{R}^3 \right\} SE(3)={T=[R0t1]  RSO(3), tR3}

  • 维数 = 6

李代数:

se(3)={ξ=[ρϕ]∈R6} \mathfrak{se}(3) = \left\{ \xi = \begin{bmatrix} \rho \\ \phi \end{bmatrix} \in \mathbb{R}^6 \right\} se(3)={ξ=[ρϕ]R6}

其中 ρ\rhoρ 是平移分量,ϕ\phiϕ 是旋转分量。

⚠️ 注意:这里的平移 ρ\rhoρ 不是最终的平移量 ttt,而是经过雅可比矩阵缩放后的结果。

4×44\times44×4 矩阵表示:

ξ∧=[ϕ∧ρ0⊤0] \xi^\wedge = \begin{bmatrix} \phi^\wedge & \rho \\ 0^\top & 0 \end{bmatrix} ξ=[ϕ0ρ0]

3.2 指数映射(se(3) → SE(3))

exp⁡(ξ∧)=[exp⁡(ϕ∧)Jl(ϕ) ρ0⊤1] \exp(\xi^\wedge) = \begin{bmatrix} \exp(\phi^\wedge) & J_l(\phi)\,\rho \\ 0^\top & 1 \end{bmatrix} exp(ξ)=[exp(ϕ)0Jl(ϕ)ρ1]

  • 旋转部分 RRR:直接由 ϕ\phiϕ 通过 SO(3)SO(3)SO(3) 指数映射得到。
  • 平移部分 ttt:由左雅可比矩阵 JlJ_lJl 作用于 ρ\rhoρ 得到:

t=Jl(ϕ)⋅ρ t = J_l(\phi) \cdot \rho t=Jl(ϕ)ρ

其中左雅可比矩阵的闭式解为:

Jl(ϕ)=I+1−cos⁡θθ2ϕ∧+θ−sin⁡θθ3(ϕ∧)2 J_l(\phi) = I + \frac{1-\cos\theta}{\theta^2}\phi^\wedge + \frac{\theta - \sin\theta}{\theta^3}(\phi^\wedge)^2 Jl(ϕ)=I+θ21cosθϕ+θ3θsinθ(ϕ)2

💻 代码对应:

  • A_matrix(v) 函数计算的就是左雅可比矩阵 JlJ_lJl
  • exp() 模板函数使用 cos_sinc_sqrt 计算出 cos⁡θ\cos\thetacosθsinc θ\text{sinc}\,\thetasincθ,然后组合出旋转矩阵 RRR 和平移向量 ttt

3.3 对数映射(SE(3) → se(3))

已知变换矩阵 T=[Rt01]T = \begin{bmatrix} R & t \\ 0 & 1 \end{bmatrix}T=[R0t1]

  • 旋转部分 ϕ\phiϕ:直接由 RRR 通过 SO(3)SO(3)SO(3) 对数映射得到。
  • 平移部分 ρ\rhoρ:由左雅可比矩阵的逆 Jl−1J_l^{-1}Jl1 作用于 ttt 得到:

ρ=Jl−1(ϕ)⋅t \rho = J_l^{-1}(\phi) \cdot t ρ=Jl1(ϕ)t

💻 代码对应:

  • A_inv(v) 函数计算的就是 Jl−1J_l^{-1}Jl1
  • A_inv_trans(v) 则是其转置,在协方差传播中用于调整雅可比矩阵的变换方向。

四、mtkmath.hpp 源码精讲

该头文件位于 MTK 库的 src/mtkmath.hpp,包含若干模板函数,提供了流形运算所需的原子操作。下面逐一剖析关键函数。

4.1 辅助模板:

template<class Manifold>
struct traits {
    typedef typename Manifold::scalar scalar;
    enum { DOF = Manifold::DOF };
    typedef vect<DOF, scalar> vectorized_type;
    typedef Eigen::Matrix<scalar, DOF, DOF> matrix_type;
};

作用:从流形类中萃取标量类型、自由度、向量化类型和协方差矩阵类型。这是一种典型的类型特征(type traits) 模式,使算法可以统一处理不同的流形(如 SO3、SE3、S2)。

📌 注意:对于基本类型 floatdouble,提供了特化版本,继承自 Scalar<float> 等(Scalar 可能是一个简单封装,定义了 DOF=1 等)。

4.2 cos_sinc_sqrt —— 数值稳定的 cos(θ) 与 sinc(θ)

template<class scalar>
std::pair<scalar, scalar> cos_sinc_sqrt(const scalar &x2) {
    using std::sqrt; using std::cos; using std::sin;
    static scalar const taylor_0_bound = boost::math::tools::epsilon<scalar>();
    static scalar const taylor_2_bound = sqrt(taylor_0_bound);
    static scalar const taylor_n_bound = sqrt(taylor_2_bound);
    assert(x2 >= 0);
    if (x2 >= taylor_n_bound) {
        scalar x = sqrt(x2);
        return std::make_pair(cos(x), sin(x)/x);
    }
    // 泰勒展开(角度极小)
    static scalar const inv[] = {1/3., 1/4., 1/5., 1/6., 1/7., 1/8., 1/9.};
    scalar cosi = 1., sinc = 1;
    scalar term = -1/2. * x2;
    for (int i=0; i<3; ++i) {
        cosi += term;
        term *= inv[2*i];
        sinc += term;
        term *= -inv[2*i+1] * x2;
    }
    return std::make_pair(cosi, sinc);
}

阈值设定taylor_n_bound ≈ sqrt(sqrt(eps)),对于 double 约为 1e-4。当 θ2<10−8\theta^2 < 10^{-8}θ2<108(即 θ<10−4\theta < 10^{-4}θ<104)时,使用泰勒级数展开计算 cos⁡θ\cos\thetacosθsinc θ\text{sinc}\,\thetasincθ

泰勒展开推导

cos⁡θ=1−θ22!+θ44!−θ66!+⋯ \cos\theta = 1 - \frac{\theta^2}{2!} + \frac{\theta^4}{4!} - \frac{\theta^6}{6!} + \cdots cosθ=12!θ2+4!θ46!θ6+

sinc(θ)=sin⁡θθ=1−θ23!+θ45!−θ67!+⋯ \text{sinc}(\theta) = \frac{\sin\theta}{\theta} = 1 - \frac{\theta^2}{3!} + \frac{\theta^4}{5!} - \frac{\theta^6}{7!} + \cdots sinc(θ)=θsinθ=13!θ2+5!θ47!θ6+

代码中利用递推关系,每次迭代同时累加两项,减少了乘除次数。

工程意义:该函数返回的 (cos⁡θ, sin⁡θ/θ)(\cos\theta,\ \sin\theta/\theta)(cosθ, sinθ/θ) 是计算旋转矩阵和雅可比矩阵的基石。小角度下的直接计算会导致灾难性抵消sin⁡θ/θ\sin\theta/\thetasinθ/θ 趋近于 1,但 1−θ2/6+⋯1 - \theta^2/6 + \cdots1θ2/6+ 精度更高),因此这种分段策略是工业级代码的标配。

4.3 hat —— 反对称矩阵生成器

template<typename Base>
Eigen::Matrix<typename Base::scalar, 3, 3> hat(const Base& v) {
    Eigen::Matrix<typename Base::scalar, 3, 3> res;
    res << 0, -v[2], v[1],
           v[2], 0, -v[0],
           -v[1], v[0], 0;
    return res;
}

将三维向量映射到 so(3)\mathfrak{so}(3)so(3) 的矩阵表示。这是所有后续公式的基石。

4.4 A_matrix —— 左雅可比矩阵 JlJ_lJl

template<typename Base>
Eigen::Matrix<typename Base::scalar, 3, 3> A_matrix(const Base & v) {
    double squaredNorm = v[0]*v[0] + v[1]*v[1] + v[2]*v[2];
    double norm = std::sqrt(squaredNorm);
    if (norm < MTK::tolerance<typename Base::scalar>()) {
        return Eigen::Matrix<...>::Identity();
    } else {
        return I + (1 - cos(norm))/squaredNorm * hat(v) +
               (1 - sin(norm)/norm)/squaredNorm * hat(v) * hat(v);
    }
}

公式

Jl=I+1−cos⁡θθ2v∧+θ−sin⁡θθ3(v∧)2 J_l = I + \frac{1-\cos\theta}{\theta^2} v^\wedge + \frac{\theta-\sin\theta}{\theta^3} (v^\wedge)^2 Jl=I+θ21cosθv+θ3θsinθ(v)2

注意代码中第三项系数写成了 (1 - sin(norm)/norm) / squaredNorm,这与 (θ−sin⁡θ)/θ3(\theta-\sin\theta)/\theta^3(θsinθ)/θ3 等价。当 norm 小于容差时,直接返回单位矩阵,因为此时高阶项可忽略。

4.5 A_inv 与 A_inv_trans —— 左雅可比矩阵的逆及其转置

template<typename Base>
Eigen::Matrix<typename Base::scalar, 3, 3> A_inv(const Base& v) {
    // ...
    if (v.norm() > MTK::tolerance<...>()) {
        res = I - 0.5*hat(v) + (1 - v.norm()*cos(v.norm()/2) / (2*sin(v.norm()/2)))
                                * hat(v)*hat(v) / v.squaredNorm();
    } else { res = I; }
    return res;
}

该公式来源于 Jl−1J_l^{-1}Jl1 的闭式解,在推导中利用了 cot⁡(θ/2)\cot(\theta/2)cot(θ/2) 的级数展开。A_inv_trans 仅符号不同,对应 Jl−⊤J_l^{-\top}Jl,用于协方差传播时的变换方向。

4.6 exp —— 指数映射模板

template<class scalar, int n>
scalar exp(vectview<scalar, n> result, vectview<const scalar, n> vec, const scalar& scale = 1) {
    scalar norm2 = vec.squaredNorm();
    std::pair<scalar, scalar> cos_sinc = cos_sinc_sqrt(scale*scale * norm2);
    scalar mult = cos_sinc.second * scale;
    result = mult * vec;
    return cos_sinc.first;
}

此函数用于将切向量 vec 映射到流形上。对于单位四元数(对应 SO(3)SO(3)SO(3)),n=3 表示虚部,实部由返回的 cos⁡θ\cos\thetacosθ 给出。参数 scale 用于调整单位(比如角速度乘以时间步长)。返回 cos⁡θ\cos\thetacosθ,便于调用者构建完整的流形元素(如四元数的标量部分)。

4.7 log —— 对数映射模板

template<class scalar, int n>
void log(vectview<scalar, n> result,
         const scalar &w, const vectview<const scalar, n> vec,
         const scalar &scale, bool plus_minus_periodicity){
    scalar nv = vec.norm();
    if(nv < tolerance<scalar>()) {
        if(!plus_minus_periodicity && w < 0) {
            // 找最大分量,处理负实部情况
            int i; nv = vec.cwiseAbs().maxCoeff(&i);
            result = scale * std::atan2(nv, w) * vect<n, scalar>::Unit(i);
            return;
        }
        nv = tolerance<scalar>();
    }
    scalar s = scale / nv * (plus_minus_periodicity ? std::atan(nv / w) : std::atan2(nv, w));
    result = s * vec;
}

该函数是 exp 的逆,用于从流形元素 (w,vec)(w, vec)(w,vec) 提取切向量。当 nv(虚部范数)很小时,需要特殊处理。若 plus_minus_periodicity 为真,则认为 (w,vec)(w, vec)(w,vec)(−w,−vec)(-w, -vec)(w,vec) 等价(如单位四元数),此时使用 atan(nv/w) 的单值分支;否则使用 atan2(nv, w) 处理负实部。当 w < 0 且不允许周期性时,通过取绝对值最大分量确定方向,避免歧义。

4.8 其他辅助函数

  • tolerance<scalar>():返回 float1e-5double1e-11,用于判断小量。
  • normalize:角度归一化,已被注释(未使用)。
  • S2_w_expw_:球面 S2S^2S2 流形的指数映射偏导,用于重力向量等二自由度状态。

五、工程应用与注意事项

5.1 在 SLAM 中的应用

  • IMU 预积分:利用 explog 处理旋转增量的累加与误差传播。
  • 误差状态卡尔曼滤波(ESKF):需要 A_matrixA_inv 来更新协方差矩阵。
  • 图优化:在 Ceres 或 g2o 中,需要定义流形上的增量加法操作(boxplus),其底层即调用指数映射。

5.2 数值稳定性要点

  • 始终使用 cos_sinc_sqrt 而非直接计算 sin⁡θ/θ\sin\theta/\thetasinθ/θ,特别是在角度极小的情况。
  • 在雅可比矩阵计算中,当 norm 小于容差时,直接返回单位阵可避免除零。
  • 对数映射中的小量处理(如 plus_minus_periodicity)保证了结果的唯一性和平滑性。

5.3 性能优化提示

  • 代码中注释了使用霍纳方案和 SSE2 并行计算的优化思路,未来可进一步加速。
  • 对于固定类型 float/double,模板特化(如 tolerance)避免了运行时分支。

六、直观理解总结(物理意义)

映射方向公式物理意义代码核心函数
代数 → 群(Exp)R=exp⁡(ϕ∧)R = \exp(\phi^\wedge)R=exp(ϕ)给定绕某轴转多少角度,求出最终姿态。积分(累加增量)A_matrix, hat, cos_sinc_sqrt
群 → 代数(Log)ϕ=log⁡(R)∨\phi = \log(R)^\veeϕ=log(R)给定两个姿态,求出它们之间的旋转差向量。求差(做减法)A_inv, log
代数 → 群(Exp)T=exp⁡(ξ∧)T = \exp(\xi^\wedge)T=exp(ξ)给定角速度+线速度(体速),求经过该速度作用后的位姿。exp 模板(内部调 cos_sinc_sqrt
群 → 代数(Log)ξ=log⁡(T)∨\xi = \log(T)^\veeξ=log(T)给定两个位姿,求出它们之间的六维扰动(误差向量)。log 模板(内部调 A_inv

七、总结

MTK 的 mtkmath.hpp 虽然代码精简,却浓缩了流形微积分的核心算法。通过深入理解 cos_sinc_sqrthatA_matrixA_inv 等函数,我们不仅掌握了 SO(3)SO(3)SO(3)SE(3)SE(3)SE(3) 的转换细节,更学会了如何在实际工程中处理数值不稳定性问题。

在机器人学中,李代数(向量)用来做"加减"和"求导"(因为它是线性空间),李群(矩阵)用来做"复合"和"变换"(因为它保证了几何约束)。mtkmath.hpp 提供的 hatA_matrixA_inv 以及 cos_sinc_sqrt,正是在底层为这两个空间的自由转换提供了数值稳定且高效的数学支撑。

如果没有这些转换,所有的优化算法(如 Ceres、g2o 或 ESKF)都无法对旋转和位姿进行正确的求导。在自动驾驶、机器人导航等领域,正确理解和实现这些底层工具,是构建高精度状态估计系统的基石。希望本文能帮助读者扫清数学障碍,更加自信地阅读和修改此类底层代码。

延伸阅读:

  • Hertzberg et al., “Integrating generic sensor fusion algorithms with sound state representations through encapsulation of manifolds”, 2011.
  • Barfoot, “State Estimation for Robotics”, Cambridge University Press.
  • Solà et al., “A Micro Lie Theory for State Estimation in Robotics”, 2018.
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐