基于 GTSAM 的因子图简单实例

本文围绕基于GTSAM的因子图简单实例展开。先介绍了SLAM例子,接着阐述GTSAM的安装与配置,然后分别给出该实例的C++和Python实现,包括源码、CMakeLists.txt脚本、数值结果,Python实现还有可视化结果,最后总结基于GTSAM对该例子进行了应用测试。

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

Title: 基于 GTSAM 的因子图简单实例



关联博文:

[1] 因子图、边缘化与消元算法的抽丝剥茧 —— Notes for “Factor Graphs for Robot Perception”

[2] 基于 GTSAM 的因子图简单实例 ⇐ \quad\Leftarrow\quad 本篇

[3] GTSAM 中的鲁棒噪声模型与 M-估计 (GTSAM Robust Noise Model and M-Estimator)

[4] 贝叶斯树定义与构建的寻行数墨 —— Notes for “The Bayes Tree: An Algorithmic Foundation for Probabilistic Robot Mapping”

[5] 贝叶斯树增量式更新的细嚼慢咽 —— Notes for “The Bayes Tree: An Algorithmic Foundation for Probabilistic Robot Mapping”
[6] iSAM2 部分状态更新算法 (I-原理解读)

[7] iSAM2 部分状态更新算法 (II - 源码阅读 - GTSAM)


I. 引言

上一篇博文中, 围绕因子图的构建和运行原理, 分析了因子图的定义、消元与边缘化算法等.

此处基于开源库 GTSAM, 构建和应用一下因子图.

就从一个简单的实例展开 —— 上一博文中的 “一个简单的 SLAM 中的因子图例子”.

SLAM 例子:

机器人先验的初始起点为 x 1 x_1 x1, 在该点上进行了一元测量 (Unary Measurement, 如 GPS、UWB 等绝对位姿测量) z 1 z_1 z1; 同时又探测到了路标 l 1 l_1 l1 并进行了二元测量 (Binary Measurement, 如激光扫描测量) z 2 z_2 z2.

然后机器人前进到了新的地点 x 2 x_2 x2, 并记录了从 x 1 x_1 x1 x 2 x_2 x2 的里程计信息; 在该点上也侦测到了路标点 l 1 l_1 l1 并进行了二元测量 z 3 z_3 z3.

机器人继续前进到 x 3 x_3 x3 并记录了这一段路程的里程计信息; 同时, 机器人在该点观察到了另一个路标点 l 2 l_2 l2 并记录了测量结果 z 4 z_4 z4.

factor_graph_2_slam
Fig. 1 一个简单的 SLAM 中的因子图例子

II. GTSAM 的安装与配置

先安装和配置环境.

序号处理
1下载源码
https://github.com/borglab/gtsam, 注意 checkout 最新的稳定版本.
2cmake-gui 查看编译参数
Picture1
3命令行运行
  mkdir build
  cd build
  cmake … -DCMAKE_INSTALL_PREFIX=指定路径/gtsam_4.2 -DGTSAM_BUILD_PYTHON=ON -DGTSAM_BUILD_DOCS=ON
  make -j4
  make install
  make python-install
注意: 替换"指定路径"为自己实际安装路径
4配置环境参数
Picture2_环境
5卸载
[不完全卸载] 编译时建立的build文件夹下, 运行命令
  make uninstall
[完全卸载] 编译时建立的build文件夹下, 运行命令
  xargs rm -rf < install_manifest.txt

III. 基于 GTSAM 的因子图实例的 C++ 实现

1. C++ 源码

一共 9 个因子, 涉及先验因子、里程计因子、二元测量因子、自定义一元测量因子等.

难点在于自定义一元测量因子的构建.

一元测量因子用于建模 UWB、GPS等, 此处以二维平面上的起始位姿状态 x 1 = ( x , y , θ ) \mathbf{x}_1=(x, y, \theta) x1=(x,y,θ) 为例, 对应的测量值为 m = ( m x , m y ) \mathbf{m}=(m_x, m_y) m=(mx,my). 则位置误差/残差为
r ( x 1 ) = [ x − m x y − m y ] \mathbf{r}(\mathbf{x}_1) = \begin{bmatrix} x - m_x \\ y - m_y \end{bmatrix} r(x1)=[xmxymy]
r \mathbf{r} r 相对于 x 1 \mathbf{x}_1 x1 在位置测量值/测量点 m \mathbf{m} m 上的 Jacobian 矩阵 (雅可比矩阵) 为
∂ r ( x 1 ) ∂ x 1 ∣ m = [ ∂ ( x − m x ) ∂ x ∂ ( x − m x ) ∂ y ∂ ( x − m x ) ∂ θ ∂ ( y − m y ) ∂ x ∂ ( y − m y ) ∂ y ∂ ( y − m y ) ∂ θ ] ∣ x = m x , y = m y = [ 1 0 0 0 1 0 ] \left. \frac{\partial \mathbf{r}(\mathbf{x}_1)}{\partial \mathbf{x}_1} \right|_{\mathbf{m}} = \left. \begin{bmatrix} \frac{\partial (x - m_x) }{\partial x } & \frac{\partial (x - m_x) }{\partial y } & \frac{\partial (x - m_x) }{\partial \theta }\\ \frac{\partial (y - m_y) }{\partial x } & \frac{\partial (y - m_y) }{\partial y } & \frac{\partial (y - m_y) }{\partial \theta } \end{bmatrix}\right|_{x=m_x, y=m_y} = \begin{bmatrix}1 &0 &0\\ 0 & 1 &0\end{bmatrix} x1r(x1) m=[x(xmx)x(ymy)y(xmx)y(ymy)θ(xmx)θ(ymy)] x=mx,y=my=[100100]
在该一元测量中 θ \theta θ 分量并不起作用, 只是为了兼容其他因子 (如 Odometry 因子) 才引入的.

构建因子时需要明确误差和 Jacobian, 是由于因子图的构建是为了求解非线性二乘问题.

#include "gtsam/geometry/Pose2.h"
#include "gtsam/geometry/Point2.h"
#include "gtsam/slam/BetweenFactor.h"
#include "gtsam/slam/BearingRangeFactor.h"
#include "gtsam/nonlinear/NonlinearFactorGraph.h"
#include "gtsam/nonlinear/LevenbergMarquardtOptimizer.h"
#include "gtsam/nonlinear/Marginals.h"
#include "gtsam/inference/Symbol.h"
#include "Eigen/Dense"
#include "iostream"

// 自定义因子 UnaryFactor 继承自 NoiseModelFactorN
class UnaryFactor: public gtsam::NoiseModelFactorN<gtsam::Pose2> {
    double mx_, my_; // mx_, my_ 代表测量到的数值

 public:
    typedef boost::shared_ptr<UnaryFactor> shared_ptr;
    // The constructor requires the variable key, the (X, Y) measurement value, and the noise model
    UnaryFactor(gtsam::Key k, double x, double y, const gtsam::SharedNoiseModel& model):
    gtsam::NoiseModelFactorN<gtsam::Pose2>(model, k), mx_(x), my_(y) {}  // 利用 key 和 noise model 初始化基类 NoiseModelFactorN
    ~UnaryFactor() override {}

    // evaluateError 中建立 (1)误差向量相对于状态变量的Jacobian 和 (2) 误差向量
    gtsam::Vector evaluateError(const gtsam::Pose2& q, 
    boost::optional<gtsam::Matrix&> H = boost::none) const override {
        if (H) (*H) = (gtsam::Matrix(2, 3) <<
                1.0, 0.0, 0.0,
                0.0, 1.0, 0.0).finished();
        return (gtsam::Vector(2) << q.x() - mx_, q.y() - my_).finished();
    }
};


int main(int argc, char** argv)
{
    gtsam::NonlinearFactorGraph graph;

    auto x_1 = gtsam::Symbol('x', 1);
    auto x_2 = gtsam::Symbol('x', 2);
    auto x_3 = gtsam::Symbol('x', 3);
    auto l_1 = gtsam::Symbol('l', 1);
    auto l_2 = gtsam::Symbol('l', 2);

    // std::freopen("results.txt", "w", stdout);  // 输出重定位

    // 第一部分 因子图的构建
    // factor 1 —— prior factor for the starting point x_1 起始点先验因子
    // x_1 变量的类型为 gtsam::Pose2
    gtsam::Pose2 startingPriorMean(0.0, 0.0, 0.0);
    auto startingPriorNoise = gtsam::noiseModel::Diagonal::Sigmas(Eigen::Vector3d(0.2, 0.2, 0.1));
    graph.addPrior(x_1, startingPriorMean, startingPriorNoise);

    // factor 2 and 3 —— odometry factors for x_1->x_2 and x_2->x_3 里程计因子
    // x_2 和 x_3 变量的类型都为 gtsam::Pose2
    gtsam::Pose2 odometry1(2.0, 0.0, 0.0);
    auto odometryNoise = gtsam::noiseModel::Diagonal::Sigmas(Eigen::Vector3d(0.25, 0.25, 0.1));
    graph.add(gtsam::BetweenFactor<gtsam::Pose2>(x_1, x_2, odometry1, odometryNoise));
    graph.add(gtsam::BetweenFactor<gtsam::Pose2>(x_2, x_3, odometry1, odometryNoise));

    // factor 4 and 5 —— prior factors for landmark l_1 and l_2 路标先验因子
    // l_1 和 l_2 变量的类型都为 gtsam::Point2
    gtsam::Point2 landmarkPriorMean1(2.0, 2.0);
    gtsam::Point2 landmarkPriorMean2(4.0, 2.0);
    auto landmarkPriorNoise = gtsam::noiseModel::Diagonal::Sigmas(Eigen::Vector2d(0.2, 0.2));
    graph.add(gtsam::PriorFactor<gtsam::Point2>(l_1, landmarkPriorMean1, landmarkPriorNoise));
    graph.add(gtsam::PriorFactor<gtsam::Point2>(l_2, landmarkPriorMean2, landmarkPriorNoise));

    // factor 6 —— unary measurement factor for x_1 起始点的一元测量, 与先验因子的区别是信息维度 (少了方向数据)
    auto startingUnaryNoise = gtsam::noiseModel::Diagonal::Sigmas(Eigen::Vector2d(0.1, 0.1));
    // 自定义因子的 noise model 的维度须与自定义因子中的误差向量维度一致
    // graph.add(boost::make_shared<UnaryFactor>(x_1, 0.0, 0.0, startingPriorNoise));
    graph.emplace_shared<UnaryFactor>(x_1, 0.01, 0.01, startingUnaryNoise);

    // factor 7 - binary measurement factor for (x_1, l_1) 二元测量
    // l_1 在 x_1 局部坐标系中的测量值
    auto measurementNoise = gtsam::noiseModel::Diagonal::Sigmas(Eigen::Vector2d(0.1, 0.2));
    gtsam::Rot2 bearing_x1_l1 = gtsam::Rot2::fromAngle(M_PI/4);
    double range_x1_l1 = sqrt(8.0);
    graph.add(gtsam::BearingRangeFactor<gtsam::Pose2, gtsam::Point2>(x_1, l_1, bearing_x1_l1, range_x1_l1, measurementNoise));

    // factor 8 - binary measurement factor for (x_2, l_1) 二元测量
    // l_1 在 x_2 局部坐标系中的测量值
    gtsam::Rot2 bearing_x2_l1 = gtsam::Rot2::fromAngle(M_PI/2);
    double range_x2_l1 = 2.0;
    graph.add(gtsam::BearingRangeFactor<gtsam::Pose2, gtsam::Point2>(x_2, l_1, bearing_x2_l1, range_x2_l1, measurementNoise));

    // factor 9 - binary measurement factor for (x_3, l_2) 二元测量
    // l_2 在 x_3 局部坐标系中的测量值
    gtsam::Rot2 bearing_x3_l2 = gtsam::Rot2::fromAngle(M_PI/2);
    double range_x3_l2 = 2.0;
    graph.add(gtsam::BearingRangeFactor<gtsam::Pose2, gtsam::Point2>(x_3, l_2, bearing_x3_l2, range_x3_l2, measurementNoise));

    graph.print("\nFactor Graph\n");  // 因子图构建的输出

    // 第二部分 非线性优化计算
    printf("Nonlinear optimization:\n");
    gtsam::Values initial;  // 一些随机值作为非线性二乘问题迭代计算的初始值, 是对真值的初步猜测, 与真值之间有偏差
    initial.insert(x_1, gtsam::Pose2(0.0, 0.1, 0.1));
    initial.insert(x_2, gtsam::Pose2(0.17, -0.1, -0.05));
    initial.insert(x_3, gtsam::Pose2(0.45, 0.1, 0.1));
    initial.insert(l_1, gtsam::Point2(0.19, 0.21));
    initial.insert(l_2, gtsam::Point2(42.0, 19.0));
    initial.print("Initial estimation:"); // 初始值的输出

    // gtsam::Values result = gtsam::LevenbergMarquardtOptimizer(graph, initial).optimize();
    // result.print("Final results:"); // 优化值的输出
    gtsam::LevenbergMarquardtParams parameters;
    parameters.setVerbosity("ERROR");
    // parameters.setVerbosityLM("SUMMARY");
    gtsam::LevenbergMarquardtOptimizer optimizer(graph, initial, parameters);
    gtsam::Values result = optimizer.optimize();
    result.print("\nFinal Result:\n");

    // 第三部分 边缘概率计算
    std::cout.precision(3);
    printf("\nMarginal Covariance:\n");
    gtsam::Marginals marginals(graph, result);
    std::cout << "\nx1 covariance:\n" << marginals.marginalCovariance(x_1) << std::endl;
    std::cout << "\nx2 covariance:\n" << marginals.marginalCovariance(x_2) << std::endl;
    std::cout << "\nx3 covariance:\n" << marginals.marginalCovariance(x_3) << std::endl;
    std::cout << "\nl1 covariance:\n" << marginals.marginalCovariance(l_1) << std::endl;
    std::cout << "\nl2 covariance:\n" << marginals.marginalCovariance(l_2) << std::endl;

}  

2. CMakeLists.txt 脚本

cmake_minimum_required(VERSION 3.0)

project(factor_graph_example)
find_package(GTSAM 4.2 REQUIRED COMPONENTS)
include_directories(${GTSAM_INCLUDE_DIR})

FILE(GLOB_RECURSE HPP_INCLUDE include/*.hpp include/*.h)
FILE(GLOB_RECURSE CPP_SOURCE source/*.cpp)
add_executable(${CMAKE_PROJECT_NAME} ${CPP_SOURCE} ${HPP_INCLUDE})
target_link_libraries(${CMAKE_PROJECT_NAME} gtsam)

3. 数值结果

Factor Graph
size: 9

Factor 0: PriorFactor on x1
  prior mean:  (0, 0, 0)
  noise model: diagonal sigmas [0.2; 0.2; 0.1];

Factor 1: BetweenFactor(x1,x2)
  measured:  (2, 0, 0)
  noise model: diagonal sigmas [0.25; 0.25; 0.1];

Factor 2: BetweenFactor(x2,x3)
  measured:  (2, 0, 0)
  noise model: diagonal sigmas [0.25; 0.25; 0.1];

Factor 3: PriorFactor on l1
  prior mean: [
        2;
        2
]
isotropic dim=2 sigma=0.2

Factor 4: PriorFactor on l2
  prior mean: [
        4;
        2
]
isotropic dim=2 sigma=0.2

Factor 5:   keys = { x1 }
isotropic dim=2 sigma=0.1

Factor 6: BearingRangeFactor
Factor 6:   keys = { x1 l1 }
  noise model: diagonal sigmas [0.1; 0.2];
ExpressionFactor with measurement: bearing : 0.785398163
range  2.82842712

Factor 7: BearingRangeFactor
Factor 7:   keys = { x2 l1 }
  noise model: diagonal sigmas [0.1; 0.2];
ExpressionFactor with measurement: bearing : 1.57079633
range  2

Factor 8: BearingRangeFactor
Factor 8:   keys = { x3 l2 }
  noise model: diagonal sigmas [0.1; 0.2];
ExpressionFactor with measurement: bearing : 1.57079633
range  2

Nonlinear optimization:
Initial estimation:
Values with 5 values:
Value l1: (Eigen::Matrix<double, 2, 1, 0, 2, 1>)
[
        0.19;
        0.21
]

Value l2: (Eigen::Matrix<double, 2, 1, 0, 2, 1>)
[
        42;
        19
]

Value x1: (gtsam::Pose2)
(0, 0.1, 0.1)

Value x2: (gtsam::Pose2)
(0.17, -0.1, -0.05)

Value x3: (gtsam::Pose2)
(0.45, 0.1, 0.1)

Initial error: 45816.3375
newError: 60.0746132
errorThreshold: 60.0746132 > 0
absoluteDecrease: 45756.2629219 >= 1e-05
relativeDecrease: 0.99868879495 >= 1e-05
newError: 1.24368959022
errorThreshold: 1.24368959022 > 0
absoluteDecrease: 58.8309235801 >= 1e-05
relativeDecrease: 0.97929758471 >= 1e-05
newError: 0.00598271925373
errorThreshold: 0.00598271925373 > 0
absoluteDecrease: 1.23770687096 >= 1e-05
relativeDecrease: 0.995189539817 >= 1e-05
newError: 0.00296591263207
errorThreshold: 0.00296591263207 > 0
absoluteDecrease: 0.00301680662167 >= 1e-05
relativeDecrease: 0.504253416167 >= 1e-05
newError: 0.00296590504323
errorThreshold: 0.00296590504323 > 0
absoluteDecrease: 7.58883373286e-09 < 1e-05
relativeDecrease: 2.55868418065e-06 < 1e-05
converged
errorThreshold: 0.00296590504323 <? 0
absoluteDecrease: 7.58883373286e-09 <? 1e-05
relativeDecrease: 2.55868418065e-06 <? 1e-05
iterations: 5 >? 100

Final Result:

Values with 5 values:
Value l1: (Eigen::Matrix<double, 2, 1, 0, 2, 1>)
[
        2.00402979248;
        2.00353272869
]

Value l2: (Eigen::Matrix<double, 2, 1, 0, 2, 1>)
[
        4.00101280265;
        2.00108370699
]

Value x1: (gtsam::Pose2)
(0.00699225259677, 0.00707591845494, -0.000329623653647)

Value x2: (gtsam::Pose2)
(2.00448395483, 0.00399757524079, -6.886908946e-05)

Value x3: (gtsam::Pose2)
(4.00290058662, 0.00216735620844, 0.000437767142559)


Marginal Covariance:

x1 covariance:
  0.00728 -0.000291  0.000719
-0.000291   0.00737  -0.00105
 0.000719  -0.00105   0.00438

x2 covariance:
   0.0397  -0.00156   0.00895
 -0.00156    0.0315 -0.000391
  0.00895 -0.000391   0.00798

x3 covariance:
 0.0701   0.012  0.0193
  0.012  0.0546 0.00783
 0.0193 0.00783  0.0147

l1 covariance:
  0.0224 -0.00333
-0.00333   0.0217

l2 covariance:
   0.0329 -0.000904
-0.000904    0.0336

IV. 基于 GTSAM 的因子图实例的 Python 实现

1. Python 源码

同样地, 难点在于一元测量因子的自定义构建.

Python 上运行画图比较方便.

"""
GTSAM Copyright 2010-2018, Georgia Tech Research Corporation,
Atlanta, Georgia 30332-0415
All Rights Reserved
Authors: Frank Dellaert, et al. (see THANKS for the full author list)

See LICENSE for the license information

Simple robotics example using odometry measurements and bearing-range (laser) measurements
Author: Alex Cunningham (C++), Kevin Deng & Frank Dellaert (Python)

++ + custom factor of the unary measurement 

"""
# pylint: disable=invalid-name, E1101

from __future__ import print_function
from functools import partial
from typing import List, Optional

import gtsam
import numpy as np
from gtsam.symbol_shorthand import L, X
import gtsam.utils.plot as gtsam_plot
import matplotlib.pyplot as plt

# Create noise models
PRIOR_NOISE = gtsam.noiseModel.Diagonal.Sigmas(np.array([0.2, 0.2, 0.1]))
ODOMETRY_NOISE = gtsam.noiseModel.Diagonal.Sigmas(np.array([0.25, 0.25, 0.1]))
MEASUREMENT_NOISE = gtsam.noiseModel.Diagonal.Sigmas(np.array([0.1, 0.2]))
UNARY_MEASUREMENT_NOISE = gtsam.noiseModel.Diagonal.Sigmas(np.array([0.1, 0.1]))
LANDMARK_PRIOR_NOISE = gtsam.noiseModel.Diagonal.Sigmas(np.array([0.2, 0.2]))


def error_unary(measurement: np.ndarray, this: gtsam.CustomFactor,
              values: gtsam.Values,
              jacobians: Optional[List[np.ndarray]]) -> float:
    """Unary Factor error function
    :param measurement: Unary measurement, to be filled with `partial`
    :param this: gtsam.CustomFactor handle
    :param values: gtsam.Values
    :param jacobians: Optional list of Jacobians
    :return: the unwhitened error
    """
    key = this.keys()[0]
    estimate = values.atPose2(key)
    # error = [estimate[0], estimate[1]] - measurement
    error = [(estimate.x() - measurement[0]), (estimate.y() - measurement[1])]
    
    print(error)

    if jacobians is not None:
        jacobians[0] = np.matrix([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
    # jacobians[0] = np.eye(2)

    return error
            


def main():
    """Main runner"""

    # Create an empty nonlinear factor graph
    graph = gtsam.NonlinearFactorGraph()

    # Create the keys corresponding to unknown variables in the factor graph
    X1 = X(1)
    X2 = X(2)
    X3 = X(3)
    L1 = L(1)
    L2 = L(2)



    # Add a prior on pose X1 at the origin. A prior factor consists of a mean and a noise model
    graph.add(
        gtsam.PriorFactorPose2(X1, gtsam.Pose2(0.0, 0.0, 0.0), PRIOR_NOISE))

    # Add odometry factors between X1,X2 and X2,X3, respectively
    graph.add(
        gtsam.BetweenFactorPose2(X1, X2, gtsam.Pose2(2.0, 0.0, 0.0),
                                 ODOMETRY_NOISE))
    graph.add(
        gtsam.BetweenFactorPose2(X2, X3, gtsam.Pose2(2.0, 0.0, 0.0),
                                 ODOMETRY_NOISE))
    


    # Add a prior on pose L1 at the origin. A prior factor consists of a mean and a noise model
    landmarkPriorMean1 = gtsam.Point2(2.0, 2.0)
    graph.add(
        gtsam.PriorFactorPoint2(L1, landmarkPriorMean1, LANDMARK_PRIOR_NOISE))


    # Add a prior on pose L2 at the origin. A prior factor consists of a mean and a noise model
    landmarkPriorMean1 = gtsam.Point2(4.0, 2.0)
    graph.add(
        gtsam.PriorFactorPoint2(L2, landmarkPriorMean1, LANDMARK_PRIOR_NOISE))


    # Add the Unary-Measurement factors on X1
    measurement = [0.01 , 0.01]
    graph.add(
        gtsam.CustomFactor(UNARY_MEASUREMENT_NOISE, [X1], partial(error_unary, measurement)))
    # 注意 此处 functools.partial 基于error_unary 函数创建可调用对象, 把原函数中的第一个参数 measurement 固定了
    # 直白一点就是 传递了 measurement 参数值
    # 不要误解为偏微分计算

    # Add Range-Bearing measurements to two different landmarks L1 and L2
    graph.add(
        gtsam.BearingRangeFactor2D(X1, L1, gtsam.Rot2.fromDegrees(45),
                                   np.sqrt(4.0 + 4.0), MEASUREMENT_NOISE))
    graph.add(
        gtsam.BearingRangeFactor2D(X2, L1, gtsam.Rot2.fromDegrees(90), 2.0,
                                   MEASUREMENT_NOISE))
    graph.add(
        gtsam.BearingRangeFactor2D(X3, L2, gtsam.Rot2.fromDegrees(90), 2.0,
                                   MEASUREMENT_NOISE))

    # Print graph
    print("Factor Graph:\n{}".format(graph))

    # Create (deliberately inaccurate) initial estimate
    initial_estimate = gtsam.Values()
    initial_estimate.insert(X1, gtsam.Pose2(0.0, 0.1, 0.1))
    initial_estimate.insert(X2, gtsam.Pose2(0.17, -0.1, -0.05))
    initial_estimate.insert(X3, gtsam.Pose2(0.45, 0.10, 0.10))
    initial_estimate.insert(L1, gtsam.Point2(0.19, 0.21))
    initial_estimate.insert(L2, gtsam.Point2(42.0, 19.0))

    # Print
    print("Initial Estimate:\n{}".format(initial_estimate))

    # Optimize using Levenberg-Marquardt optimization. The optimizer
    # accepts an optional set of configuration parameters, controlling
    # things like convergence criteria, the type of linear system solver
    # to use, and the amount of information displayed during optimization.
    # Here we will use the default set of parameters.  See the
    # documentation for the full set of parameters.
    params = gtsam.LevenbergMarquardtParams()
    params.setVerbosity("ERROR")
    optimizer = gtsam.LevenbergMarquardtOptimizer(graph, initial_estimate,
                                                  params)
    result = optimizer.optimize()
    print("\nFinal Result:\n{}".format(result))

    # Calculate and print marginal covariances for all variables
    marginals = gtsam.Marginals(graph, result)
    for (key, s) in [(X1, "X1"), (X2, "X2"), (X3, "X3"), (L1, "L1"),
                     (L2, "L2")]:
        print("{} covariance:\n{}\n".format(s,
                                            marginals.marginalCovariance(key)))
        
    for (key, s) in [(X1, "X1"), (X2, "X2"), (X3, "X3") ]:
        gtsam_plot.plot_pose2(0, result.atPose2(key), 0.5, marginals.marginalCovariance(key))
    for (key, s) in [ (L1, "L1"),(L2, "L2") ]:
        gtsam_plot.plot_point2(0, result.atPoint2(key), 0.5, marginals.marginalCovariance(key))
    plt.axis('equal')
    plt.show()


if __name__ == "__main__":
    main()

2. 数值结果

可以看出不管是 C++ 版本的结果还是 Python 版本的结果是一致的.

Factor Graph:
NonlinearFactorGraph: size: 9

Factor 0: PriorFactor on x1
  prior mean:  (0, 0, 0)
  noise model: diagonal sigmas [0.2; 0.2; 0.1];

Factor 1: BetweenFactor(x1,x2)
  measured:  (2, 0, 0)
  noise model: diagonal sigmas [0.25; 0.25; 0.1];

Factor 2: BetweenFactor(x2,x3)
  measured:  (2, 0, 0)
  noise model: diagonal sigmas [0.25; 0.25; 0.1];

Factor 3: PriorFactor on l1
  prior mean: [
	2;
	2
]
isotropic dim=2 sigma=0.2

Factor 4: PriorFactor on l2
  prior mean: [
	4;
	2
]
isotropic dim=2 sigma=0.2

Factor 5: CustomFactor on x1
isotropic dim=2 sigma=0.1

Factor 6: BearingRangeFactor
Factor 6:   keys = { x1 l1 }
  noise model: diagonal sigmas [0.1; 0.2];
ExpressionFactor with measurement: bearing : 0.785398163
range  2.82842712

Factor 7: BearingRangeFactor
Factor 7:   keys = { x2 l1 }
  noise model: diagonal sigmas [0.1; 0.2];
ExpressionFactor with measurement: bearing : 1.57079633
range  2

Factor 8: BearingRangeFactor
Factor 8:   keys = { x3 l2 }
  noise model: diagonal sigmas [0.1; 0.2];
ExpressionFactor with measurement: bearing : 1.57079633
range  2


Initial Estimate:
Values with 5 values:
Value l1: (Eigen::Matrix<double, -1, 1, 0, -1, 1>)
[
	0.19;
	0.21
]

Value l2: (Eigen::Matrix<double, -1, 1, 0, -1, 1>)
[
	42;
	19
]

Value x1: (gtsam::Pose2)
(0, 0.1, 0.1)

Value x2: (gtsam::Pose2)
(0.17, -0.1, -0.05)

Value x3: (gtsam::Pose2)
(0.45, 0.1, 0.1)


[-0.01, 0.09000000000000001]
Initial error: 45816.3375
[-0.01, 0.09000000000000001]
[-0.09431931375291222, 0.08073185880549684]
newError: 60.0746132
errorThreshold: 60.0746132 > 0
absoluteDecrease: 45756.2629219 >= 1e-05
relativeDecrease: 0.99868879495 >= 1e-05
[-0.09431931375291222, 0.08073185880549684]
[-0.021338603719452405, -0.0190455547054151]
newError: 1.24368959022
errorThreshold: 1.24368959022 > 0
absoluteDecrease: 58.8309235801 >= 1e-05
relativeDecrease: 0.97929758471 >= 1e-05
[-0.021338603719452405, -0.0190455547054151]
[-0.0026978140729723103, -0.0029251685355443906]
newError: 0.00598271925373
errorThreshold: 0.00598271925373 > 0
absoluteDecrease: 1.23770687096 >= 1e-05
relativeDecrease: 0.995189539817 >= 1e-05
[-0.0026978140729723103, -0.0029251685355443906]
[-0.0030085387600294784, -0.002923210421981185]
newError: 0.00296591263207
errorThreshold: 0.00296591263207 > 0
absoluteDecrease: 0.00301680662167 >= 1e-05
relativeDecrease: 0.504253416167 >= 1e-05
[-0.0030085387600294784, -0.002923210421981185]
[-0.0030077474032272995, -0.0029240815450598734]
newError: 0.00296590504323
errorThreshold: 0.00296590504323 > 0
absoluteDecrease: 7.58883373286e-09 < 1e-05
relativeDecrease: 2.55868418065e-06 < 1e-05
converged
errorThreshold: 0.00296590504323 <? 0
absoluteDecrease: 7.58883373286e-09 <? 1e-05
relativeDecrease: 2.55868418065e-06 <? 1e-05
iterations: 5 >? 100

Final Result:
Values with 5 values:
Value l1: (Eigen::Matrix<double, -1, 1, 0, -1, 1>)
[
	2.00402979248;
	2.00353272869
]

Value l2: (Eigen::Matrix<double, -1, 1, 0, -1, 1>)
[
	4.00101280265;
	2.00108370699
]

Value x1: (gtsam::Pose2)
(0.00699225259677, 0.00707591845494, -0.000329623653647)

Value x2: (gtsam::Pose2)
(2.00448395483, 0.00399757524079, -6.886908946e-05)

Value x3: (gtsam::Pose2)
(4.00290058662, 0.00216735620844, 0.000437767142559)


[-0.0030077474032272995, -0.0029240815450598734]
X1 covariance:
[[ 0.00728256 -0.00029085  0.0007187 ]
 [-0.00029085  0.0073673  -0.00104787]
 [ 0.0007187  -0.00104787  0.0043819 ]]

X2 covariance:
[[ 0.03966749 -0.00155955  0.00895399]
 [-0.00155955  0.03147331 -0.00039051]
 [ 0.00895399 -0.00039051  0.00797534]]

X3 covariance:
[[0.07007273 0.01202848 0.01925868]
 [0.01202848 0.05455565 0.00783016]
 [0.01925868 0.00783016 0.01468828]]

L1 covariance:
[[ 0.02237142 -0.00332628]
 [-0.00332628  0.02172322]]

L2 covariance:
[[ 0.03294629 -0.00090407]
 [-0.00090407  0.03363416]]

3. 可视化结果

python_result
Fig. 1 一个简单的 SLAM 中的因子图例子的计算结果

V. 总结

针对上一篇博文中提到的 “一个简单的 SLAM 中的因子图例子”, 基于 GTSAM 进行了简单应用测试.


版权声明:本文为博主原创文章,遵循 CC 4.0 BY 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.youkuaiyun.com/woyaomaishu2/article/details/136219021
本文作者:wzf@robotics_notes


### SVO gtsam 和 FishEye 相机模型实现 #### 半直接视觉里程计 (SVO) SVO 是一种半直接视觉里程计,最初设计用于单目相机。通过引入稀疏模型的图像对齐方式,实现了更高的处理速度[^1]。随着版本迭代,SVO 扩展到支持多摄像头配置,包括鱼眼镜头。 对于带有 GTSAM 的 SVO 实现,GTSAM 提供了一个强大的框架来进行非线性优化,特别适用于SLAM问题中的状态估计。结合鱼眼相机模型,可以构建一个鲁棒性强、精度高的视觉里程计算法。 #### 鱼眼相机模型简介 鱼眼相机因其广角视野特性,在机器人导航等领域应用广泛。其成像特点决定了传统针孔相机模型不再适用,因此需要采用专门针对鱼眼光学特性的畸变校正模型: - **径向畸变参数**:k₁, k₂, k₃... - **切向畸变参数**:p₁, p₂... 这些参数可以通过标定过程获取,并应用于后续的图像预处理阶段。 #### 使用 GTSAM 进行姿态估计 为了利用 GTSAM 库完成 SVO 中的姿态估计部分,通常会涉及到以下几个方面的工作: - 定义测量方程; - 构建因子图结构; - 设计误差函数并求解最优解; 下面给出一段简化版 Python 伪代码示例,展示了如何使用 PyGTSAM 来定义和解决位姿优化问题: ```python import numpy as np from pygtsam import symbol_shorthand_L, NonlinearFactorGraph, noiseModel, Values, LevenbergMarquardtOptimizer # 创建噪声模型 noise_model = noiseModel.Isotropic.Sigma(6, 0.1) # 初始化因子图 graph = NonlinearFactorGraph() # 添加先验约束(假设已知初始位置) prior_mean = ... # 初始猜测的位置 prior_noise = noiseModel.Diagonal.Variances(np.array([1e-6]*6)) graph.addPriorPose3(symbol_shorthand_L('x', 0), prior_mean, prior_noise) # 假设我们有一些观测数据 points_2d 和对应的三维坐标 points_3d for i in range(len(points_2d)): graph.add(BearingRangeFactor3D( symbol_shorthand_L('l', landmarks[i]), symbol_shorthand_X(i), measured_bearing_range, noise_model)) initial_estimate = Values() optimizer = LevenbergMarquardtOptimizer(graph, initial_estimate) result = optimizer.optimize() ``` 此段代码仅作为概念验证用途,实际项目中还需要考虑更多细节如特征匹配、回环闭合检测等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值