Ceres曲线拟合

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

一、曲线拟合

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

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

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

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

二、Ceres库

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

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

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

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

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

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

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

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 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<double> x_data, y_data;      // 数据

    cout<<"generating data: "<<endl;
    for ( int i=0; i<N; i++ )
    {
        double x = i/100.0;
        x_data.push_back ( x );
        y_data.push_back (
                exp ( a*x*x + b*x + c ) + rng.gaussian ( w_sigma )
        );
        cout<<x_data[i]<<" "<<y_data[i]<<endl;
        ss << x_data[i] << " " << y_data[i] << endl;
    }

    ofstream file("points.txt");
    file << ss.str();

    // 构建最小二乘问题
    ceres::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                 // 待估计参数
        );
    }

    // 配置求解器
    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<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;
}

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

  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

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

四、参考资料

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • AI人工智能时代,机器学习,深度学习作为其核心,本文主要介绍机器学习的基础算法,以详细线介绍 线性回归算法 及其 ...
    erixhao阅读 13,989评论 0 36
  • 机器学习是做NLP和计算机视觉这类应用算法的基础,虽然现在深度学习模型大行其道,但是懂一些传统算法的原理和它们之间...
    在河之简阅读 20,603评论 4 65
  • 我似疯魔,情怀有限 癫狂如我,作怪万千 随波逐流,不是我的风格 不卑不亢,才是我的标签 并非哗众取宠,仅想活出自我...
    Arvin_shem阅读 492评论 0 1
  • 叁 姜宁死了。妲己看着她,一身火红嫁衣,跳进了火海。漫天红色照的妲己眼睛酸涩,连全身雪白狐毛都 被火气烘得微烫,于...
    唇耳清风阅读 378评论 0 0
  • 这周末!完整的一天班,还比往常累。 班里这群小破孩儿们啊!我特有无语的赶脚。 回家路上大堵车,我一路都在想,我可以...
    心如美玉阅读 301评论 0 0