Polynomial curve fitting using least squares

write in front

1. Contents of this article:
Using Eigen for least squares curve fitting

2. Platform/environment
Eigen (open3d), cmake, pcl

3. Please indicate the source when reprinting:
https://blog.csdn.net/qq_41102371/article/details/131407456

curve fitting method

b-spline curve fitting implemented by pcl

Reference:
https://blog.csdn.net/qq_36686437/article/details/114160557
From the effect point of view, the results obtained by curve fitting in pcl are relatively poor.

Least squares curve fitting

principle

On the two-dimensional XOY plane, the curve is represented by the polynomial f ( x ) f(x)f(x)表示:
f ( x ) = a 0 + a 1 x + a 2 x 2 + a 3 x 3 + . . . + a n x n = ∑ i = 0 n a i x i f(x) = a_0+a_1x+a_2x^2+a_3x^3+...+a_nx^n=\sum_{i=0}^n{a_ix^i} f(x)=a0+a1x+a2x2+a3x3+...+anxn=i=0naixi
in whichi ∈ { 0 , 1 , 2... n } i\in\{0,1,2...n\}i{ 0,1,2... n } is the order of the polynomial,ai a_iaiare the coefficients of each order of the polynomial.
There is a real curve lll , composed of m points(xj, yj), j ∈ { 0, 1, . . . m} (x_j,y_j),j\in\{0,1,...m\}(xj,yj),j{ 0,1,... m } , assuminglll is a determined polynomial curvef ( x ) f(x)f ( x ) is sampled, then the points are all on a certain curve:
yj = f (xj) y_j = f(x_j)yj=f(xj)
But in fact, we want to pass a fitted curvef ( x ) f(x)f ( x ) to approximate the real pointyj y_jyjIn order to satisfy that each real point is on the curve or very close to the curve, let the distance of each point from the curve be:
ej = yj − f ( xj ) e_j = y_j-f(x_j)ej=yjf(xj)
then we want all real points to be off the curvef ( x ) f(x)The closer f ( x )
is, the better, then you need to minimize the distance from all points to the curve : minx ∑ j = 0 j = myj − f ( xj ) \mathop{min}\limits_{x}\ \sum_{j=0 }^{j=m}{y_j-f(x_j)}xmin j=0j=myjf(xj)
Suppose we take a second-order polynomialf ( x ) = a 0 + a 1 x 1 + a 2 x 2 f(x) = a_0+a_1x^1+a_2x^2f(x)=a0+a1x1+a2x2 Fitting data(xj, yj), j ∈ {0, 1, . . . m} (x_j,y_j),j\in\{0,1,...m\}(xj,yj),j{ 0,1,...m},则有方程组:
{ a 0 + a 1 x 0 + a 2 x 0 2 = y 0 a 0 + a 1 x 1 + a 2 x 1 2 = y 1 . . . a 0 + a j x 1 + a 2 x j 2 = y j . . . a 0 + a 1 x m + a 2 x m 2 = y m \begin{cases} a_0 + a_1x_0+a_2x_0^2=y_0 \\ a_0 + a_1x_1+a_2x_1^2=y_1 \\ ...\\ a_0 + a_jx_1+a_2x_j^2=y_j \\ ...\\ a_0 + a_1x_m+a_2x_m^2=y_m \end{cases} a0+a1x0+a2x02=y0a0+a1x1+a2x12=y1...a0+ajx1+a2xj2=yj...a0+a1xm+a2xm2=ym
Written in matrix form, it is
[ 1 x 0 x 0 2 1 x 1 x 1 2 . . . 1 xmxm 2 ] [ a 0 a 1 a 2 ] = [ y 0 y 1 . . . ym ] \begin{bmatrix} 1 & x_0& x_0^2 \\ 1 & x_1& x_1^2 \\ ... \\ 1 & x_m& x_m^2 \\ \end{bmatrix} \begin{bmatrix} a_0\\a_1\\a_2\\ \end{ bmatrix}= \begin{bmatrix} y_0\\y_1\\...\\y_m \end{bmatrix} 11...1x0x1xmx02x12xm2 a0a1a2 = y0y1...ym
This is actually a system of non-homogeneous linear equations A x = b Ax = bAx=For the least squares solution of b , in order not to cause ambiguity with a and x above, we use X θ = b X\theta=b=b represents, and its least squares problem is expressed as:
min θ ∣ ∣ X θ − b ∣ ∣ 2 \mathop{min}\limits_{\theta}||X\theta-b||^2imin∣∣b2
b b b isyj y_jyj, X X X is our data:
[ 1 x 0 x 0 2 1 x 1 x 1 2 . . . 1 xmxm 2 ] \begin{bmatrix} 1 & x_0& x_0^2 \\ 1 & x_1& x_1^2 \\ ... \\ 1 & x_m& x_m^2 \\ \end{bmatrix} 11...1x0x1xmx02x12xm2
What we require is the coefficient [ a 0 , a 1 , a 2 ] T [a_0,a_1,a_2]^T[a0,a1,a2]T isX θ = b X\theta=b=θ \thetain bθ
exponentXθ = b X\theta=b=The solution of b can be transformed as follows:
XTX θ = XT b X^TX\theta=X^TbXT=XT b
we assumeXTXX^TXXT Xis reversible, then there is:
θ = ( XTX ) − 1 XT b \theta=(X^TX)^{-1}X^Tbi=(XTX)1XT b
Here is a question
1. For universal least squares,XTXX^TXXT Xmay be irreversible becauseXXThe row vectorsof_ \ne ...\ne x_mx0=x1=...=xmWhen, XXThe row vector of X is linearly independent, that is, when there are no repeating points in the data, wethinkXT Xis reversible, so throughθ = ( XTX ) − 1i=(XTX)1XT bthe θ \thetawe want through the analytical solutioni

2. We can also think of XTXX^TXXT Xis irreversible, thenθ = ( XTX ) − 1 XT b \theta=(X^TX)^{-1}X^Tbi=(XTX)1XYou can refer to the derivation of T b, and use Qr decomposition when calculating:
Polynomial Curve Fitting Based on Least Squares Method: From Principles to C++ Implementing
Least Squares Problems and QR Decomposition Based on HouseHolder Transform
This article will let you thoroughly understand the least squares method ( Super detailed derivation)

code

Two solution codes are provided, one is directly through θ = (XTX) − 1 XT b \theta=(X^TX)^{-1}X^Tbi=(XTX)1XT bis implemented using Eigen's matrix operations, and the other is qr decomposition. The code directory structure is as follows. Please put least_square_fit_curve.cpp and CMakeLists.txt into src, and then use compile.bat and run.bat to compile and run. Please modify the path corresponding to the PCL.
Insert image description here

least_square_fit_curve.cpp

#include <iostream>
#include <string>
#include <vector>
#include <random>

#include <pcl/visualization/pcl_visualizer.h>
#include <pcl/visualization/pcl_plotter.h>
#include <pcl/point_types.h>
#include <pcl/io/pcd_io.h>
#include <vtkPolyLine.h>

#include <Eigen/Dense>
#include <Eigen/Core>

std::vector<Eigen::Vector2d> GenerateGaussNoise(const std::vector<Eigen::Vector2d> &points_origin, double mu, double sigma)
{
    
    
    std::vector<Eigen::Vector2d> points_noise;
    points_noise = points_origin;
    std::normal_distribution<> norm{
    
    mu, sigma};
    std::random_device rd;
    std::default_random_engine rng{
    
    rd()};

    for (size_t i = 0; i < points_noise.size(); ++i)
    {
    
    
        points_noise[i][0] = points_noise[i][0] + norm(rng);
        points_noise[i][1] = points_noise[i][1] + norm(rng);
    }
    return points_noise;
}

void DisplayLine2D(std::vector<double> vector_1, std::vector<double> vector_2, std::vector<double> coeff, double min_x, double max_x)
{
    
    
    pcl::visualization::PCLPlotter *plot_line1(new pcl::visualization::PCLPlotter);
    // std::vector<double> func1(2, 0);
    // func1[0] = c;
    // func1[1] = b;
    // func1[2] = a;
    // plot_line1->addPlotData(func1, point_min.x, point_max.x);
    std::cout << "min_x: " << min_x << " max_x: " << max_x << std::endl;
    plot_line1->addPlotData(coeff, min_x, max_x);
    plot_line1->addPlotData(vector_1, vector_2, "display", vtkChart::POINTS); // X,Y均为double型的向量
    plot_line1->setShowLegend(true);
    plot_line1->plot();
}

std::vector<Eigen::Vector2d> GeneratePolynomialCurve2D(std::vector<double> &coefficients, Eigen::Vector2d range = Eigen::Vector2d(-10, 10), double step = 0.2)
{
    
    
    std::cout << "GeneratePolynomialCurve2D" << std::endl;
    // for (auto i : coefficients)
    // {
    
    
    //     std::cout << i << std::endl;
    // }
    // std::cout << "range: " << range(0) << " " << range(1) << std::endl;
    std::vector<Eigen::Vector2d> points;
    points.resize(int((range(1) - range(0)) / step));
    double x, y;
    for (std::size_t i = 0; i < points.size(); ++i)
    {
    
    
        x = range(0) + step * i;
        y = 0;
        for (std::size_t j = 0; j < coefficients.size(); ++j)
        {
    
    
            // y = a_0*x^0 + a_1*x^1 + a_2*x^2 + a_n*x^n
            y += coefficients[j] * std::pow(x, j);
        }
        // std::cout << x << " " << y << "         ";
        points[i] = Eigen::Vector2d(x, y);
    }
    return points;
}

/// @brief 最小二乘拟合,直接用公式
/// @param data_x
/// @param data_y
/// @param coeff 多项式系数 a_0, a_1, ... a_n
/// @param order 需要拟合的阶数
void PolynomailCurveFit(const std::vector<double> &data_x,
                        const std::vector<double> &data_y,
                        std::vector<double> &coeff,
                        int order

)
{
    
    
    // Create Matrix Placeholder of size n x k, n= number of datapoints, k = order of polynomial, for exame k = 3 for cubic polynomial
    Eigen::MatrixXd X(data_x.size(), order + 1);
    Eigen::VectorXd b = Eigen::VectorXd::Map(&data_y.front(), data_y.size());
    Eigen::VectorXd A;

    // check to make sure inputs are correct
    assert(data_x.size() == data_y.size());
    assert(data_x.size() >= order + 1);
    // Populate the matrix
    for (size_t i = 0; i < data_x.size(); ++i)
    {
    
    
        for (size_t j = 0; j < order + 1; ++j)
        {
    
    
            X(i, j) = pow(data_x.at(i), j);
        }
    }
    // std::cout << T << std::endl;

    // Solve for linear least square fit
    A = (X.transpose() * X).inverse() * X.transpose() * b;
    coeff.resize(order + 1);
    for (int k = 0; k < order + 1; k++)
    {
    
    
        coeff[k] = A[k];
    }
}

/// @brief 最小二乘拟合,用Qr分解
/// @param data_x
/// @param data_y
/// @param coeff 多项式系数 a_0, a_1, ... a_n
/// @param order 需要拟合的阶数
void PolynomailCurveFitQr(const std::vector<double> &data_x,
                          const std::vector<double> &data_y,
                          std::vector<double> &coeff,
                          int order

)
{
    
    
    // Create Matrix Placeholder of size n x k, n= number of datapoints, k = order of polynomial, for exame k = 3 for cubic polynomial
    Eigen::MatrixXd X(data_x.size(), order + 1);
    Eigen::VectorXd b = Eigen::VectorXd::Map(&data_y.front(), data_y.size());
    Eigen::VectorXd A;

    // check to make sure inputs are correct
    assert(data_x.size() == data_y.size());
    assert(data_x.size() >= order + 1);
    // Populate the matrix
    for (size_t i = 0; i < data_x.size(); ++i)
    {
    
    
        for (size_t j = 0; j < order + 1; ++j)
        {
    
    
            X(i, j) = pow(data_x.at(i), j);
        }
    }
    // std::cout << T << std::endl;
    // Solve for linear least square fit
    A = X.householderQr().solve(b);
    coeff.resize(order + 1);
    for (int k = 0; k < order + 1; k++)
    {
    
    
        coeff[k] = A[k];
    }
}
void PrintPolynomailCoeff(const std::vector<double> &coeff, std::string name = "coeff")
{
    
    
    std::cout << name << ": ";
    for (auto i : coeff)
    {
    
    
        std::cout << i << " ";
    }
    std::cout << std::endl;
}

int main(int argc, char *argv[])
{
    
    
    std::chrono::high_resolution_clock::time_point all_s = std::chrono::high_resolution_clock::now();

    Eigen::Vector2d range = Eigen::Vector2d(-10, 10);
    std::vector<double> coeff_gt({
    
    1, 2, 4});
    std::vector<double> coeff_fit;
    std::vector<double> coeff_fit_qr;
    auto points_curve = GeneratePolynomialCurve2D(coeff_gt, range);
    std::vector<double> points_x;
    std::vector<double> points_y;
    // 添加噪声
    auto points_curve_noise = GenerateGaussNoise(points_curve, 0, 0.05);
    // 不添加噪声
    // auto points_curve_noise = points_curve;
    for (auto i : points_curve_noise)
    {
    
    
        points_x.push_back(i(0));
        points_y.push_back(i(1));
    }
    PolynomailCurveFit(points_x, points_y, coeff_fit, 2);
    PolynomailCurveFitQr(points_x, points_y, coeff_fit_qr, 2);
    PrintPolynomailCoeff(coeff_gt, "coeff_gt");
    PrintPolynomailCoeff(coeff_fit, "coeff_fit");
    PrintPolynomailCoeff(coeff_fit_qr, "coeff_fit_qr");

    std::chrono::high_resolution_clock::time_point all_e = std::chrono::high_resolution_clock::now();
    auto all_cost = std::chrono::duration_cast<std::chrono::milliseconds>(all_e - all_s).count();
    std::cout << "all cost: " << all_cost << " ms" << std::endl;

    DisplayLine2D(points_x, points_y, coeff_gt, range(0), range(1));
    DisplayLine2D(points_x, points_y, coeff_fit, range(0), range(1));
    DisplayLine2D(points_x, points_y, coeff_fit_qr, range(0), range(1));

    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.18)

project(LeastSquareFitCurve)

find_package(PCL 1.8 REQUIRED)
include_directories(${PCL_INCLUDE_DIRS})
link_directories(${PCL_LIBRARY_DIRS})
add_definitions(${PCL_DEFINITIONS})

add_executable(least_square_fit_curve ./least_square_fit_curve.cpp)
target_link_libraries(least_square_fit_curve ${PCL_LIBRARIES})

compile.bat

cmake -S ./src -B ./build
cmake --build ./build --parallel 8

run.bat

set PATH=D:\carlos\install\PCL 1.10.0\bin;D:\carlos\install\PCL 1.10.0\3rdParty\FLANN\bin;D:\carlos\install\PCL 1.10.0\3rdParty\VTK\bin;D:\carlos\install\PCL 1.10.0\3rdParty\Qhull\bin;D:\carlos\install\PCL 1.10.0\3rdParty\OpenNI2\Tools;%PATH%

.\build\Release\least_square_fit_curve.exe

Compile:

cd least_square_fit_curv
./compile.bat

run

run.bat

Note:

Pcl's curve drawing is used, and pcl comes with the Eigen library. You can use Open3d and Eigen provided in pcl. If you don't have pcl and open3d, you can install Eigen directly, and note out the visualization part of pcl, and install Eigen separately for reference:
cmake+Eigen library

Reference implementation of qr:
least squares problem and QR decomposition based on HouseHolder transformation.
Least squares polynomial fitting using C++ Eigen package.
Code reference for direct matrix inversion:
Fitting curves in 3D space
C++/PCL: least squares fitting Combined plane straight line, plane polynomial curve, spatial polynomial curve
C++ least squares straight line fitting, curve fitting, plane fitting, Gaussian fitting

result

The quadratic curve f ( x ) = 1 + 2 x + 4 x 2 f(x) = 1+2x+4x^2 was manually generated using codef(x)=1+2x _+4x _2 sampling points, without adding noise, the fitting result is consistent with GT. Coefficient
:
Insert image description here
Original point and curve:
Insert image description hereBecause the coefficients are consistent, the fitting result curve is consistent with the original curve and is not displayed.

In the case of adding Gaussian noise:
coefficients:
Insert image description here
original points and curves:
Insert image description here
fitting result curve:
Insert image description here
it can be seen that the results obtained by the direct method and qr decomposition are consistent

reference

3D Curve 1: Polynomial Curve https://zhuanlan.zhihu.com/p/267985141
Solving definite equations, least squares solution, solution of Ax=0, Ax=b, solving homogeneous equations, solving non-homogeneous equations Group (the derivation is very detailed)
to generate Gaussian noise https://zhuanlan.zhihu.com/p/458994530
The rest of the article has been listed

over

Mainly engaged in laser/image three-dimensional reconstruction, registration/segmentation and other commonly used point cloud algorithms. For technical exchanges and consultations, please send a private message

Guess you like

Origin blog.csdn.net/qq_41102371/article/details/131407456