Introduction to g2o Library
g2o(General Graphic Optimization, G 2 O G^2O G2O) is an open source C + + framework for solving nonlinear least squares problems based on graph optimization.
Github home page: https://github.com/RainerKuemmerle/g2o
The core classes of the library are as follows:
Graph optimization theory
For the basic theory of graph theory, please refer to another blog post of the blogger: u graph theory and graph search algorithm Learn about the relevant contents of the diagram in.
It should be noted that in g2o, vertices are used to represent nodes, which are the same object.
If nodes are used to represent optimization variables and edges are used to represent error terms, a nonlinear least squares problem can be described as a graph structure. If the definition of probability graph is adopted here, it can be called Bayesian graph or factor graph.
As shown in the following figure, the camera motion pose is constructed. In the figure, circular nodes are used to represent road markings; The camera pose is represented by triangular nodes; The camera motion model is represented by the implementation edge; The observation model is represented by dotted line:
Generally, isolated nodes can be removed in advance and nodes with more edges (i.e. larger degrees) can be optimized first.
g2o compilation and installation
First, download the source code of the g2o Library:
git clone https://github.com/RainerKuemmerle/g2o
Secondly, install dependencies. Some dependencies have been completed during the installation of Ceres Library:
sudo apt install -y qt5-qmake qt5-default libqglviewer-dev-qt5 libsuitesparse-dev libcxsparse3 libcholmod3
Then, enter the source code directory for compilation:
# New compilation directory cd g2o && mkdir build && cd build # compile cmake .. make -j12
After compilation, install the generated library file:
# install sudo make install
The default installation location is as follows:
# include /usr/local/g2o # lib /usr/local/lib/
Engineering configuration
Clion + Ubuntu 20 is still used 04 development and new project Study.
engineering structure
The structure is as follows:
. ├── CMakeLists.txt ├── include │ └── main.h └── src ├── CMakeLists.txt └── main.cpp
The subdirectory include is used to store header files; The subdirectory src is used to store the source code
CMake configuration
Cmakelists. In the root directory Txt file is as follows:
# cmake version cmake_minimum_required(VERSION 3.21) # project name project(Study) # cpp version set(CMAKE_CXX_STANDARD 14) # eigen include_directories("/usr/include/eigen3") # g2o find_package(g2o REQUIRED) include_directories(${g2o_INCLUDE_DIRS}) # opencv find_package(OpenCV REQUIRED) include_directories(${OpenCV_INCLUDE_DIRS}) # incldue include_directories(include) # src add_subdirectory(src)
Cmakelists. In the src directory Txt file is as follows:
# exec add_executable(Study main.cpp) # link opencv target_link_libraries(Study ${OpenCV_LIBS}) # link g2o target_link_libraries(Study g2o_core g2o_stuff)
Note here that blogger target_ link_ If the libraries section is directly linked to ${g2o_LIBRARIES}, an error will be reported as follows:
undefined reference to g2o::xxxxxxxx
That is, the library file is not linked successfully, so the corresponding library file is directly linked.
Header file configuration
Header file main. In the include directory H contents are as follows:
#ifndef STUDY_MAIN_H #define STUDY_MAIN_H #include <iostream> #include <cmath> #include <chrono> // Eigen #include <Eigen/Core> // OpenCV #include <opencv2/opencv.hpp> // g2o #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> // namespace using namespace std; using namespace Eigen; #endif //STUDY_MAIN_H
The Eigen library is mainly imported for matrix data representation, Opencv library is used for random number generation, and g2o library is used for nonlinear least squares solution of graph optimization.
source file
src source file main The initial contents of CPP are as follows:
#include "main.h" int main() { return 0; }
g2o library API
Using g2o library for graph optimization mainly includes the following steps:
- Define the types of nodes and edges in the graph
- Construction diagram structure
- Selected optimization algorithm
- Optimize and get results
Curve fitting problem
Here, we still take the handwritten Gauss Newton method as an example to learn the use of g2o library. For the description of the problem, see the problem description as follows: Construction of curve fitting model
Firstly, the curve fitting problem is constructed as a graph, with nodes representing the optimization variables and edges representing the error terms:
In the curve fitting problem, the variables to be optimized are the parameters of the curve model: a , b , c a,b,c a. b, c, the error term is a group of data containing noise.
Definition of node class
Node class inherits from g2o:: basevertex < int d, typename T >
- int D: Dimension of optimization variable
- typename T: data type of optimization variable
For example, build an optimization variable node class of three-dimensional Eigen::Vector3d type:
class CurveFittingVertex : public g2o::BaseVertex<3, Vector3d> {}
The name of the derived class defined here is CurveFittingVertex.
For the definition of nodes, you also need to rewrite several virtual functions.
Reset function
Class is used to optimize variables_ Initialize with the value of estimate.
As the following reset function, initialize the three-dimensional optimization variable to 0:
virtual void setToOriginImpl() override { _estimate << 0, 0, 0; }
Update function
The member function oplusImpl(const double *update) of class is used to define the method of node update, that is, it is used for processing x k + 1 = x k + Δ x x_{k+1}=x_k+\Delta x xk+1=xk+ Δ The process of x.
- const double *update: pointer data, update quantity
It should be noted that for variables representing different uses, incremental updates are calculated in different ways. For example, the curve fitting problem only needs to be updated by simple addition:
virtual void oplusImpl(const double *update) override { _estimate += Vector3d(update); }
For example, for the updating of camera pose, the perturbation model of Lie group can be used for updating (left multiplication perturbation, right multiplication perturbation)
Save and read functions
Class member functions read (istream & in) and write (istream & out) are used to save and read disks.
virtual bool read(istream &in) {} virtual bool write(ostream &out) const {}
Node definition of curve fitting problem
For the above curve fitting problem, the node is defined as follows:
// Node class, the optimization variable is 3D, and the data of Eigen::Vector3d type class CurveFittingVertex : public g2o::BaseVertex<3, Vector3d> { public: // alignment EIGEN_MAKE_ALIGNED_OPERATOR_NEW // Reset function virtual void setToOriginImpl() override { _estimate << 0, 0, 0; } // Update function virtual void oplusImpl(const double *update) override { _estimate += Vector3d(update); } // Disk reading function and disk saving function: leave blank and not use for the time being virtual bool read(istream &in) {} virtual bool write(ostream &out) const {} };
Gen member variable_ MAKE_ ALIGNED_ OPERATOR_ The new declaration solves the alignment problem when creating a new object of this type.
Definition of edge class
Edge classes inherit different classes according to whether the nodes linked by the edge are one or two.
The unary edge problem inherits from the class g2o:: baseunaryedge < int d, typename e, typename vertexxi >
- int D: Dimension of error item
- typename E: data type of error item
- typename VertexXi: node type of edge link
Binary edge problem inherits from class g2o:: basebinaryedge < int d, typename e, typename vertexxi, typename vertexj >
- int D: Dimension of error item
- typename E: data type of error item
- typename VertexXi: the type of the first node of the edge link
- typename VertexXj: the type of the second node of the edge link
For example, for the curve fitting problem, a one-dimensional double type metaclass should be constructed, and the linked node type is CurveFittingVertex:
class CurveFittingEdge : public g2o::BaseUnaryEdge<1, double, CurveFittingVertex> {}
The name of the derived class defined here is CurveFittingEdge.
Similarly, for the definition of edge, you also need to rewrite several virtual functions.
Constructor
When an edge class is instantiated, the corresponding feature needs to be passed in x x x. Use private members of classes_ X accepted:
CurveFittingEdge(double x) : BaseUnaryEdge(), _x(x) {}
In g2o, private members are used_ measurement stores observation data y y y
Error calculation function
Class is used to define the error calculation function of the edge. The error function is mainly used to obtain the value of the link node and calculate the corresponding residual value.
For example, the error function of curve fitting is as follows:
virtual void computeError() override { // Get node const CurveFittingVertex *v = static_cast<const CurveFittingVertex *> (_vertices[0]); // Gets the optimization variable (node value) of the node const Eigen::Vector3d abc = v->estimate(); // Calculation error value _error(0, 0) = _measurement - std::exp(abc(0, 0) * _x * _x + abc(1, 0) * _x + abc(2, 0)); }
In the curve fitting problem, the optimization variable is a three-dimensional vector, corresponding to curve parameters: A, b, c, and the corresponding error calculation formula is as follows:
e
r
r
o
r
=
y
−
e
x
p
(
a
x
2
+
b
x
+
c
)
error = y-exp(ax^2+bx+c)
error=y−exp(ax2+bx+c)
Where y is the observation data, and the calculated error value error is used to calculate the derivative.
Jacobian calculation function
Class is used to define the Jacobian calculation function of the edge. Similarly, the Jacobian calculation function obtains the values of the edge linked nodes and their corresponding optimization variables, so as to solve the value of the Jacobian matrix.
For example, the Jacobian calculation function of curve fitting problem is as follows:
virtual void linearizeOplus() override { // Get node const CurveFittingVertex *v = static_cast<const CurveFittingVertex *> (_vertices[0]); // Gets the optimization variable (node value) of the node const Eigen::Vector3d abc = v->estimate(); // Jacobian matrix solution double y = exp(abc[0] * _x * _x + abc[1] * _x + abc[2]); _jacobianOplusXi[0] = -_x * _x * y; _jacobianOplusXi[1] = -_x * y; _jacobianOplusXi[2] = -y; }
Note here that for the univariate edge problem, only calculation is required_ jacobianOplusXi;
For the binary boundary problem, it should be calculated at the same time_ jacobianOplusXi and_ The value of jacobianOplusXj.
Save and read functions
Class member functions read (istream & in) and write (istream & out) are used to save and read disks.
virtual bool read(istream &in) {} virtual bool write(ostream &out) const {}
Edge definition of curve fitting problem
For the above curve fitting problem, the definition of edge is as follows:
// Edge class, the error item is 1-dimensional double type data, and the node type of edge link is CurveFittingVertex type class CurveFittingEdge : public g2o::BaseUnaryEdge<1, double, CurveFittingVertex> { public: // alignment EIGEN_MAKE_ALIGNED_OPERATOR_NEW // Constructor CurveFittingEdge(double x) : BaseUnaryEdge(), _x(x) {} // Calculation of edge function error virtual void computeError() override { // Get node const CurveFittingVertex *v = static_cast<const CurveFittingVertex *> (_vertices[0]); // Gets the optimization variable (node value) of the node const Eigen::Vector3d abc = v->estimate(); // Calculate residual value _error(0, 0) = _measurement - std::exp(abc(0, 0) * _x * _x + abc(1, 0) * _x + abc(2, 0)); } // Jacobian calculation function of edge virtual void linearizeOplus() override { // Get node const CurveFittingVertex *v = static_cast<const CurveFittingVertex *> (_vertices[0]); // Gets the optimization variable (node value) of the node const Eigen::Vector3d abc = v->estimate(); // Derivation double y = exp(abc[0] * _x * _x + abc[1] * _x + abc[2]); _jacobianOplusXi[0] = -_x * _x * y; _jacobianOplusXi[1] = -_x * y; _jacobianOplusXi[2] = -y; } // Disk reading function and disk saving function: leave blank and not use for the time being virtual bool read(istream &in) {} virtual bool write(ostream &out) const {} private: double _x; // x value, y value is_ measurement };
Gen member variable_ MAKE_ ALIGNED_ OPERATOR_ The new declaration solves the alignment problem when creating a new object of this type.
Curve fitting data generation
After completing the construction of node and edge types, first generate the data used in the main function:
int main() { /*-------- Initial parameter configuration--------*/ // Actual curve parameters double ar = 1.0, br = 1.0, cr = 1.0; // Initial value of estimated curve parameters double ae = 2.0, be = 1.0, ce = 5.0; // Number of sampling observation data points int N = 100; // Noise standard deviation and its reciprocal double w_sigma = 1.0; // Random number generator cv::RNG rng; /*-------- Observation data generation--------*/ 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)); } return 0; }
Build optimizer
After the initial data is generated, the configuration of the graph model can be built in the main function.
Linear solver, error block
Firstly, the types of linear solver and error block in the problem are defined. Here, the optimization variable of curve fitting problem is three-dimensional, and the error term is one-dimensional:
// Type of error item: the optimization variable dimension of error item is 3 and the value dimension of error item is 1 typedef g2o::BlockSolver<g2o::BlockSolverTraits<3, 1>> BlockSolverType; // Type of Solver: solve using LinearSolverDense typedef g2o::LinearSolverDense<BlockSolverType::PoseMatrixType> LinearSolverType;
The types of linear solvers are LinearSolverDense, LinearSolverPCG, linearsolvercparse, LinearSolverCholmod
Gradient descent method
Then, set the gradient descent method:
auto solver = new g2o::OptimizationAlgorithmGaussNewton(g2o::make_unique<BlockSolverType>(g2o::make_unique<LinearSolverType>()));
GN method is selected here, including GN method, LM method and DogLeg method. The corresponding API s are as follows:
-
GN method
g2o::OptimizationAlgorithmGaussNewton(std::unique_ptr<Solver> solver);
-
LM method
g2o::OptimizationAlgorithmLevenberg(std::unique_ptr<Solver> solver);
-
DogLeg method
g2o::OptimizationAlgorithmDogleg(std::unique_ptr<BlockSolverBase> solver);
Curve fitting problem optimizer
The optimizer of curve fitting problem is constructed as follows:
// Type of error item: the optimization variable dimension of error item is 3 and the value dimension of error item is 1 typedef g2o::BlockSolver<g2o::BlockSolverTraits<3, 1>> BlockSolverType; // Type of linear solver: solve using LinearSolverDense typedef g2o::LinearSolverDense<BlockSolverType::PoseMatrixType> LinearSolverType; // Optimizer: GN method is used to solve the setting auto solver = new g2o::OptimizationAlgorithmGaussNewton(g2o::make_unique<BlockSolverType>(g2o::make_unique<LinearSolverType>()));
Graph model establishment
Set optimizer
First, build a graph model and set up the optimizer
// Graph model g2o::SparseOptimizer optimizer; // Set optimizer optimizer.setAlgorithm(solver); // Open debug output optimizer.setVerbose(true);
Add node
Add nodes to the built graph model:
// Instantiation node type CurveFittingVertex *v = new CurveFittingVertex(); // Optimization variable initialization v->setEstimate(Eigen::Vector3d(ae, be, ce)); // Set node id in the diagram v->setId(0); // Add nodes to graph model optimizer.addVertex(v);
Add edge
Fill the generated data points into the edge one by one by traversal:
for (int i = 0; i < N; i++) { // Instanced edge type, incoming feature x CurveFittingEdge *edge = new CurveFittingEdge(x_data[i]); // Set the edge id in the drawing edge->setId(i); // Set connected nodes: node number and node object edge->setVertex(0, v); // Set observation data edge->setMeasurement(y_data[i]); // Setting information matrix: inverse of covariance matrix edge->setInformation(Eigen::Matrix<double, 1, 1>::Identity() * 1 / (w_sigma * w_sigma)); // Add edges to graph model optimizer.addEdge(edge); }
Start optimization
Finally, start to solve the optimization problem:
/*-------- Figure optimization start--------*/ cout << "start optimization" << endl; // Initialization optimization optimizer.initializeOptimization(); // Optimization times setting optimizer.optimize(10); // Output optimization value Eigen::Vector3d abc_estimate = v->estimate();
Example: curve fitting
code
The complete code is as follows:
#include "main.h" // Node class, the optimization variable is 3D, and the data of Eigen::Vector3d type class CurveFittingVertex : public g2o::BaseVertex<3, Vector3d> { public: // alignment EIGEN_MAKE_ALIGNED_OPERATOR_NEW // Reset function virtual void setToOriginImpl() override { _estimate << 0, 0, 0; } // Update function virtual void oplusImpl(const double *update) override { _estimate += Vector3d(update); } // Disk reading function and disk saving function: leave blank and not use for the time being virtual bool read(istream &in) {} virtual bool write(ostream &out) const {} }; // Edge class, the error item is 1-dimensional double type data, and the node type of edge link is CurveFittingVertex type class CurveFittingEdge : public g2o::BaseUnaryEdge<1, double, CurveFittingVertex> { public: // alignment EIGEN_MAKE_ALIGNED_OPERATOR_NEW // Constructor CurveFittingEdge(double x) : BaseUnaryEdge(), _x(x) {} // Error calculation function of edge virtual void computeError() override { // Get node const CurveFittingVertex *v = static_cast<const CurveFittingVertex *> (_vertices[0]); // Gets the optimization variable (node value) of the node const Eigen::Vector3d abc = v->estimate(); // Calculate residual value _error(0, 0) = _measurement - std::exp(abc(0, 0) * _x * _x + abc(1, 0) * _x + abc(2, 0)); } // Jacobian calculation function of edge virtual void linearizeOplus() override { // Get node const CurveFittingVertex *v = static_cast<const CurveFittingVertex *> (_vertices[0]); // Gets the optimization variable (node value) of the node const Eigen::Vector3d abc = v->estimate(); // Derivation double y = exp(abc[0] * _x * _x + abc[1] * _x + abc[2]); _jacobianOplusXi[0] = -_x * _x * y; _jacobianOplusXi[1] = -_x * y; _jacobianOplusXi[2] = -y; } // Disk reading function and disk saving function: leave blank and not use for the time being virtual bool read(istream &in) {} virtual bool write(ostream &out) const {} private: double _x; // x value, y value is_ measurement }; int main() { /*-------- Initial parameter configuration--------*/ // Actual curve parameters double ar = 1.0, br = 1.0, cr = 1.0; // Initial value of estimated curve parameters double ae = 2.0, be = 1.0, ce = 5.0; // Number of sampling observation data points int N = 100; // Noise standard deviation and its reciprocal double w_sigma = 1.0; // Random number generator cv::RNG rng; /*-------- Observation data generation--------*/ 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)); } /*-------- Optimizer configuration--------*/ // Type of error item: the optimization variable dimension of error item is 3 and the value dimension of error item is 1 typedef g2o::BlockSolver<g2o::BlockSolverTraits<3, 1>> BlockSolverType; // Type of linear solver: solve using LinearSolverDense typedef g2o::LinearSolverDense<BlockSolverType::PoseMatrixType> LinearSolverType; // Optimizer: GN method is used to solve the setting auto solver = new g2o::OptimizationAlgorithmGaussNewton(g2o::make_unique<BlockSolverType>(g2o::make_unique<LinearSolverType>())); /*-------- Figure model configuration--------*/ // Graph model g2o::SparseOptimizer optimizer; // Set optimizer optimizer.setAlgorithm(solver); // Open debug output optimizer.setVerbose(true); // Instantiation node type CurveFittingVertex *v = new CurveFittingVertex(); // Optimization variable initialization v->setEstimate(Eigen::Vector3d(ae, be, ce)); // Set the node id in the diagram v->setId(0); // Add nodes to graph model optimizer.addVertex(v); // Adding edges to a graph model for (int i = 0; i < N; i++) { // Instanced edge type, incoming feature x CurveFittingEdge *edge = new CurveFittingEdge(x_data[i]); // Set the edge id in the drawing edge->setId(i); // Set connected nodes: node number and node object edge->setVertex(0, v); // Set observation data edge->setMeasurement(y_data[i]); // Setting information matrix: inverse of covariance matrix edge->setInformation(Eigen::Matrix<double, 1, 1>::Identity() * 1 / (w_sigma * w_sigma)); // Add edges to graph model optimizer.addEdge(edge); } /*-------- Figure optimization start--------*/ cout << "start optimization" << endl; // Solve start timing t1 chrono::steady_clock::time_point t1 = chrono::steady_clock::now(); // Initialization optimization optimizer.initializeOptimization(); // Optimization times setting optimizer.optimize(10); // Solution end timing t2 chrono::steady_clock::time_point t2 = chrono::steady_clock::now(); // Total solution time chrono::duration<double> time_used = chrono::duration_cast<chrono::duration<double>>(t2 - t1); cout << "solve time cost = " << time_used.count() << " seconds. " << endl; // Output optimization value Eigen::Vector3d abc_estimate = v->estimate(); cout << "estimated model: " << abc_estimate.transpose() << endl; return 0; }
output
The output results are as follows:
start optimization solve time cost = 0.00741664 seconds. estimated model: 0.877649 1.21235 0.931272 iteration= 0 chi2= 12378200.264531 time= 0.000666326 cumTime= 0.000666326 edges= 100 schur= 0 iteration= 1 chi2= 1618271.477542 time= 0.000780621 cumTime= 0.00144695 edges= 100 schur= 0 iteration= 2 chi2= 199423.333138 time= 0.000621562 cumTime= 0.00206851 edges= 100 schur= 0 iteration= 3 chi2= 20992.691489 time= 0.000643664 cumTime= 0.00271217 edges= 100 schur= 0 iteration= 4 chi2= 1519.455061 time= 0.000571236 cumTime= 0.00328341 edges= 100 schur= 0 iteration= 5 chi2= 132.709242 time= 0.000571791 cumTime= 0.0038552 edges= 100 schur= 0 iteration= 6 chi2= 102.041451 time= 0.000593103 cumTime= 0.0044483 edges= 100 schur= 0 iteration= 7 chi2= 102.004182 time= 0.000571012 cumTime= 0.00501932 edges= 100 schur= 0 iteration= 8 chi2= 102.004182 time= 0.000571102 cumTime= 0.00559042 edges= 100 schur= 0 iteration= 9 chi2= 102.004182 time= 0.000571705 cumTime= 0.00616212 edges= 100 schur= 0