ORB_SLAM3中如何使用boost库实现地图的序列化保存与加载

文章导读

ORB_SLAM3 相比于 ORB_SLAM2 ,新增了一个关键功能——地图保存。这一功能主要通过保存子地图 Atlas 来实现。这种设计允许 ORB_SLAM3 不仅创建和更新地图,还能保存和重新加载这些地图,使其可以在后续的定位和建图任务中使用。本文通过分享 ORB_SLAM3 中实现地图保存和加载功能的技术细节,帮助读者理解如何将已有地图保存下来,当下一次任务时,如何加载这个地图作为先验信息,从而提高系统的效率。

文章结构概览

  1. boost 库序列化原理
  2. ORB_SLAM3 地图序列化
    • 整体框架
    • ORB_SLAM3 中 7 类数据的序列化方式
    • 反序列化的方式
  3. 在 ORB_SLAM2 上添加地图保存和加载功能(实践验证)
  4. 单例模式序列化的问题

boost 库序列化原理

有关 Boost 库序列化的使用这里就不再赘述,大家可以通过 4.4 C++ Boost 数据集序列化库 - lyshark - 博客园 或者其他资料学习。下面针对 Boost 的递归序列化的特性,结合 ORB_SLAM3 为案例来讲解:

Boost 库在序列化保存时会递归地对对象进行序列化,这就意味着如果一个对象的成员变量也是一个对象,那么 Boost 序列化库会查找并调用这个成员变量对象的序列化函数。
ORB_SLAM3 中,Atlas 类有一个成员变量 mvpBackupMaps ,它是一个 std::vector<Map*> 类型的对象。当 Atlas 对象被序列化时,Boost 序列化库会对 mvpBackupMaps 进行序列化。由于 mvpBackupMaps 是一个 std::vector<Map*>,Boost 序列化库会对 std::vector 中的每一个 Map* 进行序列化。对于每一个 Map*,Boost 序列化库会查找并调用 Map 类的序列化函数。在 Map 类的序列化函数中,又有一些成员变量需要被序列化,比如 mvpBackupKeyFramesmvpBackupMapPoints 等。如果这些成员变量的类型也定义了序列化函数,那么 Boost 序列化库会继续递归地对这些成员变量进行序列化。
这个过程会一直持续下去,直到遇到基本类型的变量 (如 int、`float、‘double 等) 或者没有定义序列化函数的自定义类型变量。对于基本类型的变量,Boost 序列化库会直接将它们的值写入到序列化的输出中。对于没有定义序列化函数的自定义类型变量,Boost 序列化库会报错。
Boost 序列化库的工作方式就像是一种深度优先搜索 (DFS),它会递归地对对象的所有成员变量进行序列化,直到所有的变量都被序列化。这种方式可以确保对象的所有信息都被正确地保存和恢复,无论这个对象有多复杂。

ORB_SLAM3 地图序列化

整体框架

在了解了 boost 库对于序列化的实现以后,就可以深入 ORB_SLAM3 来了解它对于系统中的数据保存方式了。
在深入了解实现的细节前,首先先了解整体的框架,下图为 ORB_SLAM3 序列化保存与加载的整体框架(如有遗漏请评论私信纠正,十分感谢!)。
可以看到对于整个序列化保存,是先 PreSave 再进行 boost 库保存。其中 PreSave 正如其名,作用就是对数据的预处理(下面详细介绍),作用是将不合适 boost 保存的数据转换为合适的数据,然后再通过 boost 库保存。
而对于加载部分,当然是与保存的步骤相反,先进行 boost 库的加载,然后再通过 PostLoad 将数据转换为原始的在系统中的数据形式。思维导图

PreSave 和 PostLoad 的详细解释

预保存 PreSave 和 后加载 PostLoad(因为很多数据都是要保存的关键帧或者地图点的对象地址,而从文件恢复出来的地址会变化,所以要用 backup 数据把这些数据先保存为 ID 索引的形式,后面恢复完所有的地图点和关键帧了,有对应的对象地址了,再通过 Postload 后加载,通过保存的 ID 索引将地址恢复出来)

ORB_SLAM3 中 7 类数据的序列化方式

下面将 ORB_SLAM3 中要序列化的数据分为六大类型来讲解。

1. 基本数据类型的序列化(例如:long unsigned int mnId

基本数据类型可以直接使用 ar & 进行序列化和反序列化。

ar & mnId; // 序列化ID

原因:基本数据类型在内存中的表示是标准的,所以可以直接读取和写入这些值。

2. 常量数据的序列化(例如:const long unsigned int mnFrameId

常量数据需要使用 const_cast 来序列化。

ar & const_cast<long unsigned int&>(mnFrameId); // 序列化帧ID

原因:常量成员变量在对象构造后不能更改,但在反序列化过程中,你可能需要更改这些值以重建对象的状态。const_cast 允许你临时去除常量性,以便能够反序列化这些成员。

3. 自定义数据格式的序列化(例如:cv::Mat mDescriptor

自定义数据格式,如 OpenCV 的矩阵,需要特殊处理。

//此函数定义在SerializationUtils.h文件中
// 序列化cv::Mat对象的模板函数
template<class Archive>
void serializeMatrix(Archive& ar, cv::Mat& mat, const unsigned int version)
{
    int cols, rows, type; // 定义矩阵的列、行和类型
    bool continuous; // 定义矩阵是否连续

    // 如果是保存操作,则从矩阵中获取这些属性
    if (Archive::is_saving::value) {
        cols = mat.cols;
        rows = mat.rows;
        type = mat.type();
        continuous = mat.isContinuous();
    }

    // 序列化列、行、类型和连续性
    ar & cols & rows & type & continuous;

    // 如果是加载操作,则使用这些属性创建矩阵
    if (Archive::is_loading::value)
        mat.create(rows, cols, type);

    // 如果矩阵是连续的,则一次性序列化整个数据
    if (continuous) {
        const unsigned int data_size = rows * cols * mat.elemSize();
        ar & boost::serialization::make_array(mat.ptr(), data_size);
    } else {
        // 如果矩阵不连续,则逐行序列化
        const unsigned int row_size = cols * mat.elemSize();
        for (int i = 0; i < rows; i++) {
            ar & boost::serialization::make_array(mat.ptr(i), row_size);
        }
    }
}

// 序列化const cv::Mat对象的模板函数
template<class Archive>
void serializeMatrix(Archive& ar, const cv::Mat& mat, const unsigned int version)
{
    cv::Mat matAux = mat; // 创建一个非const副本

    serializeMatrix(ar, matAux, version); // 调用非const版本的序列化函数

    // 如果是加载操作,则将加载的数据复制到原始const对象
    if (Archive::is_loading::value)
    {
        cv::Mat* ptr;
        ptr = (cv::Mat*)( &mat ); // 强制转换为非const指针
        *ptr = matAux; // 将加载的数据复制到原始对象
    }
}

原因:矩阵可能有不同的大小、类型和存储方式,所以需要自定义序列化方法来逐一处理这些属性。

4. 容器 + 自定义数据格式的序列化(例如:std::vector<cv::KeyPoint> mvKeys

容器中包含自定义数据格式,如关键点向量,需要特殊处理。

//此函数定义在SerializationUtils.h文件中
// 序列化std::vector<cv::KeyPoint>对象的模板函数
template<class Archive>
void serializeVectorKeyPoints(Archive& ar, const std::vector<cv::KeyPoint>& vKP, const unsigned int version)
{
    int NumEl; // 定义关键点的数量

    // 如果是保存操作,则从向量中获取关键点的数量
    if (Archive::is_saving::value) {
        NumEl = vKP.size();
    }

    ar & NumEl; // 序列化关键点的数量

    std::vector<cv::KeyPoint> vKPaux = vKP; // 创建一个非const副本
    if (Archive::is_loading::value)
        vKPaux.reserve(NumEl); // 如果是加载操作,则预留空间

    // 遍历关键点
    for(int i=0; i < NumEl; ++i)
    {
        cv::KeyPoint KPi; // 定义单个关键点

        // 如果是加载操作,则创建一个新的关键点
        if (Archive::is_loading::value)
            KPi = cv::KeyPoint();

        // 如果是保存操作,则从副本中获取关键点
        if (Archive::is_saving::value)
            KPi = vKPaux[i];

        // 序列化关键点的各个属性
        ar & KPi.angle;
        ar & KPi.response;
        ar & KPi.size;
        ar & KPi.pt.x;
        ar & KPi.pt.y;
        ar & KPi.class_id;
        ar & KPi.octave;

        // 如果是加载操作,则将新关键点添加到副本中
        if (Archive::is_loading::value)
            vKPaux.push_back(KPi);
    }

    // 如果是加载操作,则将加载的数据复制到原始const对象
    if (Archive::is_loading::value)
    {
        std::vector<cv::KeyPoint> *ptr;
        ptr = (std::vector<cv::KeyPoint>*)( &vKP ); // 强制转换为非const指针
        *ptr = vKPaux; // 将加载的数据复制到原始对象
    }
}

原因:关键点包括多个属性,所以需要自定义序列化方法来逐一处理这些属性。

5. 使用 boost::serialization::make_array 序列化向量/矩阵(例如:Eigen::Vector3f mWorldPos

连续的内存块可以使用 boost::serialization::make_array 进行序列化。

ar & boost::serialization::make_array(mWorldPos.data(), mWorldPos.size()); // 序列化世界位置

原因:连续的内存块可以一次性序列化整个块,比逐个序列化元素更高效。

6. 自定义 DBoW 数据格式的序列化(例如:DBoW2::BowVector mBowVec

DBoW 数据格式需要在 DBoW 库源码中添加 serialize 函数。但是在 KeyFrame 中依旧是使用 ar & mBowVec,boost 库看到这个格式后会跳转到第三方库中看有没有对序列化进行定义。

//KeyFrame.h中:

ar & mBowVec; // 序列化Bow向量


//---------------------------------------------------------------------


//Thirdparty/DBoW2/DBoW2/BowVector.h中:
class BowVector: 
        public std::map<WordId, WordValue>
{
        friend class boost::serialization::access;
    template<class Archive>
    void serialize(Archive& ar, const int version)
    {
        ar & boost::serialization::base_object<std::map<WordId, WordValue> >(*this);
    }

public:
...

原因:DBoW 数据格式包括复杂的结构和属性,所以需要在 DBoW 库中添加特定的序列化方法。

7.带有关键帧或者地图点对象地址的数据要 PreSave(例如:std::vector<MapPoint*> mvpMapPoints

这类数据是要保存的关键帧或者地图点的对象地址,而从文件恢复出来的对象地址会改变,所以要用 backup 数据把这些数据先保存为 ID 索引的形式 (也就是 PreSave 要做的事情),后面恢复完所有的地图点和关键帧了,有对应的对象地址了,再通过 postload 后加载,通过保存的 ID 索引将地址恢复出来。

**流程:先 presave 成能直接序列化的基本数据类型(基本都是些地图点和关键帧 Id),然后通过 ar & xxx 序列化保存。

void KeyFrame::PreSave()
{
    //保存帧内所有地图点
    mvBackupMapPointsId.clear();
    mvBackupMapPointsId.reserve(N);
    for(int i = 0 ; i < N ; ++i)
    {
        if(mvpMapPoints[i])
            mvBackupMapPointsId.push_back(mvpMapPoints[i]->mnId);
        else
            mvBackupMapPointsId.push_back(-1);
    }

    //保存连接的关键帧ID和权重
    mBackupConnectedKeyFrameIdWeights.clear();
    for(auto it = mConnectedKeyFrameWeights.begin(),end = mConnectedKeyFrameWeights.end();it != end ; ++it)
    {
        mBackupConnectedKeyFrameIdWeights[it->first->mnId] = it->second;
    }

    //保存父关键帧ID
    mBackupParentId = -1;
    if (mpParent)
        mBackupParentId = mpParent->mnId;

    //保存子关键帧ID
    mvBackupChildrensId.clear();
    mvBackupChildrensId.reserve(mspChildrens.size());
    for(KeyFrame* pKFi : mspChildrens)
    {
        mvBackupChildrensId.push_back(pKFi->mnId);
    }

    // //保存Frame的静态变量nNextid
    // mFramenNextId = Frame::nNextId;
    ...
}

反序列化的方式

前六种情况的反序列化加载

在反序列化加载过程中,前六种情况无需人为操作,boost 库会自动根据 serialize 函数进行加载。但对于类对象的反序列化,情况会略有不同。

首先,如果你要恢复如 std::vector<KeyFrame*> mvpBackupKeyFrames 这样的类对象数据,必须为 boost 库提供一个默认的构造函数。这样做的原因是,boost 库需要使用这个默认构造函数来实例化类的对象。

例如,对于 mvpBackupKeyFrames 这个容器中的每一个 KeyFrame,boost 库会通过特定的关键帧默认构造函数来进行实例化。一旦实例化完毕,boost 库就会根据 KeyFrame.h 里的 serialize 函数来逐一恢复每个 KeyFrame 对象里保存了的数据。

KeyFrame::KeyFrame()
    : mnFrameId(0), mTimeStamp(0), mnGridCols(FRAME_GRID_COLS), mnGridRows(FRAME_GRID_ROWS),
    mfGridElementWidthInv(0), mfGridElementHeightInv(0),
    mnTrackReferenceForFrame(0), mnFuseTargetForKF(0), mnBALocalForKF(0), mnBAFixedForKF(0), mnBALocalForMerge(0),
    mnLoopQuery(0), mnLoopWords(0), mnRelocQuery(0), mnRelocWords(0), mnMergeQuery(0), mnMergeWords(0), mnBAGlobalForKF(0),
    fx(0), fy(0), cx(0), cy(0), invfx(0), invfy(0), mnPlaceRecognitionQuery(0), mnPlaceRecognitionWords(0), mPlaceRecognitionScore(0),
    mbf(0), mb(0), mThDepth(0), N(0), mvKeys(static_cast<vector<cv::KeyPoint>>(NULL)), mvKeysUn(static_cast<vector<cv::KeyPoint>>(NULL)),
    mvuRight(static_cast<vector<float>>(NULL)), mvDepth(static_cast<vector<float>>(NULL)), mnScaleLevels(0), mfScaleFactor(0),
    mfLogScaleFactor(0), mvScaleFactors(0), mvLevelSigma2(0), mvInvLevelSigma2(0), mnMinX(0), mnMinY(0), mnMaxX(0),
    mnMaxY(0), mPrevKF(static_cast<KeyFrame *>(NULL)), mNextKF(static_cast<KeyFrame *>(NULL)), mbFirstConnection(true), mpParent(NULL), mbNotErase(false),
    mbToBeErased(false), mbBad(false), mHalfBaseline(0), mbCurrentPlaceRecognition(false), mnMergeCorrectedForKF(0),
    NLeft(0), NRight(0), mnNumberOfOpt(0), mbHasVelocity(false)
{
}

对 PreSave 数据的反序列化加载

第七种情况处理的是具有预保存(presave)格式的数据,例如 mvBackupMapPointsId。相对于其他情况,这一情况的处理更为复杂,需要分为两个主要阶段来执行。

  1. 反序列化加载阶段:
    1. 目的:将之前以特定格式保存的数据重新转化为可用的数据结构。
    2. 过程:首先对数据进行反序列化加载。
  2. 后加载(PostLoad)处理阶段:
    1. 目的:确保数据结构的正确恢复,特别是关键帧或地图点的 ID 恢复为对应的对象地址。
    2. 过程:
      • 关联创建:通过 Map 中的转换,将地图中所有的关键帧和地图点 ID 与反序列化加载后得到的关键帧和地图点相关联。这一步确保了数据结构的完整性。
      • 数据打包:将上述关联打包为 map<long unsigned int, KeyFrame*>& mpMapPointId, map<long unsigned int, MapPoint*>& mpKeyFrameId
void Map::PostLoad()
{
    std::copy(mvpBackupMapPoints.begin(), mvpBackupMapPoints.end(), std::inserter(mspMapPoints, mspMapPoints.begin()));
    std::copy(mvpBackupKeyFrames.begin(), mvpBackupKeyFrames.end(), std::inserter(mspKeyFrames, mspKeyFrames.begin()));

    //将地图点ID和地图点地址关联打包起来
    map<long unsigned int, MapPoint *> mpMapPointId;
    for (MapPoint *pMPi : mspMapPoints)
    {
        if (!pMPi || pMPi->isBad())
            continue;
        mpMapPointId[pMPi->mnId] = pMPi;
    }

    //将关键帧ID和关键帧地址关联打包起来
    map<long unsigned int, KeyFrame *> mpKeyFrameId;
    for (KeyFrame *pKFi : mspKeyFrames)
    {
        if (!pKFi || pKFi->isBad())
            continue;
        mpKeyFrameId[pKFi->mnId] = pKFi;
    }
    
    ...
}
  • 地址恢复:最后,每个对象中的数据再从这个打包关联的容器中恢复出对应的对象地址。
void KeyFrame::PostLoad(map<long unsigned int, KeyFrame*>& mpKFid, map<long unsigned int, MapPoint*>& mpMPid)
{
    //设置位姿
    SetPose(Tcw);

    //恢复观测地图点
    mvpMapPoints.clear();
    mvpMapPoints.reserve(N);
    for(int i = 0; i < N; ++i)
    {
       if(mvBackupMapPointsId[i] != -1)
            mvpMapPoints[i] = mpMPid[mvBackupMapPointsId[i]];
       else
            mvpMapPoints[i] = static_cast<MapPoint *>(NULL);
    }

    //恢复共视帧以及权重
    mConnectedKeyFrameWeights.clear();
    for(auto it = mBackupConnectedKeyFrameIdWeights.begin(), end = mBackupConnectedKeyFrameIdWeights.end(); it != end; ++it)
    {
        mConnectedKeyFrameWeights[mpKFid[it->first]] = it->second;
    }

    //恢复父关键帧
    if(mBackupParentId >= 0)
        mpParent = mpKFid[mBackupParentId];
    else
        mpParent = static_cast<KeyFrame *>(NULL);

    //恢复子关键帧
    mspChildrens.clear();
    for(long unsigned int pKFIdi : mvBackupChildrensId)
    {
        mspChildrens.insert(mpKFid[pKFIdi]);
    }

    //清理备份容器
    mBackupConnectedKeyFrameIdWeights.clear();
    mvBackupChildrensId.clear();
    mvBackupMapPointsId.clear();

}

在 ORB_SLAM2 上添加地图保存和加载功能(实践验证)

ORB_SLAM2 增加初步的序列化保存加载功能(并未将数据加载到优化部分)。

保存的变量

类别变量名
MapPoints:mspMapPoints.size()
MapPoint
mnId
世界坐标
**KeyFrames:
**mspKeyFrames.size()
KeyFrame
mnId
mTimeStamp
Tcw
四元数, t
N
mvKeys[i]
x, y
Size
Angle
Response
Octave
描述子
MapPoint索引

效果

下图为初步的保存和加载功能,可以看到加载出了上一次看到的地图点以及上一次的关键帧。

效果图

单例模式序列化的问题

  1. 当使用 shared_ptr 管理类地址时,要使用 * 将智能指针恢复为对象再放序列化,不然 boost 会去智能指针里面找 serialize 函数

    bool SaveNum(std::shared_ptr<B> classB)
    {    
        std::ofstream ofs(" Map_Serialization");
        boost::archive::text_oarchive oa(ofs);
        cout << "serializing map" << endl;
        oa << *classB;
        cout << "serialized" << endl;
    }
    
  2. 对于自定义的单例,反序列化前要实例化出来,这样 boost 才找的到对象写入,因为自定义单例构造函数私有的原因,boost 无法自动构造对象。(在自定义单例中指定 boost 使用 get_instance 函数构造也不行)

  3. 对于继承 boost 库 singleton 的单例,因为构造函数是 public 的,所以很容易因为一些行为导致类构造了多个对象,然后在反序列化中就会出现段错误

  • 44
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值