通过g2o实现逆深度表示的重投影误差优化

本文详细介绍了如何使用G2O库在视觉SLAM中处理重投影误差,通过设置环境、定义辅助函数、创建特定的顶点和边类,以及实现主要函数,展示了如何构建和优化一个包含逆深度和重投影误差边的优化图,以估计相机姿态和三维点的位置。

使用g2o库进行重投影误差约束:逐步指南

第一步:设置环境:
首先包含必要的头文件,例如Eigen库、标准输入输出操作和g2o库组件。还要确保您有所需的数据文件(3d2d.txt、3d.txt、pose.txt)用于此项目。

第二步:辅助函数:
定义辅助函数如readfile,用于从文件读取数据到向量,并normalize将像素坐标转换为相机归一化坐标。

第三步:顶点和边类:
引入VertexInverseDepth和EdgeReprojection类来表示优化图中的顶点和边。

第四步:主函数:
在主函数中,使用适当的求解器和参数设置g2o优化器。将姿势顶点添加到优化器中,并固定第一个姿势。对每个三维点进行迭代,为第一次观测添加lambda顶点,并为后续观测添加重投影边。最后,使用g2o初始化和优化图。

使用的 g2o API:
SparseOptimizer: 这是一个稀疏优化器,用于管理优化问题的图结构。

BlockSolver: 该类表示用于解决优化问题的块求解器。在这里,我们使用了 BlockSolverType,其特征为动态大小的块。

LinearSolverCholmod: 这是一个基于 CHOLMOD 的线性求解器,用于求解线性方程组。它在块求解器中被使用。

OptimizationAlgorithmLevenberg: 这是优化算法之一,实现了 Levenberg-Marquardt 算法,用于在优化过程中调整步长。

BaseVertex: 顶点基类,表示优化问题中的状态变量。在代码中,我们使用了 VertexInverseDepth 类来表示逆深度参数的顶点。

BaseMultiEdge: 边基类,表示优化问题中的残差。在代码中,我们使用了 EdgeReprojection 类来表示重投影误差的边。

优化的残差:
在视觉 SLAM 中,通常优化的残差是指观测到的二维特征点与通过当前估计的相机姿态和三维点位置进行重投影得到的预测值之间的差异。这些残差通常以像素为单位,可以使用各种方式来定义,例如像素坐标之间的欧几里得距离。

在提供的代码中,EdgeReprojection 类计算了重投影误差。该类的 computeError() 方法计算了观测值与预测值之间的差异,并将其作为优化过程的残差。

状态变量:
在视觉 SLAM 中,状态变量通常包括相机的姿态和三维点的位置。在这个问题中,状态变量是优化问题的未知数,我们希望通过优化算法找到它们的最佳估计值。

在提供的代码中,相机的姿态由 VertexSE3Expmap 表示,而三维点的位置由 VertexInverseDepth 表示。这些顶点是优化问题的状态变量,它们的估计值会在优化过程中被调整以最小化残差。

自定义类:
EdgeReprojection 类:
作用:

EdgeReprojection 类用于表示优化问题中的重投影误差边。在视觉SLAM中,重投影误差是指观测到的二维特征点与通过当前估计的相机姿态和三维点位置进行重投影得到的预测值之间的差异。这些误差是优化问题的残差,通过最小化这些残差来优化相机姿态和三维点的位置。
成员函数:

computeError(): 这个函数计算了观测值与预测值之间的差异,并将其作为优化过程的残差。在这个函数中,使用了三维点的逆深度参数来计算重投影点,然后计算观测值与重投影点之间的差异。
VertexInverseDepth 类:
作用:

VertexInverseDepth 类用于表示优化问题中的逆深度顶点。在视觉SLAM中,逆深度参数是一种表示三维点位置的方式,它与相机之间的距离成反比。通过优化逆深度参数,可以估计三维点的位置,并在优化过程中调整这些参数以最小化重投影误差。
成员函数:

oplusImpl(const double *update): 这个函数实现了顶点更新的操作。在优化过程中,通过添加一个增量(通常称为步长)来更新顶点的估计值。在 VertexInverseDepth 类中,增量表示为逆深度参数的变化量,因此通过将增量添加到当前估计值来更新逆深度参数。
这些类在整个优化过程中起着关键的作用,通过定义优化问题的结构和约束,帮助优化算法找到相机姿态和三维点的最优估计值。

#include <Eigen/StdVector>
#include <iostream>
#include <stdint.h>
#include <unordered_set>
#include "g2o/core/sparse_optimizer.h"
#include "g2o/core/block_solver.h"
#include "g2o/core/solver.h"
#include "g2o/core/robust_kernel_impl.h"
#include "g2o/core/optimization_algorithm_levenberg.h"
#include "g2o/solvers/cholmod/linear_solver_cholmod.h"
#include "g2o/solvers/dense/linear_solver_dense.h"
#include "g2o/types/sba/types_six_dof_expmap.h"
// #include "g2o/math_groups/se3quat.h"
#include "g2o/solvers/structure_only/structure_only_solver.h"
#include <vector>

// read file
#include <fstream>
#include <string>

// 读取3d点
void readfile(const std::string &filename, std::vector<Eigen::Vector3d> &points)
{
    std::ifstream file(filename);
    if (!file.is_open())
    {
        std::cerr << "could not open file: " << filename << std::endl;
        return;
    }
    // read line
    std::string line;
    while (std::getline(file, line))
    {
        std::istringstream iss(line);
        double x, y, z;
        if (!(iss >> x >> y >> z))
        {
            std::cerr << "error reading file: " << filename << std::endl;
            break;
        }
        points.push_back(Eigen::Vector3d(x, y, z) * 0.01f);
    }
}
// 读取位姿
void readfile(const std::string &filename, std::vector<Eigen::Matrix4d> &points)
{
    std::ifstream file(filename);
    if (!file.is_open())
    {
        std::cerr << "could not open file: " << filename << std::endl;
        return;
    }
    // read line
    std::string line;
    double data[12] = {0};
    while (std::getline(file, line))
    {
        std::istringstream iss(line);
        for (int i = 0; i < 12; i++)
        {
            if (!(iss >> data[i]))
            {
                std::cerr << "error reading file: " << filename << std::endl;
                break;
            }
        }
        Eigen::Matrix3d R = Eigen::Map<Eigen::Matrix<double, 3, 3, Eigen::RowMajor>>(data);
        Eigen::Vector3d t = Eigen::Vector3d(data[9], data[10], data[11]) * 0.01f;
        // Rwc -> Rcw
        R.transposeInPlace();
        // twc -> tcw
        t = -R * t;
        Eigen::Matrix4d pose = Eigen::Matrix4d::Zero();
        pose.block<3, 3>(0, 0) = R;
        pose.block<3, 1>(0, 3) = t;
        pose(3, 3) = 1;
        points.push_back(pose);
    }
}

// 读取观测
void readfile(const std::string &filename, std::vector<std::vector<Eigen::Vector2d>> &points2d, std::vector<std::vector<int>> &ids)
{
    std::ifstream file(filename);
    if (!file.is_open())
    {
        std::cerr << "could not open file: " << filename << std::endl;
        return;
    }
    // read line
    std::string line;
    // u1 v1 id1 u2 v2 id2 ...
    while (std::getline(file, line))
    {
        std::istringstream iss(line);
        std::vector<Eigen::Vector2d> obs;
        std::vector<int> ob_ids;
        double u, v;
        int i;
        while (iss >> u >> v >> i)
        {
            obs.emplace_back(u, v);
            ob_ids.emplace_back(i);
        }
        points2d.push_back(std::move(obs));
        ids.push_back(std::move(ob_ids));
    }
}

// 定义逆深度顶点
class VertexInverseDepth : public g2o::BaseVertex<1, double>
{
public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW
    VertexInverseDepth() {}
    bool read(std::istream & /*is*/) override
    {
        std::cerr << __PRETTY_FUNCTION__ << " not implemented yet" << std::endl;
        return false;
    }

    bool write(std::ostream & /*os*/) const override
    {
        std::cerr << __PRETTY_FUNCTION__ << " not implemented yet" << std::endl;
        return false;
    }
    virtual void setToOriginImpl() {}
    virtual void oplusImpl(const double *update)
    {
        _estimate += update[0];
    }
};
typedef g2o::VertexSE3Expmap iVertexSE3Expmap;
typedef g2o::VertexSE3Expmap jVertexSE3Expmap;
// 定义重投影误差边
/*
观测:(ui,vi),(uj,vj)
顶点:VertexInverseDepth ,iVertexSE3Expmap,jVertexSE3Expmap
*/

class EdgeReprojection : public g2o::BaseMultiEdge<2, Eigen::Vector4d>
{
public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW
    EdgeReprojection() {}
    bool read(std::istream & /*is*/) override
    {
        std::cerr << __PRETTY_FUNCTION__ << " not implemented yet" << std::endl;
        return false;
    }

    bool write(std::ostream & /*os*/) const override
    {
        std::cerr << __PRETTY_FUNCTION__ << " not implemented yet" << std::endl;
        return false;
    }
    virtual void computeError() override
    {
        const iVertexSE3Expmap *vi = static_cast<const iVertexSE3Expmap *>(_vertices[0]);
        const jVertexSE3Expmap *vj = static_cast<const jVertexSE3Expmap *>(_vertices[1]);
        const VertexInverseDepth *vd = static_cast<const VertexInverseDepth *>(_vertices[2]);
        Eigen::Vector2d uvi = _measurement.head<2>();
        Eigen::Vector2d uvj = _measurement.tail<2>();
        const double &lambda = vd->estimate();
        Eigen::Vector3d Pci = Eigen::Vector3d(uvi(0), uvi(1), 1) / lambda;
        Eigen::Vector3d Pw = vi->estimate().inverse().map(Pci);
        Eigen::Vector3d Pcj = vj->estimate().map(Pw);
        Eigen::Vector2d uvj_ = Pcj.head<2>() / Pcj(2);
        _error = uvj - uvj_;
    }
};

// 像素坐标转相机归一化坐标
void normalize(Eigen::Vector2d &p, double fx, double fy, double cx, double cy)
{
    p(0) = (p(0) - cx) / fx;
    p(1) = (p(1) - cy) / fy;
}

int main(int argc, const char *argv[])
{
    std::vector<std::vector<Eigen::Vector2d>> points2d;
    std::vector<std::vector<int>> ids;
    readfile("/root/g2o-example/data/3d2d.txt", points2d, ids);
    // normalize
    double fx = 720, fy = 720, cx = 640, cy = 360;
    for (auto &obs : points2d)
    {
        for (auto &p : obs)
        {
            normalize(p, fx, fy, cx, cy);
        }
    }
    std::vector<Eigen::Vector3d> points3d;
    readfile("/root/g2o-example/data/3d.txt", points3d);
    std::vector<Eigen::Matrix4d> poses;
    readfile("/root/g2o-example/data/pose.txt", poses);

    // Set up the problem
    g2o::SparseOptimizer optimizer;
    typedef g2o::BlockSolver<g2o::BlockSolverTraits<Eigen::Dynamic, Eigen::Dynamic>> BlockSolverType;
    typedef g2o::LinearSolverCholmod<BlockSolverType::PoseMatrixType> LinearSolverType;
    auto linearSolver = new LinearSolverType();
    auto solver = new BlockSolverType(linearSolver);
    g2o::OptimizationAlgorithmLevenberg *algorithm = new g2o::OptimizationAlgorithmLevenberg(solver);
    optimizer.setAlgorithm(algorithm);
    optimizer.setVerbose(true);

    // add pose vertex
    for (int i = 0; i < (int)poses.size(); ++i)
    {
        g2o::VertexSE3Expmap *v = new g2o::VertexSE3Expmap();
        v->setEstimate(g2o::SE3Quat(poses[i].block<3, 3>(0, 0), poses[i].block<3, 1>(0, 3)));
        // fix the first pose
        if (i == 0)
        {
            v->setFixed(true);
        }
        v->setId(i);
        optimizer.addVertex(v);
    }
    // 定义第一次观测到3d点的相机frame id
    std::vector<int> firstObsFrameId(points3d.size(), -1);

    // for each 3d points
    for (int i = 0; i < (int)points3d.size(); ++i)
    {
        // for each camera pose
        // 第一次观测的pos
        const Eigen::Matrix4d *firstObsPose = nullptr;
        const Eigen::Vector2d *firstObsPoint = nullptr;
        double firstObsLambda = 0;

        int obs_num = 0;
        for (int j = 0; j < (int)poses.size(); ++j)
        {
            // 判断该frame是否观测到points3d[i]
            const std::vector<int> &obs_ids = ids[j];
            auto it = std::find(obs_ids.begin(), obs_ids.end(), i);
            if (it == obs_ids.end())
            {
                continue;
            }
            // 该frame观测到了points3d[i]
            int find_index = it - obs_ids.begin();
            if (++obs_num == 1)
            {
                firstObsPose = &poses[j];
                firstObsPoint = &points2d[j][find_index];
                Eigen::Vector3d Pw = points3d[i];
                Eigen::Vector3d Pc = firstObsPose->block<3, 3>(0, 0) * Pw + firstObsPose->block<3, 1>(0, 3);
                firstObsLambda = 1.0 / Pc(2);
                firstObsFrameId[i] = j;
                // add lambda vertex
                VertexInverseDepth *v = new VertexInverseDepth();
                v->setEstimate(firstObsLambda);
                v->setId(poses.size() + i);
                optimizer.addVertex(v);
            }
            else
            {
                // add edge
                EdgeReprojection *e = new EdgeReprojection();
                e->resize(3);
                e->setVertex(0, optimizer.vertex(firstObsFrameId[i]));
                e->setVertex(1, optimizer.vertex(j));
                e->setVertex(2, optimizer.vertex(poses.size() + i));
                e->setMeasurement(Eigen::Vector4d((*firstObsPoint)(0), (*firstObsPoint)(1), points2d[j][find_index](0), points2d[j][find_index](1)));
                e->setInformation(Eigen::Matrix2d::Identity());
                e->setParameterId(0, 0);
                // add robust kernel
                g2o::RobustKernelHuber *rk = new g2o::RobustKernelHuber();
                e->setRobustKernel(rk);
                optimizer.addEdge(e);
            }
        }
    }
    optimizer.initializeOptimization();
    optimizer.optimize(10);
    optimizer.save("/root/g2o-example/data/result.g2o");
}
点云配准与影像数据的联合优化是三维计算机视觉和机器人领域的重要研究方向,尤其在自动驾驶、SLAM(同步定位与地图构建)以及增强现实等应用中具有重要意义。ICP(Iterative Closest Point)和NDT(Normal Distributions Transform)作为两种经典的点云配准算法,其核心目标是通过估计最优的旋转和平移变换,使源点云与目标点云尽可能对齐。然而,将这些方法扩展到点云与影像的联合优化中,需要引入额外的几何与光度信息进行融合处理。 ### 点云与影像联合优化的基本思路 为了实现点云与影像的联合优化配准,可以考虑以下策略: 1. **利用点云与图像之间的几何一致性** 将点云投影到图像平面,并计算图像中的特征点与点云投影点之间的匹配误差。该误差可作为优化目标函数的一部分,结合ICP或NDT的点间距离误差进行联合优化。例如,在基于RGB-D相机的数据中,深度图可以直接用于点云生成,而彩色图像可用于提取纹理特征辅助配准[^5]。 2. **结合NDT的概率模型优势** NDT算法通过将点云划分为体素并建模为正态分布,能够有效表达局部几何结构[^1]。对于影像数据,可以在每个体素中引入图像特征(如颜色直方图、SIFT描述子等),从而构建多模态的NDT表示。这样,在优化过程中不仅考虑几何一致性,还利用了颜色或纹理信息,提高配准鲁棒性[^2]。 3. **ICP与光流融合的联合优化框架** 在视频序列中,图像帧之间存在运动信息。可以通过光流法估计图像帧之间的像素级位移,再将其转换为点云间的刚体变换。将光流估计的变换作为ICP的初始猜测,有助于避免陷入局部最优解,并提升配准效率[^5]。 4. **多传感器融合下的联合优化** 在实际系统中,如自动驾驶车辆,通常同时配备激光雷达(LiDAR)和相机。此时可以采用因子图(Factor Graph)或图优化(Graph-based Optimization)的方法,将ICP或NDT提供的点云配准约束与图像特征匹配约束共同建模,形成统一的优化问题。这种方法在ORB-SLAM3等系统中有广泛应用[^2]。 ### 实现建议 - **使用PCL库实现ICP/NDT点云配准** PCL(Point Cloud Library)提供了高效的ICP与NDT实现接口,支持从点云到点云的配准。可以通过`pcl::IterativeClosestPoint`类实现ICP,通过`pcl::NormalDistributionsTransform`类实现NDT[^4]。 - **图像特征提取与匹配** 使用OpenCV库提取图像特征(如SIFT、SURF、ORB等),并通过FLANN或BFMatcher进行特征匹配,得到图像间的变换估计。 - **联合优化工具链** 可以使用Ceres Solver或g2o等非线性优化库,构建包含点云配准误差项和图像重投影误差项的目标函数,进行联合优化求解。 ```cpp // 示例:使用PCL进行NDT配准 #include <pcl/io/pcd_io.h> #include <pcl/point_types.h> #include <pcl/registration/ndt.h> int main () { pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_in (new pcl::PointCloud<pcl::PointXYZ>); pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_out (new pcl::PointCloud<pcl::PointXYZ>); // 加载点云数据 pcl::io::loadPCDFile ("source.pcd", *cloud_in); pcl::io::loadPCDFile ("target.pcd", *cloud_out); pcl::NormalDistributionsTransform<pcl::PointXYZ, pcl::PointXYZ> ndt; ndt.setInputSource(cloud_in); ndt.setInputTarget(cloud_out); Eigen::Matrix4f init_guess = Eigen::Matrix4f::Identity(); pcl::PointCloud<pcl::PointXYZ> output_cloud; ndt.align(output_cloud, init_guess); std::cout << "Transformation Matrix:\n" << ndt.getFinalTransformation() << std::endl; return 0; } ``` ###
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值