淦ORB-SLAM2源码 02--ComputeKeyPointsOctTree函数(四叉树法对特征点进行平均和分发)

ComputeKeyPointsOctTree函数(四叉树法对特征点进行平均和分发)

原理

提取特征点

首先是提取特征点时就要尽量让特征点分布均匀,所以在提取时会将图片分成30*30(单位像素)的一个一个小格子(cell)来提取特征点,如下图(手绘勿见怪)
在这里插入图片描述
这里再讲一下这个里面各个颜色对应的是什么区域,里面深灰色是原图像区域,中间绿色是扩展的3像素区域,这里为什么要扩展呢?因为在提取fast角点的时候,大家知道要计算周围半径为三的像素差,那么如果这个点在最边缘上怎么办呢,这里就需要扩展出3个像素了。这3像素也不是随便扩展的,可以看到下面的代码,是根据一定的规律扩展的。
在这里插入图片描述

好了,回到正题,为什么要画分层一个一个的cell呢?最开始我也不理解,后来才明白这样做的目的就是为了在提取FAST角点的时候,能够设定不同的阈值。代码如下:
在这里插入图片描述
可以看到如果对与初始阈值在该cell里面没有提取到fast角点会尝试使用更低的阈值来提取角点,这样一来,尽可能的保证了每一个都能提取到角点。

分发特征点

特征点提取完了,那还是有可能这边一堆角点,那边一个也没有怎么办呢,现在就是来分发角点,让一个区域只有一个特征点。(这里的区域要重新划分,不是上面的cell)

划分就需要用到四叉树法了,什么是四叉树法呢,我们先来看看这个流程:

1.如果图像的宽度比较宽,就先把分成W/H份。一般的640*480的图像开始的时候只有一个node。(这里有个bug,如果图像的高大于宽的2倍,那么就会失败)
2.如果node里面的点数>1,把每个node分成四个node,如果node里面的特征点为空,就不要了,删掉。
3.新分的node的点数>1,就再分裂成4个node。如此,一直分裂。
4.终止条件:node的总数量>[公式],或者无法再进行分裂。
5.然后从每个node里面选择一个质量最好的fast点。(删除多余的特征点)

我们来看看具体的事例:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这样就完成了特征点的分发。

具体的代码实现大家可以看看后面的代码,这里分结点也是用了一个队列的结构,在同一层上采用了后加的先分裂,具体可以参考这个过程:
在这里插入图片描述

代码

ComputeKeyPointsOctTree

ComputeKeyPointsOctTree函数为特征点提取函数:

//计算四叉树的特征点,函数名字后面的OctTree只是说明了在过滤和分配特征点时所使用的方式
void ORBextractor::ComputeKeyPointsOctTree(
	vector<vector<KeyPoint> >& allKeypoints)	//所有的特征点,这里第一层vector存储的是某图层里面的所有特征点,
												//第二层存储的是整个图像金字塔中的所有图层里面的所有特征点
{
	//重新调整图像层数
    allKeypoints.resize(nlevels);

	//图像cell的尺寸,是个正方形,可以理解为边长in像素坐标
    const float W = 30;

    // 对每一层图像做处理
	//遍历所有图像
    for (int level = 0; level < nlevels; ++level)
    {
		//计算这层图像的坐标边界, NOTICE 注意这里是坐标边界,EDGE_THRESHOLD指的应该是可以提取特征点的有效图像边界,后面会一直使用“有效图像边界“这个自创名词
        const int minBorderX = EDGE_THRESHOLD-3;			//这里的3是因为在计算FAST特征点的时候,需要建立一个半径为3的圆
        const int minBorderY = minBorderX;					//minY的计算就可以直接拷贝上面的计算结果了
        const int maxBorderX = mvImagePyramid[level].cols-EDGE_THRESHOLD+3;
        const int maxBorderY = mvImagePyramid[level].rows-EDGE_THRESHOLD+3;

		//存储需要进行平均分配的特征点
        vector<cv::KeyPoint> vToDistributeKeys;
		//一般地都是过量采集,所以这里预分配的空间大小是nfeatures*10
        vToDistributeKeys.reserve(nfeatures*10);

		//计算进行特征点提取的图像区域尺寸
        const float width = (maxBorderX-minBorderX);
        const float height = (maxBorderY-minBorderY);

		//计算网格在当前层的图像有的行数和列数
        const int nCols = width/W;
        const int nRows = height/W;
		//计算每个图像网格所占的像素行数和列数
        const int wCell = ceil(width/nCols);
        const int hCell = ceil(height/nRows);

		//开始遍历图像网格,还是以行开始遍历的
        for(int i=0; i<nRows; i++)
        {
			//计算当前网格初始行坐标
            const float iniY =minBorderY+i*hCell;
			//计算当前网格最大的行坐标,这里的+6=+3+3,即考虑到了多出来3是为了cell边界像素进行FAST特征点提取用
			//前面的EDGE_THRESHOLD指的应该是提取后的特征点所在的边界,所以minBorderY是考虑了计算半径时候的图像边界
			//目测一个图像网格的大小是25*25啊
            float maxY = iniY+hCell+6;

			//如果初始的行坐标就已经超过了有效的图像边界了,这里的“有效图像”是指原始的、可以提取FAST特征点的图像区域
            if(iniY>=maxBorderY-3)
				//那么就跳过这一行
                continue;
			//如果图像的大小导致不能够正好划分出来整齐的图像网格,那么就要委屈最后一行了
            if(maxY>maxBorderY)
                maxY = maxBorderY;

			//开始列的遍历
            for(int j=0; j<nCols; j++)
            {
				//计算初始的列坐标
                const float iniX =minBorderX+j*wCell;
				//计算这列网格的最大列坐标,+6的含义和前面相同
                float maxX = iniX+wCell+6;
				//判断坐标是否在图像中
				//TODO 不太能够明白为什么要-6,前面不都是-3吗
				//!BUG  正确应该是maxBorderX-3
                if(iniX>=maxBorderX-6)
                    continue;
				//如果最大坐标越界那么委屈一下
                if(maxX>maxBorderX)
                    maxX = maxBorderX;

                // FAST提取兴趣点, 自适应阈值
				//这个向量存储这个cell中的特征点
                vector<cv::KeyPoint> vKeysCell;
				//调用opencv的库函数来检测FAST角点
                FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),	//待检测的图像,这里就是当前遍历到的图像块
                     vKeysCell,			//存储角点位置的容器
					 iniThFAST,			//检测阈值
					 true);				//使能非极大值抑制

				//如果这个图像块中使用默认的FAST检测阈值没有能够检测到角点
                if(vKeysCell.empty())
                {
					//那么就使用更低的阈值来进行重新检测
                    FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),	//待检测的图像
                         vKeysCell,		//存储角点位置的容器
						 minThFAST,		//更低的检测阈值
						 true);			//使能非极大值抑制
                }

                //当图像cell中检测到FAST角点的时候执行下面的语句
                if(!vKeysCell.empty())
                {
					//遍历其中的所有FAST角点
                    for(vector<cv::KeyPoint>::iterator vit=vKeysCell.begin(); vit!=vKeysCell.end();vit++)
                    {
						//NOTICE 到目前为止,这些角点的坐标都是基于图像cell的,现在我们要先将其恢复到当前的【坐标边界】下的坐标
						//这样做是因为在下面使用八叉树法整理特征点的时候将会使用得到这个坐标
						//在后面将会被继续转换成为在当前图层的扩充图像坐标系下的坐标
                        (*vit).pt.x+=j*wCell;
                        (*vit).pt.y+=i*hCell;
						//然后将其加入到”等待被分配“的特征点容器中
                        vToDistributeKeys.push_back(*vit);
                    }//遍历图像cell中的所有的提取出来的FAST角点,并且恢复其在整个金字塔当前层图像下的坐标
                }//当图像cell中检测到FAST角点的时候执行下面的语句
            }//开始遍历图像cell的列
        }//开始遍历图像cell的行

        //声明一个对当前图层的特征点的容器的引用
        vector<KeyPoint> & keypoints = allKeypoints[level];
		//并且调整其大小为欲提取出来的特征点个数(当然这里也是扩大了的,因为不可能所有的特征点都是在这一个图层中提取出来的)
        keypoints.reserve(nfeatures);

        // 根据mnFeatuvector<KeyPoint> & keypoints = allKeypoints[level];resPerLevel,即该层的兴趣点数,对特征点进行剔除
		//返回值是一个保存有特征点的vector容器,含有剔除后的保留下来的特征点
        //得到的特征点的坐标,依旧是在当前图层下来讲的
        keypoints = DistributeOctTree(vToDistributeKeys, 			//当前图层提取出来的特征点,也即是等待剔除的特征点
																	//NOTICE 注意此时特征点所使用的坐标都是在“半径扩充图像”下的
									  minBorderX, maxBorderX,		//当前图层图像的边界,而这里的坐标却都是在“边缘扩充图像”下的
                                      minBorderY, maxBorderY,
									  mnFeaturesPerLevel[level], 	//希望保留下来的当前层图像的特征点个数
									  level);						//当前层图像所在的图层

		//PATCH_SIZE是对于底层的初始图像来说的,现在要根据当前图层的尺度缩放倍数进行缩放得到缩放后的PATCH大小 和特征点的方向计算有关
        const int scaledPatchSize = PATCH_SIZE*mvScaleFactor[level];

        // Add border to coordinates and scale information
		//获取剔除过程后保留下来的特征点数目
        const int nkps = keypoints.size();
		//然后开始遍历这些特征点,恢复其在当前图层图像坐标系下的坐标
        for(int i=0; i<nkps ; i++)
        {
			//对每一个保留下来的特征点,恢复到相对于当前图层“边缘扩充图像下”的坐标系的坐标
            keypoints[i].pt.x+=minBorderX;
            keypoints[i].pt.y+=minBorderY;
			//记录特征点来源的图像金字塔图层
            keypoints[i].octave=level;
			//记录计算方向的patch,缩放后对应的大小, 又被称作为特征点半径
            keypoints[i].size = scaledPatchSize;
        }
    }

    // compute orientations
    //然后计算这些特征点的方向信息,注意这里还是分层计算的
    for (int level = 0; level < nlevels; ++level)
        computeOrientation(mvImagePyramid[level],	//对应的图层的图像
						   allKeypoints[level], 	//这个图层中提取并保留下来的特征点容器
						   umax);					//以及PATCH的横坐标边界
}

DistributeOctTree

在上面这个函数里面他调用了DistributeOctTree函数就是来分发特征点,删除多余的特征点。

/**
 * @brief 使用四叉树法对一个图像金字塔图层中的特征点进行平均和分发
 * 
 * @param[in] vToDistributeKeys     等待进行分配到四叉树中的特征点
 * @param[in] minX                  当前图层的图像的边界,坐标都是在“半径扩充图像”坐标系下的坐标
 * @param[in] maxX 
 * @param[in] minY 
 * @param[in] maxY 
 * @param[in] N                     希望提取出的特征点个数
 * @param[in] level                 指定的金字塔图层,并未使用
 * @return vector<cv::KeyPoint>     已经均匀分散好的特征点vector容器
 */
vector<cv::KeyPoint> ORBextractor::DistributeOctTree(const vector<cv::KeyPoint>& vToDistributeKeys, const int &minX,
                                       const int &maxX, const int &minY, const int &maxY, const int &N, const int &level)
{
    // Compute how many initial nodes
    // Step 1 根据宽高比确定初始节点数目
	//计算应该生成的初始节点个数,根节点的数量nIni是根据边界的宽高比值确定的,一般是1或者2
    // ! bug: 如果宽高比小于0.5,nIni=0, 后面hx会报错
    const int nIni = round(static_cast<float>(maxX-minX)/(maxY-minY));

	//一个初始的节点的x方向有多少个像素
    const float hX = static_cast<float>(maxX-minX)/nIni;

	//存储有提取器节点的列表
    list<ExtractorNode> lNodes;

	//存储初始提取器节点指针的vector
    vector<ExtractorNode*> vpIniNodes;

	//然后重新设置其大小
    vpIniNodes.resize(nIni);

	// Step 2 生成初始提取器节点
    for(int i=0; i<nIni; i++)
    {      
		//生成一个提取器节点
        ExtractorNode ni;

		//设置提取器节点的图像边界
		//注意这里和提取FAST角点区域相同,都是“半径扩充图像”,特征点坐标从0 开始 
        ni.UL = cv::Point2i(hX*static_cast<float>(i),0);    //UpLeft
        ni.UR = cv::Point2i(hX*static_cast<float>(i+1),0);  //UpRight
		ni.BL = cv::Point2i(ni.UL.x,maxY-minY);		        //BottomLeft
        ni.BR = cv::Point2i(ni.UR.x,maxY-minY);             //BottomRight

		//重设vkeys大小
        ni.vKeys.reserve(vToDistributeKeys.size());

		//将刚才生成的提取节点添加到列表中
		//虽然这里的ni是局部变量,但是由于这里的push_back()是拷贝参数的内容到一个新的对象中然后再添加到列表中
		//所以当本函数退出之后这里的内存不会成为“野指针”
        lNodes.push_back(ni);
		//存储这个初始的提取器节点句柄
        vpIniNodes[i] = &lNodes.back();
    }

    //Associate points to childs
    // Step 3 将特征点分配到子提取器节点中
    for(size_t i=0;i<vToDistributeKeys.size();i++)
    {
		//获取这个特征点对象
        const cv::KeyPoint &kp = vToDistributeKeys[i];
		//按特征点的横轴位置,分配给属于那个图像区域的提取器节点(最初的提取器节点)
        vpIniNodes[kp.pt.x/hX]->vKeys.push_back(kp);
    }
    
	// Step 4 遍历此提取器节点列表,标记那些不可再分裂的节点,删除那些没有分配到特征点的节点
    // ? 这个步骤是必要的吗?感觉可以省略,通过判断nIni个数和vKeys.size() 就可以吧
    list<ExtractorNode>::iterator lit = lNodes.begin();
    while(lit!=lNodes.end())
    {
		//如果初始的提取器节点所分配到的特征点个数为1
        if(lit->vKeys.size()==1)
        {
			//那么就标志位置位,表示此节点不可再分
            lit->bNoMore=true;
			//更新迭代器
            lit++;
        }
        ///如果一个提取器节点没有被分配到特征点,那么就从列表中直接删除它
        else if(lit->vKeys.empty())
            //注意,由于是直接删除了它,所以这里的迭代器没有必要更新;否则反而会造成跳过元素的情况
            lit = lNodes.erase(lit);			
        else
			//如果上面的这些情况和当前的特征点提取器节点无关,那么就只是更新迭代器 
            lit++;
    }

    //结束标志位清空
    bool bFinish = false;

	//记录迭代次数,只是记录,并未起到作用
    int iteration = 0;

	//声明一个vector用于存储节点的vSize和句柄对
	//这个变量记录了在一次分裂循环中,那些可以再继续进行分裂的节点中包含的特征点数目和其句柄
    vector<pair<int,ExtractorNode*> > vSizeAndPointerToNode;

	//调整大小,这里的意思是一个初始化节点将“分裂”成为四个,当然实际上不会有那么多,这里多分配了一些只是预防万一
    vSizeAndPointerToNode.reserve(lNodes.size()*4);

    // Step 5 根据兴趣点分布,利用4叉树方法对图像进行划分区域
    while(!bFinish)
    {
		//更新迭代次数计数器,只是记录,并未起到作用
        iteration++;

		//保存当前节点个数,prev在这里理解为“保留”比较好
        int prevSize = lNodes.size();

		//重新定位迭代器指向列表头部
        lit = lNodes.begin();

		//需要展开的节点计数,这个一直保持累计,不清零
        int nToExpand = 0;

		//因为是在循环中,前面的循环体中可能污染了这个变量,so清空这个vector
		//这个变量也只是统计了某一个循环中的点
		//这个变量记录了在一次分裂循环中,那些可以再继续进行分裂的节点中包含的特征点数目和其句柄
        vSizeAndPointerToNode.clear();

        // 将目前的子区域进行划分
		//开始遍历列表中所有的提取器节点,并进行分解或者保留
        while(lit!=lNodes.end())
        {
			//如果提取器节点只有一个特征点,
            if(lit->bNoMore)
            {
                // If node only contains one point do not subdivide and continue
				//那么就没有必要再进行细分了
                lit++;
				//跳过当前节点,继续下一个
                continue;
            }
            else
            {
                // If more than one point, subdivide
				//如果当前的提取器节点具有超过一个的特征点,那么就要进行继续细分
                ExtractorNode n1,n2,n3,n4;

				//再细分成四个子区域
                lit->DivideNode(n1,n2,n3,n4); 

                // Add childs if they contain points
				//如果这里分出来的子区域中有特征点,那么就将这个子区域的节点添加到提取器节点的列表中
				//注意这里的条件是,有特征点即可
                if(n1.vKeys.size()>0)
                {
					//注意这里也是添加到列表前面的
                    lNodes.push_front(n1);   

					//再判断其中子提取器节点中的特征点数目是否大于1
                    if(n1.vKeys.size()>1)
                    {
						//如果有超过一个的特征点,那么“待展开的节点计数++”
                        nToExpand++;

						//保存这个特征点数目和节点指针的信息
                        vSizeAndPointerToNode.push_back(make_pair(n1.vKeys.size(),&lNodes.front()));

						//?这个访问用的句柄貌似并没有用到?
                        // lNodes.front().lit 和前面的迭代的lit 不同,只是名字相同而已
                        // lNodes.front().lit是node结构体里的一个指针用来记录节点的位置
                        // 迭代的lit 是while循环里作者命名的遍历的指针名称
                        lNodes.front().lit = lNodes.begin();
                    }
                }
                //后面的操作都是相同的,这里不再赘述
                if(n2.vKeys.size()>0)
                {
                    lNodes.push_front(n2);
                    if(n2.vKeys.size()>1)
                    {
                        nToExpand++;
                        vSizeAndPointerToNode.push_back(make_pair(n2.vKeys.size(),&lNodes.front()));
                        lNodes.front().lit = lNodes.begin();
                    }
                }
                if(n3.vKeys.size()>0)
                {
                    lNodes.push_front(n3);
                    if(n3.vKeys.size()>1)
                    {
                        nToExpand++;
                        vSizeAndPointerToNode.push_back(make_pair(n3.vKeys.size(),&lNodes.front()));
                        lNodes.front().lit = lNodes.begin();
                    }
                }
                if(n4.vKeys.size()>0)
                {
                    lNodes.push_front(n4);
                    if(n4.vKeys.size()>1)
                    {
                        nToExpand++;
                        vSizeAndPointerToNode.push_back(make_pair(n4.vKeys.size(),&lNodes.front()));
                        lNodes.front().lit = lNodes.begin();
                    }
                }

                //当这个母节点expand之后就从列表中删除它了,能够进行分裂操作说明至少有一个子节点的区域中特征点的数量是>1的
                //? 分裂方式是后加的先分裂,先加的后分裂。
                lit=lNodes.erase(lit);

				//继续下一次循环,其实这里加不加这句话的作用都是一样的
                continue;
            }//判断当前遍历到的节点中是否有超过一个的特征点
        }//遍历列表中的所有提取器节点

        // Finish if there are more nodes than required features or all nodes contain just one point
        //停止这个过程的条件有两个,满足其中一个即可:
        //1、当前的节点数已经超过了要求的特征点数
        //2、当前所有的节点中都只包含一个特征点
        if((int)lNodes.size()>=N 				//判断是否超过了要求的特征点数
			|| (int)lNodes.size()==prevSize)	//prevSize中保存的是分裂之前的节点个数,如果分裂之前和分裂之后的总节点个数一样,说明当前所有的
												//节点区域中只有一个特征点,已经不能够再细分了
        {
			//停止标志置位
            bFinish = true;
        }

        // Step 6 当再划分之后所有的Node数大于要求数目时,就慢慢划分直到使其刚刚达到或者超过要求的特征点个数
        //可以展开的子节点个数nToExpand x3,是因为一分四之后,会删除原来的主节点,所以乘以3
        /**
		 * //?BUG 但是我觉得这里有BUG,虽然最终作者也给误打误撞、稀里糊涂地修复了
		 * 注意到,这里的nToExpand变量在前面的执行过程中是一直处于累计状态的,如果因为特征点个数太少,跳过了下面的else-if,又进行了一次上面的遍历
		 * list的操作之后,lNodes.size()增加了,但是nToExpand也增加了,尤其是在很多次操作之后,下面的表达式:
		 * ((int)lNodes.size()+nToExpand*3)>N
		 * 会很快就被满足,但是此时只进行一次对vSizeAndPointerToNode中点进行分裂的操作是肯定不够的;
		 * 理想中,作者下面的for理论上只要执行一次就能满足,不过作者所考虑的“不理想情况”应该是分裂后出现的节点所在区域可能没有特征点,因此将for
		 * 循环放在了一个while循环里面,通过再次进行for循环、再分裂一次解决这个问题。而我所考虑的“不理想情况”则是因为前面的一次对vSizeAndPointerToNode
		 * 中的特征点进行for循环不够,需要将其放在另外一个循环(也就是作者所写的while循环)中不断尝试直到达到退出条件。 
		 * */
        else if(((int)lNodes.size()+nToExpand*3)>N)
        {
			//如果再分裂一次那么数目就要超了,这里想办法尽可能使其刚刚达到或者超过要求的特征点个数时就退出
			//这里的nToExpand和vSizeAndPointerToNode不是一次循环对一次循环的关系,而是前者是累计计数,后者只保存某一个循环的
			//一直循环,直到结束标志位被置位
            while(!bFinish)
            {
				//获取当前的list中的节点个数
                prevSize = lNodes.size();

				//Prev这里是应该是保留的意思吧,保留那些还可以分裂的节点的信息, 这里是深拷贝
                vector<pair<int,ExtractorNode*> > vPrevSizeAndPointerToNode = vSizeAndPointerToNode;
				//清空
                vSizeAndPointerToNode.clear();

                // 对需要划分的节点进行排序,对pair对的第一个元素进行排序,默认是从小到大排序
				// 优先分裂特征点多的节点,使得特征点密集的区域保留更少的特征点
                //! 注意这里的排序规则非常重要!会导致每次最后产生的特征点都不一样。建议使用 stable_sort
                sort(vPrevSizeAndPointerToNode.begin(),vPrevSizeAndPointerToNode.end());

				//遍历这个存储了pair对的vector,注意是从后往前遍历
                for(int j=vPrevSizeAndPointerToNode.size()-1;j>=0;j--)
                {
                    ExtractorNode n1,n2,n3,n4;
					//对每个需要进行分裂的节点进行分裂
                    vPrevSizeAndPointerToNode[j].second->DivideNode(n1,n2,n3,n4);

                    // Add childs if they contain points
					//其实这里的节点可以说是二级子节点了,执行和前面一样的操作
                    if(n1.vKeys.size()>0)
                    {
                        lNodes.push_front(n1);
                        if(n1.vKeys.size()>1)
                        {
							//因为这里还有对于vSizeAndPointerToNode的操作,所以前面才会备份vSizeAndPointerToNode中的数据
							//为可能的、后续的又一次for循环做准备
                            vSizeAndPointerToNode.push_back(make_pair(n1.vKeys.size(),&lNodes.front()));
                            lNodes.front().lit = lNodes.begin();
                        }
                    }
                    if(n2.vKeys.size()>0)
                    {
                        lNodes.push_front(n2);
                        if(n2.vKeys.size()>1)
                        {
                            vSizeAndPointerToNode.push_back(make_pair(n2.vKeys.size(),&lNodes.front()));
                            lNodes.front().lit = lNodes.begin();
                        }
                    }
                    if(n3.vKeys.size()>0)
                    {
                        lNodes.push_front(n3);
                        if(n3.vKeys.size()>1)
                        {
                            vSizeAndPointerToNode.push_back(make_pair(n3.vKeys.size(),&lNodes.front()));
                            lNodes.front().lit = lNodes.begin();
                        }
                    }
                    if(n4.vKeys.size()>0)
                    {
                        lNodes.push_front(n4);
                        if(n4.vKeys.size()>1)
                        {
                            vSizeAndPointerToNode.push_back(make_pair(n4.vKeys.size(),&lNodes.front()));
                            lNodes.front().lit = lNodes.begin();
                        }
                    }

                    //删除母节点,在这里其实应该是一级子节点
                    lNodes.erase(vPrevSizeAndPointerToNode[j].second->lit);

					//判断是是否超过了需要的特征点数?是的话就退出,不是的话就继续这个分裂过程,直到刚刚达到或者超过要求的特征点个数
					//作者的思想其实就是这样的,再分裂了一次之后判断下一次分裂是否会超过N,如果不是那么就放心大胆地全部进行分裂(因为少了一个判断因此
					//其运算速度会稍微快一些),如果会那么就引导到这里进行最后一次分裂
                    if((int)lNodes.size()>=N)
                        break;
                }//遍历vPrevSizeAndPointerToNode并对其中指定的node进行分裂,直到刚刚达到或者超过要求的特征点个数

                //这里理想中应该是一个for循环就能够达成结束条件了,但是作者想的可能是,有些子节点所在的区域会没有特征点,因此很有可能一次for循环之后
				//的数目还是不能够满足要求,所以还是需要判断结束条件并且再来一次
                //判断是否达到了停止条件
                if((int)lNodes.size()>=N || (int)lNodes.size()==prevSize)
                    bFinish = true;				
            }//一直进行不进行nToExpand累加的节点分裂过程,直到分裂后的nodes数目刚刚达到或者超过要求的特征点数目
        }//当本次分裂后达不到结束条件但是再进行一次完整的分裂之后就可以达到结束条件时
    }// 根据兴趣点分布,利用4叉树方法对图像进行划分区域

    // Retain the best point in each node
    // Step 7 保留每个区域响应值最大的一个兴趣点
    //使用这个vector来存储我们感兴趣的特征点的过滤结果
    vector<cv::KeyPoint> vResultKeys;

	//调整大小为要提取的特征点数目
    vResultKeys.reserve(nfeatures);

	//遍历这个节点列表
    for(list<ExtractorNode>::iterator lit=lNodes.begin(); lit!=lNodes.end(); lit++)
    {
		//得到这个节点区域中的特征点容器句柄
        vector<cv::KeyPoint> &vNodeKeys = lit->vKeys;

		//得到指向第一个特征点的指针,后面作为最大响应值对应的关键点
        cv::KeyPoint* pKP = &vNodeKeys[0];

		//用第1个关键点响应值初始化最大响应值
        float maxResponse = pKP->response;

		//开始遍历这个节点区域中的特征点容器中的特征点,注意是从1开始哟,0已经用过了
        for(size_t k=1;k<vNodeKeys.size();k++)
        {
			//更新最大响应值
            if(vNodeKeys[k].response>maxResponse)
            {
				//更新pKP指向具有最大响应值的keypoints
                pKP = &vNodeKeys[k];
                maxResponse = vNodeKeys[k].response;
            }
        }

        //将这个节点区域中的响应值最大的特征点加入最终结果容器
        vResultKeys.push_back(*pKP);
    }

    //返回最终结果容器,其中保存有分裂出来的区域中,我们最感兴趣、响应值最大的特征点
    return vResultKeys;
}
  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: ORB-SLAM2是一款基于特征点的SLAM算,可以在实时运行中实现稠密地图的构建和定位。ORB-SLAM2的代码解析v1.2 pdf为ORB-SLAM2算代码进行详细讲解的文档。 这个文档详细介绍了ORB-SLAM2算的各个模块以及其组成部分,包括图像预处理、特征点提取、视觉里程计、回环检测、地图构建和定位等。对于每个模块,文档都进行了详细的讲解,并展示了一些代码实现和示例。 其中,ORB特征点的提取是ORB-SLAM2的一个重要特点。文档详细介绍了ORB特征点的提取与描述,并对其进行了性能优化。在视觉里程计中,文档详细介绍了基于ORB-SLAM2的相机位姿估计算,并同时对其进行了实验验证。 此外,orb-slam2码解析 v1.2 pdf还对ORB-SLAM2的一些扩展进行了介绍,如RGBD-SLAM、半稠密点云地图构建、直接视觉里程计等等。 总之,ORB-SLAM2是一个非常强大的SLAM算,通过对orb-slam2码解析 v1.2 pdf的学习,可以更好地理解其原理和实现,也为进一步研究和应用提供了参考。 ### 回答2: ORB-SLAM2是一种基于单目相机的实时稠密SLAM系统,被广泛应用于机器人、自动驾驶、增强现实等领域。ORB-SLAM2码解析v1.2 pdf是一份PDF文档,对ORB-SLAM2代码进行了详细的解析和分析。 该文档分为多个章节,首先介绍了ORB-SLAM2的概述和背景,包括SLAM系统的基本原理和ORB特征点的提取与匹配算。接着,文档对ORB-SLAM2的系统框架、流程和算进行了详细介绍,主要包括定位、建图、闭环检测和重定位等核心模块的实现细节。 文档还对ORB-SLAM2的实验结果和性能进行了评估和分析,包括系统的重定位精度、建图质量、算复杂度和实时性等指标。同时,文档还针对ORB-SLAM2的应用场景进行了讨论和展望,包括基于ORB-SLAM2的三维重建、SLAM与深度学习的融合等前沿研究方向。 总之,ORB-SLAM2码解析v1.2 pdf是一份非常有价值的文档,对想要深入了解和应用ORB-SLAM2的研究者和开发者有很大的帮助和启发作用。它不仅详细介绍了ORB-SLAM2的理论基础和实现细节,还从实验和应用角度对其性能和前景进行了评估和展望,为相关领域的技术人员提供了重要的参考和指导。 ### 回答3: ORB-SLAM2是一种基于单目或双目相机的实时视觉SLAM系统,可以在无GPS信号的情况下,通过对相机的位置和姿态的估计,构建3D环境地图。 ORB-SLAM2码解析 v1.2 PDF是一份解析ORB-SLAM2码的文档,其中包含了ORB-SLAM2的基本架构、算实现以及关键代码的详细解释。通过学习该文档,可以深入了解ORB-SLAM2的原理和实现方,从而更好地应用该系统进行SLAM操作。 该文档主要包括以下几个部分: 1.ORB-SLAM2的系统结构:介绍ORB-SLAM2的整体结构和各模块之间的关系。 2.特征提取与匹配:详细介绍ORB特征的提取和匹配算,包括ORB算原理、特征对齐和描述符生成等。 3.全局BA和回环检测:讲解ORB-SLAM2的全局优化和回环检测方,其中包括BA算流程、优化目标函数、回环检测的实现等。 4.实时定位:探讨如何实现ORB-SLAM2的实时运动估计和位姿估计,包括相机位姿估计、尺度恢复和点云重建等内容。 除此之外,该文档还对ORB-SLAM2的一些高级技术进行了讲解,如基于深度学习的特征提取、基于语义信息的地图构建等。 总之,该文档是一份非常有价值的ORB-SLAM2码解析资料,对于深入了解ORB-SLAM2的实现原理、优化方和应用场景,具有重要的参考意义。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值