slam典型应用手搓
一.雅可比矩阵的计算
假设我们有一个三维状态向量 [x, y, z],并且定义一个新的观测模型。输出两个观测值[u, v],其中
u = x^2 + y^2 + z, v = sin(x) + cos(y) + e^z,演示雅可比矩阵的计算
#include <iostream>
#include <Eigen/Dense>
#include <cmath>/*
我们可以使用一个更复杂的例子来演示雅可比矩阵的计算。假设我们有一个三维状态向量 [x, y, z],并且定义一个新的观测模型。输出两个观测值[u, v],其中
u = x^2 + y^2 + z, v = sin(x) + cos(y) + e^z
*/// 状态向量:[x, y, theta]
Eigen::Vector3d state; // 观测模型
Eigen::Vector2d observationModel(const Eigen::Vector3d& state){// 假设观测为一个3D点double x = state[0];double y = state[1];double z = state[2];double u = x * x + y * y + z;double v = std::sin(x) + std::cos(y) + std::exp(z);return Eigen::Vector2d(x, y);
}// 计算雅可比矩阵
Eigen::Matrix<double, 2 ,3> computeJacobian(const Eigen::Vector3d& state){Eigen::Matrix<double, 2, 3> J;double x = state[0];double y = state[1];double z = state[2];// 对于x和y的偏导数J(0, 0) = 2 * x; // u对x的偏导J(0, 1) = 2 * y; // u对y的偏导J(0, 2) = 1; // u对z的偏导J(1, 0) = std::cos(x); // v对x的偏导J(1, 1) = -std::sin(y); // v对y的偏导J(1, 2) = std::exp(z); // v对z的偏导return J;
}int main(int argc, char *argv[])
{// 示例状态state << 2, 3, 0; // x = 2, y = 3, theta = 0// 计算观测Eigen::Vector2d z = observationModel(state);std::cout << "观测Observation: " << z.transpose() << std::endl;// 计算雅可比矩阵Eigen::Matrix<double, 2, 3> J = computeJacobian(state);std::cout << "Jaconbian Matrix :\n" << J <<std::endl;return 0;
}
二.里程计
编写一个运动模型的实现,如里程计模型,给定控制输入预测下一个状态
#include <iostream>
#include <Eigen/Dense>/*
编写一个运动模型的实现,如里程计模型,给定控制输入预测下一个状态
*/// 状态向量[x, y, theta]
Eigen::Vector3d state;// 运动模型
Eigen::Vector3d motionModel(const Eigen::Vector3d& state, double v, double omega, double dt){double x = state[0];double y = state[1];double theta = state[2];// 更新状态double newX = x + v * std::cos(theta) * dt;double newY = y + v * std::sin(theta) * dt;double newTheta = theta + omega * dt;// 返回新的状态return Eigen::Vector3d(newX, newY, newTheta);
}int main(){// 初始状态state << 0, 0, 0;// 控制输入:线速度和角速度double v = 1.0; // 1.0m/sdouble omega = 0.1; // 0.1rad/sdouble dt = 1.0; // 时间间隔 1s// 计算新的状态Eigen::Vector3d newState = motionModel(state, v, omega, dt);std::cout << "新的状态: " << newState.transpose() << std::endl;return 0;
}
三.卡尔曼滤波
实现一个二维卡尔曼滤波器,使用二维状态向量,假设我们要跟踪物体在平面上的位置和速度。状态向量 [x, y, vx, vy].包括位置(x, y),速度(vx, vy)
#include <iostream>
#include <Eigen/Dense>/*
实现一个二维卡尔曼滤波器,使用二维状态向量,假设我们要跟踪物体在平面上的位置和速度。状态向量 [x, y, vx, vy].包括位置(x, y),速度(vx, vy)
*/class kamlanFilter{
public:kamlanFilter(){// 初始化状态向量[x, y, v_x, v_y]state = Eigen::Vector4d(0, 0, 0, 0);// 初始化协方差矩阵covariance = Eigen::Matrix4d::Identity();// 状态转移矩阵F = Eigen::Matrix4d::Identity();F(0, 2) = 1; //x = x + v_x * dtF(1, 3) = 1; //y = y + v_y * dt// 观测矩阵H = Eigen::Matrix<double, 2, 4>::Zero();H(0, 0) = 1; // 观测xH(1, 1) = 1; // 观测y// 状态噪声协方差Q = Eigen::Matrix4d::Identity() * 0.1;// 观测噪声协方差R = Eigen::Matrix2d::Identity() * 1;}void predict(){// 状态预测state = F * state;// 协方差预测covariance = F * covariance * F.transpose() + Q;}void update(const Eigen::Vector2d& measurement){// 计算创新Eigen::Vector2d y = measurement - H * state;// 计算协方差Eigen::Matrix2d S = H * covariance * H.transpose() + R;// 计算卡尔曼增益Eigen::Matrix<double, 4, 2> K = covariance * H.transpose() * S.inverse();// 更新状态state += K * y;// 更新协方差covariance = (Eigen::Matrix4d::Identity() - K * H) * covariance;}Eigen::Vector4d getState() const{return state;}private:Eigen::Vector4d state; // 状态向量Eigen::Matrix4d covariance; // 协方差矩阵Eigen::Matrix4d F; // 状态转移矩阵Eigen::Matrix<double, 2, 4> H; // 观测矩阵Eigen::Matrix4d Q; // 状态噪声协方差Eigen::Matrix2d R; // 观测噪声协方差
};int main(){kamlanFilter kf;// 模拟数据Eigen::Vector2d measurements;for(int i = 0; i < 10; i++){// 模拟测量量(假设真实值是线性运动,添加噪声)measurements(0) = i + ((double) rand() / RAND_MAX) * 0.5; // 真实x添加噪声measurements(1) = i + ((double) rand() / RAND_MAX) * 0.5; // 真实y添加噪声// 预测kf.predict();// 更新kf.update(measurements);std::cout << "当前状态估计: " << kf.getState().transpose() << std::endl;}return 0;
}
四.扩展卡尔曼滤波
要在现有的二维卡尔曼滤波器基础上实现扩展卡尔曼滤波(EKF),你需要以下几个步骤:
非线性状态转移模型:定义一个非线性状态转移函数。
非线性观测模型:定义一个非线性观测函数。
计算雅可比矩阵:在预测和更新步骤中,计算状态转移和观测的雅可比矩阵。
下面是修改后的代码示例,添加了扩展卡尔曼滤波的实现:
#include <iostream>
#include <Eigen/Dense>/*
要在现有的二维卡尔曼滤波器基础上实现扩展卡尔曼滤波(EKF),你需要以下几个步骤:非线性状态转移模型:定义一个非线性状态转移函数。
非线性观测模型:定义一个非线性观测函数。
计算雅可比矩阵:在预测和更新步骤中,计算状态转移和观测的雅可比矩阵。
下面是修改后的代码示例,添加了扩展卡尔曼滤波的实现:
*/class ExtendedKalmanFilter {
public:ExtendedKalmanFilter() {// 初始化状态向量 [x, y, v_x, v_y]state = Eigen::Vector4d(0, 0, 0, 0);// 初始化协方差矩阵covariance = Eigen::Matrix4d::Identity();// 状态噪声协方差Q = Eigen::Matrix4d::Identity() * 0.1;// 观测噪声协方差R = Eigen::Matrix2d::Identity() * 1;}void predict(double dt) {// 使用非线性状态转移模型Eigen::Vector4d newState;newState(0) = state(0) + state(2) * dt; // x = x + v_x * dtnewState(1) = state(1) + state(3) * dt; // y = y + v_y * dtnewState(2) = state(2); // 速度保持不变newState(3) = state(3); // 速度保持不变// 更新状态state = newState;// 计算雅可比矩阵 FF = Eigen::Matrix4d::Identity();F(0, 2) = dt; // ∂x/∂v_xF(1, 3) = dt; // ∂y/∂v_y// 协方差预测covariance = F * covariance * F.transpose() + Q;}void update(const Eigen::Vector2d& measurement) {// 使用非线性观测模型Eigen::Vector2d z_pred;z_pred(0) = state(0); // 观测 xz_pred(1) = state(1); // 观测 y// 计算创新Eigen::Vector2d y = measurement - z_pred;// 计算雅可比矩阵 HH = Eigen::Matrix<double, 2, 4>::Zero();H(0, 0) = 1; // ∂z/∂xH(1, 1) = 1; // ∂z/∂y// 计算协方差Eigen::Matrix2d S = H * covariance * H.transpose() + R;// 计算卡尔曼增益Eigen::Matrix<double, 4, 2> K = covariance * H.transpose() * S.inverse();// 更新状态state += K * y;// 更新协方差covariance = (Eigen::Matrix4d::Identity() - K * H) * covariance;}Eigen::Vector4d getState() const {return state;}private:Eigen::Vector4d state; // 状态向量Eigen::Matrix4d covariance; // 协方差矩阵Eigen::Matrix4d F; // 状态转移雅可比矩阵Eigen::Matrix<double, 2, 4> H; // 观测雅可比矩阵Eigen::Matrix4d Q; // 状态噪声协方差Eigen::Matrix2d R; // 观测噪声协方差
};int main() {ExtendedKalmanFilter ekf;// 模拟数据Eigen::Vector2d measurements;double dt = 1.0; // 时间步长for (int i = 0; i < 10; i++) {// 模拟测量量(假设真实值是线性运动,添加噪声)measurements(0) = i + ((double)rand() / RAND_MAX) * 0.5; // 真实 x 加噪声measurements(1) = i + ((double)rand() / RAND_MAX) * 0.5; // 真实 y 加噪声// 预测ekf.predict(dt);// 更新ekf.update(measurements);std::cout << "当前状态估计: " << ekf.getState().transpose() << std::endl;}return 0;
}
五.g2o
使用g2o优化曲线
#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;// 曲线模型的顶点,模板参数:优化变量维度和数据类型
class CurveFittingVertex : public g2o::BaseVertex<3, Eigen::Vector3d> {
public:EIGEN_MAKE_ALIGNED_OPERATOR_NEW //这是一个宏,用于在Eigen库中创建对齐的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 {}
};// 误差模型 模板参数:观测值维度,类型,连接顶点类型
class CurveFittingEdge : public g2o::BaseUnaryEdge<1, double, CurveFittingVertex> {
public:EIGEN_MAKE_ALIGNED_OPERATOR_NEWCurveFittingEdge(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
};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 = 200; // 数据点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));}// 构建图优化,先设定g2otypedef g2o::BlockSolver<g2o::BlockSolverTraits<3, 1>> BlockSolverType; // 每个误差项优化变量维度为3,误差值维度为1typedef 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);}// 执行优化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;
}