使用C++实现最小二乘曲线拟合(使用Ceres实现)

使用C++实现最小二乘曲线拟合(使用Ceres实现)

自学视觉SLAM14讲-6.3.2,自学笔记

理论部分

详细的理论部分请看视觉SLAM14讲(第二版),从132页开始的6.3节,曲线拟合问题,第一小节6.3.1-手写高斯牛顿法,这里有非常详细的介绍,此处不再赘述!!!,此外,在下面还写了我自己的一些理解。

个人理解

y = e ( a x 2 + b x + c ) + ω y=e^{(ax^2 + bx + c)} +\omega y=e(ax2+bx+c)+ω
那么对于第i个数据点,误差项ei的表达式为:
e i = y i − e ( a x i 2 + b x i + c ) e_i = y_i - e^{(ax_i^2 + bx_i + c)} ei=yie(axi2+bxi+c)
那么目标函数的表达式为:
1 2 ∑ i = 1 N ∣ ∣ e i ∣ ∣ \frac{1}{2}\sum_{i=1}^N||e_i|| 21i=1N∣∣ei∣∣
其中a、b、c是曲线的参数,也是待估计的变量;高斯牛顿法需要对每个待估计的变量求导数,进而可以计算雅可比矩阵:
∂ e i ∂ a = − x i 2 e ( a x i 2 + b x i + c ) \frac{\partial e_i}{\partial a} = -x_i^2 e^{(ax_i^2 + bx_i + c)} aei=xi2e(axi2+bxi+c)
∂ e i ∂ b = − x i e ( a x i 2 + b x i + c ) \frac{\partial e_i}{\partial b} = -x_i e^{(ax_i^2 + bx_i + c)} bei=xie(axi2+bxi+c)
∂ e i ∂ c = − e ( a x i 2 + b x i + c ) \frac{\partial e_i}{\partial c} = - e^{(ax_i^2 + bx_i + c)} cei=e(axi2+bxi+c)

那么第i个样本点xi和yi所对应的雅可比矩阵Ji的表达式为:
J i = [ ∂ e i ∂ a ∂ e i ∂ b ∂ e i ∂ c ] T J_i = [ \frac{\partial e_i}{\partial a} \frac{\partial e_i}{\partial b} \frac{\partial e_i}{\partial c} ]^T Ji=[aeibeicei]T
高斯牛顿法的增量方程为:
( ∑ i = 1 N 1 σ 2 J i J i T ) Δ x k = ∑ i = 1 N − 1 σ 2 J i e i (\sum_{i=1}^N \frac{1}{\sigma^2} \boldsymbol{J_i} \boldsymbol{J_i}^T) \Delta x_k = \sum_{i=1}^N - \frac{1}{\sigma^2} \boldsymbol{J_i} e_i (i=1Nσ21JiJiT)Δxk=i=1Nσ21Jiei

在实际编程时,会有如下标记:
H i = 1 σ 2 J i J i T \boldsymbol{H_i} = \frac{1}{\sigma^2} \boldsymbol{J_i} \boldsymbol{J_i}^T Hi=σ21JiJiT
b i = − 1 σ 2 J i e i \boldsymbol{b_i} = - \frac{1}{\sigma^2} \boldsymbol{J_i} e_i bi=σ21Jiei
成为海森矩阵,这一个3×3的矩阵。

基本概念

高斯牛顿法实现时,需要根据目标函数计算雅可比矩阵,进一步计算海森矩阵,当目标函数比较简单时,雅可比矩阵和海森矩阵的计算比较容易,但是当当待估计的参数较多,并且目标函数比较复杂时,雅可比矩阵的计算非常复杂,因此引入谷歌的Ceres库,在使用Ceres进行曲线拟合之前,有几个基础概念需要了解一下:

在这里插入图片描述

参数块 :是指x1、x2,… ,x~n ~ 是待优化的变量;
代价函数fi ,也称为残差块,在SLAM中通常是误差项ei;
目标函数 :所有代价函数的平方项经过加权求和后(再除以二分之一)。

安装Ceres

先安装依赖环境,ubuntu终端输入:

sudo apt-get install liblapack-dev libsuitesparse-dev libcxsparse3 libgflags-dev libgoogle-glog-dev libgtest-dev

在slambook2/3rdparty/ceres-solver文件夹下打开终端并输入:

mkdir build
cd build/
cmake ..
make -j4
sudo make install

安装方法也可以查看链接,视觉slam的环境部署都在这里:
视觉SLAM十四讲-环境部署

使用Ceres进行曲线拟合的操作步骤:

1、定义每个参数块:参数块通常为平凡的向量,但是在SLAM里也可以定义成四元数、李代数这种特殊的结构。如果是向量,那么我们需要为每个参数块分配一个double数组来存储变量的值。
2、定义残差块的计算方式:也就是定义代价函数的计算方式,残差块通常关联若干个参数块,对它们进行一些自定义的计算,然后返回残差值。Ceres对它们求平方和之后,作为目标函数的值。
e i = y i − e ( a x i 2 + b x i + c ) e_i = y_i - e^{(ax_i^2 + bx_i + c)} ei=yie(axi2+bxi+c)

3、定义残差块的雅可比的计算方式:在 Ceres中,你可以使用它提供的“自动求导”功能,也可以手动指定雅可比的计算过程。如果要使用自动求导,那么残差块需要按照特定的写法书写:残差的计算过程应该是一个带模板的括号运算符。这一点我们通过例子来说明。

4、将所有的参数块和残差块加入Ceres定义的Problem对象:调用Solve函数求解即可。求解之前,我们可以传入一些配置信息,例如迭代次数、终止条件等,也可以使用默认的配置。

上代码:

关键代码:

 // 构建最小二乘问题
  ceres::Problem problem; // problem 对象的实例化
  for (int i = 0; i < N; i++) {
    problem.AddResidualBlock(     // 向问题中添加误差项
      // 使用自动求导,模板参数:误差类型,输出维度,输入维度,维数要与前面struct中一致
      new ceres::AutoDiffCostFunction<CURVE_FITTING_COST, 1, 3>(
        new CURVE_FITTING_COST(x_data[i], y_data[i])
      ),
      nullptr,            // 核函数,这里不使用,为空
      abc                 // 待估计参数
    );
  }

完整代码:

//
// Created by xiang on 18-11-19.
//

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

using namespace std;

// 代价函数的计算模型(误差项的计算模型)
struct CURVE_FITTING_COST {
  CURVE_FITTING_COST(double x, double y) : _x(x), _y(y) {}

  // 残差的计算
  template<typename T>
  bool operator()(
    const T *const abc, // 模型参数,有3维,参数块定义,是指针类型
    T *residual) const {
    residual[0] = T(_y) - ceres::exp(abc[0] * T(_x) * T(_x) + abc[1] * T(_x) + abc[2]); // y-exp(ax^2+bx+c)
    return true;
  }

  const double _x, _y;    // x,y数据
};

int main(int argc, char **argv) {
  double ar = 1.0, br = 2.0, cr = 1.0;         // 真实参数值
  double ae = 2.0, be = -1.0, ce = 5.0;        // 估计参数值
  int N = 100;                                 // 数据点
  double w_sigma = 1.0;                        // 噪声Sigma值
  double inv_sigma = 1.0 / w_sigma;
  cv::RNG rng;                                 // OpenCV随机数产生器

  vector<double> x_data, y_data;      // 数据
  for (int i = 0; i < N; i++) {
    double x = i / 100.0;
    x_data.push_back(x);
    y_data.push_back(exp(ar * x * x + br * x + cr) + rng.gaussian(w_sigma * w_sigma));
  }

  double abc[3] = {ae, be, ce};

  // 构建最小二乘问题
  ceres::Problem problem; // problem 对象的实例化
  for (int i = 0; i < N; i++) {
    problem.AddResidualBlock(     // 向问题中添加误差项
      // 使用自动求导,模板参数:误差类型,输出维度,输入维度,维数要与前面struct中一致
      // 误差是1个数,因此维度是1,输入是abc待估计变量,维度是3维的;
      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_NORMAL_CHOLESKY;  // 增量方程如何求解
  options.minimizer_progress_to_stdout = true;   // 输出到cout

  ceres::Solver::Summary summary;                // 优化信息
  chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
  ceres::Solve(options, &problem, &summary);  // 开始优化
  chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
  chrono::duration<double> time_used = chrono::duration_cast<chrono::duration<double>>(t2 - t1);
  cout << "solve time cost = " << time_used.count() << " seconds. " << endl;

  // 输出结果
  cout << summary.BriefReport() << endl;
  cout << "estimated a,b,c = ";
  for (auto a:abc) cout << a << " ";
  cout << endl;

  return 0;
}

程序运行结果

高斯牛顿法的运行结果
使用Ceres的运行结果
从运行时间上看,使用高斯牛顿法比使用Ceres的自动求导法快了将近10倍!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值