(01)ORB-SLAM2源码无死角解析-(44) EPnP 源代码分析(4)→PnPsolver::qr_solve():QR分解

本人讲解关于slam一系列文章汇总链接:史上最全slam从零开始,针对于本栏目讲解的(01)ORB-SLAM2源码无死角解析-接如下:
(01)ORB-SLAM2源码无死角解析-(00)目录_最新无死角讲解:https://blog.csdn.net/weixin_43013761/article/details/123092196
 
文末正下方中心提供了本人 联系方式, 点击本人照片即可显示 W X → 官方认证 {\color{blue}{文末正下方中心}提供了本人 \color{red} 联系方式,\color{blue}点击本人照片即可显示WX→官方认证} 文末正下方中心提供了本人联系方式,点击本人照片即可显示WX官方认证
 

一、前言

通过上一篇博客建立了高斯牛顿增量方程 A Δ β = Δ B (01) \color{Green} \tag{01} \mathbf{A} \Delta \boldsymbol{\beta}= \Delta \mathbf{B} AΔβ=ΔB(01)那么接下来就是对该方程的求解,源码中使用的方法是 QR分解(豪斯霍尔德Householder变换),具体的推导过程可以参考博客: (01)ORB-SLAM2源码无死角解析-(40) EPnP 算法原理详解→理论基础四:QR分解(豪斯霍尔德Householder变换),其中的(23)式如下所示:

输入 : \color{Green}{输入}: 输入: 向量 x = ( x 1 , x 2 , ⋯   , x n ) T ≠ 0 \mathbf{x}=\left(\mathrm{x}_{1}, \mathrm{x}_{2}, \cdots, \mathrm{x}_{\mathrm{n}}\right)^{\mathrm{T}} \neq \mathbf 0 x=(x1,x2,,xn)T=0
输出 : \color{Green}{输出}: 输出: 豪斯霍尔德Householder 矩阵 H \mathbf H H,以及 σ \sigma σ,满足 H x = − σ ⋅ e 1 \mathrm{Hx}=-\sigma \cdot {\mathbf {e}}_{1} Hx=σe1
算法 : \color{Green}{算法}: 算法:
①  η = max ⁡ ( x 1 , x 2 , ⋯   , x n ) ②  x = ( x 1 η , x 2 η , ⋯   , x i η ) = ( x 1 , x 2 , ⋯   , x n ) ③  σ = sign ⁡ ( x 1 ) ④  v = ( x 1 + σ , x 2 , ⋯   , x n ) ⑤  ρ = ∣ ∣ v ∣ ∣ 2 2 / 2 ⑥  σ ← η σ (23) \color{Green} \tag{23} \begin{array}{l} ①~\eta=\max\left(\mathrm{x}_{1}, \mathrm{x}_{2}, \cdots, \mathrm{x}_{\mathrm{n}}\right) \\ ②~\mathbf{x}=(\frac{\mathrm{x}_{\mathrm{1}}}{\eta},\frac{\mathrm{x}_{\mathrm{2}}}{\eta},\cdots,\frac{\mathrm{x}_{\mathrm{i}}}{\eta})=\left(\mathrm{x}_{1}, \mathrm{x}_{2}, \cdots, \mathrm{x}_{\mathrm{n}}\right) \\ ③~\sigma=\operatorname{sign}\left(\mathrm{x}_{1}\right) \\ ④~\mathbf{v}=\left(\mathrm{x}_{1}+\sigma, \mathrm{x}_{2}, \cdots, \mathrm{x}_{\mathrm{n}}\right) \\ ⑤~\rho=||\mathbf{v}||^2_2/2 \\ ⑥~\sigma \leftarrow \eta \sigma \end{array}  η=max(x1,x2,,xn) x=(ηx1,ηx2,,ηxi)=(x1,x2,,xn) σ=sign(x1) v=(x1+σ,x2,,xn) ρ=∣∣v22/2 σησ(23)根据 ρ \rho ρ 值和向量 v \mathbf{v} v 可以计算出豪氏矩阵 H = E − 2 ⋅ v v T ∥ v ∥ 2 2 = E − ρ − 1 v v T \mathbf{H}=\mathbf{E}-2 \cdot \frac{\mathbf{vv}^{T}}{\|\mathbf{v}\|_{2}^{2}}=\mathbf{E}-\rho^{-1} \mathbf{vv}^{T} H=E2v22vvT=Eρ1vvT,另外算法输出的 σ \sigma σ 是乘上了规范化因子的值,需要注意的是,上述过程需要循环执行,因为其仅仅是对矩阵一列进行了豪斯霍尔德Householder 变换而已。变换公式如下:
H = E − 2 ⋅ v v T ∥ v ∥ 2 2 = E − ρ − 1 v v T (15) \color{Green} \tag{15} \mathbf{H}=\mathbf{E}-2 \cdot \frac{\mathbf{vv}^{T}}{\|\mathbf{v}\|_{2}^{2}}=\mathbf{E}-\rho^{-1} \mathbf{vv}^{T} H=E2v22vvT=Eρ1vvT(15)其中
ρ = ∥ v ∥ 2 2 2 = 1 2 [ ( x 1 + σ ) 2 + x 2 2 + ⋯ + x n 2 ] = 1 2 ( ∑ i = 1 n x i 2 + σ 2 + 2 σ ⋅ x 1 ) = σ ( σ + x 1 ) (18) \color{Green} \tag{18} \rho=\frac{\|\mathbf{v}\|_{2}^{2}}{2}=\frac{1}{2}\left[\left(\mathrm{x}_{1}+\sigma\right)^{2}+\mathrm{x}_{2}^{2}+\cdots+\mathrm{x}_{\mathrm{n}}^{2}\right]=\frac{1}{2}\left(\sum_{\mathrm{i}=1}^{\mathrm{n}} \mathrm{x}_{\mathrm{i}}^{2}+\sigma^{2}+2 \sigma \cdot \mathrm{x}_{1}\right)=\sigma\left(\sigma+\mathrm{x}_{1}\right) ρ=2v22=21[(x1+σ)2+x22++xn2]=21(i=1nxi2+σ2+2σx1)=σ(σ+x1)(18)
另外 A Δ β = Δ B \mathbf{A} \Delta \boldsymbol{\beta}= \Delta \mathbf{B} AΔβ=ΔB使用QR分解之后可以表示为(注意 Q 为正交矩阵,即 Q − 1 = Q T \mathbf Q 为正交矩阵,即 \mathbf Q^{-1}=\mathbf Q^{T} Q为正交矩阵,即Q1=QT)
Q R Δ β = Δ B Δ β = ( Q R ) − 1 Δ B (02) \color{Green} \tag{02} \mathbf{QR} \Delta \boldsymbol{\beta}= \Delta \mathbf{B} \\\Delta \boldsymbol{\beta}= \mathbf{(QR)}^{-1} \Delta \mathbf{B} QRΔβ=ΔBΔβ=(QR)1ΔB(02)

为了方便大家理解这里先为大家讲解一些代码,且变量与上面的公式对应起来:

( 1 ) : \color{blue}(1): (1) 首先展开一个 nc 次的循环,nc 为需要分解矩阵的 A \mathbf A A 的列数。首先注意一个变量,那就是 ppAkk,其每次循环都是指向目前所在 k 列的对角线元素地址。

( 2 ) : \color{blue}(2): (2) 对当前遍历的第k列,对角线以下的所有元素进行统计,找到最大的元素,保存在 elt 变量中,对应于上面公式的 η \eta η

( 3 ) : \color{blue}(3): (3) 以当前列对角线的元素地址 ppAik 为起点进行遍历,遍历对角线以下的所有元素。先利用 elt 变量,对元素进行归一化.即完成公式的第二步②。

( 4 ) : \color{blue}(4): (4) 代码中的 sigma 为公式③的 σ \sigma σ, 根据其执行第四步,对应源码为 *ppAkk += sigma,就是对角线上的元素 + σ \sigma σ 操作。

( 5 ) : \color{blue}(5): (5) 再次对前列对角线以下的所有元素,再进行一次遍历,注意,这次的遍历已经执行了 *ppAkk += sigma,也就是说目前遍历的列可以理解为对公式中的 v \mathbf{v} v进行遍历,遍历求得所有元素的平方和存储于变量sum。

( 6 ) : \color{blue}(6): (6) 其上的所有操作,都是为了获得两个变量,即代码中的
A1→存储每列向量(对角线元素以下)进行豪斯霍尔德Householder变换需要的 ρ = σ ( σ + x 1 ) \rho=\sigma\left(\sigma+\mathrm{x}_{1}\right) ρ=σ(σ+x1)
A2→公式(23)中的⑥,即 σ = η σ \sigma = \eta \sigma σ=ησ

( 7 ) : \color{blue}(7): (7) 进行真正的QR分,主要分两部执行 Δ β = ( Q R ) − 1 Δ B = R − 1 ( Q − 1 Δ B ) \Delta \boldsymbol{\beta}= \mathbf{(QR)}^{-1} \Delta \mathbf{B} = \mathbf R^{-1}(Q^{-1} \Delta \mathbf{B}) Δβ=(QR)1ΔB=R1(Q1ΔB),即先计算 ( Q − 1 Δ B ) (Q^{-1} \Delta \mathbf{B}) (Q1ΔB) 再计算 R − 1 ( Q − 1 Δ B ) \mathbf R^{-1}(Q^{-1} \Delta \mathbf{B}) R1(Q1ΔB)。因为 Q \mathbf{Q} Q 为正交矩阵,所以存在 Q − 1 = Q T \mathbf{Q}^{-1}=\mathbf{Q}^T Q1=QT。源码中计算完之后是直接存储于变量 pb 之中的,也就是传入的参数 CvMat * b。
 

二、代码注释

代码位于 src/PnPsolver.cc 文件中:

/**
 * @brief 使用QR分解来求解增量方程 
 * @param[in]  A   系数矩阵
 * @param[in]  b   非齐次项
 * @param[out] X   增量
 */
void PnPsolver::qr_solve(CvMat * A, CvMat * b, CvMat * X)
{
  static int max_nr = 0;        
  static double * A1, * A2;     


  const int nr = A->rows;       // 系数矩阵A的行数
  const int nc = A->cols;       // 系数矩阵A的列数

  // 判断是否需要重新分配A1 A2的内存区域
  if (max_nr != 0 && max_nr < nr) 
  {
    // 如果 max_nr != 0 说明之前已经创建了一个 last_max_nr < nr 的数组,不够我们现在使用了,需要重新分配内存;但是在重新分配之前我们需要先删除之前创建的内容
    delete [] A1;
    delete [] A2;
  }
  if (max_nr < nr) 
  {
    max_nr = nr;
    A1 = new double[nr];
    A2 = new double[nr];
  }

  double * pA = A->data.db,     // 指向系数矩阵A的数据区
         * ppAkk = pA;          // 一直都会指向对角线上的元素
  // 对系数矩阵的列展开遍历
  for(int k = 0; k < nc; k++) 
  {
    double * ppAik = ppAkk,           // 只是辅助下面的for循环中,遍历对角线元素下的当前列的所有元素
             eta = fabs(*ppAik);      // 存储当前列对角线元素下面的所有元素绝对值的最大值

    // 遍历当前对角线约束下,当前列的所有元素,并且找到它们中的最大的绝对值
    for(int i = k + 1; i < nr; i++) 
    {
      double elt = fabs(*ppAik);
      if (eta < elt) eta = elt;
        ppAik += nc;                  // 指向下一列
    }

    //? 判断靠谱不? 由于系数矩阵是雅克比,并且代价函数中的L元素都是二次项的形式,所以原则上都应该是大于0的
    if (eta == 0) 
    {
      A1[k] = A2[k] = 0.0;
      cerr << "God damnit, A is singular, this shouldn't happen." << endl;
      return;
    } 
    else
    {

      // 开始正儿八经地进行QR分解了
      // 感觉这里面使用的ription provided.是数值分析中的计算方法,和矩阵论中的定义的算法还是不一样的
      // 注意在这里面,ppAik被重ription provided.定义了,在这个结构中以这里定义的这个为准
      double * ppAik = ppAkk, 
              sum = 0.0,
              inv_eta = 1. / eta; // 卧槽还能直接+.表示浮点数啊,长见识了
      // 对当前列下面的每一行的元素展开遍历(包含位于矩阵主对角线上的元素)
      for(int i = k; i < nr; i++) 
      {
        *ppAik *= inv_eta;          // NOTICE 注意这个操作是永久的,当前指向的元素都会被“归一化”
        sum += *ppAik * *ppAik;     // 平方和
        ppAik += nc;                // 指针移动到下一行的这个元素
      }

      // 计算 sigma ,同时根据对角线元素的符号保持其为正数
      double sigma = sqrt(sum);
      if (*ppAkk < 0)               
        sigma = -sigma;
      
      *ppAkk += sigma;
      A1[k] = sigma * *ppAkk;
      A2[k] = -eta * sigma;
      // 对于后面的每一列展开遍历
      for(int j = k + 1; j < nc; j++) 
      {
        // 首先这一遍循环是为了计算tau
        // 又重新定义了
        double * ppAik = ppAkk, sum = 0;
        for(int i = k; i < nr; i++) 
        {
          sum += *ppAik * ppAik[j - k];
          ppAik += nc;
        }
        double tau = sum / A1[k];
        // 然后再一遍循环是为了修改
        ppAik = ppAkk;
        for(int i = k; i < nr; i++) 
        {
          ppAik[j - k] -= tau * *ppAik;
          ppAik += nc;
        }
      }
    }
    // 移动向下一个对角线元素
    ppAkk += nc + 1;
  }

  // b <- Qt b
  double * ppAjj = pA, * pb = b->data.db;
  // 对于每一列展开计算
  for(int j = 0; j < nc; j++) 
  {
    // 这个部分倒的确是在计算Q^T*b
    double * ppAij = ppAjj, tau = 0;
    for(int i = j; i < nr; i++)	
    {
      tau += *ppAij * pb[i];
      ppAij += nc;
    }
    //? 但是后面我就看不懂了
    tau /= A1[j];
    ppAij = ppAjj;
    for(int i = j; i < nr; i++) 
    {
      pb[i] -= tau * *ppAij;
      ppAij += nc;
    }
    ppAjj += nc + 1;
  }

  // X = R-1 b
  // backward method
  double * pX = X->data.db;
  pX[nc - 1] = pb[nc - 1] / A2[nc - 1];
  for(int i = nc - 2; i >= 0; i--) 
  {
    // 定位
    double * ppAij = pA + i * nc + (i + 1), sum = 0;

    for(int j = i + 1; j < nc; j++) 
    {
      sum += *ppAij * pX[j];    //pX[j] 就是上一步中刚刚计算出来的那个
      ppAij++;
    }
    pX[i] = (pb[i] - sum) / A2[i];  // 比较像了
  }
}

 

三、结语

计算 Δ β = ( Q R ) − 1 Δ B \Delta \boldsymbol{\beta}= \mathbf{(QR)}^{-1} \Delta \mathbf{B} Δβ=(QR)1ΔB 的时候要考虑到 Q R \mathbf{QR} QR 的特殊性, Q \mathbf{Q} Q 为正交矩阵, R \mathbf{R} R 为上三角矩阵。 通过 PnPsolver::qr_solve() 函数,就能够获得高斯牛顿迭代需要的增量。进而对 β \boldsymbol{\beta} β 进行优化。求得 β \boldsymbol{\beta} β 之后,那么进一步就可以求得相机在世界坐标系下的姿态。在接下来的博客中会进行详细的讲解
 
 
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

江南才尽,年少无知!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值