视觉SLAM十四讲(第2版)总结

2023-05-16

最近看完了《视觉SLAM十四讲(第2版):从理论到实践》(高翔等著),原书分两部分,先介绍了数学基础,然后介绍了具体的SLAM实践,非常适合零基础开始学习SLAM。

作为总结,这里并不对书中的内容做太多的重复说明,本人的水平也不足以用聊聊数字概括原书精华,因而这里采用这样一种思路:提出问题,分析求解,实践应用——来说说自己的理解。

本文从研究背景出发,引出SLAM问题的数学模型;运用数学工具分析求解模型(主要是直接求导与扰动模型);最后,对原书第13讲的代码组织进行整理,修改并注释第6讲的g2o代码以便理解和回顾。

一、问题的提法

SLAM,即同步定位与建图,顾名思义,我们需要同时估计自身的位置(定位)和环境中物体(路标)的位置(建图)。例如,在自动驾驶汽车中,传感器(摄像头,毫米波雷达,激光雷达等)测量汽车前方的道路情况和障碍物情况,构建地图(考虑到路上的行人,车辆等时刻在变化,至少得是局部地图)以及汽车在该地图中的方位,用于规划汽车的局部行驶路径以躲避障碍物,这就可以运用到SLAM技术。另外,不考虑建图时,SLAM中的视觉里程计也可以帮助我们估计汽车的运动情况。

在SLAM中,我们用“位姿”来表示“位置”,因为“位姿”不仅包括物体的空间位置还包括其朝向,通常用矩阵来表示;用空间点的集合来表示地图(点云地图,在此基础上可构建其他符合需求的地图)。典型的SLAM问题可写成如下形式的优化问题:

                          

其中, 表示位姿; 表示空间点坐标; 表示在位姿  下对  的观测结果,在视觉SLAM中,通常是图像像素点位置;函数  表示观测方程;函数  表示核函数,用于避免由于错误匹配等导致的二范数增长过快,提高系统的鲁棒性。

针对上述优化问题,我们主要需要解决以下两个问题:

1、如何构建该模型?主要涉及  和  的匹配问题,即那个空间点被那个姿态的相机“看到”?

2、如何求解该模型?考虑到  通常是有约束矩阵,如何求梯度?

二、分析求解

1、问题1:如何构建模型

在原书第13讲,作者给出的做法是:

1) 初始化。检测第一帧图像的特征点并计算对应的空间点(称之为“路标”)。此时,检测到的“路标”便被第一帧对应的位姿  看到。

2) 匹配相邻两帧图像的特征点,并统计数量。如果匹配的数量足够多,则不增加新的特征点(也就不增加新的路标),因此  观测到的路标包含于  观测到的路标集合之中,具体哪个路标被  看到由匹配情况确定。

3) 如果匹配的数量太少,则重新计算特征点,增加相应的路标,即  观测到了新增加的所有路标。

2、问题2:如何优化

首先指出,对于一般优化问题,待优化变量为向量,此时我们可以通过矩阵求导法则得到雅可比矩阵(相当于导数),然后运用梯度下降法或者高斯牛顿法等优化方法,求得最优解。在优化过程,关键是找到待优化变量每次迭代时的增量。(原书第6讲)

针对问题2,我们有两种选择:1)用某种向量表示位姿,这样在优化过程中便不存在对矩阵求导的问题;2)利用某种方式求位姿(变换矩阵)的增量。

第一种方法中,我们可以采用 [旋转向量;平移向量] 或者 [单位四元数;平移向量] 的形式表示位姿。此时,观测方程  的形式比较复杂,也不容易写出其对待优化变量的导数(雅可比矩阵)。但是,幸运的是,求导可借助 Ceres, g2o 等优化库提供的自动求导工具,我们只需要计算  即可。原书作者在第9讲《后端1》中即采用了这种做法。

第二种方法中,我们引入李群李代数,先将矩阵(李群)映射到向量(李代数),消除旋转矩阵的自身的约束,进而实现求导,然后再更新位姿。

通常,我们有:

                                                    

则:

                              

(上述公式涉及齐次坐标和非齐次坐标的转换;采用非正式记号,因为并不是直接对矩阵求导)

                                              ,  是向量,此项符合求导规则

因此,问题的关键在与求:

                                                                   

引入李代数  ,我们有:

                                                              

则:

                                                     

这里,我们指出:

                                            

1)直接求导

我们将  在  处做一阶泰勒展开 :

                                         

其中  为  在  处的导数。根据原书6.2.2节,在一次迭代中求解:

                                                 

即可求得本次迭代的最优增量   。

的计算如下:

                                            

根据原书第四讲《李群与李代数》,可得:

可见,这里计算  的形式十分复杂。同时,我们强调,用这里的  求得的增量  可直接叠加用于计算李代数上的更新值 ,但:

                                                               

我们可以直接得到李群上的更新值:

                                                      

我们可以考虑始终用李代数表示位姿,这实际上就相当于第一种方法(但不等同,因为    才表示平移向量)。

2)扰动模型

我们令:

                                             

在已知本次迭代的初始值  时,上式可看作 的函数。因  是小量,我们将其在 0 处一阶泰勒展开:

                             

其中  为  在  处的导数。的计算如下(参考第4讲):

                                   

                                                

                                                

用这里的 求得的增量  不可直接叠加用于计算李代数上的更新值,即 ,但我们有李群上的增量:

                                                             

从而李群上的更新值:

                                                          

跟直接求导相比,扰动模型更简单,同时也方便使用矩阵来表示位姿。

根据上述过程,扰动模型相当于在李群上施加了一个扰动  并在李代数上求扰动结果关于  的在  处的导数。

三、设计框架

至此,我们知道如何建模:

                                 

以及可以求解该模型了。

具体实现上,参考作者在第13讲给出的设计框架和代码,整理SLAM模块组成如下图所示。实际工程肯定比这个复杂和细致的多,这里仅作理解用。整个模块可按照接口,功能,组件,支持分为四层,将每一层包含的元素分别定义成类,编写成头文件并加以引用;上层元素类的成员通常包括下层元素类,每个元素类的方法根据算法确定。最后,作者定义了一个Config类用于链接数据。  

该讲的实现效果如下所示:

                    

考虑到实际工程中,有不同的前端、后端及回环检测和校正策略,这里仅对图优化库 g2o 的代码进行注释,以便更好地掌握该通用工具。该代码取自第6讲,但是做了修改,采用二元边而不是一元边(应用时没有必要这么做)以便注释。代码如下:

#include <iostream>
#include <g2o/core/base_vertex.h>
#include <g2o/core/base_binary_edge.h>
#include <g2o/core/block_solver.h>
#include <g2o/core/optimization_algorithm_levenberg.h>
#include <g2o/solvers/dense/linear_solver_dense.h>
#include <Eigen/Core>
#include <opencv2/core/core.hpp>
#include <cmath>
#include <chrono>

using namespace std;

//****** 利用 g2o 拟合曲线方程:y = exp(ax^2+bx+c) + w, w为高斯噪声 **************

/* g2o 使用步骤:
 * 1. 自定义顶点和边:通过继承的方式;或使用库中预定义的类型
 * 2. 配置求解器
 * 3. 添加顶点和边
 * 4. 求解
*/

// 1. 通过继承 g2o::BaseVertex 和 g2o::BaseBinaryEdge 自定义边

/// 设计两个顶点类,其中 [a;b] 为一个二维向量顶点类,c 为另一个double顶点类                                        
class CurveFittingVertex1: public g2o::BaseVertex<2, Eigen::Vector2d> /// 二维向量顶点类
                                            /// < 顶点维度,数据类型 > 
{ 
public :
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW //结构体包含eigen成员必须进行宏定义 EIGEN_MAKE_ALIGNED_OPERATOR_NEW, 保证内存对齐
  
  virtual void setToOriginImpl() { _estimate << 0, 0; } ///重置

  virtual void oplusImpl(const double *update) { _estimate += Eigen::Vector2d(update); } /// 顶点更新函数,利用增量更新顶点
  
  virtual bool read(istream &in) {} /// 无读写操作,留空
  virtual bool write(ostream &out) const {}
};

class CurveFittingVertex2 : public g2o::BaseVertex<1,double> /// double顶点类
{
public:
  virtual void setToOriginImpl() { _estimate = 0.0; }
  virtual void oplusImpl(const double *update) { _estimate += update[0]; }
  virtual bool read(istream &in) {} /// 无读写操作,留空
  virtual bool write(ostream &out) const {}
};

/// 二元边,连接二维向量顶点和double顶点
class CurveFittingEdge: public g2o::BaseBinaryEdge<1, double, CurveFittingVertex1,CurveFittingVertex2>
                                             /// < 观测值维度,观测值类型,连接的顶点类1,连接的顶点类2 > 
{
public :
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
  CurveFittingEdge(double x) : BaseBinaryEdge(), _x(x) {} ///CurveFittingEdge类的构造函数
  
  void computeError() ///计算边所代表的误差
  {
   const CurveFittingVertex1 *v1 = static_cast<const CurveFittingVertex1 *>( _vertices[0] ); 
   /// static_cast 类型转换,把父类的_vertice 转换为子类的CurveFittingVertex
   const CurveFittingVertex2 *v2 = static_cast<const CurveFittingVertex2 *>( _vertices[1] );
   /// _vertices[] 为边的成员,存储顶点信息;对二元边,_vertices[] 的大小为2
   
   const Eigen::Vector2d ab = v1->estimate(); /// 获取v1顶点的当前值
   const double c = v2->estimate(); /// 获取v2顶点的当前值
   
  // _error(0,0) = _measurement - std::exp( ab(0,0)*_x*_x + ab(1,0)*_x + c ); /// 也可以采用下述方式赋值
   _error << _measurement - exp(ab[0]*_x*_x + ab[1]*_x + c); /// _error 的类型为 Eigen::Matrix<double, D, 1, Eigen::ColMajor>
                                                               /// _measurement 的类型为声明明边类型时的观测值类型,这里是 double
  }
  
  virtual void linearizeOplus() override ///计算雅可比矩阵J,用于计算海塞矩阵 H=J^T*J 和 误差 b=-J^T*_error
  {
    const CurveFittingVertex1 *v1 = static_cast<const CurveFittingVertex1 *>(_vertices[0]);
    const CurveFittingVertex2 *v2 = static_cast<const CurveFittingVertex2 *>(_vertices[1]);
    
    const Eigen::Vector2d ab = v1->estimate();
    const double c = v2->estimate();
    
    double y = exp(ab[0]*_x*_x + ab[1]*_x + c);
    _jacobianOplusXi[0] = -_x*_x*y; /// _error 对 ab 的导数,为 1x2 的矩阵
    _jacobianOplusXi[1] = - _x*y;
    _jacobianOplusXj[0] = -y; /// _error 对 c 的导数,为 1x1 的矩阵
    
    //** J = [_jacobianOplusXi, _jacobianOplusXj]    
  }
  
  virtual bool read(istream &in) {}
  virtual bool write(ostream &out) const {}
  
public :
  double _x; // x值,y值为 _ measurement
};

int main(int argc, char **argv)
{
  // 生成数据
  double a = 1.0, b = 2.0, c = 1.0; /// 真实参数值
  int N = 100;                      /// 数据点个数
  double w_sigma = 1.0;             /// 噪声Sigma值
  cv::RNG rng;                      /// OpenCV随机数产生器
 
  vector<double> x_data, y_data;    /// 数据序列
 
  cout << "generating data: " << endl;
  for (int i=0; i<N; i++)
  {
    double x = i/100.0; // i 是int型,要除以100.0 保证结果为double
    x_data.push_back(x);
    y_data.push_back( exp(a*x*x + b*x +c) + rng.gaussian(w_sigma) );
    cout << x_data[i] << "\t" << y_data[i] << endl;
  }
  
  // 2. 配置求解器
  /// ref: https://www.jianshu.com/p/36f2eac54d2c
  typedef g2o::BlockSolver<g2o::BlockSolverTraits<2,1>> BlockSolverType;
  /// 定义块求解器,将用于生成 H 和 b 的结构;       pose,landmark:都表示变量的维度
  /// 将生成 H 和 b 结构 :
  ///                       H = Hpp Hpl   b = bp
  ///                           Hlp Hll       bl
  BlockSolverType::LinearSolverType *linearSolverType = new g2o::LinearSolverDense<BlockSolverType::PoseMatrixType>(); 
  ///定义线性求解器; 这里用到了 Schur 消元,参见第9讲,线性求解器的结构与 Hpp 相同,即 ::PoseMatrixType
    
  BlockSolverType *solver_ptr = new BlockSolverType(linearSolverType); //生成块求解器对象
   
  g2o::OptimizationAlgorithmLevenberg *solver = new g2o::OptimizationAlgorithmLevenberg(solver_ptr); /// 配置算法
  g2o::SparseOptimizer optimizer; /// 声明图模型(优化器);
  optimizer.setAlgorithm(solver); /// 把(求解器+算法)导入模型
  optimizer.setVerbose(true); /// 输出到调试
  
  // 3. 添加顶点和边
  CurveFittingVertex1 *v1 = new CurveFittingVertex1();
  v1->setEstimate( Eigen::Vector2d(0,0) ); /// 顶点赋初值
  v1->setId(0);
  optimizer.addVertex(v1); /// 添加顶点,Id为0
   
  CurveFittingVertex2 *v2 = new CurveFittingVertex2();
  v2->setEstimate(0); /// 顶点赋初值
  v2->setId(1);
  v2->setMarginalized(true); /// ! ! ! ! 这里必须要设置边缘化,因为 v2 对应 landmark, 在配置求解器时被消元了
  optimizer.addVertex(v2); /// 添加顶点,Id为1
   
  for (int i=0; i<N; i++) /// 100组数据,100条边
  {
    CurveFittingEdge *edge = new CurveFittingEdge(x_data[i]); ///构造CurveFittingEdge类的对象edge
    edge->setId(i);
    edge->setVertex(0,v1); /// edge连接顶点 v1 和 v2
    edge->setVertex(1,v2);
    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();
  // 4. 求解
  optimizer.initializeOptimization();
  optimizer.optimize(100); /// 最大迭代次数100
  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::Vector2d ab_estimate = v1->estimate();
  double c_estimate = v2->estimate();
  cout << "True value of model: a = 1.0, b = 2.0, c = 1.0\n";
  cout << "Estimated model: \n" << "a_estimated = " << ab_estimate[0] 
                                << "\nb_estimated = " << ab_estimate[1]
                                << "\nc_estimated =" << c_estimate << endl;
   
  return 0;  
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

视觉SLAM十四讲(第2版)总结 的相关文章

  • Docker 应用实践-仓库篇

    目前 Docker 官方维护了一个公共仓库 Docker Hub xff0c 用于查找和与团队共享容器镜像 xff0c 界上最大的容器镜像存储库 xff0c 拥有一系列内容源 xff0c 包括容器社区开发人员 开放源代码项目和独立软件供应商
  • 浅谈网络中接口幂等性设计问题

    所谓幂等性设计 xff0c 就是说 xff0c 一次和多次请求某一个资源应该具有同样的副作用 用数学的语言来表达就是 xff1a f x 61 f f x 在数学里 xff0c 幂等有两种主要的定义 在某二元运算下 xff0c 幂等元素是指
  • 分布式系统中的补偿机制设计问题

    我们知道 xff0c 应用系统在分布式的情况下 xff0c 在通信时会有着一个显著的问题 xff0c 即一个业务流程往往需要组合一组服务 xff0c 且单单一次通信可能会经过 DNS 服务 xff0c 网卡 交换机 路由器 负载均衡等设备
  • 关于基于标准库函数与基于HAL库函数的stm32编程方式的差异

    在之前的博客中 xff0c 我已经使用过通过标准库函数和HAL库函数对stm32进行编译工作 xff0c 在这篇博文里 xff0c 我将对之前的进行总结 关于标准库函数 由于stm32系列有着很多不同的芯片 xff0c 其所使用的寄存器也大
  • curl wget pip git-clone yum apt-get的区别

    在linux中 xff0c 会常用到这些命令进行文件下载 xff0c 软件安装以及url访问 xff0c 但总是分不清楚什么时候用什么命令去下载或者安装和访问 这里将这几个命令的用法和区别进行一个说明 xff0c 方便大家学习和记忆 1 首
  • CSS 三种样式

    本节我们要学习一下 CSS 样式的几种形式 xff0c 在实际应用中向 HTML 中引入 CSS 样式的方法有三种 xff0c 分别是行内样式 内部样式 外部样式 我们会依次学习这三种方式的优缺点以及应用场景 xff0c 本节我们先来讲一下
  • JavaFx-报错WindowsNativeRunloopThread

    问题 解决办法 需要卸载掉JDK1 8 并且将环境变量中的 34 JAVA HOME 34 指向改成JDK11的目录 点赞 收藏 关注 便于以后复习和收到最新内容 有其他问题在评论区讨论 或者私信我 收到会在第一时间回复 在本博客学习的技术
  • Gradle-JDK版本问题导致运行失败

    问题 解决办法 因为当前我们使用jdk8去运行Gradle 但是Gradle意思是必须使用11 43 的jdk版本 下面这个问题就是因为 我们默认是 改为 点赞 收藏 关注 便于以后复习和收到最新内容 有其他问题在评论区讨论 或者私信我 收
  • 权力的游戏,我是小股东,咋办?

    案例简述 xff1a 某初创业公司 xff0c 有A B两个大股东 xff0c 股份份额一样大 xff0c 另外还有一个小股东C A股东负责市场和销售 xff0c B股东负责研发和技术 xff0c B曾经是C的上司 xff0c 将C带入公司
  • Java-高版本没有jre的问题

    解决方案 jre 文件夹是可以用命令自动生成的 xff0c 在window环境中 xff0c 进入jdk目录所在的文件夹 xff0c 运行下面命令就会自动 生成jre文件夹 bin span class token punctuation
  • Java-ForkJoinPool(线程池-工作窃取算法)

    文章目录 概述工作窃取算法工作窃取算法的优缺点使用 ForkJoinPool 进行分叉和合并ForkJoinPool使用RecursiveActionRecursiveTask Fork Join 案例Demo 概述 Fork 就是把一个大
  • JavaFx-缺少JavaFX运行时组件,需要这些组件才能运行此应用程序

    问题 报错 缺少JavaFX运行时组件 需要这些组件才能运行此应用程序 解决办法 解决办法额外添加一个类似启动类的java文件 然后将需要启动的文件以class添加到launch里就行 span class token keyword pu
  • Mysql-解决Truncated incorrect DOUBLE value xxx

    问题 出现这种问题一般来说就是多表操作的时候 使用的字段类型不一致导致的 查询除外 我们来看下真实案例 在hd user表中parentId是binint类型 而在hd user increment copy1 96 表中parentId是
  • Mysql-解决创建存储函数This function has none of DETERMINISTIC

    问题 当二进制日志启用后 xff0c 这个变量就会启用 它控制是否可以信任存储函数创建者 xff0c 不会创建写入二进制日志引起不安全事件的存储函数 如果设置为0 xff08 默认值 xff09 xff0c 用户不得创建或修改存储函数 xf
  • JPA-ids for this class must be manually assigned before calling save (使用数据库的自增)

    问题 Spring Data JPA ids for this class must be manually assigned before calling save id的生成错误 xff0c 在调用 save 方法之前 xff0c 必须
  • Java-gradle编译忽略警告

    使用gradle打包的时候出现好多警告 如何忽略大部分的警告呢 使用如下配置即可 tasks span class token punctuation span span class token function withType span
  • JPA-排除实体类里不存在于数据库的字段

    在实体类与数据库表建立映射关系时添加 64 Table 注解 当表中不存在实体类中的某个属性的时候 就需要用到 64 Transient 注解 如果不好使那么在 64 Transient基础上在添加 64 Column updatable
  • SpringBoot-快速搭建一套JPA

    文章目录 结构Mavenapplication yml实体类daoservicecontroller测试 结构 Maven span class token tag span class token tag span class token
  • IntelliJ IDEA-Gradle-SpringBoot搭建

    前提条件 IntelliJ IDEA Gradle教学 Gradle 全局镜像配置和优先使用Maven 将Gradle进行安装和配置 创建项目 配置项目设置 指定自己的gradle的安装位置 以及仓库位置 用户主目录 用户主目录 Gradl
  • 我的喜马拉雅FM开播啦!

随机推荐

  • SpringBoot-JAP-JpaSpecificationExecutor详解

    文章目录 SpringBoot JAP JpaSpecificationExecutor详解使用方法接口介绍自定义工厂 SpringBoot JAP JpaSpecificationExecutor详解 JpaSpecificationEx
  • SwitchHosts-快速切换Hosts

    SwitchHosts是一个管理 快速切换Hosts小工具 xff0c 开源软件 xff0c 一键切换Hosts配置 xff0c 非常实用 xff0c 高效 其主要功能特性包括 xff1a 下载地址 https github com old
  • Java-新年抽奖-消息自动化发送脚本

    我们公司7点半开年会 然后大约8点半开始抽奖抢 使用腾讯会议的方式进行发关键字消息然后截图方式抽奖 然而我还在地铁上 手速慢的我只抽到了3等奖小米耳机一个 然后我回家后迫不及待第一时间赶紧使用java写一个机器人脚本 疯狂发消息 一言难尽啊
  • Java多线程-CompletableFuture(链式)

    线程池这个大家都知道 xff0c 是为了提高效率 xff0c 可以类比生活 xff0c 如果开个店 xff0c 需要几个员工 xff0c 正常的操作都是雇佣员工 xff0c 而不是每天使用临时工 xff0c 这样用完就解雇掉 xff0c 对
  • Java-Javassist(字节码修改)

    文章目录 开篇Javassist 常用类Javassist 的使用依赖代码示例 如何实现类似 AOP 的功能 开篇 说起 AOP 小伙伴们肯定很熟悉 xff0c 无论是 JDK 动态代理或者是 CGLIB 等 xff0c 其底层都是通过操作
  • Java多线程-Pip管道

    管道的意思 就是向一个管子一样从一端到另一端 只支持单方向的数据传输 需要注意的不能在同一个线程使用管道否则会导致死锁的情况 发生和接收必须在不同线程 通过使用管道 xff0c 实现不同线程间的通信 xff0c 而无需借助于临时文件之类的东
  • 新版本代码自动生成(MybatisPlus-generator) 代码示例+问题解决

    虽然MybatisPlus官网上已经给出了新版本代码生成器的核心依赖和核心代码 xff0c 但对于没接触过的小伙伴还是比较困难上手 x1f62d xff0c 本文将展现如何使用MybatisPlus generator快速生成代码 目录 1
  • 虚拟机如何使用共享文件夹传文件

    项目场景 xff1a 在使用VMware平台 xff0c ubuntu操作系统时 xff0c ftp文件传输一直报错 问题描述 xff1a 尝试了多种 xff0c 更改电脑设置 xff0c 甚至重装虚拟机 xff0c 始终如下图报错 解决方
  • 强化学习入门DQN详解

    Deep Q Network 参考资料 xff1a B站莫烦 xff1a https www bilibili com video BV13W411Y75P spm id from 61 333 337 search card all cl
  • 某项目因为多次流标导致实际项目时间严重压缩,我该咋办?

    问题 xff1a 某政府项目 xff0c 三个月前就开始招标 xff0c 因各种原因 xff0c 流标三次 xff0c 导致时间拖太长 原计划一期工期三个月 43 xff0c 1月底上线 xff0c 但因为招投标影响直到一个月前签订了合同
  • ROS创建工作空间及功能包流程总结整理(python)

    ROS创建工作空间及功能包流程总结整理 xff08 python xff09 参考资料 xff1a B站赵虚左 xff1a https www bilibili com video BV1Ci4y1L7ZZ p 61 19 amp vd s
  • ROS自定义发布消息类型

    ROS自定义发布消息类型 xff1a 在 ROS 通信协议中 xff0c 数据载体是一个较为重要组成部分 xff0c 在上一案例中 xff0c ROS 中通过 std msgs 封装了一些原生的数据类型 比如 String Int32 In
  • ROS服务通信:自定义数据文件以及服务端和客户端代码编写流程及步骤详解

    ROS服务通信具体实现流程 demo xff1a 实现两个整型数相加求和 xff0c 客户端发送两个整型数 xff0c 服务端对其求和 服务通信也需要自定义服务数据类型 xff0c 即自定义srv文件 xff0c 该过程和自定义msg文件非
  • ROS TF静态坐标变换实现

    ROS TF静态坐标变换实现 法一 xff1a 编码实现 发布方代码实现 xff1a 创建功能包并添加依赖 catkin create pkg tf static roscpp rospy std msgs tf2 tf2 ros tf2
  • ROS:Gazebo导入自定义环境

    Gazebo导入自定义环境 之前的案例gazebo中导入的是一个空世界empty world xff0c 这里会介绍如何导入房屋数目等自定义的环境 xff08 1 xff09 启动 gazebo 打开构建面板 xff0c 绘制仿真环境 xf
  • ROS导航实现:SLAM建图(slam_gmapping)与保存(map_server)

    导航实现 xff1a SLAM建图 先安装相关的ROS功能包 安装 gmapping 包 用于构建地图 sudo apt install ros lt ROS版本 gt gmapping 安装地图服务包 用于保存与读取地图 sudo apt
  • ROS导航实现:amcl定位

    ROS导航实现 xff1a amcl定位 xff08 1 xff09 首先编写启动amcl的launch文件 xff0c 这里建议复制粘贴模板 xff0c 再修改相关的参数即可 xff0c 步骤如下 xff1a 主目录下进入amcl文件 r
  • ROS导航实现之路径规划

    导航实现之路径规划 move base 功能包提供了基于动作 action 的路径规划实现 xff0c move base 可以根据给定的目标点 xff0c 控制机器人底盘运动至目标位置 xff0c 并且在运动过程中会连续反馈机器人自身的姿
  • 创建个人网站(github pages)并将站点一键托管到Github

    创建个人网站 xff08 github pages xff09 并将站点一键托管到Github 内容 xff1a 使用网站生成器mkdocs将markdown文件生成wiki站点并挂载到github的流程总结 亮点 xff1a 个人网站一键
  • 视觉SLAM十四讲(第2版)总结

    最近看完了 视觉SLAM十四讲 xff08 第2版 xff09 xff1a 从理论到实践 xff08 高翔等著 xff09 xff0c 原书分两部分 xff0c 先介绍了数学基础 xff0c 然后介绍了具体的SLAM实践 xff0c 非常适