当前位置: 首页 > news >正文

【视觉SLAM十四讲】后端 1

文章目录

    • 状态估计问题
    • 线性系统和 KF
      • 预测
      • 更新
      • 总结
    • 非线性系统和 EKF
      • 线性化过程
      • 预测
      • 更新
      • 代码实现
    • BA 与图优化
      • 投影模型和代价函数
      • BA 的求解
      • 稀疏化和边缘化
      • 鲁棒核函数
      • 代码实现
        • BAL 数据集格式
        • Ceres BA 代码

这一章内容不算多,但难度并不低,主要是因为有着大量的数学式子要推导,我会尽可能将推导拆碎一些方便理解(所以不要被大量的数学式子吓到,只是因为大部分中间式和推导都写出来了,实际上没那么多)。强烈建议对着写写实现代码,不然很难将理论落地

状态估计问题

回顾我们之前提到过的运动和观测方程,在 t=0t=0t=0t=Nt=Nt=N ,有位姿 x0,⋯,xkx_0,\cdots,x_kx0,,xk ,路标 y1,⋯,yjy_1,\cdots,y_jy1,,yj

为了简化表达,我们设定 xkx_kxkkkk 时刻的所有未知量
xk=def{xk,y1,⋯,yj}x_k \overset{\text{def}}{=}\{x_k,y_1,\cdots,y_j\} xk=def{xk,y1,,yj}
此时运动方程和观测方程可以简化为
{xk=f(xk−1,uk)+ωkzk=h(xk)+vk\begin{cases} x_k=f(x_{k-1},u_k)+\omega_k\\z_k=h(x_k)+v_k \end{cases} {xk=f(xk1,uk)+ωkzk=h(xk)+vk
由于每个方程都受到噪声的影响,所以这里的 xxxyyy 可以看成是服从某个概率分布的随机变量。我们希望通过使用过去的 0 到 k 中的数据来估计现在的状态分布
P(xk∣x0,u1:k,z1:k)P(x_k|x_0,u_{1:k},z_{1:k}) P(xkx0,u1:k,z1:k)
按照贝叶斯法则 P(A∣B)=P(B∣A)⋅P(A)P(B)P(A|B)=\frac{P(B|A) \cdot P(A)}{P(B)}P(AB)=P(B)P(BA)P(A)
P(xk∣x0,u1:k,z1:k)∝P(zk∣xk)P(xk∣x0,u1:k,z1:k)P(x_k|x_0,u_{1:k},z_{1:k}) \propto P(z_k|x_k)P(x_k|x_0,u_{1:k},z_{1:k}) P(xkx0,u1:k,z1:k)P(zkxk)P(xkx0,u1:k,z1:k)
让我们回忆一下非线性优化中关于贝叶斯法则的内容:

在这个情境下,事件:状态 X 的取值;证据:实际观测数据 Z ;先验 P(X)P(X)P(X) :根据 0∼k−10\sim k-10k1 的各种数据推测得到的 kkk 时刻状态;
似然 P(Z∣X)P(Z|X)P(ZX) :假设状态是 X ,我们观测到数据 Z 的可能性有多大;后验 P(X∣Z)P(X|Z)P(XZ) :在观测到 kkk 时刻的观测数据 Z 时,状态 X 的概率分布

回到式子中来,可以理解为:根据 0∼k0\sim k0k 时刻所有数据得到的状态概率分布,正比于“对于假设的 kkk 时刻状态,最符合的观测数据”与“根据 0∼k−10\sim k-10k1 的各种数据推测得到的 kkk 时刻状态”的乘积。即:后验 = 似然 * 先验

对于这里的先验,它是由过去所有状态估计得来的,所以也可以展开为积分形式
P(xk∣x0,u1:k,z1:k−1)=∫P(xk∣xk−1,x0,u1:k,z1:k−1)P(xk−1∣x0,u1:k,z1:k−1)dxk−1P(x_k|x_0,u_{1:k},z_{1:k-1})=\int P(x_k|x_{k-1},x_0,u_{1:k},z_{1:k-1})P(x_{k-1}|x_0,u_{1:k},z_{1:k-1})dx_{k-1} P(xkx0,u1:k,z1:k1)=P(xkxk1,x0,u1:k,z1:k1)P(xk1x0,u1:k,z1:k1)dxk1
自此,我们已经得到了状态估计的贝叶斯估计,要进一步的使用它,有两种方法:

  1. 滤波派:以卡尔曼滤波为代表,基于马尔科夫性,认为 kkk 时刻状态只与 k−1k-1k1 时刻状态有关
  2. 优化派:以非线性优化为主体,考虑 kkk 时刻之前的所有状态

线性系统和 KF

当我们假设了马尔科夫性,那么 kkk 只与 k−1k-1k1 时刻状态有关,状态估计的贝叶斯估计还可以进一步简化
P(xk∣x0,u1:k,z1:k−1)=∫P(xk∣xk−1,uk)P(xk−1∣x0,u1:k−1,z1:k−1)dxk−1P(x_k|x_0,u_{1:k},z_{1:k-1})=\int P(x_k|x_{k-1},u_{k})P(x_{k-1}|x_0,u_{1:k-1},z_{1:k-1})dx_{k-1} P(xkx0,u1:k,z1:k1)=P(xkxk1,uk)P(xk1x0,u1:k1,z1:k1)dxk1
卡尔曼滤波建立在线性高斯系统上的,即满足

  1. 线性运动方程和观测方程
    {xk=Akxk−1+uk+ωkzk=Ckxk+vk\begin{cases} x_k=A_kx_{k-1}+u_k+\omega_k \\ z_k=C_kx_k+v_k \end{cases} {xk=Akxk1+uk+ωkzk=Ckxk+vk

  2. 状态和噪声满足高斯分布
    ωk∼N(0,Rk)vk∼N(0,Qk)\omega_k\sim N(0,R_k)\qquad v_k\sim N(0,Q_k) ωkN(0,Rk)vkN(0,Qk)

卡尔曼滤波可以分为预测和更新两个步骤( 为方便区分和表达,x^\hat{x}x^ 表示后验, xˇ\check{x}xˇ 表示先验,后续的 RRRQQQ 不再带下标。如果不想看推导的也可以直接跳到后面看公式)

预测

从上一时刻状态出发,根据输入信息(带噪声)推断当前时刻的状态分布,就是预测

根据高斯分布的性质,对于 y=Ax+b+ϵy=Ax+b+\epsilony=Ax+b+ϵ ,如果 x∼N(μx,Σx)ϵ∼N(0,Σϵ)x \sim N(\mu_x, \Sigma_x)\quad \epsilon \sim N(0, \Sigma_\epsilon)xN(μx,Σx)ϵN(0,Σϵ) ,那么 y∼N(Aμx+b,AΣxAT+Σϵ)y \sim N(A\mu_x + b, A\Sigma_x A^T + \Sigma_\epsilon)yN(Aμx+b,AΣxAT+Σϵ)

  • k−1k-1k1后验分布:P(xk−1∣x0,u1:k−1,z1:k−1)=N(x^k−1,P^k−1)P(x_{k-1}|x_0,u_{1:k-1},z_{1:k-1})=N(\hat{x}_{k-1},\hat{P}_{k-1})\quadP(xk1x0,u1:k1,z1:k1)=N(x^k1,P^k1) 其中 x^k−1\hat{x}_{k-1}x^k1是状态估计均值,P^k−1\hat{P}_{k-1}P^k1是协方差矩阵
  • 状态转移模型:P(xk∣xk−1,uk)=N(Akxk−1+uk,R)P(x_k|x_{k-1},u_k)=N(A_kx_{k-1}+u_k,R)\quadP(xkxk1,uk)=N(Akxk1+uk,R) 其中 AkA_kAk 是状态转移矩阵,uku_kuk 是控制输入, RkR_kRk 是过程噪声协方差

那么 P(xk∣x0,u1:k,z1:k−1)=∫N(Akxk−1+uk,R)⋅N(x^k−1,P^k−1))dxk−1P(x_k|x_0,u_{1:k},z_{1:k-1})=\int N(A_kx_{k-1}+u_k,R)\cdot N(\hat{x}_{k-1},\hat{P}_{k-1}))dx_{k-1}P(xkx0,u1:k,z1:k1)=N(Akxk1+uk,R)N(x^k1,P^k1))dxk1 ,即两个高斯分布的卷积

  1. 期望计算
    其期望 E[xk∣⋯]=E[Akxk−1+uk+ωk∣x0,u1:k,z1:k−1]\mathbb{E}[x_k|\cdots]=\mathbb{E}[A_kx_{k-1}+u_k+\omega_k|x_0,u_{1:k},z_{1:k-1}]E[xk]=E[Akxk1+uk+ωkx0,u1:k,z1:k1]

    • E[Akxk−1∣⋯]=AkE[xk−1∣⋯]=Akx^k−1\mathbb{E}[A_kx_{k-1}|\cdots]=A_k\mathbb{E}[x_{k-1}|\cdots]= A_k\hat{x}_{k-1}E[Akxk1]=AkE[xk1]=Akx^k1
    • E[uk∣⋯]=uk\mathbb{E}[u_k|\cdots]=u_kE[uk]=uk (确定项)
    • E[ωk∣⋯]=0\mathbb{E}[\omega_k|\cdots]=0E[ωk]=0 (零均值噪声)

    E[xk∣⋯]=Akx^k−1+uk\mathbb{E}[x_k|\cdots]=A_k\hat{x}_{k-1}+u_kE[xk]=Akx^k1+uk

  2. 协方差计算
    定义状态估计误差项 xk−E[xk]=Ak(xk−1−x^k−1)+ωkx_k-\mathbb{E}[x_k]=A_k(x_{k-1}-\hat{x}_{k-1})+\omega_kxkE[xk]=Ak(xk1x^k1)+ωk ,已知 Cov[x]=E[(x−E[x])(x−E[x])T]\text{Cov}[x]=\mathbb{E}[(x-\mathbb{E}[x])(x-\mathbb{E}[x])^T]Cov[x]=E[(xE[x])(xE[x])T]
    其协方差 Cov[xk]=E[(Ak(xk−1−x^k−1)+ωk)(Ak(xk−1−x^k−1)+ωk)T]\text{Cov}[x_k]=\mathbb{E}[(A_k(x_{k-1}-\hat{x}_{k-1})+\omega_k)(A_k(x_{k-1}-\hat{x}_{k-1})+\omega_k)^T]Cov[xk]=E[(Ak(xk1x^k1)+ωk)(Ak(xk1x^k1)+ωk)T]
    第二项计算内层转置得到:(xk−1−x^k−1)TAkT+ωkT(x_{k-1}-\hat{x}_{k-1})^TA^T_k+\omega_k^T(xk1x^k1)TAkT+ωkT
    展开得到:E[Ak(xk−1−x^k−1)(xk−1−x^k−1)TAKT]+E[Ak(xk−1−x^k−1)ωkT]+E[ωkAk(xk−1−x^k−1T)]+E[ωkωkT]\mathbb{E}[A_k(x_{k-1}-\hat{x}_{k-1})(x_{k-1}-\hat{x}_{k-1})^TA_K^T]+\mathbb{E}[A_k(x_{k-1}-\hat{x}_{k-1})\omega_k^T]+\mathbb{E}[\omega_kA_k(x_{k-1}-\hat{x}_{k-1}^T)]+\mathbb{E}[\omega_k\omega_k^T]E[Ak(xk1x^k1)(xk1x^k1)TAKT]+E[Ak(xk1x^k1)ωkT]+E[ωkAk(xk1x^k1T)]+E[ωkωkT]

    • 第一项:⋯=AkE[(xk−1−x^k−1)(xk−1−x^k−1)T]AkT=AkPk−1^AkT\cdots=A_k\mathbb{E}[(x_{k-1}-\hat{x}_{k-1})(x_{k-1}-\hat{x}_{k-1})^T]A^T_k=A_k\hat{P_{k-1}}A_k^T=AkE[(xk1x^k1)(xk1x^k1)T]AkT=AkPk1^AkT
    • 第二项和第三项:因为 ωk\omega_kωkxk−1x_{k-1}xk1 独立,故均等于 000
    • 第四项:E[ωkωkT]=R\mathbb{E}[\omega_k\omega_k^T]=RE[ωkωkT]=R

    Cov[xk]=AkP^k−1AkT+R\text{Cov}[x_k]=A_k\hat{P}_{k-1}A^T_k+RCov[xk]=AkP^k1AkT+R 最后我们得到了 P(xk∣x0,u1:k,z1:k−1)=N(Akx^k−1+uk,AkP^k−1AkT+R)P(x_k|x_0,u_{1:k},z_{1:k-1})=N(A_k\hat{x}_{k-1}+u_k,A_k\hat{P}_{k-1}A_k^T+R)P(xkx0,u1:k,z1:k1)=N(Akx^k1+uk,AkP^k1AkT+R) ,即 kkk 时刻的先验状态估计分布。为简化表达可定义

xˇk=Akx^k−1+ukPˇk=AkP^k−1AkT+R\check{x}_k=A_k\hat{x}_{k-1}+u_k \qquad \check{P}_k=A_k\hat{P}_{k-1}A_k^T+R\ xˇk=Akx^k1+ukPˇk=AkP^k1AkT+R 

更新

从当前时刻的预测状态出发,根据观测信息(带噪声)修正状态估计,得到更准确的后验分布,就是更新

为了求解 kkk 时刻的后验状态估计分布,我们还需要求解 kkk 时刻的似然

这里方法和上面求解先验的一样,都是可以通过期望和协方差得到的,不再赘述:P(zk∣xk)=N(Ckxk,Qk)P(z_k|x_k)=N(C_kx_k,Q_k)P(zkxk)=N(Ckxk,Qk) ,那么:
N(x^k,P^k)=N(Ckxk,Q)⋅N(xˇk,Pˇk)N(\hat{x}_k,\hat{P}_k)=N(C_kx_k,Q)\cdot N(\check{x}_k,\check{P}_k) N(x^k,P^k)=N(Ckxk,Q)N(xˇk,Pˇk)

已知高斯分布可以表达为(归一化常数和指数):
f(x)=1(2π)π2∣Σ∣12exp⁡(−12(x−μ)TΣ−1(x−μ))f(x)=\frac{1}{(2\pi)^\frac{\pi}{2}|\Sigma|^\frac{1}{2}}\exp({-\frac{1}{2}(x-\mu)^T\Sigma^{-1}(x-\mu)}) f(x)=(2π)2π∣Σ211exp(21(xμ)TΣ1(xμ))
其中 xxx 是我们要估计的系统状态,μ\muμ 则是 xxx 的期望值

因为我们只关心和 xkx_kxk 有关的部分,所以这里舍去归一化常数,只保留指数部分展开得到
(xk−x^k)TP^k−1(xk−x^k)=(zk−Ckxk)TQ−1(zk−Ckxk)+(xk−xˇk)TPˇk−1(xk−xˇk)(x_k-\hat{x}_k)^T\hat{P}^{-1}_k(x_k-\hat{x}_k)=(z_k-C_kx_k)^TQ^{-1}(z_k-C_kx_k)+(x_k-\check{x}_k)^T\check{P}^{-1}_k(x_k-\check{x}_k) (xkx^k)TP^k1(xkx^k)=(zkCkxk)TQ1(zkCkxk)+(xkxˇk)TPˇk1(xkxˇk)

两边展开(单纯比较长而已,其实不难算):
xkTP^k−1xk−2x^kTP^k−1xk+x^kTP^k−1x^k=xkT(CkTQ−1Ck+Pˇk−1)xk−2(zkTQ−1Ck+xˇkTPˇk−1)xk+(zkTQ−1zk+xˇkTPˇk−1xˇk)x_k^T \hat{P}^{-1}_k x_k - 2\hat{x}_k^T \hat{P}^{-1}_k x_k + \hat{x}_k^T \hat{P}^{-1}_k \hat{x}_k=x_k^T (C_k^T Q^{-1} C_k + \check{P}^{-1}_k) x_k - 2(z_k^T Q^{-1} C_k + \check{x}_k^T \check{P}^{-1}_k) x_k + (z_k^T Q^{-1} z_k + \check{x}_k^T \check{P}^{-1}_k \check{x}_k) xkTP^k1xk2x^kTP^k1xk+x^kTP^k1x^k=xkT(CkTQ1Ck+Pˇk1)xk2(zkTQ1Ck+xˇkTPˇk1)xk+(zkTQ1zk+xˇkTPˇk1xˇk)
取二次项系数
P^k−1=CkTQ−1Ck+Pˇk−1\hat{P}^{-1}_k=C_k^TQ^{-1}C_k+\check{P}^{-1}_k P^k1=CkTQ1Ck+Pˇk1
两边同乘 P^k\hat{P}_kP^k
I=P^kCkTQ−1Ck+P^kPˇk−1I=\hat{P}_kC_k^TQ^{-1}C_k+\hat{P}_k\check{P}^{-1}_k I=P^kCkTQ1Ck+P^kPˇk1
定义卡尔曼增益为
K=P^kCkTQ−1K=\hat{P}_kC_k^TQ^{-1} K=P^kCkTQ1
从而得到协方差更新公式(后续的式子转换都是以建立 kkk 时刻的先验与后验关系为核心)
I=KCk+P^kPˇk−1⇒P^k=(I−KCk)PˇkI=KC_k+\hat{P}_k\check{P}^{-1}_k \quad \Rightarrow \quad \hat{P}_k=(I-KC_k)\check{P}_k I=KCk+P^kPˇk1P^k=(IKCk)Pˇk
如果将这里的协方差更新公式代回到卡尔曼增益的定义中,我们就等到了卡尔曼增益的标准表达式
K=PˇkCkT(CkPˇkCkT+Q)−1K=\check{P}_kC^T_k(C_k\check{P}_kC^T_k+Q)^{-1} K=PˇkCkT(CkPˇkCkT+Q)1
取第一项系数
−2x^kTP^k−1xk=−2zkTQ−1Ckxk−2xˇkTPˇk−1xk-2\hat{x}_k^T \hat{P}^{-1}_k x_k=-2z^T_kQ^{-1}C_kx_k-2\check{x}^T_k\check{P}^{-1}_kx_k 2x^kTP^k1xk=2zkTQ1Ckxk2xˇkTPˇk1xk
整理并转置得到(注意标量的转置等于它本身)
P^k−1x^k=CkTQ−1zk−Pˇk−1xˇk\hat{P}_k^{-1}\hat{x}_k=C_k^TQ^{-1}z_k-\check{P}^{-1}_k\check{x}_k P^k1x^k=CkTQ1zkPˇk1xˇk
两边乘以 P^k\hat{P}_kP^k 并代回 K=P^kCkTQ−1K=\hat{P}_kC_k^TQ^{-1}K=P^kCkTQ1 得到
x^k=P^kCkTQ−1zk−P^kPˇk−1xˇk=Kzk+(I−KCk)xˇk=xˇk+K(zk−Ckxˇk)\begin{align} \hat{x}_k &= \hat{P}_kC_k^TQ^{-1}z_k-\hat{P}_k\check{P}_k^{-1}\check{x}_k \\ &= Kz_k+(I-KC_k)\check{x}_k \\ &=\check{x}_k+K(z_k-C_k\check{x}_k) \end{align} x^k=P^kCkTQ1zkP^kPˇk1xˇk=Kzk+(IKCk)xˇk=xˇk+K(zkCkxˇk)

总结

至此,预测和更新的主要公式我们都已推导完毕,这里做个总结

  1. 预测
    xˇk=Akx^k−1+uk,Pˇk=AkP^k−1AkT+R\check{x}_k=A_k\hat{x}_{k-1}+u_k,\quad \check{P}_k=A_k\hat{P}_{k-1}A^T_k+R xˇk=Akx^k1+uk,Pˇk=AkP^k1AkT+R

  2. 更新
    先计算卡尔曼增益:
    K=PˇkCkT(CkPˇkCkT+Qk)−1K=\check{P}_kC_k^T(C_k\check{P}_kC^T_k+Q_k)^{-1} K=PˇkCkT(CkPˇkCkT+Qk)1
    然后计算后验概率的分布:
    x^=xˇk+K(zk−Ckxˇk)P^k=(I−KCk)Pˇk\hat{x}=\check{x}_k+K(z_k-C_k\check{x}_k)\\ \hat{P}_k=(I-KC_k)\check{P}_k x^=xˇk+K(zkCkxˇk)P^k=(IKCk)Pˇk

非线性系统和 EKF

理解完卡尔曼滤波之后,我们必须要明白 SLAM 中的运动方程和观测方程通常是非线性的。一个高斯分布在经过非线性变换后,其结果往往不再是高斯分布,所以在非线性系统中,我们必须将一个非高斯分布近似为高斯分布

扩展卡尔曼滤波(EKF)和卡尔曼滤波(KF)本质上是一样的,只不过适用于非线性系统。其实现思路并不复杂:在某个点附近,对运动方程和观测方程进行一阶泰勒展开,保留一阶项(线性部分),然后按照线性系统推导

线性化过程

x^k−1\hat{x}_{k-1}x^k1 处对运动方程 f(xk−1,uk)f(x_{k-1},u_k)f(xk1,uk) 进行一阶泰勒展开
xk≈f(x^k−1,uk)+∂f∂xk−1∣x^k−1(xk−1−x^k−1)+ωkx_k \approx f(\hat{x}_{k-1},u_k)+\left.\frac{\partial f}{\partial x_{k-1}}\right|_{\hat{x}_{k-1}}(x_{k-1}-\hat{x}_{k-1})+\omega_k xkf(x^k1,uk)+xk1fx^k1(xk1x^k1)+ωk
记偏导数为
F=∂f∂xk−1∣x^k−1F=\left.\frac{\partial f}{\partial x_{k-1}}\right|_{\hat{x}_{k-1}} F=xk1fx^k1
同理,在 x^k\hat{x}_kx^k 处对运动方程 h(xk,uk)h(x_k,u_k)h(xk,uk) 进行一阶泰勒展开
zk≈h(xˇk)+∂h∂xk∣x^k+vkz_k\approx h(\check{x}_k)+\left.\frac{\partial h}{\partial x_k}\right|_{\hat{x}_k}+v_k zkh(xˇk)+xkhx^k+vk
记偏导数为
H=∂h∂xk∣x^kH=\left.\frac{\partial h}{\partial x_k}\right|_{\hat{x}_k} H=xkhx^k

预测

推导其实和卡尔曼滤波是极其相似的,这里就只展示结果了

状态预测:
xˇk=f(x^k−1,uk)\check{x}_k=f(\hat{x}_{k-1},u_k) xˇk=f(x^k1,uk)
协方差预测:
Pˇk=FkP^k−1FkT+R\check{P}_k=F_k\hat{P}_{k-1}F_k^T+R Pˇk=FkP^k1FkT+R

更新

卡尔曼增益:
K=PˇkHkT(HkPˇkHkT+Q)−1K=\check{P}_kH^T_k(H_k\check{P}_kH^T_k+Q)^{-1} K=PˇkHkT(HkPˇkHkT+Q)1
状态更新
x^k=xˇk+K(zk−h(xˇk))\hat{x}_k=\check{x}_k+K(z_k-h(\check{x}_k)) x^k=xˇk+K(zkh(xˇk))
协方差更新
P^k=(I−KHk)Pˇk\hat{P}_k=(I-KH_k)\check{P}_k P^k=(IKHk)Pˇk

代码实现

class ExtendedKalmanFilter{
public:ExtendedKalmanFilter() : state_size_(8){// 初始化状态向量state_ = Eigen::VectorXd::Zero(state_size_);// 初始化状态协方差矩阵P_ = Eigen::MatrixXd::Identity(state_size_, state_size_);// 初始化过程噪声协方差矩阵R_ = Eigen::MatrixXd::Zero(state_size_, state_size_);R_.diagonal() << 0.1, 0.1, 0.1, 0.05, 0.5, 0.5, 0.5, 0.2;// 初始化测量噪声协方差矩阵Q_ = Eigen::MatrixXd::Zero(4, 4);Q_.diagonal() << 0.1, 0.1, 0.05, 0.05;// 初始化雅可比矩阵F_ = Eigen::MatrixXd::Identity(state_size_, state_size_);H_ = Eigen::MatrixXd::Zero(4, state_size_);H_.block(0, 0, 4, 4) = Eigen::MatrixXd::Identity(4, 4);}// 状态转移函数(运动模型)Eigen::VectorXd stateTransitionFunction(const Eigen::VectorXd& state, double dt){Eigen::VectorXd new_state = state;// 位置更新new_state(0) += state(4) * dt;new_state(1) += state(5) * dt;new_state(2) += state(6) * dt;// yaw 更新new_state(3) += state(7) * dt;return new_state;}// 计算状态转移雅可比矩阵Eigen::MatrixXd computeStateJacobian(const Eigen::VectorXd& state, double dt){Eigen::MatrixXd J = Eigen::MatrixXd::Identity(state_size_, state_size_);// 位置对速度的偏导J(0, 4) = dt;J(1, 5) = dt;J(2, 6) = dt;J(3, 7) = dt;return J;}// 观测函数(测量模型)Eigen::VectorXd observationFunction(const Eigen::VectorXd& state){Eigen::VectorXd observation = state.head(4);  // x, y, z, yawreturn observation;}// 计算观测函数雅可比矩阵Eigen::MatrixXd computeObservationJacobian(const Eigen::VectorXd& state){Eigen::MatrixXd J = Eigen::MatrixXd::Zero(4, state_size_);J.block(0, 0, 4, 4) = Eigen::MatrixXd::Identity(4, 4);return J;}// 预测步骤void predict(double dt){// 计算状态转移函数的雅可比矩阵F_ = computeStateJacobian(state_, dt);// 预测状态state_ = stateTransitionFunction(state_, dt);// 预测协方差P_ = F_ * P_ * F_.transpose() + R_;}// 更新步骤void update(const Eigen::VectorXd& z){// 计算观测函数的雅可比矩阵H_ = computeObservationJacobian(state_);// 计算卡尔曼增益Eigen::MatrixXd tmp = H_ * P_ * H_.transpose() + Q_;Eigen::MatrixXd K = P_ * H_.transpose() * tmp.inverse();// 计算观测残差: y = z - h(x)Eigen::VectorXd z_pred = observationFunction(state_);Eigen::VectorXd y = z - z_pred;// 角度归一化if (y(3) > M_PI) y(3) -= 2 * M_PI;if (y(3) < -M_PI) y(3) += 2 * M_PI;// 更新状态state_ = state_ + K * y;// 更新协方差Eigen::MatrixXd I = Eigen::MatrixXd::Identity(state_size_, state_size_);P_ = (I - K * H_) * P_;std::cout << "state: " << state_.transpose() << std::endl;}// 获取当前状态Eigen::VectorXd getState() const { return state_; }// 获取当前协方差Eigen::MatrixXd getCovariance() const { return P_; }// 设置初始状态void setState(const Eigen::VectorXd& init_state) { state_ = init_state; }private:// 状态向量:[x, y, z, yaw, vx, vy, vz, v_yaw]Eigen::VectorXd state_;              // 状态向量Eigen::MatrixXd P_;                  // 状态协方差矩阵Eigen::MatrixXd R_;                  // 过程噪声协方差矩阵Eigen::MatrixXd Q_;                  // 测量噪声协方差矩阵Eigen::MatrixXd F_;                  // 状态转移函数的雅可比矩阵Eigen::MatrixXd H_;                  // 观测函数的雅可比矩阵int state_size_;                     // 状态向量大小
};int main()
{ExtendedKalmanFilter ekf;// 设置初始状态 [x, y, z, yaw, vx, vy, vz, v_yaw]Eigen::VectorXd init_state(8);init_state << 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.1;ekf.setState(init_state);double dt = 0.1;int step = 10;for (int i = 0; i < step; ++i){std::cout << "step: " << i << std::endl;// 预测ekf.predict(dt);// 生成模拟观测Eigen::VectorXd true_state = ekf.getState();Eigen::VectorXd z(4);z << true_state(0) + 0.05 * (rand() / (double)RAND_MAX - 0.5),true_state(1) + 0.05 * (rand() / (double)RAND_MAX - 0.5),true_state(2) + 0.02 * (rand() / (double)RAND_MAX - 0.5),true_state(3) + 0.02 * (rand() / (double)RAND_MAX - 0.5);// 更新步骤ekf.update(z);// 输出结果std::cout << "True State: " << true_state.transpose() << std::endl;std::cout << "Estimated State: " << ekf.getState().transpose() << std::endl;std::cout << "Covariance: " << ekf.getCovariance() << std::endl;}
}

BA 与图优化

“Bundle Adjustment” 直译过来就是光束调整,即考虑从任意特征点发射出来的几束光线,通过调整个相机姿态和各特征点的空间位置,使这些光线最终收束到相机的光心(简单点说就是要让所有图像中的特征点看起来都来自同一个空间位置)、

投影模型和代价函数

回顾一下前面的坐标系转换,从世界坐标系中的 ppp 点出发,考虑相机的内外参数和畸变,最后投影成像素坐标

  1. 世界坐标系 →\rightarrow 相机坐标系
    P′=Rp+t=[X′,Y′,Z′]TP'=Rp+t=[X',Y',Z']^T P=Rp+t=[X,Y,Z]T

  2. PPP 投影至归一化平面
    Pc=[uc,vc,1]T=[X′/Z′,Y′/Z′,1]TP_c=[u_c,v_c,1]^T=[X'/Z',Y'/Z',1]^T Pc=[uc,vc,1]T=[X/Z,Y/Z,1]T

  3. 去畸变(这里仅考虑径向)
    {uc′=uc(1+k1rc2+k2rc4)vc′=vc(1+k1rc2+k2rc4)\begin{cases} u_c'=u_c(1+k_1r_c^2+k_2r_c^4)\\v_c'=v_c(1+k_1r_c^2+k_2r_c^4) \end{cases} {uc=uc(1+k1rc2+k2rc4)vc=vc(1+k1rc2+k2rc4)

  4. 相机坐标系 →\rightarrow 像素坐标系
    {us′=fxuc′+cxvs=fyvc′+cy\begin{cases} u_s'=f_xu_c'+c_x\\v_s=f_yv_c'+c_y \end{cases} {us=fxuc+cxvs=fyvc+cy

这个过程也是我们前面讲到过的观测方程:z=h(x,y)z=h(x,y)z=h(x,y)

如果假定 xxx 为此时相机的位姿(外参 R,tR,tR,t ),对应的李群为 TTT ,李代数为 ξ\xiξ 。路标 yyy 即三维点 ppp ,观测数据即像素坐标 z=def[us,vs]Tz\overset{def}{=}[u_s,v_s]^Tz=def[us,vs]T 。那么,此次观测的误差可表示为:
e=z−h(T,p)e=z-h(T,p) e=zh(T,p)
考虑上其他时刻的数据,设 zi,jz_{i,j}zi,j 为在位姿 TiT_{i}Ti 处观测路标 pjp_jpj 产生的数据,那么代价函数可写为:
12∑i=1m∑j=1n∣∣eij∣∣2=12∑i=1m∑j=1n∣∣zij−h(Ti,pj)∣∣2\frac{1}{2}\sum\limits_{i=1}^m\sum\limits_{j=1}^n||e_{ij}||^2=\frac{1}{2}\sum\limits_{i=1}^m\sum\limits_{j=1}^n||z_{ij}-h(T_i,p_j)||^2 21i=1mj=1n∣∣eij2=21i=1mj=1n∣∣zijh(Ti,pj)2

BA 的求解

在前面的非线性优化中,我们已经提过了误差项,不同的是,那一章的误差项是针对单个位姿和路标的,但是在整体 BA 的目标函数上,我们要定义自变量为所有待优化的变量

和前面的最小重投影误差不同,虽然原理是相同的,但是前面视觉里程计中使用的是交替优化,先优化位姿,再利用得到位姿优化 3D 点;BA 则是联合优化,同时优化所有位姿和 3D 点

设对相机姿态的偏导为 FFF ,对路标点的偏导为 EEE ,目标函数变为:
12∣∣f(x+Δx)∣∣2≈12∑i=1m∑j=1n∣∣eij+FijΔξi+EijΔpj∣∣2\frac{1}{2}||f(x+\Delta x)||^2\approx\frac{1}{2}\sum\limits_{i=1}^m\sum\limits_{j=1}^n||e_{ij}+F_{ij}\Delta\xi_i+E_{ij}\Delta p_j||^2 21∣∣f(x+Δx)221i=1mj=1n∣∣eij+FijΔξi+EijΔpj2
BA 的变量是针对整体的,所以把所有位姿作为一个变量 xcx_cxc ,所有空间点作为一个变量 xpx_pxp
xc=[ξ1,ξ2,⋯,ξm]T∈R6mxp=[p1,p2,⋯,pn]T∈R3nx_c=[\xi_1,\xi_2,\cdots,\xi_m]^T \in \mathbb{R}^{6m}\\ x_p=[p_1,p_2,\cdots,p_n]^T\in\mathbb{R}^{3n} xc=[ξ1,ξ2,,ξm]TR6mxp=[p1,p2,,pn]TR3n
那么目标函数可简化为:
12∣∣f(x+Δx)∣∣2=12∣∣e+FΔxc+EΔxp∣∣2\frac{1}{2}||f(x+\Delta x)||^2=\frac{1}{2}||e+F\Delta x_c+E\Delta x_p||^2 21∣∣f(x+Δx)2=21∣∣e+FΔxc+EΔxp2

该式将许多小型二次项之和变为矩阵形式,那么这里的雅可比矩阵 EEEFFF 必须是整体目标函数对整体变量的导数,其中由对每个误差项的导数 FijF_{ij}FijRijR_{ij}Rij 组合而成

于是我们又回到了经典的增量线性方程
HΔx=gH\Delta x=g HΔx=g
以高斯牛顿法为例,将变量归类为位姿和空间点两种,雅可比矩阵可分块为
J=[FE]J=[F~~E] J=[F  E]
HHH 矩阵为
H=JTJ=[FTFFTEETFETE]H=J^TJ=\begin{bmatrix}F^TF & F^TE \\ E^TF & E^TE\end{bmatrix} H=JTJ=[FTFETFFTEETE]
因为考虑了所有的优化变量,这个线性方程的维度将非常大,如果直接对 HHH 求逆的话计算起来会十分困难。幸运的是,这个矩阵是有特殊结构的,可以借此简化运算

稀疏化和边缘化

已知每个误差项只依赖于相机位姿和路标点,因此雅可比矩阵 JJJ 是高度稀疏的,而继承于 JJJHHH 矩阵更是增强了这种稀疏性

误差项只描述了在 TiT_iTi 观测到 pjp_jpj 的情况,只涉及到第 iii 个相机位姿和第 jjj 个路标点,ss对其余部分的导数为 0 ,所以该误差项对应的雅可比可写为
Jij(x)=(02×6,⋯02×6,∂eij∂Ti,02×6,⋯02×3,⋯02×3,∂eij∂pj,02×3,⋯02×3)J_{ij}(x)=\left( 0_{2\times6},\cdots0_{2\times6},\frac{\partial e_{ij}}{\partial T_i},0_{2\times6},\cdots0_{2\times3},\cdots0_{2\times3},\frac{\partial e_{ij}}{\partial p_j},0_{2\times3},\cdots0_{2\times3} \right) Jij(x)=(02×6,02×6,Tieij,02×6,02×3,02×3,pjeij,02×3,02×3)

可以看出,除了表示相机姿态的偏导 ∂eij∂Ti\frac{\partial e_{ij}}{\partial T_i}Tieij 维度为 2×62\times62×6 ,对路标点的偏导 ∂eij∂pj\frac{\partial e_{ij}}{\partial p_j}pjeij 维度为 2×32\times32×3 ,其余地方均为 0

如图,如果相机分别在位姿 C1C_1C1C2C_2C2 处,观察到的特征点情况为:

在这里插入图片描述

那么雅可比矩阵和 HHH 矩阵如下图所示:

在这里插入图片描述

对于这种稀疏结构的 HHH 矩阵,我们可以利用 Schur\text{Schur}Schur 消元求解。如下图,我们将 HHH 矩阵分为四块,BBBCCC 为对角块矩阵,分别与相机位姿和路标的维度相同

在这里插入图片描述

此时对应的增量方程可以由 HΔx=gH\Delta x=gHΔx=g 变为:
[BEETC][ΔxcΔxp]=[vω]\begin{bmatrix} B&E\\E^T&C \end{bmatrix}\begin{bmatrix} \Delta x_c\\\Delta x_p \end{bmatrix}=\begin{bmatrix} v\\\omega \end{bmatrix} [BETEC][ΔxcΔxp]=[vω]
其中:

  • B=FTFB=F^TFB=FTF :相机-相机块,维度为 6×66\times66×6
  • C=ETEC=E^TEC=ETE :路标-路标块,维度为 3×33\times33×3
  • E=FTEE=F^TEE=FTE :相机-路标耦合块,维度为 6×36\times36×3

由于一般矩阵求逆的难度远远大于对角块矩阵,所以我们要对一般矩阵 EEE 进行高斯消元
[I−EC−10I][BEETC][ΔxcΔxp]=[I−EC−10I][vω]\begin{bmatrix} I&-EC^{-1}\\0&I \end{bmatrix}\begin{bmatrix} B&E\\E^T&C \end{bmatrix}\begin{bmatrix} \Delta x_c\\\Delta x_p \end{bmatrix}=\begin{bmatrix} I&-EC^{-1}\\0&I \end{bmatrix}\begin{bmatrix} v\\\omega \end{bmatrix} [I0EC1I][BETEC][ΔxcΔxp]=[I0EC1I][vω]
整理得到:
[B−EC−1ET0ETC][ΔxcΔxp]=[v−EC−1ωω]\begin{bmatrix} B-EC^{-1}E^T&0\\E^T&C \end{bmatrix}\begin{bmatrix} \Delta x_c\\\Delta x_p \end{bmatrix}=\begin{bmatrix} v-EC^{-1}\omega\\\omega \end{bmatrix} [BEC1ETET0C][ΔxcΔxp]=[vEC1ωω]
消元之后,第一行就变成了和 Δxp\Delta x_pΔxp 无关的项,单独提取出来我们就得到了关于位姿部分的增量方程:
[B−EC−1ET]Δxc=v−EC−1ω\begin{bmatrix} B-EC^{-1}E^T \end{bmatrix}\Delta x_c=v-EC^{-1}\omega [BEC1ET]Δxc=vEC1ω
该方程没有什么特殊的结构了,就只是一个线性方程

求解该方程得到 Δxc\Delta x_cΔxc ,再代回去即可得到 Δxp\Delta x_pΔxp

回到位姿的增量方程,我们定义
S=[B−EC−1ET]S=\begin{bmatrix} B-EC^{-1}E^T \end{bmatrix} S=[BEC1ET]
SSS 矩阵的稀疏性是不规则的,从图中可以看出,有色部分即为两个位置有着共同的观测点,白色则表示没有
在这里插入图片描述

鲁棒核函数

在前面的 BA 问题中,我们定义目标函数为最小化误差项的二次项范数平方和,但是如果存在较大的误差项的话,其二范数增长速度会很快,从而让收敛速度慢很多,甚至可能导致正确结果偏移。因此我们需要引入核函数,核函数通过将误差的二范数度量替换成其他增长速度没那么快的函数,同时保证其光滑性质。

以常见的 Huber 核为例:
H(e)={12e2∣e∣≤δδ(∣e∣−12δ)otherwiseH(e)=\begin{cases} \frac{1}{2}e^2\qquad\qquad\qquad|e|\leq\delta\\\delta(|e|-\frac{1}{2}\delta)\qquad\quad \text{otherwise} \end{cases} H(e)={21e2eδδ(e21δ)otherwise
在这里插入图片描述

代码实现

BAL 数据集格式

这里利用了 BAL 数据集,可以通过 https://grail.cs.washington.edu/projects/bal/index.html 获取,这里以 problem-16-22106-pre.txt 为例

对于数据集,其格式为:

  • 第一行:相机数量,三维点数量,二维观测数量
  • 第二行~第(2+num_observations-1)行:相机索引,三维点索引,x,y
  • 第(2+num_observations-1)行~第(2+num_observations+num_cameras-1)行:外参(旋转+平移),焦距,畸变系数共 9 个参数
  • 最后 num_points 行:三维点的X,Y,Z
Ceres BA 代码

代码中 BALProblem 类的定义和实现可以在ceres库里面的example获取,直接加入工作空间即可使用

#include <ceres/ceres.h>
#include <ceres/rotation.h>
#include <Eigen/Core>
#include <sophus/se3.hpp>
#include <iostream>
#include "bal_problem.h"
#include "matplotlibcpp.h"namespace plt = matplotlibcpp;class SnavelyReprojectionError {
public:SnavelyReprojectionError(double observation_x, double observation_y) : observed_x(observation_x), observed_y(observation_y) {}template <typename T>bool operator()(const T* const camera, const T* const point, T* residuals) const{T prediction[2];CamProjectionWithDistortion(camera, point, prediction);residuals[0] = prediction[0] - T(observed_x);residuals[1] = prediction[1] - T(observed_y);return true;}// camera : 9 维数组:[0-2] 为旋转向量,[3-5] 为平移向量,[6-8] 为相机内参:6为焦距(设fx=fy),8、9为二阶和四阶径向畸变参数// point : 3D 位置// predictions : 以图像中心为原点的 2D 预测点template <typename T>static bool CamProjectionWithDistortion(const T* camera, const T* point, T* predictions){// 罗德里格斯公式 // p = R * pT p[3];ceres::AngleAxisRotatePoint(camera, point, p);// 平移向量      // p = p + tp[0] += camera[3];p[1] += camera[4];p[2] += camera[5];// 归一化平面T xp = -p[0] / p[2];T yp = -p[1] / p[2];// 获取二阶和四阶径向畸变参数const T& l1 = camera[7];const T& l2 = camera[8];// 计算畸变后的坐标T r2 = xp * xp + yp * yp;T distortion = T(1.0) + r2 * (l1 + l2 * r2);const T& focal = camera[6];predictions[0] = focal * distortion * xp;predictions[1] = focal * distortion * yp;return true;}static ceres::CostFunction* Create(const double observed_x, const double observed_y){return (new ceres::AutoDiffCostFunction<SnavelyReprojectionError, 2, 9, 3>(  // 2 表示残差维度,9 表示相机参数维度,3 表示点的维度new SnavelyReprojectionError(observed_x, observed_y)));}private:double observed_x;double observed_y;
};class OptimizationMonitor : public ceres::IterationCallback
{
public:OptimizationMonitor(){start_time_ = std::chrono::steady_clock::now();}ceres::CallbackReturnType operator()(const ceres::IterationSummary& summary){auto current_time = std::chrono::steady_clock::now();auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(current_time - start_time_);// 记录每次迭代的信息iterations_.push_back(summary.iteration);costs_.push_back(summary.cost);times_.push_back(duration.count() / 1000.0);cost_changes_.push_back(summary.cost_change);gradient_norms_.push_back(summary.gradient_norm);std::cout << "iteration: " << summary.iteration<< ", cost: " << summary.cost<< ", cost_change: " << summary.cost_change<< ", time: " << times_.back() << "s" << std::endl;return ceres::SOLVER_CONTINUE;}// matplotlib 可视化void plotResults(){try{// 创建子图布局plt::figure_size(1200, 800);// 子图1:代价函数下降曲线plt::subplot(2, 2, 1);plt::semilogy(iterations_, costs_);plt::title("Cost Function Convergence");plt::xlabel("Iterations");plt::ylabel("Cost (log scale)");plt::grid(true);// 子图2:每次迭代时间plt::subplot(2, 2, 2);plt::plot(iterations_, times_);plt::title("Cumulative Time");plt::xlabel("Iterations");plt::ylabel("Time (s)");plt::grid(true);// 子图3:代价变化plt::subplot(2, 2, 3);plt::semilogy(iterations_, cost_changes_);plt::title("Cost Change");plt::xlabel("Iterations");plt::ylabel("Cost Change (log scale)");plt::grid(true);// 子图4:梯度范数plt::subplot(2, 2, 4);plt::semilogy(iterations_, gradient_norms_);plt::title("Gradient Norm");plt::xlabel("Iterations");plt::ylabel("Gradient Norm (log scale)");plt::grid(true);// 显示所有子图plt::tight_layout();plt::show();}catch (const std::exception& e){std::cerr << "Error: " << e.what() << std::endl;}}private:std::chrono::steady_clock::time_point start_time_;std::vector<int> iterations_;std::vector<double> costs_;std::vector<double> times_;std::vector<double> cost_changes_;std::vector<double> gradient_norms_;
};void SolveBA(ceres::examples::BALProblem& bal_problem)
{const int point_block_size = bal_problem.point_block_size();const int camera_block_size = bal_problem.camera_block_size();double* points = bal_problem.mutable_points();double* cameras = bal_problem.mutable_cameras();// observations 是一个大小为 2 * num_observations 的数组,其中每两个元素表示一个观测const double* observations = bal_problem.observations();ceres::Problem problem;for (int i = 0; i < bal_problem.num_observations(); ++i){ceres::CostFunction* cost_function;// 每个残差块以一个点和一个相机作为输入,输出一个二维残差cost_function = SnavelyReprojectionError::Create(observations[2 * i + 0], observations[2 * i + 1]);ceres::LossFunction* loss_function = new ceres::HuberLoss(1.0);// 获取参数块的指针:起始指针 + 参数块大小 * 对应索引double* camera = cameras + camera_block_size * bal_problem.camera_index()[i];double* point = points + point_block_size * bal_problem.point_index()[i];problem.AddResidualBlock(cost_function, loss_function, camera, point);}std::cout << "Solving ceres BA ..." << std::endl;ceres::Solver::Options options;options.linear_solver_type = ceres::LinearSolverType::SPARSE_SCHUR;options.minimizer_progress_to_stdout = true;options.max_num_iterations = 100;// 记录优化过程中的信息OptimizationMonitor monitor;options.callbacks.push_back(&monitor);options.update_state_every_iteration = true;ceres::Solver::Summary summary;ceres::Solve(options, &problem, &summary);std::cout << summary.FullReport() << "\n";// 绘制优化结果monitor.plotResults();
}int main()
{ceres::examples::BALProblem bal_problem("../test/data", false);SolveBA(bal_problem);return 0;
}

可视化结果如图:

在这里插入图片描述

http://www.dtcms.com/a/479137.html

相关文章:

  • 网站开发团队 人员运营和营销有什么区别
  • 杭州网站seo推广小程序价格为什么比网站建设高
  • 解码Linux文件IO之系统IO
  • 重庆做网站的公司网站开发都做什么
  • 商丘做网站一般多少钱军事新闻直播在线观看
  • LibGDX游戏开发性能优化实战:对象池模式在LibGDX中的应用
  • 网站 空间 租用帝国网站地图模板
  • 贸易网站源码电子商务网站规划方案
  • mysql读写分离中间件Atlas安装部署及使用
  • MySQL ORDER BY 深度解析:索引排序规则与关键配置参数阈值​
  • electron 套壳
  • 网站建设技术架构为了推广公众号可以采取的方法有
  • 网站建设蓝色工匠美创网站建设优势
  • 项目1:FFMPEG推流器讲解(五):FFMPEG时间戳、时间基、时间转换的讲解
  • 如何让自己网站排名提高步骤怎么写
  • 承德网站网站建设做外贸生意用哪个网站最好
  • 一、前置基础(MVC学习前提)_核心特性_【C# OOP 入门】从生活例子看懂类、继承、多态和封装,避坑指南来了!
  • RNN代码实战专项
  • 金蝶云·星瀚 | 生产制造成本核算终极实操手册(从0到1,含两套完整案例)
  • 千灯网站建设自由贸易试验区网站建设方案
  • 理解 JavaScript 中的 this 上下文保存
  • LLC系列--变压器
  • qwen2.5vl 模型配置记录
  • 无锡网站建设制作设计wordpress模板淘客
  • 平原县网站seo优化排名深入解析wordpress(原书第2版)
  • 云手机 手游专用虚拟手机
  • 网站开发模块就业前景怎么建设游网站主页
  • 神卓 N600:内网穿透需求的高效安全之选
  • 以营销导向型建设网站方案深圳福永网站建设
  • 企业网站带后台模板包括搜索引擎排名、网页标签优化、相关链接交换、网络广告投放等