Pinocchio代码阅读——三维刚体运动
1. 四元数
在Pinocchio中
二维位姿由 $\mathbf{P_4}$ 表示:
三维位姿由 $\mathbf{P_7}$ 表示:
复数 $\cos{\theta} + \sin{\theta}i$
四元数 $\cos{\frac{\theta}{2}} + \sin{\frac{\theta}{2}}\mathbf{u}$ ($w, \mathbf{v})$
单位四元数($|q| = 1$)可以表示三维空间中的任意绕单位轴 $\mathbf{u}$ 旋转角度 $\theta$ 的旋转。
单位四元数 $q$ 和 $-q$ 表示同一个旋转,这一性质称为 SO(3) 的双覆盖(double cover)。在数值计算中,我们通常约定取 $w \ge 0$ 的那一个来保证唯一性。
2. 三维exp和log
2.1 $[\omega]_\times$的exp
2.1.1 $[\omega]_\times$的多项式
$[\omega]\times$的特征根根方程为三次多项式,根据凯莱哈密顿定理,$[\omega]^3\times$ 一定可以用低次表示。经过计算:
旋量也有相似的结论:
2.1.2 $e^{[\omega]_\times}$
2.1.2 $t$——平移部分
根据 $\mathbf{V}(\omega){\mathbf{V}(\omega)}^{-1}=\mathbf{I}$ ,整理多项式系数得到:
即
3. Pinocchio库中的log实现
3.1 log3
- 函数签名
1
2
3
4
5
6
7
8template<typename QuaternionLike>
Eigen::Matrix<
typename QuaternionLike::Scalar,
3, 1,
PINOCCHIO_EIGEN_PLAIN_TYPE(typename QuaternionLike::Vector3)::Options>
log3(
const Eigen::QuaternionBase<QuaternionLike> & quat,
typename QuaternionLike::Scalar & theta)
- 输入:
quat—— 单位四元数(通过Eigen::QuaternionBase接受任意兼容类型) - 输出:
- 返回值:3×1 旋转向量 $\boldsymbol{\omega} \in \mathbb{R}^3$
- 引用参数
theta:旋转角度 $\theta$(范围 $[0, \pi]$)
- 类型与常量准备
1
2
3
4
5
6
7
8
9
10typedef typename QuaternionLike::Scalar Scalar;
static constexpr int Options = ...;
typedef Eigen::Matrix<Scalar, 3, 1, Options> Vector3;
Vector3 res;
const Scalar norm_squared = quat.vec().squaredNorm();
static const Scalar eps = Eigen::NumTraits<Scalar>::epsilon();
static const Scalar ts_prec = TaylorSeriesExpansion<Scalar>::template precision<2>();
const Scalar norm = math::sqrt(norm_squared + eps * eps);
norm_squared= $|\mathbf{v}|^2$,即四元数虚部的平方范数eps:机器精度,用于正则化(避免除以零)ts_prec:泰勒展开的阈值,由TaylorSeriesExpansion<Scalar>::precision<2>()计算norm= $\sqrt{|\mathbf{v}|^2 + \epsilon^2}$:保证即使在零旋转时也有正数值,供atan2使用
- 符号归一化(关键步骤)
1
2
3
4
5const Scalar pos_neg = if_then_else(GE, quat.w(), Scalar(0), Scalar(+1), Scalar(-1));
Eigen::Quaternion<Scalar, Options> quat_pos;
quat_pos.w() = pos_neg * quat.w();
quat_pos.vec() = pos_neg * quat.vec();
由于 $q$ 和 $-q$ 表示同一旋转,为保证对数映射的唯一性,我们强制 $w \ge 0$:
- 若原
w < 0,则取负四元数(pos_neg = -1),虚部也相应变号 - 归一化后,角度 $\theta \in [0, \pi]$,保证了唯一性
- 计算角度 $\theta$
1
2
3
4
5
6
7
8const Scalar theta_2 = math::atan2(norm, quat_pos.w()); // in [0,pi]
const Scalar y_x = norm / quat_pos.w(); // nonnegative
const Scalar y_x_sq = norm_squared / (quat_pos.w() * quat_pos.w());
theta = if_then_else(
LT, norm_squared, ts_prec,
Scalar(2.) * (Scalar(1) - y_x_sq / Scalar(3)) * y_x,
Scalar(2.) * theta_2);
这里分两种情况:
精确分支(角度较大):$\theta = 2\theta2 = 2\arctan2(|\mathbf{v}|, w{\text{pos}})$
小角度分支(norm_squared < ts_prec):使用泰勒展开。令 $x = |\mathbf{v}| / w_{\text{pos}}$,则:
这个展开避免了在接近零时 atan2 的精度损失。
- 计算缩放因子
inv_sinc1
2
3
4
5const Scalar th2_2 = theta * theta / Scalar(4); // (theta/2)^2
const Scalar inv_sinc = if_then_else(
LT, norm_squared, ts_prec,
Scalar(2) * (Scalar(1) + th2_2 / Scalar(6) + Scalar(7) / Scalar(360) * th2_2 * th2_2),
theta / math::sin(theta_2));
inv_sinc 对应公式中的 $\theta / \sin(\theta/2)$:
精确分支:$\theta / \sin(\theta_2)$,其中 $\theta_2 = \theta/2$
小角度分支:利用 $\sin x = x - x^3/6 + x^5/120 - \cdots$:
代码中的 th2_2 = x^2,展开式与公式完全一致。
- 计算旋转向量
1
2
3
4for (Eigen::Index k = 0; k < 3; ++k)
res[k] = inv_sinc * quat_pos.vec()[k];
return res;
最终得到:
这正是 SO(3) 对数映射的公式。
- 数值稳定性策略总结
| 问题 | 解决方案 |
|---|---|
| $q$ 和 $-q$ 二义性 | 符号归一化:强制 $w \ge 0$ |
| 小角度时 $\sin(\theta/2) \to 0$ | 泰勒展开代替直接计算 |
| 除零风险 | 加 $\epsilon$ 正则化 |
3.2 log6
1 | template<typename Vector3Like, typename QuaternionLike, typename MotionDerived> |
在更完整的 run 函数中,log3 被用于合成 SE(3) 上的速度:1
2mout.linear() = vec - 0.5 * w.cross(vec) + beta * w.cross(w.cross(vec))
mout.angular() = w
其中 $w$ 是 log3 返回的旋转向量,vec 是位移向量,beta 是依赖于 $\theta$ 的系数。这个公式来源于 SE(3) 对数映射的线性部分,用于从位姿变化合成空间速度。1
2
3
4
5
6const Scalar th_2_squared = t2 / Scalar(4);
const Scalar beta_alt = (Scalar(1) / Scalar(3) - th_2_squared / Scalar(45)) / Scalar(4);
const Scalar beta = if_then_else(
LE, theta, TaylorSeriesExpansion<Scalar>::template precision<3>(),
static_cast<Scalar>(beta_alt),
static_cast<Scalar>(Scalar(1) / t2 - cot_th_2 * Scalar(0.5) / theta));
这里泰勒展开符号似乎是Pinocchio库写错了,已经提了issue。
3. Pinocchio库中的exp实现
3.1 轴角转四元数
1 | /** Set \c *this from an angle-axis \a aa and returns a reference to \c *this |
3.2 旋量exp
1 | /// |
依然是对小角度进行了泰勒展开处理。
3.2 平移exp
1 | /// \brief The se3 -> SE3 exponential map, using quaternions to represent the output rotation. |
3.2.1 函数签名与作用
exp6 实现了从李代数 se(3)(运动旋量/twist)到李群 SE(3)(刚体变换)的指数映射,输出采用 平移 + 单位四元数 的表示($\mathbb{R}^3 \times S^3$)。
函数签名如下:1
2template<typename MotionDerived, typename Config_t>
void exp6(const MotionDense<MotionDerived> & motion, Eigen::MatrixBase<Config_t> & qout)
- 输入:
motion是一个MotionDense类型,表示空间运动旋量,包含角速度angular()(记为 $\mathbf{w}$)和线速度linear()(记为 $\mathbf{v}$)。 - 输出:
qout是一个 7 维向量(或兼容的 Eigen 表达式),前 3 个元素为平移向量 $\mathbf{t}$,后 4 个元素为单位四元数 $\mathbf{q}$(顺序为 $x, y, z, w$,与 Eigen 的Quaternion内存布局一致)。 - 功能:计算单位时间内的指数映射:其中 $\hat{\xi}$ 是
se(3)的 4×4 矩阵表示。
3.2.2 类型与常量准备
1 | static constexpr int Options = PINOCCHIO_EIGEN_PLAIN_TYPE(Config_t)::Options; |
- 提取标量类型、向量类型,并定义四元数类型。
eps为机器精度,用于后续防止除零。
3.2.3 提取角速度与线速度
1 | const typename MotionDerived::ConstAngularType & w = motion.angular(); |
w为角速度向量,v为线速度向量。
3.2.4 计算角度与三角函数
1 | const Scalar t2 = w.squaredNorm() + eps * eps; |
t2为角速度模长平方(加eps²避免零模长导致除零)。t为角度 $\theta$。SINCOS是 Pinocchio 提供的宏/函数,同时计算sin(t)和cos(t),分别存入st和ct。
3.2.5 计算系数 $\alpha$ 和 $\beta$(带泰勒展开)
1 | const Scalar inv_t2 = Scalar(1) / t2; |
- 当
t < ts_prec时使用泰勒展开,否则使用精确公式。- $\alpha = \frac{1-\cos\theta}{\theta^2}$:
- 精确:
(1 - ct) * inv_t2 - 泰勒:$\frac{1}{2} - \frac{\theta^2}{24} + O(\theta^4)$,代码中
0.5 - t2/24(因为t2 ≈ θ²)。
- 精确:
- $\beta = \frac{\theta - \sin\theta}{\theta^3}$:
- 精确:
(t - st) * inv_t2 / t,即(θ - sinθ)/θ³。 - 泰勒:$\frac{1}{6} - \frac{\theta^2}{120} + O(\theta^4)$,代码中
1/6 - t2/120。
- 精确:
- $\alpha = \frac{1-\cos\theta}{\theta^2}$:
- 使用
if_then_else和LT实现无分支选择,利于向量化。
3.2.6 计算平移部分
1 | Eigen::Map<Vector3> trans_(qout.derived().template head<3>().data()); |
- 将
qout的前 3 个元素映射为Vector3(平移向量)。 - 直接计算:
noalias()避免临时变量,提高效率。
3.2.7 计算旋转部分(四元数)
1 | typedef Eigen::Map<Quaternion_t> QuaternionMap_t; |
- 将
qout的后 4 个元素映射为四元数。 - 调用之前解析过的
exp3函数,将角速度w转换为单位四元数,直接写入quat_。 - 注意
exp3要求输出四元数的coeffs()顺序为(x, y, z, w),与 Eigen 内存布局一致
