当前位置: 首页 > news >正文

GTSAM 中自定义因子(Custom Factor)的详解和实战示例 - 指南

1. 因子图回顾

在 GTSAM 中:

  • 因子图(Factor Graph) 是联合概率分布的图表示,变量作为节点,因子作为边。

  • 每个因子 f(x) 约束一组变量,通常写成残差函数:

    r(x)=h(x)−z r(x) = h(x) - z r(x)=h(x)z

    • h(x):预测量(模型)
    • z:观测量
    • r(x):残差(约束)

目标是最小化所有残差的加权平方和(非线性最小二乘)。


2. 因子类体系结构

在 GTSAM 中,因子类的继承关系如下:

Factor└── NonlinearFactor├── NoiseModelFactor (常用基类,带噪声模型)│       ├── NoiseModelFactor1│       ├── NoiseModelFactor2│       └── ...└── CustomFactor (通用接口)

选择哪种方式?

  • 简单 1~N 个变量,推荐继承 NoiseModelFactorN(如 NoiseModelFactor2<Pose3, Point3>)。
  • 需要更大灵活性(可变数量变量、特殊残差形式),用 CustomFactor

3. CustomFactor 的核心接口

CustomFactor 本质上是 NonlinearFactor 的一个实现,它要求你传入一个 lambda/函数对象 来定义残差和 Jacobian。

核心构造函数:

CustomFactor::CustomFactor(
const SharedNoiseModel& noiseModel,
const KeyVector& keys,
const ErrorFunction& errorFunction);

其中:

  • noiseModel:噪声模型(如 noiseModel::Isotropic::Sigma(dimension, sigma)

  • keys:因子关联的变量键(Key 的 vector)

  • errorFunction:一个 std::function,签名为:

    std::function< Vector(const Values&, std::vector<Matrix>&) >
    • 输入:Values(变量集合),和一个 std::vector<Matrix>& H(存放 Jacobian)
    • 输出:残差向量 Vector

4. 实现自定义因子的步骤

Step 1. 定义残差函数

例如,一个约束:

r(x1,x2)=f(x1,x2)−z r(x_1, x_2) = f(x_1, x_2) - z r(x1,x2)=f(x1,x2)z

可以写成 lambda:

auto errorFun = [](const Values& values, std::vector<Matrix>& H) {Pose3 x1 = values.at<Pose3>(X1);Pose3 x2 = values.at<Pose3>(X2);// 残差(比如相对位姿约束)Pose3 between = x1.between(x2);Vector6 r = between.logmap(Z); // 假设观测是 Pose3 Z// Jacobian(如果 H 非空,才填充)if (!H.empty()) {// H.size() == number of keysif (H[0].rows() != 0) {H[0] = ...; // dr/dx1}if (H[1].rows() != 0) {H[1] = ...; // dr/dx2}}return r;};

Step 2. 构造因子

auto noise = noiseModel::Diagonal::Sigmas((Vector(6) << 0.1,0.1,0.1, 0.1,0.1,0.1).finished());
auto factor = boost::make_shared<CustomFactor>(noise, KeyVector{X1, X2}, errorFun);

Step 3. 插入因子图

NonlinearFactorGraph graph;
graph.add(factor);

5. 继承式因子(另一种写法)

有时希望写成一个类,便于管理:

class MyFactor : public NoiseModelFactor2<Pose3, Point3> {Point3 measured_;public:MyFactor(Key key1, Key key2, Point3 measured,const SharedNoiseModel& model): NoiseModelFactor2<Pose3, Point3>(model, key1, key2),measured_(measured) {}Vector evaluateError(const Pose3& pose, const Point3& point,boost::optional<Matrix&> H1, boost::optional<Matrix&> H2) const override {Point3 pred = pose.transformFrom(point, H1, H2);return pred - measured_;}};

特点:

  • 更类型安全,直接拿到模板参数类型。
  • 自动处理 Jacobian 维度。
  • 推荐用于固定变量数目的因子。

6. 常见噪声模型

  • 各向同性noiseModel::Isotropic::Sigma(dim, sigma)
  • 对角协方差noiseModel::Diagonal::Sigmas(sigmas)
  • 满协方差noiseModel::Gaussian::Covariance(cov)
  • 鲁棒核noiseModel::Robust::Create(mEstimator, baseModel)

7. 常见坑

  1. Jacobian 没有正确填

  2. 维度错误

  3. 不稳定的 logmap

  4. 性能问题


8. 进阶例子

1. IMU 预积分因子

GTSAM 自带 ImuFactor,但如果要自定义:

实现时推荐继承 NoiseModelFactor6<Pose3, Vector3, Bias, Pose3, Vector3, Bias>


2. 自定义代价函数因子(例如点到平面的约束)

r=n⊤(Rp+t−q) r = n^\top (R p + t - q) r=n(Rp+tq)

class PointPlaneFactor : public NoiseModelFactor1<Pose3> {Point3 point_;Unit3 normal_;Point3 planePoint_;public:PointPlaneFactor(Key poseKey, Point3 point, Unit3 normal, Point3 planePoint,const SharedNoiseModel& model): NoiseModelFactor1<Pose3>(model, poseKey),point_(point), normal_(normal), planePoint_(planePoint) {}Vector evaluateError(const Pose3& pose, OptionalMatrixType H) const override {Point3 p_world = pose.transformFrom(point_, H);double dist = normal_.point3().dot(p_world - planePoint_);return (Vector(1) << dist).finished();}};

9.综合示例

示例1-简单入门

下面一个 Point2 约束因子 的例子:

目标:


目录结构

my_gtsam_project/
├── CMakeLists.txt
└── src/└── main.cpp

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyGTSAMFactorExample)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_BUILD_TYPE Release)
find_package(GTSAM REQUIRED)   # 确保你安装了 GTSAM
add_executable(main src/main.cpp)
target_link_libraries(main GTSAM::gtsam)

src/main.cpp

#include <gtsam/nonlinear/NonlinearFactor.h>#include <gtsam/nonlinear/NonlinearFactorGraph.h>#include <gtsam/nonlinear/Values.h>#include <gtsam/nonlinear/GaussNewtonOptimizer.h>#include <gtsam/nonlinear/Marginals.h>#include <gtsam/geometry/Point2.h>#include <gtsam/inference/Symbol.h>#include <iostream>using namespace gtsam;// ============ 自定义因子 ============// 继承 NoiseModelFactor1,表示这个因子依赖 1 个变量:Point2class MyPriorFactor:public gtsam::NoiseModelFactor1<gtsam::Point2>{public:MyPriorFactor(gtsam::Key key,const gtsam::Point2&measured,const gtsam::SharedNoiseModel&model):gtsam::NoiseModelFactor1<gtsam::Point2>(model,key), m_measured(measured){}// 残差函数:预测值 - 观测值gtsam::Vector evaluateError(const  gtsam::Point2& x, boost::optional<gtsam::Matrix&> H) const override{if (H){*H= (gtsam::Matrix(2, 2) << 1, 0, 0, 1).finished(); // dr/dp = I}return x - m_measured;}protected:private:gtsam::Point2 m_measured;//观测值};// ============ 主函数 ============int main() {gtsam::NonlinearFactorGraph graph;// 定义变量 keygtsam::Key x = gtsam::Symbol('x', 0);// 构造噪声模型(2维,sigma=0.1)auto noise = gtsam::noiseModel::Isotropic::Sigma(2, 0.1);// 添加自定义因子 (约束 x ~ (1,2))graph.add(boost::make_shared<MyPriorFactor>(x, gtsam::Point2(1.0, 2.0), noise));// 初始值gtsam::Values initial;initial.insert(x, gtsam::Point2(0.0, 0.0));// 优化gtsam::GaussNewtonParams params;params.setVerbosity("ERROR");gtsam::GaussNewtonOptimizer optimizer(graph, initial, params);gtsam::Values result = optimizer.optimize();// 输出结果std::cout << "Initial estimate: " << initial.at<gtsam::Point2>(x).transpose() << std::endl;std::cout << "Optimized result: " << result.at<gtsam::Point2>(x).transpose() << std::endl;}return 0;}

编译 & 运行

mkdir build && cd build
cmake ..
make
./main

输出示例:

Initial error: 250
newError: 0
errorThreshold: 0 < 0
iterations: 1 >? 100
Initial estimate: 0 0
Optimized result: 1 2

示例2-进阶

一个 二元因子(比如约束两个 Point2 的距离 = d),展示 NoiseModelFactor2 的用法。

目标:

  • 定义 Point2DistanceFactor,继承自 NoiseModelFactor2<Point2, Point2>
  • 约束 两个点之间的欧氏距离 ≈ d
  • 用高斯牛顿优化,把两个点调到满足约束。

src/main.cpp

#include <gtsam/nonlinear/NonlinearFactor.h>#include <gtsam/nonlinear/NonlinearFactorGraph.h>#include <gtsam/nonlinear/Values.h>#include <gtsam/nonlinear/GaussNewtonOptimizer.h>#include <gtsam/nonlinear/Marginals.h>#include <gtsam/geometry/Point2.h>#include <gtsam/inference/Symbol.h>#include <iostream>#include <cmath>using namespace gtsam;// ============ 二元因子:约束两点距离 ============// 残差 r = ||p1 - p2|| - dclass Point2DistanceFactor : public gtsam::NoiseModelFactor2<gtsam::Point2, gtsam::Point2> {double measured_d_;  // 观测的距离public:Point2DistanceFactor(gtsam::Key key1, gtsam::Key key2, double measured_d, const gtsam::SharedNoiseModel& model): gtsam::NoiseModelFactor2<gtsam::Point2, Point2>(model, key1, key2), measured_d_(measured_d) {}Vector evaluateError(const gtsam::Point2& p1, const gtsam::Point2& p2,boost::optional<gtsam::Matrix&> H1, boost::optional<gtsam::Matrix&> H2) const override {gtsam::Vector2 diff = p1 - p2;double dist = diff.norm();   // ||p1 - p2||double err = dist - measured_d_;// Jacobiansif (H1 || H2) {// 避免除以 0gtsam::Vector2 unit = (dist > 1e-9) ? diff / dist : gtsam::Vector2(0.0, 0.0);if (H1) *H1 = unit.transpose();   // dr/dp1if (H2) *H2 = -unit.transpose();  // dr/dp2}return gtsam::Vector1(err);}};// ============ 主函数 ============int main() {gtsam::NonlinearFactorGraph graph;gtsam::Key x1 = gtsam::Symbol('x', 1);gtsam::Key x2 = gtsam::Symbol('x', 2);// 噪声模型:1 维残差,sigma=0.1auto noise = gtsam::noiseModel::Isotropic::Sigma(1, 0.1);// 添加约束:希望 x1, x2 之间的距离 = 2.0graph.add(boost::make_shared<Point2DistanceFactor>(x1, x2, 2.0, noise));// 约束 x1 在 (0,0),噪声较小(sigma=1e-3 表示强约束)auto priorNoise = gtsam::noiseModel::Isotropic::Sigma(2, 1e-2);graph.add(boost::make_shared<gtsam::PriorFactor<gtsam::Point2>>(x1, gtsam::Point2(0.0, 0.0), priorNoise));// 初始值:两个点很接近,不满足约束gtsam::Values initial;initial.insert(x1, gtsam::Point2(0.0, 0.0));initial.insert(x2, gtsam::Point2(0.5, 0.0));// 优化gtsam::LevenbergMarquardtParams params;params.setVerbosity("ERROR");gtsam::LevenbergMarquardtOptimizer optimizer(graph, initial, params);gtsam::Values result = optimizer.optimize();// 输出结果std::cout << "Initial:\n";std::cout << "  x1 = " << initial.at<gtsam::Point2>(x1).transpose() << "\n";std::cout << "  x2 = " << initial.at<gtsam::Point2>(x2).transpose() << "\n";std::cout << "Optimized:\n";std::cout << "  x1 = " << result.at<gtsam::Point2>(x1).transpose() << "\n";std::cout << "  x2 = " << result.at<gtsam::Point2>(x2).transpose() << "\n";double final_dist = (result.at<gtsam::Point2>(x1) - result.at<gtsam::Point2>(x2)).norm();std::cout << "Final distance = " << final_dist << "\n";return 0;}

运行结果

Initial error: 112.5
newError: 1.13625e-12
errorThreshold: 1.13625e-12 > 0
absoluteDecrease: 112.5 >= 1e-05
relativeDecrease: 1 >= 1e-05
newError: 1.21964775949e-28
errorThreshold: 1.21964775949e-28 > 0
absoluteDecrease: 1.13624977089e-12 < 1e-05
relativeDecrease: 1 >= 1e-05
converged
errorThreshold: 1.21964775949e-28 ? 100
Initial:x1 = 0 0x2 = 0.5   0
Optimized:x1 = -1.52999989329e-17                  0x2 = 2 0
Final distance = 2

可以看到,优化器自动把两个点调成了 相距 2.0,并且 Jacobian 是自定义的。


10. 总结

  • 简单场景:继承 NoiseModelFactorN,实现 evaluateError
  • 复杂/动态场景:用 CustomFactor + lambda。
  • 核心原则:残差维度与噪声模型一致,Jacobian 正确。
  • 调试技巧:用 GaussNewtonOptimizerLevenbergMarquardtOptimizer,开启 debugPrint 查看因子贡献。

11、自定义因子计算 Jacobian(H 矩阵)附件


因子定义回顾

误差函数是:

r=∥p1−p2∥−d r = \| p_1 - p_2 \| - d r=p1p2d

其中

  • p1=(x1,y1)Tp_1 = (x_1, y_1)^Tp1=(x1,y1)T
  • p2=(x2,y2)Tp_2 = (x_2, y_2)^Tp2=(x2,y2)T
  • ddd 是观测距离。

残差是 标量(1 维),所以返回 Vector1(err)


Jacobian 推导

我们需要分别计算:

∂r∂p1,∂r∂p2 \frac{\partial r}{\partial p_1}, \quad \frac{\partial r}{\partial p_2} p1r,p2r

  1. 定义差向量

Δ=p1−p2=[x1−x2y1−y2] \Delta = p_1 - p_2 = \begin{bmatrix} x_1 - x_2 \\ y_1 - y_2 \end{bmatrix} Δ=p1p2=[x1x2y1y2]

  1. 距离

∥Δ∥=(x1−x2)2+(y1−y2)2 \| \Delta \| = \sqrt{(x_1-x_2)^2 + (y_1-y_2)^2} ∥Δ∥=(x1x2)2+(y1y2)2

  1. 残差

r=∥Δ∥−d r = \| \Delta \| - d r=∥Δ∥d

  1. p1p_1p1 求导

∂r∂p1=ΔT∥Δ∥ \frac{\partial r}{\partial p_1} = \frac{\Delta^T}{\|\Delta\|} p1r=∥Δ∥ΔT

这是一个 1×2 向量

  1. p2p_2p2 求导

∂r∂p2=−ΔT∥Δ∥ \frac{\partial r}{\partial p_2} = -\frac{\Delta^T}{\|\Delta\|} p2r=∥Δ∥ΔT

同样是 1×2 向量


代码对应关系

在 C++ 里我们写:

Vector2 diff = p1 - p2;
double dist = diff.norm();  // ||p1 - p2||
Vector2 unit = (dist > 1e-9) ? diff / dist : Vector2(0.0, 0.0);
if (H1) *H1 = unit.transpose();   // dr/dp1 : 1x2
if (H2) *H2 = -unit.transpose();  // dr/dp2 : 1x2
  • unit = diff / dist 就是 Δ/∥Δ∥\Delta / \|\Delta\|Δ/∥Δ∥
  • unit.transpose()1x2 矩阵,符合 GTSAM 要求。

形状确认

  • 残差 r 是标量 → Vector1

  • 对于 p1(2 维变量):

    • Jacobian 应该是 1x2 矩阵
  • 对于 p2 也是 1x2 矩阵

所以在 GTSAM 里:

  • *H1*H2 的类型是 Eigen::MatrixXd(动态),但我们保证返回 1x2

总结


http://www.hskmm.com/?act=detail&tid=21093

相关文章:

  • AtCoder AGC073 A 题解
  • CF506C Mr. Kitayuta vs. Bamboos 51nod1457 小K vs. 竹子 题目分析
  • 北京 意大利 硕士学签/D签 vfs代办 材料清单
  • temp
  • 我有园子了
  • 使用 Jenkins 的流水线方案实施 CI/CD
  • 加密的病例单
  • 详细介绍:视频融合平台EasyCVR构筑智慧交通可视化管理与智能决策中枢
  • docker 在x86上build arm 镜像
  • 9.29软工
  • 不一样的.NET烟火,基于Roslyn的开源代码生成器
  • 复刻江协旋钮控制模块
  • 告别硬编码!5个让Web自动化脚本更稳定的定位策略
  • 从零开始,使用Idea工具搭建一个springboot项目
  • 最优/极值问题的算法选择
  • 第三方控件库的添加和使用
  • C4NR PVP服务器1.2 天穹炮塔更新
  • 树形dp [JOI Open 2020] 发电站 / Power Plant
  • 深入解析:灵画-AI绘画小程序
  • AT_arc156_b [ARC156B] Mex on Blackboard
  • 产品排序
  • 环形链表II-leetcode
  • ubuntu20.04安装nvidia显卡
  • [线段树系列 #6] 标记永久化
  • 9.29
  • c语言switch和if语句
  • Qt(制作一个方便的文本编辑器)
  • Java EE初阶启程记05---线程安全 - 指南
  • DataGridView表格控件使用说明
  • 题解:P7126 [Ynoi2008] rdCcot