slam 基本算法 --- 分别使用 【高斯牛顿,g2o】进行曲线拟合 (理论+实践)

本文深入探讨并实践了SLAM领域中使用高斯牛顿法及g2o库进行曲线拟合的方法,通过具体实例讲解了两种算法的实现细节与优化流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


通过本次简单实践,详解高斯牛顿和g2o的应用
参照《slam14讲 第二版》

一. 曲线拟合 — 高斯牛顿

1.1 问题描述

问题: 求解一条满足以下方程的曲线
y = e x p ( a x 2 + b x + c ) + w y = exp(ax^2 +bx +c) + w y=exp(ax2+bx+c)+w

  • a , b , c a,b,c a,b,c 为待求的参数, w w w 为高斯噪声,服从 ( 0 , σ 2 ) (0,\sigma^2) (0,σ2);

构建最小二乘问题:
假设我们有 N 个关于 x, y 的观测数据点,想根据这些数据点求出曲线的参数。那么,可以求解下面的最小二乘问题以估计曲线参数:
F = min ⁡ a , b , c 1 2 ∑ i = 1 N ∥ y i − exp ⁡ ( a x i 2 + b x i + c ) ∥ 2 F=\min _{a, b, c} \frac{1}{2} \sum_{i=1}^{N}\left\|y_{i}-\exp \left(a x_{i}^{2}+b x_{i}+c\right)\right\|^{2} F=a,b,cmin21i=1Nyiexp(axi2+bxi+c)2

使用高斯牛顿法,那么我们的目标函数为 f i = e i = y i − exp ⁡ ( a x i 2 + b x i + c ) f_i=e_i=y_{i}-\exp \left(a x_{i}^{2}+b x_{i}+c\right) fi=ei=yiexp(axi2+bxi+c),即误差项;待优化的状态量为: χ = [ a b c ] \chi=\begin{bmatrix}a\\b\\c\end{bmatrix} χ=abc
求出每个误差项对状态变量的导数,即可以构成雅克比矩阵,维数为 1 × 3 1\times3 1×3,即:
J i = [ ∂ e i ∂ a ∂ e i ∂ b ∂ e i ∂ c ] J_i=\begin{bmatrix}\frac{\partial e_i}{\partial a}\\\frac{\partial e_i}{\partial b}\\\frac{\partial e_i}{\partial c}\end{bmatrix} Ji=aeibeicei
那么高斯牛顿法的增量方程为: ( ∑ i = 1 N J i ( σ 2 ) − 1 J i T ) △ χ k = ∑ i = 1 N − J i ( σ 2 ) − 1 e i \left(\sum_{i=1}^NJ_i{(\sigma^2)}^{-1}J_i^T\right)\bigtriangleup\chi_k=\sum_{i=1}^N-J_i{(\sigma^2)}^{-1}e_i (i=1NJi(σ2)1JiT)χk=i=1NJi(σ2)1ei

  • N N N 为生成的噪声数据点数量,代码中取 100;
  • △ χ k \bigtriangleup\chi_k χk 表示第 k k k 次迭代时,状态量的增量值,需要求出的;
  • σ 2 \sigma^2 σ2 为方差,是因为上面的观测噪声为 w w w为一位高斯分布,所以是方差;
    然而slam问题中则是协方差矩阵 Σ \mathbf \Sigma Σ,为什么存在这个协方差矩阵呢? 那是因为状态量与观测值之间是不相等的,并且其中的噪声为高维高斯分布,(后面对这个问题再详细分析,目前受知识所限

1.2 代码

  1. 根据方程,生成噪声数据
  2. 给状态量设定初始值
  3. 高斯牛顿迭代,使用 l d l T ldl^T ldlT 求解出线性方程 H △ χ k = b H \bigtriangleup\chi_k=b Hχk=b,找到最优的状态量
  4. 最后使用 pcl plotter 画图
#include <iostream>
#include <chrono>
#include <opencv2/opencv.hpp>
#include <Eigen/Core>
#include <Eigen/Dense>

#include <pcl/point_types.h>
#include <pcl/io/pcd_io.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/visualization/pcl_visualizer.h>
#include <pcl/filters/statistical_outlier_removal.h>
#include <pcl/visualization/pcl_plotter.h>

using namespace std;
using namespace Eigen;
using namespace pcl;
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随机数产生器

  // 定义点云使用的格式:这里用的是XYZRGB
  // typedef pcl::PointXYZRGB PointT;
  // typedef pcl::PointCloud<PointT> PointCloud;

  // pcl::PointCloud<PointT>::Ptr cloud(new pcl::PointCloud<PointT>);

  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));

    // PointT p;
    // p.x = x;
    // p.y = exp(ar * x * x + br * x + cr) + rng.gaussian(w_sigma * w_sigma);
    // p.z = 1;

    // cloud->points.push_back(p);
  }

  // 开始Gauss-Newton迭代
  int iterations = 100;          // 迭代次数
  double cost = 0, lastCost = 0; // 本次迭代的cost和上一次迭代的cost

  chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
  for (int iter = 0; iter < iterations; iter++)
  {

    Matrix3d H = Matrix3d::Zero(); // Hessian = J^T W^{-1} J in Gauss-Newton
    Vector3d b = Vector3d::Zero(); // bias
    cost = 0;

    for (int i = 0; i < N; i++)
    {
      double xi = x_data[i], yi = y_data[i]; // 第i个数据点
      double error = yi - exp(ae * xi * xi + be * xi + ce);
      Vector3d J;                                         // 雅可比矩阵
      J[0] = -xi * xi * exp(ae * xi * xi + be * xi + ce); // de/da
      J[1] = -xi * exp(ae * xi * xi + be * xi + ce);      // de/db
      J[2] = -exp(ae * xi * xi + be * xi + ce);           // de/dc

      H += inv_sigma * inv_sigma * J * J.transpose();
      b += -inv_sigma * inv_sigma * error * J;

      cost += error * error;
    }

    // 求解线性方程 Hx=b
    Vector3d dx = H.ldlt().solve(b);
    if (isnan(dx[0]))
    {
      cout << "result is nan!" << endl;
      break;
    }

    if (iter > 0 && cost >= lastCost)
    {
      cout << "cost: " << cost << ">= last cost: " << lastCost << ", break." << endl;
      break;
    }

    ae += dx[0];
    be += dx[1];
    ce += dx[2];

    lastCost = cost;

    cout << "total cost: " << cost << ", \t\tupdate: " << dx.transpose() << "\t\testimated params: " << ae << "," << be << "," << ce << endl;
  }

  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 << "estimated abc = " << ae << ", " << be << ", " << ce << endl;

  visualization::PCLPlotter *plot_(new visualization::PCLPlotter("curve fitting by gaussNewton"));
  plot_->setBackgroundColor(1, 1, 1);
  plot_->setTitle("curve fitting by gaussNewton");
  plot_->setXTitle("Elevation");
  plot_->setYTitle("Point number");

  vector<double> y_estimate_data; // 数据
  vector<double> y_truth_date;    // 数据
  for (int i = 0; i < x_data.size(); i++)
  {
    double xi = x_data[i]; // 第i个数据点
    double yi = exp(ae * xi * xi + be * xi + ce);
    y_estimate_data.push_back(yi);
    y_truth_date.push_back(exp(ar * xi * xi + br * xi + cr));
  }
  plot_->addPlotData(x_data, y_truth_date, "truth-line", vtkChart::LINE); //X,Y均为double型的向量

  plot_->addPlotData(x_data, y_estimate_data, "estimate-line", vtkChart::LINE); //X,Y均为double型的向量

  plot_->addPlotData(x_data, y_data, "rander-point", vtkChart::POINTS); //X,Y均为double型的向量
  plot_->plot();                                                        //绘制曲线
  return 0;
}

1.3 结果

在这里插入图片描述

二. 曲线拟合 — g2o

2.1 问题描述

g2o 是一个基于图优化的库,图优化是一种将非线性优化图论结合起来的理论;
图优化,是把优化问题表现成图(Graph)(指图论中的图)的一种方式,一组优化变量和变量之间的误差项,使用图来图来描述,使它们之间的关系更加直观化;

用顶点表示优化变量,用边表示误差项

在这里插入图片描述

  • 三角形和圆圈,都是顶点,待优化的变量;
  • 红色与蓝色的线,都是边,描述相连顶点优化变量关系,也就是误差项;

在这里插入图片描述

  • 曲线拟合对应的图优化模型;
  • 整个问题只有一个顶点:曲线模型的参数 a, b, c;而每个带噪声的数据点,构成了一个个误差项,也就是图优化的边;
  • 它们是一元边,即只连接一个顶点;

图优化中一条边可以连接一个、两个或多个顶点,这主要反映在每个误差与多少个优化变量有关 对应 超边,与超图;

g2o求解步骤:

  1. 定义顶点和边的类型;
  2. 构建图;
  3. 选择优化算法;
  4. 调用 g2o 进行优化,返回结果。

2.2 代码

在g2o中,内置了三大类:

  • VertexSE3Expmap,这个表示李代数的位姿; 顶点

  • VertexSBAPointXYZ,表示空间点的位置; 顶点

  • EdgeProjectXYZ2UV,表示投影方程的边。边

没有和曲线拟合相关的,所以我们需要自定义顶点和边;内置类的定义可以去看源码;

#include <iostream>
#include <g2o/core/g2o_core_api.h>
#include <g2o/core/base_vertex.h>
#include <g2o/core/base_unary_edge.h>
#include <g2o/core/block_solver.h>
#include <g2o/core/optimization_algorithm_levenberg.h>
#include <g2o/core/optimization_algorithm_gauss_newton.h>
#include <g2o/core/optimization_algorithm_dogleg.h>
#include <g2o/solvers/dense/linear_solver_dense.h>
#include <Eigen/Core>
#include <opencv2/core/core.hpp>
#include <cmath>
#include <chrono>
using namespace std;

定义顶点

要点: ( 初始值设置 setToOriginImpl,以及状态量更新方式 oplusImpl )

  1. class CurveFittingVertex : public g2o::BaseVertex<3, Eigen::Vector3d>
    定义曲线拟合顶点;继承基础顶点类;3表示顶点代表的待优化变量为3维,数据类型是Eigen::Vector3d;

  2. EIGEN_MAKE_ALIGNED_OPERATOR_NEW
    表示数据对齐,固定格式; 类成员变量如果是固定大小对象需要加上此句;
    而对于动态变量(例如Eigen::VectorXd)会动态分配内存,因此会自动地进行内存对齐

  3. setToOriginImpl()
    设置顶点初始估计的值, 估计值变量为 _estimate;或是重置状态值;重载内置函数;

  4. oplusImpl(const double *update)
    重载状态量更新方式; _estimate += Eigen::Vector3d(update) ,这里为三维向量相加; 在slam中则为: 更新小量乘以估计值,左乘更新,如:

    virtual void oplusImpl(const double* update_)  {
    Eigen::Map<const Vector6d> update(update_);
    setEstimate(SE3Quat::exp(update)*estimate()); }
    
  5. 读写操作 按需自己定义; 这里留空
    bool read(std::istream& is);
    bool write(std::ostream& os) const;

//顶点 // 曲线模型的顶点,模板参数:优化变量维度和数据类型
class CurveFittingVertex : public g2o::BaseVertex<3, Eigen::Vector3d>
{
public:
  // 类成员变量如果是固定大小对象需要加上 EIGEN_MAKE_ALIGNED_OPERATOR_NEW
  // 而对于动态变量(例如Eigen::VectorXd)会动态分配内存,因此会自动地进行内存对齐
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW
  // 重置
  virtual void setToOriginImpl() override
  {
    _estimate << 0, 0, 0;
  }

  // 更新
  virtual void oplusImpl(const double *update) override
  {
    _estimate += Eigen::Vector3d(update);
  }

  // 存盘和读盘:留空
  virtual bool read(istream &in) {}

  virtual bool write(ostream &out) const {}
};

定义边

要点: ( 初始值设置 setToOriginImpl,以及状态量更新方式 oplusImpl )

  1. class CurveFittingEdge : public g2o::BaseUnaryEdge<1, double, CurveFittingVertex>
    定义曲线拟合边;继承一元边类
    1表示观测值是1维的,也就是误差项也是1维的,因为误差项也就是理论值/估计值减去实际观测值,当然和观测值维数一样;数据类型为 double;
    CurveFittingVertex 表示边连接的顶点的数据类型 ; 因为是一元边,所以只有一个顶点类型;

  2. CurveFittingEdge(double x) : BaseUnaryEdge(), _x(x) {}
    边的构造函数;给类成员变量_x 赋值;给每条边对象存储一个_x值;

  3. computeError()函数
    描述了error的计算过程,即观测的值,减去计算出的值;(实际观测值减去理论计算值)
    _measurement 测量值变量 ;因为误差是一维的所以 赋值时是: _error(0, 0);
    const CurveFittingVertex *v = static_cast<const CurveFittingVertex *>(_vertices[0]) ;
    _vertices[0] 对应 “class CurveFittingEdge : public g2o::BaseUnaryEdge<1, double, CurveFittingVertex>”绑定的第一个顶点 CurveFittingVertex;
    在main函数中 edge->setVertex(0, v);:第一个 参数 0 表示将此边连接到 CurveFittingVertex类型节点上,本问题中只有一个顶点v,后面就只接了 v;

  4. linearizeOplus()
    书写雅克比矩阵;
    每个误差项对状态变量的导数,即可以构成雅克比矩阵,对于本问题雅克比矩阵维数为 1 × 3 1\times3 1×3,即: e i e_i ei 实际测量值减去理论计算值
    J i = [ ∂ e i ∂ a ∂ e i ∂ b ∂ e i ∂ c ] = [ ∂ ( y i − exp ⁡ ( a x i 2 + b x i + c ) ) ∂ a ∂ ( y i − exp ⁡ ( a x i 2 + b x i + c ) ) ∂ b ∂ ( y i − exp ⁡ ( a x i 2 + b x i + c ) ) ∂ c ] = [ − x i 2 exp ⁡ ( a x i 2 + b x i + c ) − x i exp ⁡ ( a x i 2 + b x i + c ) − exp ⁡ ( a x i 2 + b x i + c ) ] J_i=\begin{bmatrix}\frac{\partial e_i}{\partial a}\\\frac{\partial e_i}{\partial b}\\\frac{\partial e_i}{\partial c}\end{bmatrix}=\begin{bmatrix}\frac{\partial(y_i-\exp\left(ax_i^2+bx_i+c\right))}{\partial a}\\\frac{\partial(y_i-\exp\left(ax_i^2+bx_i+c\right))}{\partial b}\\\frac{\partial(y_i-\exp\left(ax_i^2+bx_i+c\right))}{\partial c}\end{bmatrix}=\begin{bmatrix}-x_i^2\exp\left(ax_i^2+bx_i+c\right)\\-x_i\exp\left(ax_i^2+bx_i+c\right)\\-\exp\left(ax_i^2+bx_i+c\right)\end{bmatrix} Ji=aeibeicei=a(yiexp(axi2+bxi+c))b(yiexp(axi2+bxi+c))c(yiexp(axi2+bxi+c))=xi2exp(axi2+bxi+c)xiexp(axi2+bxi+c)exp(axi2+bxi+c)

如果绑定了两个顶点,在linearizeOplus()中则需要写清楚误差分别对于两个顶点的偏导数。第一个为 _jacobianOplusXi,第二个为 _jacobianOplusXj

//边 //误差模型 模板参数:观测值维度,类型,连接顶点类型
class CurveFittingEdge : public g2o::BaseUnaryEdge<1, double, CurveFittingVertex>
{
public:
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW

  CurveFittingEdge(double x) : BaseUnaryEdge(), _x(x) {}

  // 计算曲线模型误差
  virtual void computeError() override
  {
    const CurveFittingVertex *v = static_cast<const CurveFittingVertex *>(_vertices[0]);
    const Eigen::Vector3d abc = v->estimate();
    _error(0, 0) = _measurement - std::exp(abc(0, 0) * _x * _x + abc(1, 0) * _x + abc(2, 0));
  }

  // 计算雅可比矩阵
  virtual void linearizeOplus() override
  {
    const CurveFittingVertex *v = static_cast<const CurveFittingVertex *>(_vertices[0]);
    const Eigen::Vector3d abc = v->estimate();
    double y = exp(abc[0] * _x * _x + abc[1] * _x + abc[2]);
    _jacobianOplusXi[0] = -_x * _x * y;
    _jacobianOplusXi[1] = -_x * y;
    _jacobianOplusXi[2] = -y;
  }

  virtual bool read(istream &in) {}

  virtual bool write(ostream &out) const {}

public:
  double _x; // x 值, y 值为 _measurement
};

mian() —生成观测数据

生成100 个 关于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));
		  }

构建图

要点

  • solver 求解器
  1. typedef g2o::BlockSolver<g2o::BlockSolverTraits<3, 1>> BlockSolverType;
    // 每个误差项优化变量维度为3,误差值维度为1;

  2. typedef g2o::LinearSolverDense<BlockSolverType::PoseMatrixType> LinearSolverType;
    // 线性求解器类型

  3. g2o::OptimizationAlgorithmGaussNewton
    // GN

  • 图模型
  1. g2o::SparseOptimizer optimizer;// 图模型

  2. 往图中增加顶点

     CurveFittingVertex *v = new CurveFittingVertex();
     v->setEstimate(Eigen::Vector3d(ae, be, ce));
     v->setId(0);
     optimizer.addVertex(v); // 添加
    
  3. 往图中增加边

     for (int i = 0; i < N; i++)
     {
       CurveFittingEdge *edge = new CurveFittingEdge(x_data[i]);
       edge->setId(i);
       edge->setVertex(0, v);                                                                   // 设置连接的顶点
       edge->setMeasurement(y_data[i]);                                                         // 观测数值
       edge->setInformation(Eigen::Matrix<double, 1, 1>::Identity() * 1 / (w_sigma * w_sigma)); // 信息矩阵:协方差矩阵之逆
       optimizer.addEdge(edge);  // 添加
     }
    
		 // 构建图优化,先设定g2o
		  typedef g2o::BlockSolver<g2o::BlockSolverTraits<3, 1>> BlockSolverType;           // 每个误差项优化变量维度为3,误差值维度为1
		  typedef g2o::LinearSolverDense<BlockSolverType::PoseMatrixType> LinearSolverType; // 线性求解器类型
	
		  // 梯度下降方法,可以从GN, LM, DogLeg 中选
		  auto solver = new g2o::OptimizationAlgorithmGaussNewton(
		      g2o::make_unique<BlockSolverType>(g2o::make_unique<LinearSolverType>()));
		  g2o::SparseOptimizer optimizer; // 图模型
		  optimizer.setAlgorithm(solver); // 设置求解器
		  optimizer.setVerbose(true);     // 打开调试输出
		
		  // 往图中增加顶点
		  CurveFittingVertex *v = new CurveFittingVertex();
		  v->setEstimate(Eigen::Vector3d(ae, be, ce));
		  v->setId(0);
		  optimizer.addVertex(v);
		
		  // 往图中增加边
		  for (int i = 0; i < N; i++)
		  {
		    CurveFittingEdge *edge = new CurveFittingEdge(x_data[i]);
		    edge->setId(i);
		    edge->setVertex(0, v);                                                                   // 设置连接的顶点
		    edge->setMeasurement(y_data[i]);                                                         // 观测数值
		    edge->setInformation(Eigen::Matrix<double, 1, 1>::Identity() * 1 / (w_sigma * w_sigma)); // 信息矩阵:协方差矩阵之逆
		    optimizer.addEdge(edge);
		  }

执行优化和结果输出

要点

  1. 初始化优化器,设置迭代次数

    optimizer.initializeOptimization();
    optimizer.optimize(10);

  2. 优化结果
    Eigen::Vector3d abc_estimate = v->estimate();
    Eigen::Vector3d 维数为 3 × 1 3 \times 1 3×1; 矩阵

		// 执行优化
		  cout << "start optimization" << endl;
		  chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
		  
		  optimizer.initializeOptimization();
		  optimizer.optimize(10);
		  
		  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;
		
		  // 输出优化值
		  Eigen::Vector3d abc_estimate = v->estimate();
		  cout << "estimated model: " << abc_estimate.transpose() << endl;
		
		  return 0;
}

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值