3、三维空间刚体运动(下):李群与李代数基础、SO(3)与SE(3)的指数/对数映射、BCH公式与扰动模型、Sophus库使用
好,咱们接着聊。上一节我们把旋转矩阵、变换矩阵这些“硬骨头”啃完了。但你有没有想过一个问题——我们在优化位姿的时候,比如做BA(Bundle Adjustment),总不能直接去优化一个矩阵吧?矩阵有9个参数,但自由度只有3(旋转)或6(位姿)。直接优化,约束条件一大堆,搞不好就优化到非旋转矩阵上去了。
这时候,李群和李代数就登场了。说白了,它们就是一套“在流形上做优化”的工具。我个人觉得,这是VIO里最“数学”的一块,但也是最有意思的一块。搞懂了它,你再看那些优化库的代码,心里就有底了。
3.1 李群与李代数:为什么需要它们?
先说说“群”是什么。群就是一个集合加上一种运算,满足封闭性、结合律、有单位元和逆元。旋转矩阵构成的集合,加上矩阵乘法,就是一个群,我们叫它特殊正交群SO(3)。变换矩阵构成的群,叫特殊欧氏群SE(3)。
但问题是,SO(3)和SE(3)在局部看起来是“平坦”的吗?不是。它们是流形,是弯曲的。你想想看,在球面上走直线,走着走着方向就变了。在SO(3)上做加法,结果很可能不在SO(3)上。
李代数就是流形在单位元处的切空间。它是一个向量空间,我们可以在这个向量空间里做加法、做优化,然后再通过指数映射回到流形上。这就是整个思路的核心。
核心思想:在李代数(向量空间)上做优化,再通过指数映射回到李群(流形)上。
我记得我第一次接触这个概念时,觉得特别绕。后来在项目里调试一个VIO系统,发现位姿优化总是不收敛,检查了半天,发现是直接对旋转矩阵做了加法,导致矩阵不再是正交阵。从那以后,我就老老实实用李代数了。
3.2 SO(3)与SE(3)的指数/对数映射
指数映射,就是把李代数上的向量,映射到李群上的元素。对数映射则是反过来。
3.2.1 SO(3)的指数映射
对于SO(3),它的李代数是so(3)。so(3)里的元素是三维向量,或者说是三维向量的反对称矩阵。一个三维向量φ,它的反对称矩阵记作φ^。
指数映射的公式,就是罗德里格斯公式:
R = exp(φ^) = I + (sinθ/θ) * φ^ + ((1-cosθ)/θ²) * (φ^)²
其中θ = ||φ||。你看,这个公式是不是很眼熟?其实就是旋转向量到旋转矩阵的转换。没错,李代数so(3)本质上就是旋转向量。
对数映射就是反过来,从旋转矩阵R求旋转向量φ:
θ = arccos((tr(R)-1)/2)
φ = (θ/(2sinθ)) * (R - R^T)^∨
这里(·)^∨表示从反对称矩阵恢复成向量。
个人经验:在实际代码中,要注意θ接近0时的数值稳定性。当θ很小时,直接用公式会有除零风险。我一般会判断一下,如果θ小于1e-6,就直接用泰勒展开近似。
3.2.2 SE(3)的指数映射
SE(3)的李代数是se(3)。se(3)里的元素是一个六维向量,记作ξ = [ρ, φ]^T。其中ρ是平移部分,φ是旋转部分。
指数映射的公式稍微复杂一点:
T = exp(ξ^) = [R, t; 0, 1]
其中:
R = exp(φ^)
t = J * ρ
这里的J就是之前讲过的雅可比矩阵:
J = (sinθ/θ) * I + (1 - sinθ/θ) * aa^T + ((1-cosθ)/θ) * a^
其中a = φ/θ。
对数映射就是从T = [R, t; 0, 1]求ξ = [ρ, φ]^T。先通过R求φ,再通过t和J求ρ:
ρ = J^(-1) * t
3.3 BCH公式与扰动模型
现在问题来了:我们在李代数上做加法,对应到李群上是什么?
答案是:不是简单的乘法。两个李代数指数映射的乘积,等于它们“和”的指数映射,但这个“和”不是普通的加法,而是由BCH公式给出的。
BCH公式长这样:
ln(exp(A) * exp(B)) = A + B + 1/2[A,B] + 1/12[A,[A,B]] - 1/12[B,[A,B]] + ...
其中[A,B] = AB - BA,是李括号。
这个公式在实际中很少直接用,因为太复杂了。我们更常用的是它的近似形式,也就是扰动模型。
扰动模型的核心思想:在李代数上加上一个微小扰动,看李群上的变化。这样就把李群上的乘法,近似成了李代数上的加法。
对于SO(3),左扰动模型是这样的:
R * exp(φ^) ≈ exp((R * φ)^) * R
这个公式在求导时特别有用。我们想求R * p对R的导数,就可以用扰动模型,把对R的求导转化成对φ的求导。
我记得在写VIO的雅可比矩阵时,一开始总是搞不清楚该用左扰动还是右扰动。后来我统一用左扰动,因为左扰动的雅可比形式更简洁,而且和大多数开源代码(比如OKVIS、VINS-Mono)保持一致。
3.4 Sophus库使用
理论说完了,咱们来看看实际怎么用。Sophus是一个C++库,专门用来处理SO(3)和SE(3)的。它封装了指数映射、对数映射、扰动模型等操作,用起来非常方便。
先看一个简单的例子:
#include <sophus/so3.hpp>
#include <sophus/se3.hpp>
#include <iostream>
int main() {
// 创建一个SO(3)对象,从旋转矩阵
Eigen::Matrix3d R = Eigen::AngleAxisd(M_PI/4, Eigen::Vector3d(0,0,1)).toRotationMatrix();
Sophus::SO3d SO3_R(R);
// 从旋转向量
Sophus::SO3d SO3_v(0, 0, M_PI/4);
// 从四元数
Eigen::Quaterniond q(R);
Sophus::SO3d SO3_q(q);
// 对数映射:SO(3) -> so(3)
Eigen::Vector3d so3 = SO3_R.log();
std::cout << "so3: " << so3.transpose() << std::endl;
// 指数映射:so(3) -> SO(3)
Sophus::SO3d SO3_updated = Sophus::SO3d::exp(so3 + Eigen::Vector3d(0.1, 0, 0));
// 扰动模型:左乘一个小扰动
Sophus::SO3d SO3_perturbed = Sophus::SO3d::exp(Eigen::Vector3d(0.01, 0, 0)) * SO3_R;
return 0;
}
再看SE(3)的例子:
#include <sophus/se3.hpp>
int main() {
// 创建一个SE(3)对象
Eigen::Matrix3d R = Eigen::AngleAxisd(M_PI/4, Eigen::Vector3d(0,0,1)).toRotationMatrix();
Eigen::Vector3d t(1, 0, 0);
Sophus::SE3d SE3_Rt(R, t);
// 对数映射:SE(3) -> se(3)
Eigen::Matrix<double, 6, 1> se3 = SE3_Rt.log();
std::cout << "se3: " << se3.transpose() << std::endl;
// 指数映射:se(3) -> SE(3)
Sophus::SE3d SE3_updated = Sophus::SE3d::exp(se3 + Eigen::Matrix<double, 6, 1>::Zero());
// 对点进行变换
Eigen::Vector3d pt(1, 2, 3);
Eigen::Vector3d pt_transformed = SE3_Rt * pt;
return 0;
}
避坑指南:我曾经在Sophus的版本上踩过坑。老版本(比如1.0.0)的接口和新版本(比如22.04)的接口不一样。老版本用SO3Group,新版本用SO3d。建议统一用新版本,并且注意Eigen的对齐问题,特别是STL容器里放Sophus对象时,要加上EIGEN_MAKE_ALIGNED_OPERATOR_NEW。
3.5 知识体系总览
为了让你更直观地理解这一章的知识结构,我画了一张图:
这张图把这一章的核心逻辑串起来了。左边是李群,右边是李代数,中间通过指数/对数映射连接。下面是BCH公式和Sophus库,它们是实际工程中用的工具。
嗯,这一章的内容确实有点抽象。但别急,等你真正开始写VIO代码,处理位姿优化时,你就会发现这些工具是多么好用。下一节我们会把这些知识用到实际的VIO前端中,看看怎么用李代数来优化相机位姿。
我的建议:学这一章,不要死记公式。你只需要记住两件事:第一,优化要在李代数上做;第二,Sophus库帮你处理了所有映射细节。剩下的,多写代码,多调试,自然就理解了。