本文介绍如何使用Ceres库实现曲线拟合。
一、曲线拟合
所谓曲线拟合,就是给定一组x和y的值,它们大体上满足一条曲线方程,但受噪声影响,并不精确满足方程。在这种情况下求取曲线方程的参数。例如,给定100对x和y的值,把它们画在坐标系上如图所示:
假设我们事先知道(或者猜测)该曲线方程的形式为
那么就可以构造一个最小二乘问题以估计其中的未知参数a、b和c。该最小二乘问题的代价函数为:
这是一个非线性优化问题,目标是求使上式最小的a、b和c。
二、Ceres库
Ceres是一个C++库,用于求解通用的最小二乘问题,因此非常适合上面介绍的曲线拟合。而且Ceres的使用也非常简单,大体上分为如下三步:
- 定义代价函数;
- 构建最小二乘问题,向问题中添加误差项,此时会用到第一步的代价函数;
- 配置求解器,开始求解。
下面用一个实际的例子来说明。
三、使用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;
}
代码中需要关注的有这么几点:
- 定义代价函数的方式是自定义一个结构体。只需要在结构体中实现
operator()
方法,就算是给Ceres提供了一个调用接口,由Ceres内部在合适的时候调用该方法计算代价函数。 - 构建最小二乘问题时,需要将所有误差项依次添加进去。而每个误差项又是由前面定义的结构体构成的。需要注意的是,每个误差项需要指定代价函数求导的方法,有三种方法可供选择:自动求导、数值求导和自行推导。一般采用自动求导,既方便,又节约运行时时间。
- 以自动求导为例,
ceres::AutoDiffCostFunction
是个模板类,后两个模板参数为输出维度和输入维度,必须与前面定义的结构体中的residual
和abc
的维度一样。 - 使用
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