ORB-SLAM2 --- LoopClosing::SearchAndFuse函数

news2025/1/17 1:47:51

目录

1.函数作用

2. code及解析

3. ORBmatcher::Fuse函数解析(闭环调用版)


1.函数作用

        将闭环相连关键帧组mvpLoopMapPoints 投影到当前关键帧组中,进行匹配,新增或替换当前关键帧组中KF的地图点。

2. code及解析

/**
 * @brief 将闭环相连关键帧组mvpLoopMapPoints 投影到当前关键帧组中,进行匹配,新增或替换当前关键帧组中KF的地图点
 * 因为 闭环相连关键帧组mvpLoopMapPoints 在地图中时间比较久经历了多次优化,认为是准确的
 * 而当前关键帧组中的关键帧的地图点是最近新计算的,可能有累积误差
 * 
 * @param[in] CorrectedPosesMap         矫正的当前KF对应的共视关键帧及Sim3变换
 */
void LoopClosing::SearchAndFuse(const KeyFrameAndPose &CorrectedPosesMap)
{

    // 定义ORB匹配器
    ORBmatcher matcher(0.8);

    // Step 1 遍历待矫正的当前KF的相连关键帧
    for(KeyFrameAndPose::const_iterator mit=CorrectedPosesMap.begin(), mend=CorrectedPosesMap.end(); mit!=mend;mit++)
    {
        KeyFrame* pKF = mit->first;
        // 矫正过的Sim 变换
        g2o::Sim3 g2oScw = mit->second;
        cv::Mat cvScw = Converter::toCvMat(g2oScw);

        // Step 2 将mvpLoopMapPoints投影到pKF帧匹配,检查地图点冲突并融合
        // mvpLoopMapPoints:与当前关键帧闭环匹配上的关键帧及其共视关键帧组成的地图点
        vector<MapPoint*> vpReplacePoints(mvpLoopMapPoints.size(),static_cast<MapPoint*>(NULL));
        // vpReplacePoints:存储mvpLoopMapPoints投影到pKF匹配后需要替换掉的新增地图点,索引和mvpLoopMapPoints一致,初始化为空
        // 搜索区域系数为4
        matcher.Fuse(pKF,cvScw,mvpLoopMapPoints,4,vpReplacePoints);

        // Get Map Mutex
        // 之所以不在上面 Fuse 函数中进行地图点融合更新的原因是需要对地图加锁
        unique_lock<mutex> lock(mpMap->mMutexMapUpdate);
        const int nLP = mvpLoopMapPoints.size();
        // Step 3 遍历闭环帧组的所有的地图点,替换掉需要替换的地图点
        for(int i=0; i<nLP;i++)
        {
            MapPoint* pRep = vpReplacePoints[i];
            if(pRep)
            {
                // 如果记录了需要替换的地图点
                // 用mvpLoopMapPoints替换掉vpReplacePoints里记录的要替换的地图点
                pRep->Replace(mvpLoopMapPoints[i]);
            }
        }
    }
}

        这里传入的参数@CorrectedPosesMap是当前帧mpCurrentKF的共视关键帧经过修正之后的世界坐标系下的坐标。

        遍历待矫正的当前mpCurrentKF的相连关键帧,取出此帧pKF与此帧在世界坐标系下的坐标cvScw

        mvpLoopMapPoints是在ComputeSim3函数中计算得到的,其存储的是闭环匹配关键帧和它的共视关键帧的所有地图点。

vector<MapPoint*> vpReplacePoints(mvpLoopMapPoints.size(),static_cast<MapPoint*>(NULL));

        初始化vpReplacePoints,其大小和mvpLoopMapPoints大小相同。

        用Fuse函数将当前关键帧闭环匹配上的关键帧及其共视关键帧组成的地图点投影到当前关键帧,融合地图点。

matcher.Fuse(pKF,cvScw,mvpLoopMapPoints,4,vpReplacePoints);

        @pKF:与mpCurrentKF闭环的关键帧的共视关键帧

        @cvScw:世界坐标系到mpCurrentKF闭环的关键帧的共视关键帧的变换矩阵

        @mvpLoopMapPoints:闭环匹配关键帧和它的共视关键帧的所有地图点。

        @4:重投影区域

        @vpReplacePoints:待替换的地图点

        这步结束以后,我们融合了pKF帧和mvpLoopMapPoints中的地图点。对于pKF中没有的地图点直接添加;对于pKF中存在的地图点,我们进行替换操作。

/**
 * @brief 替换地图点,更新观测关系
 * 
 * @param[in] pMP       用该地图点来替换当前地图点
 */
void MapPoint::Replace(MapPoint* pMP)
{
    // 同一个地图点则跳过
    if(pMP->mnId==this->mnId)
        return;

    //要替换当前地图点,有两个工作:
    // 1. 将当前地图点的观测数据等其他数据都"叠加"到新的地图点上
    // 2. 将观测到当前地图点的关键帧的信息进行更新


    // 清除当前地图点的信息,这一段和SetBadFlag函数相同
    int nvisible, nfound;
    map<KeyFrame*,size_t> obs;
    {
        unique_lock<mutex> lock1(mMutexFeatures);
        unique_lock<mutex> lock2(mMutexPos);
        obs=mObservations;
        //清除当前地图点的原有观测
        mObservations.clear();
        //当前的地图点被删除了
        mbBad=true;
        //暂存当前地图点的可视次数和被找到的次数
        nvisible = mnVisible;
        nfound = mnFound;
        //指明当前地图点已经被指定的地图点替换了
        mpReplaced = pMP;
    }

    // 所有能观测到原地图点的关键帧都要复制到替换的地图点上
    //- 将观测到当前地图的的关键帧的信息进行更新
    for(map<KeyFrame*,size_t>::iterator mit=obs.begin(), mend=obs.end(); mit!=mend; mit++)
    {
        // Replace measurement in keyframe
        KeyFrame* pKF = mit->first;

        if(!pMP->IsInKeyFrame(pKF))
        {   
            // 该关键帧中没有对"要替换本地图点的地图点"的观测
            pKF->ReplaceMapPointMatch(mit->second, pMP);// 让KeyFrame用pMP替换掉原来的MapPoint
            pMP->AddObservation(pKF,mit->second);// 让MapPoint替换掉对应的KeyFrame
        }
        else
        {
            // 这个关键帧对当前的地图点和"要替换本地图点的地图点"都具有观测
            // 产生冲突,即pKF中有两个特征点a,b(这两个特征点的描述子是近似相同的),这两个特征点对应两个 MapPoint 为this,pMP
            // 然而在fuse的过程中pMP的观测更多,需要替换this,因此保留b与pMP的联系,去掉a与this的联系
            //说白了,既然是让对方的那个地图点来代替当前的地图点,就是说明对方更好,所以删除这个关键帧对当前帧的观测
            pKF->EraseMapPointMatch(mit->second);
        }
    }

    //- 将当前地图点的观测数据等其他数据都"叠加"到新的地图点上
    pMP->IncreaseFound(nfound);
    pMP->IncreaseVisible(nvisible);
    //描述子更新
    pMP->ComputeDistinctiveDescriptors();

    //告知地图,删掉我
    mpMap->EraseMapPoint(this);
}

3. ORBmatcher::Fuse函数解析(闭环调用版)

/**
 * @brief 闭环矫正中使用。将当前关键帧闭环匹配上的关键帧及其共视关键帧组成的地图点投影到当前关键帧,融合地图点
 * 
 * @param[in] pKF                   当前关键帧
 * @param[in] Scw                   当前关键帧经过闭环Sim3 后的世界到相机坐标系的Sim变换
 * @param[in] vpPoints              与当前关键帧闭环匹配上的关键帧及其共视关键帧组成的地图点
 * @param[in] th                    搜索范围系数
 * @param[out] vpReplacePoint       替换的地图点
 * @return int                      融合(替换和新增)的地图点数目
 */
int ORBmatcher::Fuse(KeyFrame *pKF, cv::Mat Scw, const vector<MapPoint *> &vpPoints, float th, vector<MapPoint *> &vpReplacePoint)
{
    // Get Calibration Parameters for later projection
    const float &fx = pKF->fx;
    const float &fy = pKF->fy;
    const float &cx = pKF->cx;
    const float &cy = pKF->cy;

    // Decompose Scw
    // Step 1 将Sim3转化为SE3并分解
    cv::Mat sRcw = Scw.rowRange(0,3).colRange(0,3);
    const float scw = sqrt(sRcw.row(0).dot(sRcw.row(0)));// 计算得到尺度s
    cv::Mat Rcw = sRcw/scw;// 除掉s
    cv::Mat tcw = Scw.rowRange(0,3).col(3)/scw;// 除掉s
    cv::Mat Ow = -Rcw.t()*tcw;

    // Set of MapPoints already found in the KeyFrame
    // 当前帧已有的匹配地图点
    const set<MapPoint*> spAlreadyFound = pKF->GetMapPoints();

    int nFused=0;
    // 与当前帧闭环匹配上的关键帧及其共视关键帧组成的地图点
    const int nPoints = vpPoints.size();

    // For each candidate MapPoint project and match
    // 遍历所有的地图点
    for(int iMP=0; iMP<nPoints; iMP++)
    {
        MapPoint* pMP = vpPoints[iMP];

        // Discard Bad MapPoints and already found
        // 地图点无效 或 已经是该帧的地图点(无需融合),跳过
        if(pMP->isBad() || spAlreadyFound.count(pMP))
            continue;

        // Get 3D Coords.
        // Step 2 地图点变换到当前相机坐标系下
        cv::Mat p3Dw = pMP->GetWorldPos();

        // Transform into Camera Coords.
        cv::Mat p3Dc = Rcw*p3Dw+tcw;

        // Depth must be positive
        if(p3Dc.at<float>(2)<0.0f)
            continue;

        // Project into Image
        // Step 3 得到地图点投影到当前帧的图像坐标
        const float invz = 1.0/p3Dc.at<float>(2);
        const float x = p3Dc.at<float>(0)*invz;
        const float y = p3Dc.at<float>(1)*invz;

        const float u = fx*x+cx;
        const float v = fy*y+cy;

        // Point must be inside the image
        // 投影点必须在图像范围内
        if(!pKF->IsInImage(u,v))
            continue;

        // Depth must be inside the scale pyramid of the image
        // Step 4 根据距离是否在图像合理金字塔尺度范围内和观测角度是否小于60度判断该地图点是否有效
        const float maxDistance = pMP->GetMaxDistanceInvariance();
        const float minDistance = pMP->GetMinDistanceInvariance();
        cv::Mat PO = p3Dw-Ow;
        const float dist3D = cv::norm(PO);

        if(dist3D<minDistance || dist3D>maxDistance)
            continue;

        // Viewing angle must be less than 60 deg
        cv::Mat Pn = pMP->GetNormal();

        if(PO.dot(Pn)<0.5*dist3D)
            continue;

        // Compute predicted scale level
        const int nPredictedLevel = pMP->PredictScale(dist3D,pKF);

        // Search in a radius
        // 计算搜索范围
        const float radius = th*pKF->mvScaleFactors[nPredictedLevel];

        // Step 5 在当前帧内搜索匹配候选点
        const vector<size_t> vIndices = pKF->GetFeaturesInArea(u,v,radius);

        if(vIndices.empty())
            continue;

        // Match to the most similar keypoint in the radius
        // Step 6 寻找最佳匹配点(没有用到次佳匹配的比例)
        const cv::Mat dMP = pMP->GetDescriptor();

        int bestDist = INT_MAX;
        int bestIdx = -1;
        for(vector<size_t>::const_iterator vit=vIndices.begin(); vit!=vIndices.end(); vit++)
        {
            const size_t idx = *vit;
            const int &kpLevel = pKF->mvKeysUn[idx].octave;

            if(kpLevel<nPredictedLevel-1 || kpLevel>nPredictedLevel)
                continue;

            const cv::Mat &dKF = pKF->mDescriptors.row(idx);

            int dist = DescriptorDistance(dMP,dKF);

            if(dist<bestDist)
            {
                bestDist = dist;
                bestIdx = idx;
            }
        }

        // If there is already a MapPoint replace otherwise add new measurement
        // Step 7 替换或新增地图点
        if(bestDist<=TH_LOW)
        {
            MapPoint* pMPinKF = pKF->GetMapPoint(bestIdx);
            if(pMPinKF)
            {
                // 如果这个地图点已经存在,则记录要替换信息
                // 这里不能直接替换,原因是需要对地图点加锁后才能替换,否则可能会crash。所以先记录,在加锁后替换
                if(!pMPinKF->isBad())
                    vpReplacePoint[iMP] = pMPinKF;
            }
            else
            {
                // 如果这个地图点不存在,直接添加
                pMP->AddObservation(pKF,bestIdx);
                pKF->AddMapPoint(pMP,bestIdx);
            }
            nFused++;
        }
    }
    // 融合(替换和新增)的地图点数目
    return nFused;
}

        先将Sim3转化为SE3并分解,得到从世界坐标系到帧pKF坐标系的变换Rcw、tcw以及相机光心Ow

        遍历每一个地图点(pKF和其共视关键帧的地图点):

        ①地图点无效 或 已经是该帧的地图点(无需融合),跳过

if(pMP->isBad() || spAlreadyFound.count(pMP))

        ②地图点变换到当前相机坐标系下,如果深度小于0则跳过

        ③得到地图点投影到当前帧的图像坐标u,v;即将地图点投影到pKF帧的像素坐标系中。判断投影点是否在有效范围内pKF->IsInImage(u,v)

        ④根据距离是否在图像合理金字塔尺度范围内和观测角度是否小于60度判断该地图点是否有效,无效则跳过

        ⑤若通过这些测试,则将待匹配的地图点的u,v以及搜索区域输入到GetFeaturesInArea函数中,最终返回待匹配地图点在当前帧pKF的匹配索引vIndices

ORB-SLAM2 ---- Frame::GetFeaturesInArea函数解析icon-default.png?t=MBR7https://blog.csdn.net/qq_41694024/article/details/128227154

        ⑥将地图点pMP的描述子取出,与帧pKF中的待匹配地图点对应的特征点的的描述子索引vIndices中每个特征点进行匹配,寻找最佳匹配的地图点在pKF帧中的索引bestIdx以及两特征点最小汉明距离bestDist

        ⑦如果这个待匹配地图点pMP与当前帧pKF的索引为bestIdx的地图点的匹配汉明距离小于我们设置的阈值TH_LOW,我们认为匹配有效。

        更新观测关系:

        取出与帧pKF中与pMP匹配成功的地图点放在变量pMPinKF中:

        Ⅰ.如果这个地图点已经存在,则记录要替换信息,记录在vpReplacePoint[iMP](imp是pKF及共视关键帧地图点的索引) = pMPinKF;中。这里不能直接替换,原因是需要对地图点加锁后才能替换,否则可能会crash。所以先记录,在加锁后替换。

        Ⅱ.如果这个地图点不存在,直接添加(说明只有特征点没有地图点)

                pMP->AddObservation(pKF,bestIdx);
                pKF->AddMapPoint(pMP,bestIdx);

        向上层函数返回成功融合的地图点数量nFused

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/158800.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第21章 随机游走

第21章 随机游走 随机游走的建模场景是某个对象按照随机选择的方向行走一个步数序列。 21.1赌徒破产 假设一个赌徒一开始有n美元赌注&#xff0c;他要进行一系列1美元投注。如果他赢得一局&#xff0c;则拿回他的赌注外加1美元。如果他输了&#xff0c;那么他将失去1美元。 …

MySQL中InnoDB的事务隔离

文章目录前言一、事务介绍二、事务的四大特性三、事务的隔离性四、事务隔离的实现前言 我们在实际开发中&#xff0c;执行某个业务&#xff0c;肯定不是简单的操作某一句SQL语句&#xff0c;而是多条SQL语句。那么这多条SQL语句必须是全部成功执行&#xff0c;或者全部失败。才…

[L1 - 10分合集]吃鱼还是吃肉

L1-063 吃鱼还是吃肉 分数 10 作者 陈越 单位 浙江大学 题目&#xff1a; 国家给出了 8 岁男宝宝的标准身高为 130 厘米、标准体重为 27 公斤&#xff1b;8 岁女宝宝的标准身高为 129 厘米、标准体重为 25 公斤。 现在你要根据小宝宝的身高体重&#xff0c;给出补充营养的建议…

最近发现关于计算机网络的1个秘密

最近闲着没啥事翻开之前谢希仁老师第7版的《计算机网络》这本书,结果发现了1个惊天的秘密。 首先是互联网与互连网的区别,一般我们常说的互联网是Internet,是指因特网,其起源于阿帕网。或许很多读者看到这里就觉得有什么秘密可言,不都是常识了吗?看你大惊小怪的。 我们不妨看看…

spring cloud、gradle、父子项目、微服务框架搭建---rabbitMQ延时队列(七)

总目录 https://preparedata.blog.csdn.net/article/details/120062997 文章目录总目录一、rabbit延时插件下载二、rabbit插件安装三、项目中配置延时队列四、定义消息通道五、生成消息六、监听消息&#xff0c;进行消费延时队列的配置是对上片文章的延伸扩展 https://prepare…

paddledetection推理代码结构

https://github.com/PaddlePaddle/PaddleDetection/blob/release%2F2.5/deploy/pipeline/README.mdhttps://github.com/PaddlePaddle/PaddleDetection/blob/release%2F2.5/deploy/pipeline/README.md GitHub - leeguandong/Xiaobao: videoclip&#xff0c;视频剪辑应用videocl…

Go 1.19.3 error原理简析

Go error是一个很痛的话题(真心难用) 标准库 error 的定义 // The error built-in interface type is the conventional interface for // representing an error condition, with the nil value representing no error. type error interface {Error() string }error 是一个…

windows10安装wireshark

win10安装wireshark并使用windows10安装wireshark下载WIRESHARK下载Win10Pcapwindows10安装wireshark 你好&#xff01; 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章&#xff0c;了解一下Markdown的基本语法知…

javaEE 初阶 — JUC(java.util.concurrent) 的常见类

文章目录1. Callable 接口1.1 Callable 的用法2. ReentrantLock2.1 ReentrantLock 的缺陷2.1 ReentrantLock 的优势3. 原子类4. 信号量 Semaphore5. CountDownLatch6. 相关面试题1. Callable 接口 类似于 Runnable 一样。 Runnable 用来描述一个任务&#xff0c;描述的任务没有…

【Spring源码】21. 关于循环依赖的N个问题

完成了applyMergedBeanDefinitionPostProcessors()方法&#xff0c;后面有一段关于判断Bean是否需要提前曝光的逻辑&#xff08;如下图红框框中部分&#xff09;在这段逻辑中涉及到了著名的循环依赖&#xff0c;提到循环依赖基本必讲三级缓存&#xff0c;好吧&#xff0c;这篇就…

CANOpen中SDO和PDO的COB-ID理解

CAN 总线是一种串行通信协议&#xff0c;具有较高的通信速率的和较强的抗干扰能力&#xff0c;可以作为现场总线应用于电磁噪声较大的场合。由于 CAN 总线本身只定义ISO/OSI 模型中的第一层&#xff08;物理层&#xff09;和第二层&#xff08;数据链路层&#xff09;&#xf…

(8)go-micro微服务Mysql配置

文章目录一 gorm介绍二 gorm安装1.1 下载依赖1.2 使用MySQL驱动三 CURD操作1. 查询1.1 单行查询1.2 多行查询2. 插入数据3. 更新数据4. 删除数据四 初始化连接五 使用六 最后一 gorm介绍 Go语言中的database/sql包提供了保证SQL或类SQL数据库的泛用接口&#xff0c;并不提供具…

redis: jedis连接超时(需要手动注入连接超时检测的配置)

相关版本说明 服务端&#xff1a; redis_version: 6.2.8 客户端&#xff1a; springBoot: 2.7.7 jedis: 3.8.0 问题 偶发redis连接超时&#xff0c;刷新就又好了&#xff0c;服务日志错误信息如下&#xff1a; JedisConnectionException: Unexpected end of stream.原因 …

Linux利用httpd搭建局域网yum源

本例环境&#xff1a;vmwareworkstation16 proCentOS7.9 mast节点&#xff1a;192.168.195.110 用于配置httpd并发布本地yum源 node节点&#xff1a;192.168.195.111 用于验证mast节点的yum源是否可用 思路&#xff1a;1.在mast节点挂载/上传镜像后配置本地yum源 2.利用本…

JSP三种脚本

脚本可以编写Java语句、变量、方法或表达式。 1.普通脚本 语法: <% Java代码%> <% page contentType"text/html;charsetUTF-8" language"java" %><html><head> <title>Title</title></head><body>&l…

对u盘的分区进行删除和格式化

一、说明 当usb盘&#xff0c;或者SD卡用作启动盘后&#xff0c;将出现多个盘符、多个分区&#xff1b;若将此盘重新当文件盘&#xff0c;需要删除以前的分区&#xff0c;并重新格式化后&#xff0c;才能使用。 二、使用Diskpart在Windows 10中对USB进行分区删除 2.1 尝试磁盘…

重启之后,台式机网络不能连接怎么办

目录 1.问题 2.排查过程 3.心得 1.问题 前天电脑意外断电后,再启动发现网络变成了未连接状态.查看本地连接显示已启动,但IPv4和IPv6未连接.当时做了一些尝试,没有收到效果,直到今天问题才得以解决. 2.排查过程 Windows网络诊断为:DNS服务器未响应.后来花了一部分时间在DNS…

ruoyi-vue集成magic-api(一)

ruoyi虽然带了强大的代码生成器&#xff0c;面对比较通用的CRUD还是游刃有余的&#xff0c;但在项目开发阶段&#xff0c;需求总是经常变化的&#xff0c;数据结构和逻辑也经常变化&#xff0c;我们需要的是快速验证功能逻辑&#xff0c;代码生成器可帮不上忙&#xff0c;每次需…

一、java编写登录功能

java编写登录功能 文章目录java编写登录功能前言编程学习记录一、登录逻辑简述二、代码实现1.创建USER表2.前端代码3.创建User类4.创建LoginServlet类5.创建JDBCUtils类6.创建UserDao类7.创建FailServlet类9.创建SuccessServlet 类11.配置tomcat 服务12.启动服务前言 编程学习…

SpringCloud Netfllix复习之Hystrix

文章目录写作背景Hystrix是什么Hystrix的核心功能上手实战RestTemplate整合HystrixOpenFeign整合HystrixOpenFeign与Hystrix整合的各种参数如何配置&#xff1f;源码验证基于HystrixCommand注解实现熔断源码分析初始化资源线程池的源码OpenFeign与Hystrix整合执行请求的源码写作…