【视觉SLAM十四讲】后端 1


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

状态估计问题

回顾我们之前提到过的运动和观测方程,在 t = 0 t=0 t=0 t = N t=N t=N ,有位姿 x 0 , ⋯   , x k x_0,\cdots,x_k x0,,xk ,路标 y 1 , ⋯   , y j y_1,\cdots,y_j y1,,yj

为了简化表达,我们设定 x k x_k xk k k k 时刻的所有未知量
x k = def { x k , y 1 , ⋯   , y j } x_k \overset{\text{def}}{=}\{x_k,y_1,\cdots,y_j\} xk=def{xk,y1,,yj}
此时运动方程和观测方程可以简化为
{ x k = f ( x k − 1 , u k ) + ω k z k = h ( x k ) + v k \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
由于每个方程都受到噪声的影响,所以这里的 x x x y y y 可以看成是服从某个概率分布的随机变量。我们希望通过使用过去的 0 到 k 中的数据来估计现在的状态分布
P ( x k ∣ x 0 , u 1 : k , z 1 : 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 ( x k ∣ x 0 , u 1 : k , z 1 : k ) ∝ P ( z k ∣ x k ) P ( x k ∣ x 0 , u 1 : k , z 1 : 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 − 1 0\sim k-1 0k1 的各种数据推测得到的 k k k 时刻状态;
似然 P ( Z ∣ X ) P(Z|X) P(ZX) :假设状态是 X ,我们观测到数据 Z 的可能性有多大;后验 P ( X ∣ Z ) P(X|Z) P(XZ) :在观测到 k k k 时刻的观测数据 Z 时,状态 X 的概率分布

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

对于这里的先验,它是由过去所有状态估计得来的,所以也可以展开为积分形式
P ( x k ∣ x 0 , u 1 : k , z 1 : k − 1 ) = ∫ 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 ) d x k − 1 P(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. 滤波派:以卡尔曼滤波为代表,基于马尔科夫性,认为 k k k 时刻状态只与 k − 1 k-1 k1 时刻状态有关
  2. 优化派:以非线性优化为主体,考虑 k k k 时刻之前的所有状态

线性系统和 KF

当我们假设了马尔科夫性,那么 k k k 只与 k − 1 k-1 k1 时刻状态有关,状态估计的贝叶斯估计还可以进一步简化
P ( x k ∣ x 0 , u 1 : k , z 1 : k − 1 ) = ∫ P ( x k ∣ x k − 1 , u k ) P ( x k − 1 ∣ x 0 , u 1 : k − 1 , z 1 : k − 1 ) d x k − 1 P(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. 线性运动方程和观测方程
    { x k = A k x k − 1 + u k + ω k z k = C k x k + v k \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 , R k ) v k ∼ N ( 0 , Q k ) \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ˇ 表示先验,后续的 R R R Q Q Q 不再带下标。如果不想看推导的也可以直接跳到后面看公式)

预测

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

根据高斯分布的性质,对于 y = A x + b + ϵ y=Ax+b+\epsilon y=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 Σ x A T + Σ ϵ ) y \sim N(A\mu_x + b, A\Sigma_x A^T + \Sigma_\epsilon) yN(Aμx+b,AΣxAT+Σϵ)

  • k − 1 k-1 k1后验分布: P ( x k − 1 ∣ x 0 , u 1 : k − 1 , z 1 : 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})\quad P(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 ( x k ∣ x k − 1 , u k ) = N ( A k x k − 1 + u k , R ) P(x_k|x_{k-1},u_k)=N(A_kx_{k-1}+u_k,R)\quad P(xkxk1,uk)=N(Akxk1+uk,R) 其中 A k A_k Ak 是状态转移矩阵, u k u_k uk 是控制输入, R k R_k Rk 是过程噪声协方差

那么 P ( x k ∣ x 0 , u 1 : k , z 1 : k − 1 ) = ∫ N ( A k x k − 1 + u k , R ) ⋅ N ( x ^ k − 1 , P ^ k − 1 ) ) d x k − 1 P(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 [ x k ∣ ⋯   ] = E [ A k x k − 1 + u k + ω k ∣ x 0 , u 1 : k , z 1 : 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 [ A k x k − 1 ∣ ⋯   ] = A k E [ x k − 1 ∣ ⋯   ] = A k x ^ 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 [ u k ∣ ⋯   ] = u k \mathbb{E}[u_k|\cdots]=u_k E[uk]=uk (确定项)
    • E [ ω k ∣ ⋯   ] = 0 \mathbb{E}[\omega_k|\cdots]=0 E[ωk]=0 (零均值噪声)

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

  2. 协方差计算
    定义状态估计误差项 x k − E [ x k ] = A k ( x k − 1 − x ^ k − 1 ) + ω k x_k-\mathbb{E}[x_k]=A_k(x_{k-1}-\hat{x}_{k-1})+\omega_k xkE[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 [ x k ] = E [ ( A k ( x k − 1 − x ^ k − 1 ) + ω k ) ( A k ( x k − 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]
    第二项计算内层转置得到: ( x k − 1 − x ^ k − 1 ) T A k T + ω k T (x_{k-1}-\hat{x}_{k-1})^TA^T_k+\omega_k^T (xk1x^k1)TAkT+ωkT
    展开得到: E [ A k ( x k − 1 − x ^ k − 1 ) ( x k − 1 − x ^ k − 1 ) T A K T ] + E [ A k ( x k − 1 − x ^ k − 1 ) ω k T ] + E [ ω k A k ( x k − 1 − x ^ k − 1 T ) ] + E [ ω k ω k T ] \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]

    • 第一项: ⋯ = A k E [ ( x k − 1 − x ^ k − 1 ) ( x k − 1 − x ^ k − 1 ) T ] A k T = A k P k − 1 ^ A k T \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 ωk x k − 1 x_{k-1} xk1 独立,故均等于 0 0 0
    • 第四项: E [ ω k ω k T ] = R \mathbb{E}[\omega_k\omega_k^T]=R E[ωkωkT]=R

    Cov [ x k ] = A k P ^ k − 1 A k T + R \text{Cov}[x_k]=A_k\hat{P}_{k-1}A^T_k+R Cov[xk]=AkP^k1AkT+R 最后我们得到了 P ( x k ∣ x 0 , u 1 : k , z 1 : k − 1 ) = N ( A k x ^ k − 1 + u k , A k P ^ k − 1 A k T + 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) ,即 k k k 时刻的先验状态估计分布。为简化表达可定义

x ˇ k = A k x ^ k − 1 + u k P ˇ k = A k P ^ k − 1 A k T + 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 

更新

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

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

这里方法和上面求解先验的一样,都是可以通过期望和协方差得到的,不再赘述: P ( z k ∣ x k ) = N ( C k x k , Q k ) P(z_k|x_k)=N(C_kx_k,Q_k) P(zkxk)=N(Ckxk,Qk) ,那么:
N ( x ^ k , P ^ k ) = N ( C k x k , 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 ∣ Σ ∣ 1 2 exp ⁡ ( − 1 2 ( 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μ))
其中 x x x 是我们要估计的系统状态, μ \mu μ 则是 x x x 的期望值

因为我们只关心和 x k x_k xk 有关的部分,所以这里舍去归一化常数,只保留指数部分展开得到
( x k − x ^ k ) T P ^ k − 1 ( x k − x ^ k ) = ( z k − C k x k ) T Q − 1 ( z k − C k x k ) + ( x k − x ˇ k ) T P ˇ k − 1 ( x k − 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)

两边展开(单纯比较长而已,其实不难算):
x k T P ^ k − 1 x k − 2 x ^ k T P ^ k − 1 x k + x ^ k T P ^ k − 1 x ^ k = x k T ( C k T Q − 1 C k + P ˇ k − 1 ) x k − 2 ( z k T Q − 1 C k + x ˇ k T P ˇ k − 1 ) x k + ( z k T Q − 1 z k + x ˇ k T P ˇ k − 1 x ˇ 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 = C k T Q − 1 C k + 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}_k P^k
I = P ^ k C k T Q − 1 C k + P ^ k P ˇ k − 1 I=\hat{P}_kC_k^TQ^{-1}C_k+\hat{P}_k\check{P}^{-1}_k I=P^kCkTQ1Ck+P^kPˇk1
定义卡尔曼增益为
K = P ^ k C k T Q − 1 K=\hat{P}_kC_k^TQ^{-1} K=P^kCkTQ1
从而得到协方差更新公式(后续的式子转换都是以建立 k k k 时刻的先验与后验关系为核心)
I = K C k + P ^ k P ˇ k − 1 ⇒ P ^ k = ( I − K C k ) P ˇ k I=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 ˇ k C k T ( C k P ˇ k C k T + Q ) − 1 K=\check{P}_kC^T_k(C_k\check{P}_kC^T_k+Q)^{-1} K=PˇkCkT(CkPˇkCkT+Q)1
取第一项系数
− 2 x ^ k T P ^ k − 1 x k = − 2 z k T Q − 1 C k x k − 2 x ˇ k T P ˇ k − 1 x k -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 − 1 x ^ k = C k T Q − 1 z k − P ˇ k − 1 x ˇ 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}_k P^k 并代回 K = P ^ k C k T Q − 1 K=\hat{P}_kC_k^TQ^{-1} K=P^kCkTQ1 得到
x ^ k = P ^ k C k T Q − 1 z k − P ^ k P ˇ k − 1 x ˇ k = K z k + ( I − K C k ) x ˇ k = x ˇ k + K ( z k − C k x ˇ 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 = A k x ^ k − 1 + u k , P ˇ k = A k P ^ k − 1 A k T + 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 ˇ k C k T ( C k P ˇ k C k T + Q k ) − 1 K=\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 ( z k − C k x ˇ k ) P ^ k = ( I − K C k ) 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 ( x k − 1 , u k ) f(x_{k-1},u_k) f(xk1,uk) 进行一阶泰勒展开
x k ≈ f ( x ^ k − 1 , u k ) + ∂ f ∂ x k − 1 ∣ x ^ k − 1 ( x k − 1 − x ^ k − 1 ) + ω k x_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)+xk1f x^k1(xk1x^k1)+ωk
记偏导数为
F = ∂ f ∂ x k − 1 ∣ x ^ k − 1 F=\left.\frac{\partial f}{\partial x_{k-1}}\right|_{\hat{x}_{k-1}} F=xk1f x^k1
同理,在 x ^ k \hat{x}_k x^k 处对运动方程 h ( x k , u k ) h(x_k,u_k) h(xk,uk) 进行一阶泰勒展开
z k ≈ h ( x ˇ k ) + ∂ h ∂ x k ∣ x ^ k + v k z_k\approx h(\check{x}_k)+\left.\frac{\partial h}{\partial x_k}\right|_{\hat{x}_k}+v_k zkh(xˇk)+xkh x^k+vk
记偏导数为
H = ∂ h ∂ x k ∣ x ^ k H=\left.\frac{\partial h}{\partial x_k}\right|_{\hat{x}_k} H=xkh x^k

预测

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

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

更新

卡尔曼增益:
K = P ˇ k H k T ( H k P ˇ k H k T + Q ) − 1 K=\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 ( z k − 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 − K H k ) 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, yaw
        return 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” 直译过来就是光束调整,即考虑从任意特征点发射出来的几束光线,通过调整个相机姿态和各特征点的空间位置,使这些光线最终收束到相机的光心(简单点说就是要让所有图像中的特征点看起来都来自同一个空间位置)、

投影模型和代价函数

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

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

  2. P P P 投影至归一化平面
    P c = [ u c , v c , 1 ] T = [ X ′ / Z ′ , Y ′ / Z ′ , 1 ] T P_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. 去畸变(这里仅考虑径向)
    { u c ′ = u c ( 1 + k 1 r c 2 + k 2 r c 4 ) v c ′ = v c ( 1 + k 1 r c 2 + k 2 r c 4 ) \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 像素坐标系
    { u s ′ = f x u c ′ + c x v s = f y v c ′ + c y \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)

如果假定 x x x 为此时相机的位姿(外参 R , t R,t R,t ),对应的李群为 T T T ,李代数为 ξ \xi ξ 。路标 y y y 即三维点 p p p ,观测数据即像素坐标 z = d e f [ u s , v s ] T z\overset{def}{=}[u_s,v_s]^T z=def[us,vs]T 。那么,此次观测的误差可表示为:
e = z − h ( T , p ) e=z-h(T,p) e=zh(T,p)
考虑上其他时刻的数据,设 z i , j z_{i,j} zi,j 为在位姿 T i T_{i} Ti 处观测路标 p j p_j pj 产生的数据,那么代价函数可写为:
1 2 ∑ i = 1 m ∑ j = 1 n ∣ ∣ e i j ∣ ∣ 2 = 1 2 ∑ i = 1 m ∑ j = 1 n ∣ ∣ z i j − h ( T i , p j ) ∣ ∣ 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 点

设对相机姿态的偏导为 F F F ,对路标点的偏导为 E E E ,目标函数变为:
1 2 ∣ ∣ f ( x + Δ x ) ∣ ∣ 2 ≈ 1 2 ∑ i = 1 m ∑ j = 1 n ∣ ∣ e i j + F i j Δ ξ i + E i j Δ p j ∣ ∣ 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 的变量是针对整体的,所以把所有位姿作为一个变量 x c x_c xc ,所有空间点作为一个变量 x p x_p xp
x c = [ ξ 1 , ξ 2 , ⋯   , ξ m ] T ∈ R 6 m x p = [ p 1 , p 2 , ⋯   , p n ] T ∈ R 3 n x_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
那么目标函数可简化为:
1 2 ∣ ∣ f ( x + Δ x ) ∣ ∣ 2 = 1 2 ∣ ∣ e + F Δ x c + E Δ x p ∣ ∣ 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

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

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

稀疏化和边缘化

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

误差项只描述了在 T i T_i Ti 观测到 p j p_j pj 的情况,只涉及到第 i i i 个相机位姿和第 j j j 个路标点,ss对其余部分的导数为 0 ,所以该误差项对应的雅可比可写为
J i j ( x ) = ( 0 2 × 6 , ⋯ 0 2 × 6 , ∂ e i j ∂ T i , 0 2 × 6 , ⋯ 0 2 × 3 , ⋯ 0 2 × 3 , ∂ e i j ∂ p j , 0 2 × 3 , ⋯ 0 2 × 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)

可以看出,除了表示相机姿态的偏导 ∂ e i j ∂ T i \frac{\partial e_{ij}}{\partial T_i} Tieij 维度为 2 × 6 2\times6 2×6 ,对路标点的偏导 ∂ e i j ∂ p j \frac{\partial e_{ij}}{\partial p_j} pjeij 维度为 2 × 3 2\times3 2×3 ,其余地方均为 0

如图,如果相机分别在位姿 C 1 C_1 C1 C 2 C_2 C2 处,观察到的特征点情况为:

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

此时对应的增量方程可以由 H Δ x = g H\Delta x=g HΔx=g 变为:
[ B E E T C ] [ Δ x c Δ x p ] = [ 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 = F T F B=F^TF B=FTF :相机-相机块,维度为 6 × 6 6\times6 6×6
  • C = E T E C=E^TE C=ETE :路标-路标块,维度为 3 × 3 3\times3 3×3
  • E = F T E E=F^TE E=FTE :相机-路标耦合块,维度为 6 × 3 6\times3 6×3

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

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

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

鲁棒核函数

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

以常见的 Huber 核为例:
H ( e ) = { 1 2 e 2 ∣ e ∣ ≤ δ δ ( ∣ e ∣ − 1 2 δ ) otherwise H(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 * p
        T p[3];
        ceres::AngleAxisRotatePoint(camera, point, p);

        // 平移向量      // p = p + t
        p[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;
}

可视化结果如图:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值