【SLAM】Ceres优化库超详细解析

news2024/11/16 7:47:32

Ceres是由Google开发的开源C++通用非线性优化库,与g2o并列为目前视觉SLAM中应用最广泛的优化算法库。

对于任何一个优化问题,我们首先需要对问题进行建模,之后采用合适的优化方法,进行求解。在求解的过程中,往往需要进行梯度下降求取最优,这里涉及了导数计算。所以在代码中使用Ceres进行优化时,需要包含基本内容:建模、优化、求导方法


Ceres库简介

Ceres库主要用于求解无约束或者有界约束的最小二乘问题。其数学形式如下:

min ⁡ x 1 2 ∑ i ρ i ( ∥ f i ( x 1 , ⋯   , x k ) ∥ 2 ) s.t. l j ≤ x j ≤ u j \begin{split} \min_{\boldsymbol{x}} &\quad \frac{1}{2}\sum_{i} \rho_i\left(\left\|f_i\left(x_1, \cdots ,x_k\right)\right\|^2\right) \\ \text{s.t.} &\quad l_j \le x_j \le u_j \end{split} xmins.t.21iρi(fi(x1,,xk)2)ljxjuj

我们的任务就是找到一组满足约束 l j ≤ x j ≤ u j l_j \le x_j \le u_j ljxjuj x 1 , ⋯   , x k x_1, \cdots, x_k x1,,xk,使得优化目标函数

1 2 ∑ i ρ i ( ∥ f i ( x 1 , ⋯   , x k ) ∥ 2 ) \begin{split}\frac{1}{2}\sum_{i} \rho_i\left(\left\|f_i\left(x_1, \cdots ,x_k\right)\right\|^2\right)\end{split} 21iρi(fi(x1,,xk)2)

取值最小。

其中的一些变量的解释:

  • 优化参数 x 1 , ⋯   , x k x_1, \cdots, x_k x1,,xk参数块(ParameterBlock),它们的取值就是我们要寻找的解
  • l j , u j l_j, u_j lj,uj:第 j j j个优化参数 x j x_j xj的下界和上界
  • 表达式 ρ i ( ∥ f i ( x 1 , ⋯   , x k ) ∥ 2 ) \rho_i\left(\left\|f_i\left(x_1, \cdots ,x_k\right)\right\|^2\right) ρi(fi(x1,,xk)2)残差块(ResidualBlock)
  • f i ( ⋅ ) f_i(\cdot) fi()代价函数(CostFunction)
  • ρ i ( ⋅ ) \rho_i(\cdot) ρi()损失函数,或者核函数(LossFunction)。它的意义主要是为了降低野点(outliers)对于解的影响

代价函数同时负责计算残差项( f i ( x 1 , ⋯   , x k ) f_i\left(x_1, \cdots ,x_k\right) fi(x1,,xk))和雅可比矩阵( J i \mathcal{J}_i Ji

J i = ∂ ∂ x i f i ( x 1 , ⋯   , x k ) ∀ i ∈ 1 , ⋯   , k \mathcal{J}_i = \frac{\partial}{\partial x_i} f_i\left(x_1, \cdots ,x_k\right) \qquad \forall i \in {1,\cdots,k} Ji=xifi(x1,,xk)i1,,k

很多时候我们说最小二乘都是拿来做曲线拟合的,实际上只要能够把问题描述成上面的数学形式,就都可以使用Ceres来求解。使用起来也比较简单,只要按照教程介绍的套路,提供代价函数的计算方式,描述清楚每个残差块以及核函数即可。

如下面的示例代码所示,一般我们需要定义三个对象,problem用于描述将要求解的问题,options提供了很多配置项,而summary用于记录求解过程

ceres::Problem problem;
ceres::Solver::Options options;
ceres::Solver::Summary summary;

Ceres的求解过程包括构建最小二乘和求解最小二乘问题两部分,其中构建最小二乘问题的相关方法均包含在Ceres::Problem类中,涉及的成员函数主要包括Problem::AddResidualBlock()和Problem::AddParameterBlock()。

注意:Ceres Solver只接受最小二乘优化,也就是 min ⁡ r T r \min r^{T} r minrTr;若要对残差加权重,使用马氏距离,即 min ⁡ r T P − 1 r \min r^{T} P^{-1} r minrTP1r,则要对信息矩阵 P − 1 P^{-1} P1做Cholesky分解,即 L L T = P − 1 L L^{T} = P^{-1} LLT=P1,则 d = r T ( L L T ) r = ( L T r ) T ( L T r ) d = r^{T} (L L^{T}) r = (L^T r)^T (L^T r) d=rT(LLT)r=(LTr)T(LTr) ,令 r ′ = L T r r^{'} = L^T r r=LTr ,最终 min ⁡ r ′ T r ′ \min r^{'T} r^{'} minrTr


ceres使用流程

在这里插入图片描述

使用 Ceres Solver 求解非线性优化问题,主要包括以下几部分:

  1. 【STEP 1】构建优化问题(ceres::Problem)

  2. 【STEP 2】构建代价函数(ceres::CostFunction)或残差(residual)

  3. 【STEP 3】添加代价函数、核函数:通过ceres::Problem::AddResidualBlock添加代价函数(cost function)、核函数(loss function)和输入参数块(即待优化状态量块)

  4. 【STEP 4】配置求解器(ceres::Solver::Options)

  5. 【STEP 5】运行求解器(ceres::Solve(options, &problem, &summary))

下面将会对其中的重要的几个STEP进行详细讲解。


构建代价函数(STEP2)

在这里插入图片描述

代价函数最重要的功能就是计算残差向量和雅可比矩阵。Ceres提供了三种求导方法,分别是:解析求导、自动求导、数值求导

在SLAM中,使用的一般都是解析求导,这种方法需要自己填入雅克比函数

基础代价函数类

先看下最基础的代价函数类的声明:

class CostFunction {
 public:
  virtual bool Evaluate(double const* const* parameters,
                        double* residuals,
                        double** jacobians) const = 0;
  const vector<int32>& parameter_block_sizes();
  int num_residuals() const;

 protected:
  vector<int32>* mutable_parameter_block_sizes();
  void set_num_residuals(int num_residuals);
};

CostFunction的输入参数块的数量和大小以及输出的数量,分别存储在CostFunctio::parameter_block_sizes_CostFunction::num_residuals_中。从此类继承的子类应使用对应的访问器设置这两个成员。当使用Problem::AddResidualBlock()添加代价函数时,此信息将由Problem验证。

CostFunction中有一个纯虚函数需要子类进行重写,该纯虚函数的定义为:

bool CostFunction::Evaluate(double const *const *parameters,
						    double *residuals,
						    double **jacobians) const;

纯虚函数的形参的解释:

  • parameters:输入参数块,大小为parameter_block_sizes_.size()的数组(输入参数块的数量),parameters[i]是大小为parameter_block_sizes_[i]的数组(第i个输入参数块的维度)
  • residuals:残差,大小为num_residuals_的数组
  • jacobians:雅可比矩阵,大小为parameter_block_sizes_.size()的数组的数组(输入参数块的数量),jacobians[i]是大小为num_residuals乘parameter_block_sizes_[i]的行优先数组(残差对第i个输入参数块求导,即大小为残差维度 乘 第i个输入参数块维度,再转化为行优先数据组)

jacobians的详细表达式为:

jacobians[i][r * parameter_block_sizes_[i] + c] = ∂ residual [ r ] ∂ parameters [ i ] [ c ] \text{jacobians[i][r * parameter\_block\_sizes\_[i] + c]}=\frac{\displaystyle \partial \text{residual}[r]}{\displaystyle \partial \text{parameters}[i][c]} jacobians[i][r * parameter_block_sizes_[i] + c]=parameters[i][c]residual[r]

parameters和residuals不能为nullptr,jacobians可能为nullptr。如果jacobians为nullptr,则用户只需计算残差即可。

纯虚函数的返回值指示残差或雅可比的计算是否成功。

如果输入参数块的大小和残差向量的大小在编译时已知(这是常见情况),则可以使用SizedCostFunction。这些已知值可以指定为模板参数,并且用户只需要实现CostFunction::Evaluate()

template<int kNumResiduals, int... Ns>
class SizedCostFunction : public CostFunction {
 public:
  virtual bool Evaluate(double const* const* parameters,
                        double* residuals,
                        double** jacobians) const = 0;
};

该模板参数的含义为:

  • kNumResiduals:残差的维度,即num_residuals_的值
  • Ns:不定参数的数量为输入参数块的数量,即parameter_block_sizes_.size(),每个不定参数对应的值为每个输入参数块的维度,即parameter_block_sizes_[i]的值

Ceres提供的三种求导方法,都是基于上面的基础代价函数类实现的。

解析求导

解析求导(Analytic Derivatives),也被称为自定义求导解析求导需要自行定义迭代求导时的雅克比矩阵和残差。需要以下几个步骤:

  1. 构建一个继承自ceres::SizedCostFunction的类,同样,对于模板参数的数字,第一个为残差的维度,后面几个为输入参数块的维度
  2. 重载纯虚函数Evaluate(…),根据输入参数块,实现残差和雅克比矩阵的计算

在ceres中的代价函数里面Evaluate()采用的是纯虚函数:

virtual bool Evaluate(double const* const* parameters,
                      double* residuals,
                      double** jacobians) const = 0;

下面以最小二乘 min ⁡ x 1 2 ( 10 − x ) 2 \underset{x}{\min} \frac{1}{2}(10-x)^{2} xmin21(10x)2为例解析求导。显然残差为 ( 10 − x ) (10−x) (10x),而雅可比矩阵维度为 1 ∗ 1 1∗1 11且为常数 − 1 -1 1。在求导的Evaluate()函数中定义残差和雅克比如下:

class SimpleCostFunction : public ceres::SizedCostFunction<1, 1> {
 public:
  virtual ~SimpleCostFunction() {};

  // 形参: 输入参数块, 残差, 雅可比矩阵
  virtual bool Evaluate(double const* const* parameters, double *residuals,
    double** jacobians) const {
    const double x = parameters[0][0];
    residuals[0] = 10 - x;
    if (jacobians != NULL && jacobians[0] != NULL) {
      jacobians[0][0] = -1;
    }
    return true;
  }
};

ceres::CostFunction* cost_function = new SimpleCostFunction();

下面以最小二乘 min ⁡ x 1 2 ( k − x T y ) 2 \underset{x}{\min} \frac{1}{2}(k-x^Ty)^{2} xmin21(kxTy)2为例解析求导,其中 x x x y y y为二维向量变量, k k k为常数。显然残差为 k − x T y k-x^Ty kxTy

class SimpleCostFunction : public ceres::SizedCostFunction<1, 2, 2> {
 public:
  virtual ~SimpleCostFunction(double k) : k_(k) {};

  // 形参: 输入参数块, 残差, 雅可比矩阵
  virtual bool Evaluate(double const* const* parameters, double *residuals,
    double** jacobians) const {
    const double x0 = parameters[0][0];
    const double x1 = parameters[0][1];
    const double y0 = parameters[1][0];
    const double y1 = parameters[1][1];
    residuals[0] = k_ - x0 * y0 - x1 * y1;
    if (jacobians != NULL && jacobians[0] != NULL) {
      jacobians[0][0] = -y0;
      jacobians[0][1] = -y1;
      jacobians[1][0] = -x0;
      jacobians[1][1] = -x1;
    }
    return true;
  }

 private:
  double k_;
};

ceres::CostFunction* cost_function = new SimpleCostFunction();

对于,ceres::SizedCostFunction<x, y, z, …>的几个模板参数与Evaluate函数的形参对应关系:

  • x x x决定了residuals的维度

  • y , z , . . . y, z, ... y,z,...的个数决定了输入参数块的个数,即parameters的第一维度,而 y y y z z z本身的值决定了每个输入参数块的维度,即parameters的第二维度

  • y , z , . . . y, z, ... y,z,...的个数决定了输入参数块的个数,即jacobians的第一维度,而 x x x的值(行数)和 y y y z z z本身的值(列数)的乘积决定了每个输入参数块的雅可比矩阵的维度,即jacobians的第二维度(按行优先的顺序排列)

自动求导

自动求导(Automatic Derivatives)是Ceres很神奇的一个功能,它能够对于一些数据形式较为基础的表达式,自动求解出导数形式。采用的原理是对偶数(dual numbers)和Jets格式实现的,不理解具体细节也不影响使用。

如果想要了解自动求导的原理,可以参考文章:Ceres的数值求导与自动求导的原理。

自动求导的代价函数类定义为AutoDiffCostFunction类,其函数声明为:

template <typename CostFunctor,
       int kNumResiduals,  // Number of residuals, or ceres::DYNAMIC.
       int... Ns>          // Size of each parameter block
class AutoDiffCostFunction : public
SizedCostFunction<kNumResiduals, Ns> {
 public:
  AutoDiffCostFunction(CostFunctor* functor, ownership = TAKE_OWNERSHIP);
  // Ignore the template parameter kNumResiduals and use
  // num_residuals instead.
  AutoDiffCostFunction(CostFunctor* functor,
                       int num_residuals,
                       ownership = TAKE_OWNERSHIP);
};

想要使用自动求导,就必须构造代价函数结构体CostFunctor(采用类模板形式),并在其结构体内对模板括号运算符() 重载,定义残差。在重载的()运算符形参中,最后一个为残差(唯一的非常量形参),前面几个为输入参数块。函数返回true表示成功

注意:采用自动求导时,即使是整数也要写成浮点型,否则会报错Jet类型匹配错误。

下面以最小二乘 min ⁡ x 1 2 ( 10 − x ) 2 \underset{x}{\min} \frac{1}{2}(10-x)^{2} xmin21(10x)2为例自动求导。显然残差为 ( 10 − x ) (10−x) (10x)

struct CostFunctor {
  // 形参: 输入参数块, 残差
  template<typename T>
  bool operator()(const T *const x, T *residual) const {
    residual[0] = 10.0 - x[0];
    return true;
  }
};

// 模板参数: 代价函数结构体, 残差维度, 输入参数块维度
ceres::CostFunction *cost_function
    = new ceres::AutoDiffCostFunction<CostFunctor, 1, 1>(
        new CostFunctor());

下面以最小二乘 min ⁡ x 1 2 ( k − x T y ) 2 \underset{x}{\min} \frac{1}{2}(k-x^Ty)^{2} xmin21(kxTy)2为例自动求导,其中 x x x y y y为二维向量变量, k k k为常数。显然残差为 k − x T y k-x^Ty kxTy

class CostFunctor {
  CostFunctor(double k): k_(k) {}
  // 形参: 输入参数块, 残差
  template <typename T>
  bool operator()(const T* const x , const T* const y, T* e) const {
    e[0] = k_ - x[0] * y[0] - x[1] * y[1];
    return true;
  }

 private:
  double k_;
};

// 模板参数: 代价函数结构体, 残差维度, 输入参数块维度
ceres::CostFunction* cost_function
    = new ceres::AutoDiffCostFunction<CostFunctor, 1, 2, 2>(
        new CostFunctor(1.0));                     ^  ^  ^
                                                   |  |  |
                       Dimension of residual ------+  |  |
                       Dimension of x ----------------+  |
                       Dimension of y -------------------+

对于,ceres::AutoDiffCostFunction<CostFunctor, x, y, z, …>的几个模板参数与CostFunctor的模板括号运算符()的形参对应关系:

  • x x x决定了residuals的维度

  • y , z , . . . y, z, ... y,z,...的个数决定了输入参数块的个数,即模板括号运算符()的常量形参(除最后一个形参)个数,而 y y y z z z本身的值决定了每个输入参数块的维度,即每个常量形参的维度

  • x x x决定了模板括号运算符()的非常量形参(最后一个形参)的维度

数值求导

数值求导(Numeric derivatives)核心思想是,当增量很小时,可以采用 lim ⁡ Δ x → 0 f ( x + Δ x ) − f ( x ) Δ x \underset{\Delta x \to 0}{\lim} \frac{f(x+\Delta x)-f(x)}{\Delta x} Δx0limΔxf(x+Δx)f(x)近似求导,由此我们不需要实际计算导数的表达式,也可以从数值上进行近似。为此,求导方式的代码只需要写清楚残差怎么计算即可。

数值求导的代价函数类定义为NumericDiffCostFunction类,其函数声明为:

template <typename CostFunctor,
          NumericDiffMethodType method = CENTRAL,
          int kNumResiduals,  // Number of residuals, or ceres::DYNAMIC.
          int... Ns>          // Size of each parameter block.
class NumericDiffCostFunction : public
SizedCostFunction<kNumResiduals, Ns> {
};

数值求导法也是构造代价函数结构体CostFunctor,来定义残差。但在重载括号运算符() 时,没有使用模板,类型都固定为double。在重载的()运算符形参中,最后一个为残差(唯一的非常量形参),前面几个为输入参数块。函数返回true表示成功。另一个不同点是,在实例化代价函数类时需要额外指定一个模板参数NumericDiffMethodType

数值求导方法NumericDiffMethodType又具体分为:前向求导、中心求导和Ridders方法,对应的精度和耗时依次增加。官方建议在不考虑时间约束时采用Ridders方法,中心求导是一个均衡的选择。

下面以最小二乘 min ⁡ x 1 2 ( 10 − x ) 2 \underset{x}{\min} \frac{1}{2}(10-x)^{2} xmin21(10x)2为例自动求导。显然残差为 ( 10 − x ) (10−x) (10x)

struct CostFunctor {
  // 形参: 输入参数块, 残差
  bool operator()(const double *const x, double *residual) const {
    residual[0] = 10.0 - x[0];
    return true;
  }
};

// 模板参数: 代价函数结构体, ceres::CENTRAL, 残差维度, 输入参数块维度
ceres::CostFunction *cost_function
    = new ceres::NumericDiffCostFunction<CostFunctor, ceres::CENTRAL, 1, 1>(
        new CostFunctor());

下面以最小二乘 min ⁡ x 1 2 ( k − x T y ) 2 \underset{x}{\min} \frac{1}{2}(k-x^Ty)^{2} xmin21(kxTy)2为例数值求导,其中 x x x y y y为二维向量变量, k k k为常数。显然残差为 k − x T y k-x^Ty kxTy

class CostFunctor {
  CostFunctor(double k): k_(k) {}
  // 形参: 输入参数块, 残差
  bool operator()(const double* const x,
                  const double* const y,
                  double* residuals) const {
    residuals[0] = k_ - x[0] * y[0] + x[1] * y[1];
    return true;
  }

 private:
  double k_;
};

// 模板参数: 代价函数结构体, 残差维度, 输入参数块维度
ceres::CostFunction* cost_function
    = new ceres::NumericDiffCostFunction<CostFunctor, ceres::CENTRAL, 1, 2, 2>(
        new CostFunctor(1.0));                                  ^     ^  ^  ^
                                                                |     |  |  |
                                    Finite Differencing Scheme -+     |  |  |
                                    Dimension of residual ------------+  |  |
                                    Dimension of x ----------------------+  |
                                    Dimension of y -------------------------+

对于,ceres::NumericDiffCostFunction<CostFunctor, method, x, y, z, …>的几个模板参数与CostFunctor的括号运算符()的形参对应关系:

  • x x x决定了residuals的维度

  • y , z , . . . y, z, ... y,z,...的个数决定了输入参数块的个数,即括号运算符()的常量形参(除最后一个形参)个数,而 y y y z z z本身的值决定了每个输入参数块的维度,即每个常量形参的维度

  • x x x决定了模板括号运算符()的非常量形参(最后一个形参)的维度

求导方法的选择

求导方法的选择的依据是多方面的,一方面是精度和速度,另一方面是使用的便捷性。毫无疑问,在代码优化的情况下,解析求导方式求解速度最快,但缺点是需要人工计算雅克比矩阵,失去了便捷性。数值求导几乎是万能的,只是时间稍慢。而自动求导,虽然较为方便,但也存在问题,例如传入的变量类型若不是较为基础的数据格式则不可用自动求导

官方给出了几种方法的计算时间如下:

求导方法耗时
Rat43Analytic255
Rat43AnalyticOptimized92
Rat43NumericDiffForward262
Rat43NumericDiffCentral517
Rat43NumericDiffRidders3760
Rat43AutomaticDiff129

(多种求导方法耗时比较。从上到下:解析求导、优化后的解析求导、向前数值求导、中心数值求导、Ridders数值求导、自动求导)

可以看出,代码优化较好的解析法求导速度最快,其次是自动求导,数值求导相对较慢


构建优化问题并求解(STEP1/3/4/5)

在这里插入图片描述

构建优化问题

  1. 声明ceres::Problem problem

  2. 通过AddResidualBlock()将代价函数(cost function)、核函数(loss function)和输入参数块添加到problem

这里以自动求导为例:

ceres::CostFunction *cost_function =
  new ceres::AutoDiffCostFunction<CostFunctor, 1, 1>(new CostFunctor());

ceres::Problem problem;
problem.AddResidualBlock(cost_function, NULL, &x);
  1. 配置求解器,并计算,输出结果
ceres::Solver::Options options;
options.max_num_iterations = 25;
options.linear_solver_type = ceres::DENSE_QR;
options.minimizer_progress_to_stdout = true;

ceres::Solver::Summary summary;
ceres::Solve(options, &problem, &summary);
std::cout << summary.BriefReport() << "\n";

添加残差块

一般情况下,problem对象可以调用AddResidualBlock()函数添加残差块,包括代价函数、核函数、输入参数块,该函数的原型如下:

ResidualBlockId Problem::AddResidualBlock(CostFunction *cost_function, 
                                          LossFunction *loss_function, 
                                          const vector<double *> parameter_blocks);
ResidualBlockId Problem::AddResidualBlock(CostFunction *cost_function, 
                                          LossFunction *loss_function,
                                          double *x0, double *x1, ...);

该函数的形参含义为:

  • cost_function:代价函数,包含了输入参数块的数量和大小以及输出的数量,AddResidualBlock()函数内部会检测传入的输入参数块是否和代价函数中定义的维数一致,维度不一致时程序会强制退出
  • loss_function:核函数,或损失函数,用于处理参数中含有野值的情况,避免错误量测对估计的影响,常用参数包括HuberLoss、CauchyLoss等;该参数也可以取NULL或nullptr,此时损失函数为单位函数
  • parameter_blocks/x0,x1…:输入参数块,可一次性传入所有参数的指针容器或依次传入所有参数的指针

例如:

double x1[] = {1.0, 2.0, 3.0};
double x2[] = {1.0, 2.0, 5.0, 6.0};
double x3[] = {3.0, 6.0, 2.0, 5.0, 1.0};
vector<double*> v1;
v1.push_back(x1);
vector<double*> v2;
v2.push_back(x2);
v2.push_back(x1);

Problem problem;

ceres::CostFunction *cost_function1 =
  new ceres::AutoDiffCostFunction<CostFunctor, 1, 3>(new CostFunctor());
ceres::CostFunction *cost_function2 =
  new ceres::AutoDiffCostFunction<CostFunctor, 1, 4, 3>(new CostFunctor());

problem.AddResidualBlock(cost_function1, nullptr, x1);
problem.AddResidualBlock(cost_function2, nullptr, x2, x1);
problem.AddResidualBlock(cost_function1, nullptr, v1);
problem.AddResidualBlock(cost_function2, nullptr, v2);

problem对象也可以调用AddParameterBlock()显式添加输入参数块,这会导致额外的正确性检查;但是,如果参数块不存在,AddResidualBlock()会隐式添加它们,因此不需要显式调用AddParameterBlock()。

也就是说,AddResidualBlock()在添加残差块的同时,该函数内部已经隐式地调用了AddParameterBlock()函数进行参数块的设置。但在一些情况下,如果想要显示地传入参数模(通常出现在需要对优化参数进行重新参数化的情况),可以使用AddParameterBlock()函数

void Problem::AddParameterBlock(double *values, int size);
void Problem::AddParameterBlock(double *values, int size,
                                LocalParameterization *local_parameterization);
void Problem::AddParameterBlock(double *values, int size, Manifold *manifold);

其中,第一种函数原型除了会增加一些额外的参数检查之外,功能上和隐式地进行参数块的设置并没有太大区别。第二/三种函数原型则会额外传入LocalParameterization/Manifold,用于重构优化参数的维数。在2.2.0版本之后,LocalParameterization接口被弃用,需要使用Manifold代替。

注意:problem内的参数块使用map进行管理,无论添加多少次,都不会重复

核函数

对于最小二乘问题,最小化可能会遇到包含异常值的输入项,即完全虚假的测量值,因此使用损失函数来减少其影响非常重要。

自定义核函数类需要继承基类LossFunction,并对其中的纯虚函数进行实现。当然也可以使用Ceres预先定义好的较常用的子类。

class LossFunction {
 public:
  virtual void Evaluate(double s, double out[3]) const = 0;
};

关键方法是LossFunction::Evaluate(),它给定一个非负标量 s s s,核函数 ρ \rho ρ,计算:

o u t = [ ρ ( s ) , ρ ′ ( s ) , ρ ′ ′ ( s ) ] out = \begin{bmatrix}\rho(s), & \rho'(s), & \rho''(s)\end{bmatrix} out=[ρ(s),ρ(s),ρ′′(s)]

其中, s s s是残差的平方。

下面以柯西核函数(ceres预先定义好的CauchyLoss)为例:

// Inspired by the Cauchy distribution
//   rho(s) = log(1 + s).
// At s = 0: rho = [0, 1, -1].
class CERES_EXPORT CauchyLoss : public LossFunction {
 public:
  // 可以看出形参a为尺度参数
  explicit CauchyLoss(double a) : b_(a * a), c_(1 / b_) {}
  void Evaluate(double, double*) const override;
 private:
  // b = a^2
  const double b_;
  // c = 1 / a^2
  const double c_;
};

void CauchyLoss::Evaluate(double s, double rho[3]) const {
  const double sum = 1.0 + s * c_;
  const double inv = 1.0 / sum;
  // 'sum' and 'inv' are always positive, assuming that 's' is.
  rho[0] = b_ * log(sum);
  rho[1] = std::max(std::numeric_limits<double>::min(), inv);
  rho[2] = - c_ * (inv * inv);
}

Ceres预定义的一些LossFunction。为了简单起见,我们描述了它们的未缩放版本。下图以图形方式说明了它们的形状。

下面梳理一下常见的损失函数:

LossFunction类别公式
TrivialLoss ρ ( s ) = s \rho(s) = s ρ(s)=s
HuberLoss ρ ( s ) = { s s ≤ 1 2 s − 1 s > 1 \rho(s) = \begin{cases} s & s \le 1\\ 2 \sqrt{s} - 1 & s > 1 \end{cases} ρ(s)={s2s 1s1s>1
SoftLOneLoss ρ ( s ) = 2 ( 1 + s − 1 ) \rho(s) = 2 (\sqrt{1+s} - 1) ρ(s)=2(1+s 1)
CauchyLoss ρ ( s ) = log ⁡ ( 1 + s ) \rho(s) = \log(1 + s) ρ(s)=log(1+s)
ArctanLoss ρ ( s ) = arctan ⁡ ( s ) \rho(s) = \arctan(s) ρ(s)=arctan(s)
TolerantLoss ρ ( s , a , b ) = b log ⁡ ( 1 + e ( s − a ) / b ) − b log ⁡ ( 1 + e − a / b ) \rho(s,a,b) = b \log(1 + e^{(s - a) / b}) - b \log(1 + e^{-a / b}) ρ(s,a,b)=blog(1+e(sa)/b)blog(1+ea/b)

LocalParameterization

LocalParameterization类的作用是解决非线性优化中的过参数化问题。所谓过参数化,即输入参数块的实际自由度小于参数本身的自由度

除了上文的AddParameterBlock()函数之外,还可以用下面的函数设置:

void Problem::SetParameterization(double *values,
								  LocalParameterization *local_parameterization);

例如,在SLAM中,许多优化问题中,尤其是传感器融合问题,必须对存在于被称为流形的空间中的量进行建模。流形空间,局部看起来像欧几里得空间。更准确地说,流形上的每个点都有一个与流形相切的线性空间。它的尺寸等于流形本身的固有尺寸,该固有尺寸小于或等于嵌入流形的周围空间。例如,三维空间中球体上一点的切线空间是与该点处的球体相切的二维平面。切线空间有趣的原因有两个:

  • 它们是欧几里得空间,因此通常的向量空间运算适用于那里,这使得数值运算变得容易
  • 切线空间中的运动转化为沿流形的运动,垂直于切空间的运动不会转化为流形上的运动

例如:在SLAM的优化问题中,采用单位四元数表示位姿时,由于单位四元数本身的约束(模长为1),实际的自由度为3而非4。同时,单位四元数/旋转矩阵并不支持广义的加法运算,即使用普通的加法就会打破其constraint,比如,旋转矩阵加旋转矩阵得到的就不再是旋转矩阵,单位四元数加上单位四元数就不再是单位四元数。

因此,我们在使用ceres对其进行迭代更新的时候就需要自定义其更新方式了,此时就需要使用到LocalParameterization。

LocalParaneterization本身是一个虚基类,用户在自行定义使用的子类时,需要将所有的纯虚函数都实现一遍。当然也可以使用Ceres预先定义好的较常用的子类。

class LocalParameterization {
 public:
  virtual ~LocalParameterization() {}
  // 流型空间中的加法,即参数正切空间上的更新函数
  virtual bool Plus(const double* x,
                    const double* delta,
                    double* x_plus_delta) const = 0;
  // 计算雅克比矩阵
  virtual bool ComputeJacobian(const double* x, double* jacobian) const = 0;
  // 一般不用
  virtual bool MultiplyByJacobian(const double* x,
                                  const int num_rows,
                                  const double* global_matrix,
                                  double* local_matrix) const;
  // 参数块的实际维数,可能存在冗余
  virtual int GlobalSize() const = 0;
  // 参数块在正切空间上的维数
  virtual int LocalSize() const = 0;
};

下面以单位四元数(ceres预先定义好的QuaternionParameterization)为例,解释如何实现参数本地化。

由于由单位四元数表示的传感器的旋转/方向,它的实际维数为4,但是正切空间上的维数为3。因此,增量 d e l t a delta delta就是一个3维的。那单位四元数怎么和3维表示的 d e l t a delta delta进行计算呢?旋转向量(它正好就是3维的,且与单位四元数之间可以相互转化)。流型中的加法可以用符号 ⊞ \boxplus 表示。

设单位四元数 s = [ w , x , y , z ] s=[w,x,y,z] s=[w,x,y,z],对应的旋转向量为 ϕ = [ p , q , r ] \phi = [p,q,r] ϕ=[p,q,r] ,定义 θ = p 2 + q 2 + r 2 \theta=\sqrt{p^{2}+q^{2}+r^{2}} θ=p2+q2+r2 ,是 ϕ \phi ϕ 的模(旋转角), ∣ ∣ a ∧ ∣ ∣ = 1 ||a^{\wedge}|| = 1 ∣∣a∣∣=1,是 ϕ \phi ϕ 的方向向量(旋转轴), 即 ϕ = θ a ∧ \phi = \theta a^{\wedge} ϕ=θa,其中 ∧ \wedge 为向量到反对称矩阵的转换符。

旋转向量到单位四元数的映射为:

[ c o s ( θ ) , s i n ( θ ) θ p , s i n ( θ ) θ q , s i n ( θ ) θ r ] \Big[cos(\theta), \frac{sin(\theta)}{\theta}p, \frac{sin(\theta)}{\theta}q, \frac{sin(\theta)}{\theta}r \Big] [cos(θ),θsin(θ)p,θsin(θ)q,θsin(θ)r]

单位四元数相对于旋转矢量的雅克比矩阵计算方法,即:

J 4 × 3 = d s / d ϕ = d [ w , x , y , z ] T / d [ p , q , r ] \mathcal{J}_{4 \times 3}=d \mathcal{s} / d \mathcal{\phi} = d [w, x, y, z]^T /d [p,q,r] J4×3=ds/dϕ=d[w,x,y,z]T/d[p,q,r]

对应Jacobian维数为4行3列。这部分的详细推导可以参考:【Ceres】(二)LocalParameterization参数化、[ceres-solver] From QuaternionParameterization to LocalParameterization 、优化库——ceres(二)深入探索ceres::Problem

class CERES_EXPORT QuaternionParameterization : public LocalParameterization {
 public:
  virtual ~QuaternionParameterization() {}
  // 重载的Plus函数给出了四元数的更新方法
  // 参数:优化前的四元数x,用旋转向量表示的增量delta,以及更新后的四元数x_plus_delta。
  // 函数首先将增量delta由旋转向量转换为四元数,随后采用标准四元数乘法对四元数进行更新
  virtual bool Plus(const double* x,
                    const double* delta,
                    double* x_plus_delta) const;
  // 计算雅可比矩阵,存储方式为行优先
  virtual bool ComputeJacobian(const double* x,
                               double* jacobian) const;

  // 四元数本身的实际维数为4,但在内部优化时,ceres采用的是旋转向量,维数为3
  // GlobalSize:真正的维数是一个4维的
  virtual int GlobalSize() const { return 4; }
  // LocalSize:ceres优化时使用的维数是一个3维的
  virtual int LocalSize() const { return 3; }
};

bool QuaternionParameterization::Plus(const double* x,
                                      const double* delta,
                                      double* x_plus_delta) const {
  // 将旋转矢量转换为四元数形式
  const double norm_delta =
      sqrt(delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]);
  if (norm_delta > 0.0) {
    const double sin_delta_by_delta = (sin(norm_delta) / norm_delta);
    double q_delta[4];
    q_delta[0] = cos(norm_delta);
    q_delta[1] = sin_delta_by_delta * delta[0];
    q_delta[2] = sin_delta_by_delta * delta[1];
    q_delta[3] = sin_delta_by_delta * delta[2];
    // 采用四元数乘法更新
    QuaternionProduct(q_delta, x, x_plus_delta);
  } else {
    for (int i = 0; i < 4; ++i) {
      x_plus_delta[i] = x[i];
    }
  }
  return true;
}

// 雅可比矩阵是4*3维的,代码里使用一个size为12的数组存储
bool QuaternionParameterization::ComputeJacobian(const double* x,
                                                 double* jacobian) const {
  jacobian[0] = -x[1]; jacobian[1]  = -x[2]; jacobian[2]  = -x[3];
  jacobian[3] =  x[0]; jacobian[4]  =  x[3]; jacobian[5]  = -x[2];
  jacobian[6] = -x[3]; jacobian[7]  =  x[0]; jacobian[8]  =  x[1];
  jacobian[9] =  x[2]; jacobian[10] = -x[1]; jacobian[11] =  x[0];
  return true;
}

下面看下LocalParaneterization的具体用法。例如:在SLAM中,若使用四元数进行优化,可以使用Ceres预先定义的QuaternionParameterization对优化参数进行重构:

problem.AddParameterBlock(quaternion, 4);   // 直接传递4维参数

ceres::LocalParameterization* local_param =
  new ceres::QuaternionParameterization();
// 重构参数,优化时实际使用的是3维的等效旋转矢量(参数map管理)
problem.AddParameterBlock(quaternion, 4, local_param);

Ceres预定义的一些LocalParaneterization

LocalParaneterization类别含义
QuaternionParameterization四元数
EigenQuaternionParameterization除四元数排序采用Eigen的实部最后外,与QuaternionParameterization完全一致
IdentityParameterizationconstLocalSize与GlobalSize一致,相当于不传入LocalParameterization
SubsetParameterizationGlobalSize为2,LocalSize为1,用于第一维不需要优化的情况
HomogeneousVectorParameterization具有共面约束的空间点
ProductParameterization7维位姿变量一同优化,而前4维用四元数表示的情况

Manifold

在2.2.0版本之后,LocalParameterization接口被弃用,需要使用Manifold代替。它们之间的使用场景/使用方法很多都是类似的。这里就不详细介绍了。

除了上文的AddParameterBlock()函数之外,还可以用下面的函数设置:

void Problem::SetManifold(double *values, Manifold *manifold);
class Manifold {
 public:
  virtual ~Manifold();
  // 参数块的实际维数,可能存在冗余
  virtual int AmbientSize() const = 0;
  // 参数块在正切空间上的维数
  virtual int TangentSize() const = 0;

  // 流型空间中的加法,即参数正切空间上的更新函数
  virtual bool Plus(const double* x,
                    const double* delta,
                    double* x_plus_delta) const = 0;
  // 计算雅克比矩阵
  virtual bool PlusJacobian(const double* x, double* jacobian) const = 0;
  // 一般不用
  virtual bool RightMultiplyByPlusJacobian(const double* x,
                                           const int num_rows,
                                           const double* ambient_matrix,
                                           double* tangent_matrix) const;
  // 流型空间中的减法,即参数正切空间上的更新函数
  virtual bool Minus(const double* y,
                     const double* x,
                     double* y_minus_x) const = 0;
  // 计算雅克比矩阵
  virtual bool MinusJacobian(const double* x, double* jacobian) const = 0;
};

Options类

Solver::Options含有的参数种类繁多,但是在大多数情况下,绝大多数参数我们都会使用Ceres的默认设置,这里只列出一些常用或较为重要的参数:

  • minimizer_type:迭代求解方法,可选线性搜索方法(LINEAR_SEARCH)或信赖域方法(TRUST_REGION),默认为TRUST_REGION方法;由于大多数情况我们都会选择LM或DOGLEG方法,该选项一般直接采用默认值
  • trust_region_strategy_type:信赖域策略,可选LEVENBERG_MARQUARDTDOGLEG,默认为LEVENBERG_MARQUARDT
  • linear_solver_type:信赖域方法中求解线性方程组所使用的求解器类型,默认为DENSE_QR,其他可选项如下:DENSE_QR(QR分解,用于小规模最小二乘问题求解)、DENSE_NORMAL_CHOLESKY&SPARSE_NORMAL_CHOLESKY(Cholesky分解,用于具有稀疏性的大规模非线性最小二乘问题求解)、CGNR(使用共轭梯度法求解稀疏方程)、DENSE_SCHUR&SPARSE_SCHUR(SCHUR分解,用于BA问题求解)、ITERATIVE_SCHUR(使用共轭梯度SCHUR求解BA问题)
  • linear_solver_ordering:线性方程求解器的消元顺序,默认为NULL,即由Ceres自行决定消元顺序
  • min_linear_solver_iteration/max_linear_solver_iteration:线性求解器的最小/最大迭代次数,默认为0/500,一般不需要更改
  • max_num_iterations:求解器的最大迭代次数
  • max_solver_time_in_seconds:求解器的最大运行秒数
  • num_threads:Ceres求解时使用的线程数,在老版本的Ceres中还有一个针对线性求解器的线程设置选项,num_linear_solver_threads,最新版本的Ceres中该选项已被取消;虽然为了保证程序的兼容性,用户依旧可以设置该参数,但Ceres会自动忽略该参数,并没有实际意义

Summary类

Solver::Summary包含了求解器本身和求解中各变量的信息,许多成员函数与Solver::Options一致,这里只给出两个常用的成员函数:

// 输出单行的简单总结
string Solver::Summary::BriefReport() const;
// 输出多行的完整总结
string Solver::Summary::FullReport() const;

ceres实战案例

CmakeLists.txt配置

cmake_minimum_required(VERSION 2.8)
project(ceres)

find_package(Ceres REQUIRED)
include_directories(${CERES_INCLUDE_DIRS})

add_executable(test test.cpp)
target_link_libraries(test ${CERES_LIBRARIES})

示例:ceres入门例子

一个简单的求解 min ⁡ x 1 2 ( 10 − x ) 2 \underset{x}{\min} \frac{1}{2}(10-x)^{2} xmin21(10x)2 的优化问题,代码如下:

#include<iostream>
#include<ceres/ceres.h>

// 第一部分:构建代价函数结构体,重载()符号
struct CostFunctor {
   template <typename T>
   bool operator()(const T* const x, T* residual) const {
     residual[0] = T(10.0) - x[0];
     return true;
   }
};

// 主函数
int main(int argc, char** argv) {
  // 寻优参数x的初始值,为5
  double initial_x = 5.0;
  double x = initial_x;

  // 第二部分:构建寻优问题
  ceres::Problem problem;
  // 使用自动求导,将之前的代价函数结构体传入,第一个1是输出维度,即残差的维度,第二个1是输入维度,即待寻优参数x的维度
  ceres::CostFunction* cost_function =
    new ceres::AutoDiffCostFunction<CostFunctor, 1, 1>(new CostFunctor); 
  // 向问题中添加误差项,本问题比较简单,添加一个就行
  problem.AddResidualBlock(cost_function, NULL, &x); 

  // 第三部分: 配置并运行求解器
  ceres::Solver::Options options;
  options.linear_solver_type = ceres::DENSE_QR;    //配置增量方程的解法
  options.minimizer_progress_to_stdout = true;    //输出到cout
  ceres::Solver::Summary summary;    //优化信息
  ceres::Solve(options, &problem, &summary);    //求解!!!

  std::cout << summary.BriefReport() << "\n";    //输出优化的简要信息
  // 最终结果
  std::cout << "x : " << initial_x << " -> " << x << "\n";

  return 0;
}

示例:曲线拟合

拟合非线性函数的曲线 y = e a x 2 + b x + c y=e^{a x^{2}+b x+c} y=eax2+bx+c(其中, a = 3 a=3 a=3 b = 2 b=2 b=2 c = 1 c=1 c=1,待拟合数据点: [ 0 , 1 ] [0,1] [0,1]之间均匀生成的1000个数据点,加上方差为1的白噪声)。代码如下:

#include<iostream>
#include<opencv2/core/core.hpp>
#include<ceres/ceres.h>

// 构建代价函数结构体,abc为输入参数块,residual为残差。
struct CURVE_FITTING_COST
{
  CURVE_FITTING_COST(double x, double y): _x(x), _y(y) {}
  template <typename T>
  bool operator()(const T* const abc, T* residual) const {
    residual[0] = _y - ceres::exp(abc[0] * _x * _x + abc[1] * _x + abc[2]);
    return true;
  }
  const double _x, _y;
};

// 主函数,abc初始化为0
int main()
{
  // 参数初始化设置,
  double abc[3] = {0, 0, 0};

  // 生成待拟合曲线的数据散点,储存在Vector里
  std::vector<double> x_data, y_data;
  double a = 3, b = 2, c = 1, w = 1;
  cv::RNG rng;
  for (int i = 0; i < 1000; i++) {
    double x = i / 1000.0;
    x_data.push_back(x);
    y_data.push_back(exp(a * x * x + b * x + c) + rng.gaussian(w));
  }

  // 反复使用AddResidualBlock方法,将每个点的残差累计求和构建最小二乘优化式
  // 不使用核函数,输入参数块是abc
  ceres::Problem problem;
  for (int i = 0; i < 1000; i++) {
    problem.AddResidualBlock(
      new ceres::AutoDiffCostFunction<CURVE_FITTING_COST, 1, 3>(
        new CURVE_FITTING_COST(x_data[i], y_data[i])), nullptr, abc);
  }

  // 配置求解器并求解,输出结果
  ceres::Solver::Options options;
  options.linear_solver_type = ceres::DENSE_QR;
  options.minimizer_progress_to_stdout = true;
  ceres::Solver::Summary summary;
  ceres::Solve(options, &problem, &summary);
  std::cout << "a= " << abc[0] << std::endl;
  std::cout << "b= " << abc[1] << std::endl;
  std::cout << "c= " << abc[2] << std::endl;

  return 0;
}

详细可以参考文章:一文助你Ceres 入门——Ceres Solver新手向全攻略。

示例:最小化鲍威尔方程

假设 x = [ x 1 , x 2 , x 3 , x 4 ] x=[x_1,x_2,x_3,x_4] x=[x1,x2,x3,x4],它的方程表示为 F ( x ) = [ f 1 ( x ) , f 2 ( x ) , f 3 ( x ) , f 4 ( x ) ] F(x)=[f_1(x),f_2(x),f_3(x),f_4(x)] F(x)=[f1(x),f2(x),f3(x),f4(x)]
f 1 ( x ) = x 1 + 10 x 2 f 2 ( x ) = 5 ( x 3 − x 4 ) f 3 ( x ) = ( x 2 − 2 x 3 ) 2 f 4 ( x ) = 10 ( x 1 − x 4 ) 2 \begin{split} f_1(x)=x_1+10x_2 \\ f_2(x)=\sqrt{5}(x_3-x_4) \\ f_3(x)=(x_2-2x_3)^2 \\ f_4(x)=\sqrt{10}(x_1-x_4)^2 \end{split} f1(x)=x1+10x2f2(x)=5 (x3x4)f3(x)=(x22x3)2f4(x)=10 (x1x4)2
找到一组 x x x使得 1 2 ∥ F ( x ) ∥ 2 \frac{1}{2}\left\|F(x)\right\|^2 21F(x)2最小。代码如下:

#include <vector>
#include "ceres/ceres.h"

//创建四个残差块,也就是四个仿函数
struct F1 {
  template <typename T>
  bool operator()(const T* const x1, const T* const x2, T* residual) const {
    residual[0] = x1[0] + 10.0 * x2[0];
    return true;
  }
};

struct F2 {
  template <typename T>
  bool operator()(const T* const x3, const T* const x4, T* residual) const {
    residual[0] = sqrt(5.0) * (x3[0] - x4[0]);
    return true;
  }
};

struct F3 {
  template <typename T>
  bool operator()(const T* const x2, const T* const x3, T* residual) const {
    residual[0] = (x2[0] - 2.0 * x3[0]) * (x2[0] - 2.0 * x3[0]);
    return true;
  }
};

struct F4 {
  template <typename T>
  bool operator()(const T* const x1, const T* const x4, T* residual) const {
    residual[0] = sqrt(10.0) * (x1[0] - x4[0]) * (x1[0] - x4[0]);
    return true;
  }
};

int main(int argc, char** argv) {
  // 设置初始值
  double x1 = 3.0;
  double x2 = -1.0;
  double x3 = 0.0;
  double x4 = 1.0;

  // 加入problem
  ceres::Problem problem;
  problem.AddResidualBlock(
    new ceres::AutoDiffCostFunction<F1, 1, 1, 1>(new F1), NULL, &x1, &x2);
  problem.AddResidualBlock(
    new ceres::AutoDiffCostFunction<F2, 1, 1, 1>(new F2), NULL, &x3, &x4);
  problem.AddResidualBlock(
    new ceres::AutoDiffCostFunction<F3, 1, 1, 1>(new F3), NULL, &x2, &x3);
  problem.AddResidualBlock(
    new ceres::AutoDiffCostFunction<F4, 1, 1, 1>(new F4), NULL, &x1, &x4);

  // 设置options
  ceres::Solver::Options options;
  options.max_num_iterations = 100;  // 迭代次数
  options.linear_solver_type = ceres::DENSE_QR;
  options.minimizer_progress_to_stdout = true;
  ceres::Solver::Summary summary;
  ceres::Solve(options, &problem, &summary);
  std::cout << summary.FullReport() << "\n";
  std::cout << "Final x1 = " << x1
            << ", x2 = " << x2
            << ", x3 = " << x3
            << ", x4 = " << x4
            << "\n";

  return 0;
}

详细可以参考文章:Ceres Solver:从入门到使用。

这样看起来有点复杂,把输入参数块和残差全部拆开来了,也可以合并起来看:

#include <vector>
#include "ceres/ceres.h"

//创建四个残差块,也就是四个仿函数
struct CostFunctor {
  template <typename T>
  bool operator()(const T* const x, T* residual) const {
    residual[0] = x[0] + 10.0 * x[1];
    residual[1] = sqrt(5.0) * (x[2] - x[3]);
    residual[2] = (x[1] - 2.0 * x[2]) * (x[1] - 2.0 * x[2]);
    residual[3] = sqrt(10.0) * (x[0] - x[3]) * (x[0] - x[3]);
    return true;
  }
};

int main(int argc, char** argv) {
  // 设置初始值
  double x = {3.0-1.00.01.0}// 加入problem
  ceres::Problem problem;
  problem.AddResidualBlock(
    new ceres::AutoDiffCostFunction<CostFunctor, 4, 4>(new CostFunctor),
      NULL, &x);

  // 设置options
  ceres::Solver::Options options;
  options.max_num_iterations = 100;  // 迭代次数
  options.linear_solver_type = ceres::DENSE_QR;
  options.minimizer_progress_to_stdout = true;
  ceres::Solver::Summary summary;
  ceres::Solve(options, &problem, &summary);
  std::cout << summary.FullReport() << "\n";
  std::cout << "Final x1 = " << x[0]
            << ", x2 = " << x[1]
            << ", x3 = " << x[2]
            << ", x4 = " << x[3]
            << "\n";

  return 0;
}

示例: 基于李代数的视觉SLAM位姿优化

基于李代数进行视觉SLAM位姿优化时,可以得到:

  • 残差(预测值 - 观测值)

r ( ξ ) = K exp ⁡ ( ξ ∧ ) P − u r(\xi)=K \exp \left(\xi^{\wedge}\right) P-u r(ξ)=Kexp(ξ)Pu

  • 雅克比矩阵

J = ∂ r ( ξ ) ∂ ξ = [ f x Z ′ 0 − X ′ f x Z ′ 2 − X ′ Y ′ f z Z 2 f x + X ′ 2 f x Z 2 − Y ′ f x Z ′ 0 f y Z ′ − Y ′ f y Z 2 − f y − Y ′ 2 f y Z ′ 2 X ′ Y ′ f y Z ′ 2 X ′ f y Z ′ ] ∈ R 2 × 6 \begin{array}{c} J &= &\frac{\partial r(\xi)}{\partial \xi} \\ &= &\left[\begin{array}{ccccc} \frac{f_{x}}{Z^{\prime}} & 0 & -\frac{X^{\prime} f_{x}}{Z^{\prime 2}} & -\frac{X^{\prime} Y^{\prime} f_{z}}{Z^{2}} & f_{x}+\frac{X^{\prime 2} f_{x}}{Z^{2}} & -\frac{Y^{\prime} f_{x}}{Z^{\prime}} \\ 0 & \frac{f_{y}}{Z^{\prime}} & -\frac{Y^{\prime} f_{y}}{Z^{2}} & -f_{y}-\frac{Y^{\prime 2} f_{y}}{Z^{\prime 2}} & \frac{X^{\prime} Y^{\prime} f_{y}}{Z^{\prime 2}} & \frac{X^{\prime} f_{y}}{Z^{\prime}} \end{array}\right] \in \mathbb{R}^{2 \times 6} \end{array} J==ξr(ξ)[Zfx00ZfyZ′2XfxZ2YfyZ2XYfzfyZ′2Y′2fyfx+Z2X′2fxZ′2XYfyZYfxZXfy]R2×6

该雅可比矩阵的求解方式,可以参考文章:视觉SLAM位姿优化时误差函数雅克比矩阵的计算、BA优化中Jacobian矩阵的计算

核心代码如下:

代价函数的构造:

// 重投影误差,残差2维的,优化变量是6维度
class BAGNCostFunctor : public ceres::SizedCostFunction<2, 6> {
 public:
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW   //对齐
  // 第一个是像素观测值,第二个是准备用来重投影的三维点
  BAGNCostFunctor(Eigen::Vector2d observed_p, Eigen::Vector3d observed_P) :
    observed_p_(observed_p), observed_P_(observed_P) {}
  virtual ~BAGNCostFunctor() {}

  // 输入参数块、残差、雅可比矩阵
  virtual bool Evaluate(double const* const* parameters,
    double *residuals, double **jacobians) const {
      // 获取位姿,并将三维点进行重投影到成像平面
      Eigen::Map<const Eigen::Matrix<double, 6, 1> > T_se3(*parameters);
      Sophus::SE3 T_SE3 = Sophus::SE3::exp(T_se3);
      Eigen::Vector3d Pc = T_SE3 * observed_P_;

      // 内参矩阵
      Eigen::Matrix3d K;
      double fx = 520.9, fy = 521.0, cx = 325.1, cy = 249.7;
      K << fx, 0, cx, 0, fy, cy, 0, 0, 1;
      
      // 计算残差
      Eigen::Vector2d residual = observed_p_ - (K * Pc).hnormalized();
      residuals[0] = residual[0];
      residuals[1] = residual[1];

      if(jacobians != NULL) {
          if(jacobians[0] != NULL) {
              // 2*6的雅克比矩阵
              Eigen::Map<Eigen::Matrix<double, 2, 6, Eigen::RowMajor> > J(jacobians[0]);

              double x = Pc[0];
              double y = Pc[1];
              double z = Pc[2];

              double x2 = x * x;
              double y2 = y * y;
              double z2 = z * z;

              J(0,0) =  fx / z;
              J(0,1) =  0;
              J(0,2) = -fx * x / z2;
              J(0,3) = -fx * x * y / z2;
              J(0,4) =  fx + fx * x2 / z2;
              J(0,5) = -fx * y / z;
              J(1,0) =  0;
              J(1,1) =  fy / z;
              J(1,2) = -fy * y / z2;
              J(1,3) = -fy - fy * y2 / z2;
              J(1,4) =  fy * x * y / z2;
              J(1,5) =  fy * x / z;
          }
      }

      return true;
  }

 private:
  const Eigen::Vector2d observed_p_;
  const Eigen::Vector3d observed_P_;
};

构造优化问题,并求解相机位姿:

Sophus::Vector6d se3;

// 在当前problem中添加代价函数残差块,损失函数为NULL采用默认的最小二乘误差即L2范数,优化变量为 se3
ceres::Problem problem;
for(int i = 0; i < n_points; ++i) {
  ceres::CostFunction *cost_function;
  cost_function = new BAGNCostFunctor(p2d[i], p3d[i]);
  problem.AddResidualBlock(cost_function, NULL, se3.data());
}

ceres::Solver::Options options;
options.dynamic_sparsity = true;
options.max_num_iterations = 100;    // 迭代100次
options.sparse_linear_algebra_library_type = ceres::SUITE_SPARSE;
options.minimizer_type = ceres::TRUST_REGION;
options.linear_solver_type = ceres::SPARSE_NORMAL_CHOLESKY;
options.trust_region_strategy_type = ceres::DOGLEG;
options.minimizer_progress_to_stdout = true;
options.dogleg_type = ceres::SUBSPACE_DOGLEG;

ceres::Solver::Summary summary;
ceres::Solve(options, &problem, &summary);
std::cout << summary.BriefReport() << "\n";

std::cout << "estimated pose: \n" << Sophus::SE3::exp(se3).matrix() << std::endl;

详细可以参考文章:Ceres-Solver 从入门到上手视觉SLAM位姿优化问题。


相关阅读

  • Ceres Solver官方文档
  • Ceres详解(一) Problem类
  • 优化库——ceres(一)快速概览

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/738056.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用 Nginx 禁止国外 IP 访问我的网站...

先来说说为啥要写这篇文章&#xff0c;之前看了下 Nginx 的访问日志&#xff0c;发现每天有好多国外的 IP 地址来访问我的网站&#xff0c;并且访问的内容基本上都是恶意的。因此我决定禁止国外 IP 来访问我的网站。 想要实现这个功能有很多方法&#xff0c;下面我就来介绍基于…

(动态规划) 132. 分割回文串 II ——【Leetcode每日一题】

❓ 132. 分割回文串 II 难度&#xff1a;困难 给你一个字符串 s&#xff0c;请你将 s 分割成一些子串&#xff0c;使每个子串都是回文。 返回符合要求的 最少分割次数 。 示例 1&#xff1a; 输入&#xff1a;s “aab” 输出&#xff1a;1 解释&#xff1a;只需一次分割就…

Vision Transformer (ViT)介绍

paper&#xff1a;An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale 摘要 把transformer直接应用于图像块序列&#xff0c;也可以在图像分类任务上表现很好。通过在大数据集上预训练&#xff0c;然后迁移到中等规模和小规模数据集上&#xff0c;…

Android之签字板

文章目录 前言一、效果图二、实现步骤1.GestureSignatureView类2.xml布局3.Activity类(kotlin)4.Activity类(Java)5.动态申请权限(kotlin)6.动态申请权限(Java) 总结 前言 随着公司发展需求&#xff0c;很多金融APP都会涉及到需要用户签字的环节&#xff0c;所以在此贴出代码以…

软考高级架构师笔记-9系统架构

目录 1. 前文回顾 & 考情分析2. 软件架构概述3. 软件架构风格3.1 层次架构风格3.2 面向服务架构风格4. 软件架构复用5. 特定领域软件体系结构DSSA6. ABSD7. 质量属性8. 架构评估9 结语1. 前文回顾 & 考情分析 前文回顾: 软考高级架构师笔记-1计算机硬件软考高级架构师…

TCP 协议(三)十种核心机制

1.确认应答&#xff08;可靠机制&#xff09; 2.超时重传&#xff08;可靠机制&#xff09; 3.连接管理&#xff08;可靠机制&#xff09; 4.滑动窗口&#xff08;效率机制&#xff09; 5.流量控制&#xff08;效率机制&#xff09; 6.拥塞控制&#xff08;效率机制&#xff09…

优维低代码实践:权限设置

优维低代码技术专栏&#xff0c;是一个全新的、技术为主的专栏&#xff0c;由优维技术委员会成员执笔&#xff0c;基于优维7年低代码技术研发及运维成果&#xff0c;主要介绍低代码相关的技术原理及架构逻辑&#xff0c;目的是给广大运维人提供一个技术交流与学习的平台。 优维…

js两种对象混合写,返回的是哪一个

<script>function jiafa() {this.name "xuhaitao";this.age 36;var obj {};obj.xx "hunkxu";obj.yy "88";return obj;}var aa new jiafa();console.log(aa);</script> 打印&#xff1a; FR&#xff1a;徐海涛(hunk xu)

3D引擎龙头Unity:元宇宙和AI活跃玩家

Unity是用于创建和操作交互式实时3D内容的世界领先平台。凭借灵活的编辑器、友好的开发环境、丰富的工具套件&#xff0c;Unity吸引了大量开发者&#xff0c;全球排名前1000的移动游戏70%以上使用了Unity的创作和运营解决方案&#xff0c;如今&#xff0c;Unity引擎在工业场景、…

leaflet地图移动防抖问题

现在有这么一个需求&#xff0c;当移动地图时&#xff0c;需要获取当前地图范围属于那个城市。如果频繁移动地图&#xff0c;会不停的调用接口获取当前地图视图所属城市&#xff0c;所以加个防抖&#xff0c;减少请求。代码示例&#xff1a;<!DOCTYPE html> <html>…

【Leetcode】36. 有效的数独

一、题目 1、题目描述 请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图) 注意:…

微信小程序生态15- 批量提交微信小程序审核的一种方式

文章导航 微信小程序生态1-初识小程序 微信小程序生态2-创建一个微信小程序 微信小程序生态3-微信小程序登录流程设计 微信小程序生态4-扫普通二维码进入小程序、打开短链接进入小程序 微信小程序生态5-微信公众号扫码登录PC端网页 微信小程序生态6-微信公众号授权登录(适用于…

字节跳动(抖音),软件测试四面,面试题总结!走过路过不要错过

面试一 1、 简单做一下自我介绍 2、 简要介绍一下项目/你负责的模块/选一个模块说一下你设计的用例 3 、get请求和post请求的区别 4、 如何判断前后端bug/3xx是什么意思 5、 说一下XXX项目中你做的接口测试/做了多少次 6、 http和https的区别 7、 考了几个ADB命令/查看连…

JAVA_WEB 购物商城(WEB端)

仓库地址&#xff1a;https://gitee.com/ThMyGitee/EeasyEeasyCityFrontEnd.git CSDN的友友们&#xff0c;项目如果适合您的话&#xff0c;麻烦给个小小的Star&#xff0c;谢谢啦&#xff01; JAVA_WEB 购物商城(WEB端) 效果图: 技术选型: 后端技术栈 Jsp Servlet &#x…

【力扣算法01】之最接近的三数之和

文章目录 前言问题描述示例 1示例 2提示 解题思路代码分析完整代码运行效果如图执行示例代码1执行示例代码2 完结 前言 近期已经将python 的大部分内容讲完了, 接下来的一段时间会着重于算法和面试题相关的内容, 确保学有所用, 同时也为准备进入大厂的童靴们做个铺垫, 记得关注…

抖音seo源码-源代码开发搭建-开源部署操作日志

一、开发者前言大环境概述 抖音seo源码开发是一项非常重要的技术&#xff0c;在抖音上&#xff0c;有很多视频&#xff0c;如果你想让自己的视频脱颖而出&#xff0c;那么就需要优化自己的seo源码。不过&#xff0c;为了保护用户的隐私&#xff0c;抖音并不公开其seo算法的细节…

Alluxio入门手册

1. 定义 Alluxio&#xff08;之前名为 Tachyon&#xff09;是世界上第一个以内存为中心的虚拟的分布式存储系统。 它统一了数据访问的方式&#xff0c;为上层计算框架和底层存储系统构建了桥梁。应用只需要连接Alluxio即可访问存储在底层任意存储系统中的数据。此外&#xff0c…

太难了,别再为难我们这些测试工程师了好吗?

前言 有不少技术友在测试群里讨论&#xff0c;近期的面试越来越难了&#xff0c;要背的八股文越来越多了,考察得越来越细&#xff0c;越来越底层&#xff0c;明摆着就是想让我们徒手造航母嘛&#xff01;实在是太为难我们这些测试工程师了。 这不&#xff0c;为了帮大家节约时…

从零开发短视频电商 Jmeter基础介绍和使用心得

文章目录 基本Jmeter主要元件元件执行顺序提取器Json提取器边界值提取器 高级动态参数参数化前后关联设置Jmeter语言为中文环境非GUI模式压测分布式压测监控模板 基本 Jmeter主要元件 1、测试计划&#xff1a;是使用 JMeter 进行测试的起点&#xff0c;它是其它 JMeter测试元…

一、Zabbix介绍及6.0部署

Zabbix 一、Zabbix介绍1、zabbix是什么&#xff1f;2、zabbix的监控原理是什么&#xff1f;3、Zabbix 6.0新特性:4、Zabbix 6.0 功能组件&#xff1a; 二、Zabbix 6.0 部署1、部署 zabbix 服务端2、部署数据库3、编译安装 zabbix Server 服务端4、部署 Web 前端&#xff0c;进行…