Ceres曲线拟合

本文介绍如何使用Ceres库实现曲线拟合。

一、曲线拟合

所谓曲线拟合,就是给定一组x和y的值,它们大体上满足一条曲线方程,但受噪声影响,并不精确满足方程。在这种情况下求取曲线方程的参数。例如,给定100对x和y的值,把它们画在坐标系上如图所示:

Ceres曲线拟合_第1张图片

假设我们事先知道(或者猜测)该曲线方程的形式为

那么就可以构造一个最小二乘问题以估计其中的未知参数a、b和c。该最小二乘问题的代价函数为:

这是一个非线性优化问题,目标是求使上式最小的a、b和c。

二、Ceres库

Ceres是一个C++库,用于求解通用的最小二乘问题,因此非常适合上面介绍的曲线拟合。而且Ceres的使用也非常简单,大体上分为如下三步:

  1. 定义代价函数;
  2. 构建最小二乘问题,向问题中添加误差项,此时会用到第一步的代价函数;
  3. 配置求解器,开始求解。

下面用一个实际的例子来说明。

三、使用Ceres库实现曲线拟合

代码下载地址:https://github.com/jingedawang/CeresCurveFitting

鉴于代码很简短,不妨先全部贴出来,再仔细讲解。

#include 
#include 
#include 
#include 
#include 

using namespace std;

// 代价函数的计算模型
struct CURVE_FITTING_COST
{
    CURVE_FITTING_COST ( double x, double y ) : _x ( x ), _y ( y ) {}
    // 残差的计算
    template 
    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 a=1.0, b=2.0, c=1.0;         // 真实参数值
    int N=100;                          // 数据点
    double w_sigma=1.0;                 // 噪声Sigma值
    cv::RNG rng;                        // OpenCV随机数产生器
    double abc[3] = {0,0,0};            // abc参数的估计值

    stringstream ss;

    vector x_data, y_data;      // 数据

    cout<<"generating data: "< (
                        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;   // 输出到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 time_used = chrono::duration_cast>( t2-t1 );
    cout<<"solve time cost = "<

代码中需要关注的有这么几点:

  1. 定义代价函数的方式是自定义一个结构体。只需要在结构体中实现operator()方法,就算是给Ceres提供了一个调用接口,由Ceres内部在合适的时候调用该方法计算代价函数。
  2. 构建最小二乘问题时,需要将所有误差项依次添加进去。而每个误差项又是由前面定义的结构体构成的。需要注意的是,每个误差项需要指定代价函数求导的方法,有三种方法可供选择:自动求导、数值求导和自行推导。一般采用自动求导,既方便,又节约运行时时间。
  3. 以自动求导为例,ceres::AutoDiffCostFunction是个模板类,后两个模板参数为输出维度和输入维度,必须与前面定义的结构体中的residualabc的维度一样。
  4. 使用ceres::Solver::Options配置求解器。这个类有许多字段,每个字段都提供了一些枚举值供用户选择。所以需要时只要查一查文档就知道怎么设置了。

程序的编译就不赘述了。该程序在0.00104641s内运行完毕,迭代22次,最终将误差从18249降到了50.97。优化结果为:

estimated a,b,c = 0.891943 2.17039 0.944142

最终拟合出的曲线如下图所示,还算比较完美了。

Ceres曲线拟合_第2张图片

四、参考资料

《视觉SLAM十四讲》第6讲 非线性优化 高翔
Ceres Solver Ceres官网
Ceres优化 徐尚
自动求导的二三事 Dark_Scope
Introduction to AUTOMATIC DIFFERENTIATION Alexey Radul

你可能感兴趣的:(Ceres曲线拟合)