g2o 学习

2023-11-07

参考文章,如需详细请看原文

SLAM从0到1——6. 图优化g2o:从看懂代码到动手编写(长文) - yikang的文章 -  知乎

https://zhuanlan.zhihu.com/p/121628349

从零开始一起学习SLAM | 理解图优化,一步步带你看懂g2o代码

从零开始一起学习SLAM | 掌握g2o顶点编程套路

从零开始一起学习SLAM | 掌握g2o边的代码套路 - 计算机视觉life - 博客园

  

编写一个图优化的程序需要从底层到顶层逐渐搭建,参照g2o官方框架图(上方),

步骤可以分为6步

  1. 创建一个线性求解器LinearSolver。
  2. 创建BlockSolver,并用上面定义的线性求解器初始化。
  3. 创建总求解器solver,并从GN/LM/DogLeg 中选一个作为迭代策略,再用上述块求解器BlockSolver初始化。
  4. 创建图优化的核心:稀疏优化器(SparseOptimizer)。
  5. 定义图的顶点和边,并添加到SparseOptimizer中。
  6. 设置优化参数,开始执行优化。

下面详细展开

(1)创建一个线性求解器LinearSolver

这一步中我们可以选择不同的求解方式来求解线性方程,g2o中提供的求解方式主要有:

  • LinearSolverCholmod :使用sparse cholesky分解法,继承自LinearSolverCCS。
  • LinearSolverCSparse:使用CSparse法,继承自LinearSolverCCS。
  • LinearSolverPCG :使用preconditioned conjugate gradient 法,继承自LinearSolver。
  • LinearSolverDense :使用dense cholesky分解法,继承自LinearSolver。
  • LinearSolverEigen: 依赖项只有eigen,使用eigen中sparse Cholesky 求解,因此编译好后可以方便的在其他地方使用,性能和CSparse差不多,继承自LinearSolver。

(2)创建BlockSolver,并用定义的线性求解器初始化

BlockSolver有两种定义方式:

// 固定变量的solver。 p代表pose的维度(是流形manifold下的最小表示),l表示landmark的维度
using BlockSolverPL = BlockSolver< BlockSolverTraits<p, l> >;

// 可变尺寸的solver。Pose和Landmark在程序开始时并不能确定,所有参数在中间过程中被确定
using BlockSolverX = BlockSolverPL<Eigen::Dynamic, Eigen::Dynamic>;

此外g2o还预定义了以下几种常用类型:

  • BlockSolver_6_3 :表示pose 是6维,观测点是3维,用于3D SLAM中的BA。
  • BlockSolver_7_3:在BlockSolver_6_3 的基础上多了一个scale。
  • BlockSolver_3_2:表示pose 是3维,观测点是2维。

(3)创建总求解器solver

注意看程序中只使用了一行代码进行创建:右侧是初始化;左侧含有我们选择的迭代策略,在这一部分,我们有三迭代策略可以选择:

  • g2o::OptimizationAlgorithmGaussNewton
  • g2o::OptimizationAlgorithmLevenberg
  • g2o::OptimizationAlgorithmDogleg

(4)创建图优化的核心:稀疏优化器

根据程序中的代码示例,创建稀疏优化器:

g2o::SparseOptimizer  optimizer;

设置求解方法:

SparseOptimizer::setAlgorithm(OptimizationAlgorithm* algorithm)

设置优化过程输出信息:

SparseOptimizer::setVerbose(bool verbose)

(5)定义图的顶点和边,并添加到SparseOptimizer中

【g2o ch6】 https://www.bilibili.com/video/BV1FY411E7Dp?share_source=copy_web&vd_source=562cd1ac901311fec010f8ca07d6387b    这个up 有几个视频是讲 顶点和边的

(6)设置优化参数,开始执行优化

设置SparseOptimizer的初始化、迭代次数、保存结果等。

初始化:

SparseOptimizer::initializeOptimization(HyperGraph::EdgeSet& eset)

设置迭代次数:

SparseOptimizer::optimize(int iterations,bool online)

 (5) 展开说说

1. 点 Vertex

在g2o中定义Vertex有一个通用的类模板:BaseVertex。在结构框图中可以看到它的位置就是HyperGraph继承的根源。

同时在图中我们注意到BaseVertex具有两个参数D/T,这两个参数非常重要,我们来看一下:

  • D 是int 类型,表示vertex的最小维度,例如3D空间中旋转是3维的,则 D = 3
  • T 是待估计vertex的数据类型,例如用四元数表达三维旋转,则 T 就是Quaternion 类型
static const int Dimension = D; ///< dimension of the estimate (minimal) in the manifold space

typedef T EstimateType;
EstimateType _estimate;

特别注意的是这个D不是顶点(状态变量)的维度,而是其在流形空间(manifold)的最小表示。

>>>如何自己定义Vertex

在我们动手定义自己的Vertex之前,可以先看下g2o本身已经定义了一些常用的顶点类型:

VertexSE2 : public BaseVertex<3, SE2>  
//2D pose Vertex, (x,y,theta)

VertexSE3 : public BaseVertex<6, Isometry3> //Isometry3使欧式变换矩阵T,实质是4*4矩阵
//6d vector (x,y,z,qx,qy,qz) (note that we leave out the w part of the quaternion)

VertexPointXY : public BaseVertex<2, Vector2>
VertexPointXYZ : public BaseVertex<3, Vector3>
VertexSBAPointXYZ : public BaseVertex<3, Vector3>

// SE3 Vertex parameterized internally with a transformation matrix 
                //  and externally with its exponential map
VertexSE3Expmap : public BaseVertex<6, SE3Quat>

// SBACam Vertex, (x,y,z,qw,qx,qy,qz),(x,y,z,qx,qy,qz) 
           //    (note that we leave out the w part of the quaternion.
// qw is assumed to be positive,
        // otherwise there is an ambiguity in qx,qy,qz as a rotation
VertexCam : public BaseVertex<6, SBACam>

// Sim3 Vertex, (x,y,z,qw,qx,qy,qz),7d vector,(x,y,z,qx,qy,qz) 
            // (note that we leave out the w part of the quaternion.
VertexSim3Expmap : public BaseVertex<7, Sim3>

但是!如果在使用中发现没有我们可以直接使用的Vertex,那就需要自己来定义了。一般来说定义Vertex需要重写这几个函数(注意注释):

virtual bool read(std::istream& is);
virtual bool write(std::ostream& os) const;
// 分别是读盘、存盘函数,一般情况下不需要进行读/写操作的话,仅仅声明一下就可以

virtual void oplusImpl(const number_t* update);
//顶点更新函数

virtual void setToOriginImpl();
//顶点重置函数,设定被优化变量的原始值。

请注意里面的oplusImpl函数,是非常重要的函数,主要用于优化过程中增量△x 的计算。根据增量方程计算出增量后,通过这个函数对估计值进行调整,因此该函数的内容要重视。

根据上面四个函数可以得到定义顶点的基本格式:

class myVertex: public g2o::BaseVertex<Dim, Type>
  {
      public:
      EIGEN_MAKE_ALIGNED_OPERATOR_NEW

      myVertex(){}

      virtual void read(std::istream& is) {}
      virtual void write(std::ostream& os) const {}

      virtual void setOriginImpl()
      {
          _estimate = Type();
      }
      virtual void oplusImpl(const double* update) override
      {
          _estimate += update;
      }
  }

如果还不太明白,那么继续看下面的实例:

class CurveFittingVertex: public g2o::BaseVertex<3, Eigen::Vector3d>
{
    public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW  // 字节对齐

    virtual void setToOriginImpl() // 重置,设定被优化变量的原始值
    {
        _estimate << 0,0,0;
    }

    virtual void oplusImpl( const double* update ) // 更新
    {
        _estimate += Eigen::Vector3d(update);   //update强制类型转换为Vector3d
    }
    // 存盘和读盘:留空
    virtual bool read( istream& in ) {}
    virtual bool write( ostream& out ) const {}
};

另外值得注意的是,优化变量更新并不是所有时候都可以像上面两个一样直接 += 就可以,这要看优化变量使用的类型(是否对加法封闭)。

>>> 向图中添加顶点

接着上面定义完的顶点,我们把它添加到图中:

CurveFittingVertex* v = new CurveFittingVertex();
v->setEstimate( Eigen::Vector3d(0,0,0) );  // 设定初始值
v->setId(0);                               // 定义节点编号
optimizer.addVertex( v );                  // 把节点添加到图中

三个步骤对应三行代码,注释已经解释了作用。

2.边 Edge

图优化中的边:BaseUnaryEdge,BaseBinaryEdge,BaseMultiEdge 分别表示一元边,两元边,多元边。

顾名思义,一元边可以理解为一条边只连接一个顶点,两元边理解为一条边连接两个顶点(常见),多元边理解为一条边可以连接多个(3个以上)顶点。

以最常见的二元边为例分析一下他们的参数:D, E, VertexXi, VertexXj:

  • D 是 int 型,表示测量值的维度 (dimension)
  • E 表示测量值的数据类型
  • VertexXi,VertexXj 分别表示不同顶点的类型
BaseBinaryEdge<2, Vector2D, VertexSBAPointXYZ, VertexSE3Expmap>

上面这行代码表示二元边,参数1是说测量值是2维的;参数2对应测量值的类型是Vector2D,参数3和4表示两个顶点也就是优化变量分别是三维点 VertexSBAPointXYZ,和李群位姿VertexSE3Expmap。

>>> 如何定义一个边

除了上面那行定义语句,还要复写一些重要的成员函数:

virtual bool read(std::istream& is);
virtual bool write(std::ostream& os) const;
// 分别是读盘、存盘函数,一般情况下不需要进行读/写操作的话,仅仅声明一下就可以

virtual void computeError();
// 非常重要,是使用当前顶点值计算的测量值与真实测量值之间的误差

virtual void linearizeOplus();
// 非常重要,是在当前顶点的值下,该误差对优化变量的偏导数,也就是Jacobian矩阵

除了上面四个函数,还有几个重要的成员变量以及函数:

_measurement; // 存储观测值
_error;  // 存储computeError() 函数计算的误差
_vertices[]; // 存储顶点信息,比如二元边,_vertices[]大小为2
//存储顺序和调用setVertex(int, vertex) 和设定的int有关(0或1)

setId(int);  // 定义边的编号(决定了在H矩阵中的位置)
setMeasurement(type);  // 定义观测值
setVertex(int, vertex);  // 定义顶点
setInformation();  // 定义协方差矩阵的逆

有了上面那些重要的成员变量和成员函数,就可以用来定义一条边了:

class myEdge: public g2o::BaseBinaryEdge<errorDim, errorType, Vertex1Type, Vertex2Type>
  {
      public:
      EIGEN_MAKE_ALIGNED_OPERATOR_NEW      

      myEdge(){}     
      virtual bool read(istream& in) {}
      virtual bool write(ostream& out) const {}      
      virtual void computeError() override
      {
          // ...
          _error = _measurement - Something;
      }    
      // 求误差对优化变量的偏导数,雅克比矩阵
      virtual void linearizeOplus() override  
      {
          _jacobianOplusXi(pos, pos) = something;
          // ...         
          /*
          _jocobianOplusXj(pos, pos) = something;
          ...
          */
      }      
      private:
      data
  }

让我们继续看curveftting这个实例,这里定义的边是简单的一元边:

// (误差)边的模型    模板参数:观测值维度,类型,连接顶点类型
class CurveFittingEdge: public g2o::BaseUnaryEdge<1,double,CurveFittingVertex>
{
public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW
    CurveFittingEdge( double x ): BaseUnaryEdge(), _x(x) {}
    // 计算曲线模型误差
    void computeError()
    {
        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 bool read( istream& in ) {}
    virtual bool write( ostream& out ) const {}
public:
    double _x;  // x 值, y 值为 _measurement
};

上面的例子都比较简单,下面这个是3D-2D点的PnP 问题,也就是最小化重投影误差问题,这个问题非常常见,使用最常见的二元边,弄懂了这个基本跟边相关的代码就能懂了:

//继承自BaseBinaryEdge类,观测值2维,类型Vector2D,顶点分别是三维点、李群位姿
class G2O_TYPES_SBA_API EdgeProjectXYZ2UV : public  
               BaseBinaryEdge<2, Vector2D, VertexSBAPointXYZ, VertexSE3Expmap>{
  public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW;

    //1. 默认初始化
    EdgeProjectXYZ2UV();

    //2. 计算误差
    void computeError()  {
      //李群相机位姿v1
      const VertexSE3Expmap* v1 = static_cast<const VertexSE3Expmap*>(_vertices[1]);
      // 顶点v2
      const VertexSBAPointXYZ* v2 = static_cast<const VertexSBAPointXYZ*>(_vertices[0]);
      //相机参数
      const CameraParameters * cam
        = static_cast<const CameraParameters *>(parameter(0));
     //误差计算,测量值减去估计值,也就是重投影误差obs-cam
     //估计值计算方法是T*p,得到相机坐标系下坐标,然后在利用camera2pixel()函数得到像素坐标。
      Vector2D obs(_measurement);
      _error = obs - cam->cam_map(v1->estimate().map(v2->estimate()));
    }

    //3. 线性增量函数,也就是雅克比矩阵J的计算方法
    virtual void linearizeOplus();

    //4. 相机参数
    CameraParameters * _cam; 
    bool read(std::istream& is);
    bool write(std::ostream& os) const;
};

这个程序中比较难以理解的地方是:

_error = obs - cam->cam_map(v1->estimate().map(v2->estimate()));//误差=观测-投影
  • cam_map 函数功能是把相机坐标系下三维点(输入)用内参转换为图像坐标(输出)。
  • map函数是把世界坐标系下三维点变换到相机坐标系。
  • v1->estimate().map(v2->estimate())意思是用V1估计的pose把V2代表的三维点,变换到相机坐标系下。

>>>向图中添加边

和添加点有一点类似,下面是添加一元边:

// 往图中增加边
    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 );
    }

但在SLAM中我们经常要使用的二元边(前后两个位姿),那么此时:

index = 1;
for ( const Point2f p:points_2d )
{
    g2o::EdgeProjectXYZ2UV* edge = new g2o::EdgeProjectXYZ2UV();
    edge->setId ( index );  // 边的b编号
    edge->setVertex ( 0, dynamic_cast<g2o::VertexSBAPointXYZ*> ( optimizer.vertex ( index ) ) );
    edge->setVertex ( 1, pose );
    edge->setMeasurement ( Eigen::Vector2d ( p.x, p.y ) );  // 设置观测的特征点图像坐标
    edge->setParameterId ( 0,0 );
    edge->setInformation ( Eigen::Matrix2d::Identity() );
    optimizer.addEdge ( edge );
    index++;
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

g2o 学习 的相关文章

  • 当其源是 https uri 时如何使 wpf MediaElement 播放

    在 wpf 独立应用程序 exe 中 我在主窗口中包含了 MediaElement
  • C# 和月历,选择多个日期

    我正在制作一个程序 可以帮助人们用 C 为某个部门 预订 订单 他们需要能够选择不同月份的多个日期 我更愿意拥有它 这样他们就可以单击一个日期 然后按住 Shift 键单击另一个日期以选择这两个日期之间的所有日期 并控制单击以进行单选 取消
  • 使用 Xamarin.Forms 和 Zxing 生成 QR 码

    我在网上看到了很多关于这个的内容 旧帖子 但似乎没有什么对我有用 我正在尝试从字符串中生成二维码并将其显示在应用程序中 这就是我一开始的情况 qrCode new ZXingBarcodeImageView BarcodeFormat Ba
  • OpenGL缓冲区更新[重复]

    这个问题在这里已经有答案了 目前我正在编写一个模拟水的程序 以下是我所做的步骤 创建水面 平面 创建VAO 创建顶点缓冲区对象 在其中存储法线和顶点 将指针绑定到此 VBO 创建索引缓冲区对象 然后我使用 glDrawElements 渲染
  • 为什么在 C++ 中声明枚举时使用 typedef?

    我已经很多年没有写过任何 C 了 现在我正试图重新开始 然后我遇到了这个并考虑放弃 typedef enum TokenType blah1 0x00000000 blah2 0X01000000 blah3 0X02000000 Toke
  • C# Outlook 从收件人获取 CompanyName 属性

    我目前正在使用 C 编写 Outlook 2010 AddIn 我想要的是从我从 AppointmentItem 中提取的 Recipient 对象中获取 CompanyName 属性 因此 有了 AppointmentItem 的收件人
  • PrivateObject 找不到属性

    我的结构基本上如下所示 abstract class A protected string Identificator get set private void DoSomething DoSomethingSpecific protect
  • 检测 TextBox 中的 Tab 键按下

    I am trying to detect the Tab key press in a TextBox I know that the Tab key does not trigger the KeyDown KeyUp or the K
  • 为什么 std::function 不是有效的模板参数,而函数指针却是?

    我已经定义了名为的类模板CallBackAtInit其唯一目的是在初始化时调用函数 构造函数 该函数在模板参数中指定 问题是模板不接受std function作为参数 但它们接受函数指针 为什么 这是我的代码 include
  • 编写具有多种类型的泛型扩展方法时的类型推断问题

    我正在为 IEnumerable 编写一个通用扩展方法 用于将对象列表映射到另一个映射对象列表 这就是我希望该方法的工作方式 IList
  • 将 2 个字节转换为整数

    我收到一个 2 个字节的端口号 最低有效字节在前 我想将其转换为整数 以便我可以使用它 我做了这个 char buf 2 Where the received bytes are char port 2 port 0 buf 1 port
  • 在 mvc4 中创建通用 mvc 视图

    我以前也提过类似的问题 没有得到答案 如何创建一个通用的 mvc4 视图 该视图可以显示传递给它的模型列表或单个模型 模型可以是个人 组织或团体 无论传递给它的是什么 如果您正在寻找类似的东西 model MyViewModel
  • 在 Qt 中播放通知(频率 x)声音 - 最简单的方法?

    Qt 5 1 或更高版本 我需要播放频率为 x 的通知声音 n 毫秒 如果我能像这样组合音调那就太好了 1000Hz 持续 2 秒 然后 3000Hz 持续 1 秒 最简单的方法是使用文件 WAV MP3 例如如此处所述 如何用Qt播放声音
  • 如何测试某些代码在 C++ 中无法编译? [复制]

    这个问题在这里已经有答案了 可能的重复 单元测试编译时错误 https stackoverflow com questions 605915 unit test compile time error 我想知道是否可以编写一种单元测试来验证给
  • 在二进制数据文件的标头中放入什么

    我有一个模拟 可以读取我们创建的大型二进制数据文件 10 到 100 GB 出于速度原因 我们使用二进制 这些文件依赖于系统 是从我们运行的每个系统上的文本文件转换而来的 所以我不关心可移植性 当前的文件是 POD 结构的许多实例 使用 f
  • 值和类型的简洁双向静态 1:1 映射

    我将从我想象如何使用我想要创建的代码开始 它不必完全像这样 但它是我在标题中所说的 简洁 的一个很好的例子 就我而言 它是将类型映射到相关的枚举值 struct bar foo
  • 使用 IdentityDbContext 和 Code First 自动迁移表位置和架构的实体框架?

    我正在尝试使用 IdentityDbContext 类设置自动迁移更新 并将更改传播到整个数据库的实际 DbContext 在进入代码之前 在使用自动迁移实现 IdentityDbContext 时 我收到此错误 影响迁移历史系统表位置的自
  • IDisposable 的显式实现

    虽然有很多关于IDisposable在 SO 上找到 我还没有找到答案 我通常遵循这样的做法 当我的一个班级拥有一个IDisposable对象然后它也实现IDisposable并打电话Dispose在拥有的对象上 然而最近我遇到了一个类 它
  • 如何在c中断言两个类型相等?

    在 C 中如何断言两种类型相等 在 C 中 我会使用 std is same 但搜索 StackOverflow 和其他地方似乎只能给出 C 和 C 的结果 在C中没有办法做到这一点吗 请注意 这不是询问变量是否具有某种类型 而是询问两个类
  • 是否可以使用 Dapper 流式传输大型 SQL Server 数据库结果集?

    我需要从数据库返回大约 500K 行 请不要问为什么 然后 我需要将这些结果保存为 XML 更紧急 并将该文件通过 ftp 传输到某个神奇的地方 我还需要转换结果集中的每一行 现在 这就是我正在做的事情 TOP 100结果 使用 Dappe

随机推荐

  • java数组(二)

    数组 Arrays 数组是一种数据结构 它是一组相同类型的元素的集合 该集合中的每个元素都通过一个索引访问 在Java中 数组是一个对象 它可以存储在堆上或栈上 具体取决于它如何创建 数组可以是单维数组或多维数组 数组是一个固定长度的数据结
  • C++(基础入门笔记)

    1搭建环境 include
  • TCP/IP/UDP/RTP/RTCP协议

    IP 指定目的地址和源地址 Version 版本号 四个字节 IHL IP Header Length IP头长度 Total Length 总的长度 总的长度减去IP头长度 数据长度 Identification Flags Fragme
  • linux OSI七层模型、TCP/IP协议栈及每层结构大揭秘

    学习Linux 就算是像小编我这样的小萌新 也知道OSI模型 什么 你不知道 好吧 这篇秘籍拿走 不谢 一 两个协议 1 OSI 协议模型 7层 国际协议 PDU 协议数据单元对等层次之间传递的数据单位 OSI协议 7层 国际协议 PDU
  • 伺服控制-canopen

    通迅中断参数设 1 中断 停止 伺服驱动 电机停转 开启抱死动作 1 配置 opration 2 设置 3速度模式 3 设置 伺服松轴 4 设置 伺服使能 运行 1 使能 true 2 启用 true 停止 1 使能 false 2 松轴
  • Linux下安装Nginx服务并设置开机自启动

    目录 一 安装Nginx 二 开机自启动服务配置 一 安装Nginx 系统平台 CentOS Linux release 7 9 2009 Core 64位 Nginx安装版本 liunx 64位 nginx 1 20 2 tar gz 安
  • VSCode进行Lua调试

    1 安装EmmyLua 2 添加配置 3 开始调试 要选择要调试的进程就可以了
  • Docker中快速搭建SFTP

    1 拉docker镜像 docker pull atmoz sftp 2 初始化 创建容器 docker run name internalsftp p 22 22 d atmoz sftp anonymous hello123 opt U
  • 震惊~Signal <XXX> missing in the sensitivity list is added for synthesis purposes在verilog中警告的解决方法

    你是否还在为秃头而烦恼 你是否还在通宵为了赶项目而焦急 朋友 你不是 一个人 看看吧 下面是这个bug的描述 WARNING HDLCompiler 91 F FPGA Doc AD test uart v Line 56 Signal
  • java通过JDBC连接mysql8.0数据库,并对数据库中的表进行增删改查操作

    目录 一 JDBC简介 二 添加依赖 三 JDBC操作数据库的步骤 四 JDBC操作数据库 增删改查 一 新增数据 二 删除数据 三 修改数据 四 查询数据 五 多表连接查询 一 JDBC简介 Java数据库连接 Java Database
  • vue-cli配置sass,使用全局sass变量

    vue cli创建vue2 0项目 安装sass步骤 创建vue2 0项目 vue init webpack 项目名 安装sass步骤 npm install node sass 4 14 1 npm install sass loader
  • JMP、Hook

    include stdio h include tchar h include windows h offset 目标地址 jmp指令起始地址 5 跳转指令解码 0xe9 offset offset 有符号整型 四字节 它等于jmp指令的下
  • 微信小程序——仿盒马鲜生APP

    前段时间 随着马化腾现身全国多地用微信小程序乘坐公交的新闻出现 微信小程序的热度可谓是更上了一层 微信小程序现身至今 因其不用下载就可使用的方便等优点 发展趋势一直良好 盒马鲜生的问世也是充满了热度 实现了快速配送 可谓是阿里巴巴对线下超市
  • 科幻风的卡片视频播放

    上一篇博文展示了卡片中的VR展示 那篇主要是卡片的3D转动来展示未显示的部分图片 这篇 我们来点科幻的 我们在卡片中播放视频的同时来拖动卡片或转动它 像下面那样 这个主要依赖了两个库 具体代码如下
  • 浅谈js宏观任务、微观任务

    js运行机制分为同步异步 异步又分为宏观事件和微观事件 同步异步 js是一门单线程语言 因此js在同一个时间里只能做一件事 单线程意味着 如果在同个时间有多个任务的话 这些任务就需要排队 前一个执行完成才能执行下一个任务 同步任务 同步任务
  • Echarts:象形柱图实现水塔水位的动画、水球图和液位柱子图

    一 象形柱图 1 vue中使用象形柱图 效果图 2 代码实现
  • axios调接口 与缓存

    axios getarr1 let url https www fastmock site mock f5427ab2a4d20236b4aed7a0579d2213 api shop list 调接口 后端给的地址 this axios
  • 【React】手把手学习React - 元素渲染

    前言 大家好 分享了这么多文章发现无论是项目实战还是源码分析或者是基本使用 绝大多数文章都是关于Vue的 还没有关于React的分享 接下来从今天开始我们将一起学习一下React React与vue一样也是一个非常火爆的前端库 关于Reac
  • 设计模式--策略模式

    文章目录 策略 Strategy 模式 策略模式的收银软件 策略模式的特点 使用场景 优缺点 策略模式和工厂模式的结合 策略 Strategy 模式 本质 分离算法 选择实现 策略模式 针对一组算法 将每一个算法封装到具有共同接口的独立的类
  • g2o 学习

    参考文章 如需详细请看原文 SLAM从0到1 6 图优化g2o 从看懂代码到动手编写 长文 yikang的文章 知乎 https zhuanlan zhihu com p 121628349 从零开始一起学习SLAM 理解图优化 一步步带你