分享

手撕ORB_SLAM2系列--跟踪--图像帧

 SLAM之路 2022-04-24

图像帧是整个系统运行基础,是跟踪的输入信息;

图像帧包括主要成员有:

特征点提取器、时间戳、内参矩阵、畸变参数、特征点数量、特征点集合、特征点对应的词袋向量和特征向量、描述子、特征点对应地图点集合、网格中特征点分布集合、相机姿态、帧编号、参考帧等等

图像帧计算整体流程如下:

第1步

收集图像金字塔构造参数;

第2步

构造图像金字塔;

第3步

计算fast关键点

第4步

特征点均云化

第5步

计算描述子

第6步

特征点网格化

01

收集金字塔构造参数

图像金字塔的构建主要依赖于参数:

1、金字塔层数,mnScaleLevels;

2、金字塔层间缩放系数,mfScaleFactor;

3、金字塔层间缩放系数的自然对数,mfLogScaleFactor;

4、各层图像缩放系数,mvScaleFactors;

5、各层图像缩放系数的倒数,mvInvScaleFactors;

6、mvLevelSigma2;

注:以小写字母m(member的首字母)开头的变量表示类的成员变量,第二(甚至到第三个)表示该成员的数据类型;

mnScaleLevels = mpORBextractorLeft->GetLevels();//获取金字塔层数mfScaleFactor = mpORBextractorLeft->GetScaleFactor(); //获取每层间的缩放因子mfLogScaleFactor = log(mfScaleFactor ); //计算每层间缩放因子的自然对数mvScaleFactors = mpORBextractorLeft->GetScaleFactors(); //获取各层图像的缩放因子mvInvScaleFactors = mpORBextractorLeft->GetInverseScaleFactors(); //获取各层图像的缩放因子的倒数mvLevelSigma2 = mpORBextractorLeft->GetScaleSigmaSquares(); //mvInvLevelSigma2 = mpORBextractorLeft>GeInversetScaleSigmaSquares();

02

构造图像金字塔

图像金字塔的基本构造如图所示;

对于每层金字塔图像,由三部分组成:

1、图像区,绿色框

2、fast关键点提取边界处理区,黑色框绿色框之间;

3、高斯模糊处理区,红色框绿色框之间;

针对红色框与绿色框的空白区域,会在图像缩放时利用特定方法补充;

具体构造方法为:

1、构造完整图像区域,即红色框区域(假定红绿边框距离30)

     sz表示图像区长宽尺寸;

     wholeSize表示红色框区长宽尺寸;

     temp表示红色框区完整图象;

     mvImagePyramid[level]是引用的temp中的绿色框区,表示原图像;

   注:640和480假设是第0层原始图像的长宽尺寸,若其他层需要根据缩放系数进行调整,即640/pow(1.2, n-1);

cv::Size sz(640,480);cv::Size wholeSize(640+30*2,480+30*2);temp(wholeSize, image.type()); mvImagePyramid[level] =temp(Rect(EDGE_THREAHOLD, EDGE_THREAHOLD,sz.width, sz.height));

2、将各层缩放后图像放入对应层的绿色框区,并对外部进行补充插值;

resize函数:依照缩放系数,将上一层绿色框区图像调整缩放至当前层图像尺寸(若提供缩放后图像尺寸sz,则x和y方向缩放系数设为0);

resize(

mvImagePyramid[level-1],  //源图像               

mvImagePyramid[level],     //目标图像    

sz,   //缩放后图像的长宽大小cv::Size类型

0,    //分别为x和y方向缩放系数;

0,            

cv::INTER_LINEAR); //缩放方法

copyMakeBorder:将源图像复制到目标图像指定区域,并对空白部分进行插值补充;

copyMakeBorder(

mvImagePyramid[level],  //源图像                            

temp,                               //目标图像                               

EDGE_THREAHOLDS,  //上下左右预留宽度

EDGE_THREAHOLDS,

EDGE_THREAHOLDS, 

EDGE_THREAHOLDS,

BORDER_REFLECT_101);//填充方法

填充方法:BORDER_REFLECT_101、BORDER_REPLICATE、BORDER_CONSTANT;

注:对第0层无需 缩放,直接将源图像复制到图像指定区域并插值;

resize(mvImagePyramid[level-1],                mvImagePyramid[level],            sz, 0, 0,            cv::INTER_LINEAR); //缩放方法
copyMakeBorder(mvImagePyramid[level], temp, EDGE_THREAHOLDS,EDGE_THREAHOLDS,EDGE_THREAHOLDS, EDGE_THREAHOLDS,BORDER_REFLECT_101);

03

计算fast关键点

1、关于各层金字塔特征点数量的分配

可根据金字塔尺寸的长度比或面积比进行分配:

mnFeaturesPerLevel.resize(nlevels) ;//vector<int>,表示每层金字塔提取特征数目
float factor =1.0f/scaleFactor;//分配比例
float nDesiredFeaturesPerScale = nfeatures*(1-factor)/(1-(float)pow(factor,nlevels);//第零层分配的特征点数量,即上图公式
int sumFeatures=0;//累计各层特征点数量
for(int level=0;level<nlevels-1;++level){ mnFeaturesPerLevel[level] = cvRound(nDesiredFeaturesPerScale);//特征数量是整数 sumFeatures+=mnFeatuesPerLevel[level]; //利用求和,保证最后一层得到剩余特征 nDesiredFeaturesPerScale*=factor; } //根据缩放系数得到相邻层的特征数量
//由于存在四舍五入情况,所以最后一层取剩余所有数量mnFeaturesPerLevel[nlevels-1] = std::max(nfeatures-sumFeatues, 0);

2、计算各层金字塔fast关键点位置

将紫色框,按照网格大小30进行分隔,逐个网格提取所有满足阈值的fast关键点,大概率最后所有网格的特征点数量之和超过该层的目标,我们将会在特征点均匀化中处理(注:紫色和绿色框间距离就是为应对边界附近的特征点),最后fast的关键点坐标是以紫色框左上角为远点

1、划分紫色框区域网格;

2、遍历网格,获得该网格的x和y的范围;

3、针对x和y的范围,求解该网格的fast关键点;

vector<cv::KeyPoint> vToDistributeKeys; //存储需进行平均分配的特征点vToDistributeKeys.reserve(nfeatures*10); //分配足够大的空间
//划分网格const int nCols = width/W; //当前层的网格列数const int nRows = height/W; //当前层网格行数const int wCell = ceil(width/nCols); //当前层的x向单个网格所占列数const int hCell = cel(height/nRows); //当前层的y向单个网格所占行数
//遍历网格,提取特征点For(int i=0;i<nRows;++i){ const float iniY = minBorderY+i*hCell; //当前网格y向初始坐标; float maxY = iniY+hCell+6; //当前网格y向最大坐标; //判断iniY和maxY是否超过边界 for(int j=0;j<nCols;++j) //开始遍历列方向 { const float iniX = minBorderX+j*wCell; //当前网格y向初始坐标; float maxX = iniX+wCell+6; //当前网格y向最大坐标; //判断iniX和maxX是否超过边界 //提取当前网格FAST兴趣点 vector<cv::KeyPoint> vKeysCell; Fast(mvImagePyramid[level].rowRange(iniY, maxY).rowRange(iniX,maxX), vKeysCell, iniThFast), //检测阈值 True) //使能非极大值抑制 //如果特征点为空,则重新调用FAST更新检测阈值为minThFAST重新检测 //遍历vKeysCell跟新特征点坐标,因为特征点探测的坐标都是基于网格 (*vit).pt.x+=j*wCell; (*vit).pt.y+=i*hCell;//恢复到基于紫色的坐标系中 vToDistributeKeys.push_back(*vit); }}

04

fast关键点均匀化并满足数量要求

1、将整个图像视为初始节点;

2、将当前节点每个都分成四个节点;

3、若划分节点数量等于当前层特征数量要求,则每个节点内取响应值最大的关键点,实现特征点均匀化;若小于当前层特征数量要求,则返回第2步;

1、准备初始节点,可分裂性、关键点归属、四个角点,节点链表;

//创建节点链表准备工作const int nIni = round(width/height);   //宽高比,决定初始生成的初始结点数const float hX =static_cast<float>(width/nIni);//初始x方向一个节点像素数量list<ExtractorNode> lNodes;  //存储有提取节点的链表,list便于删除vector<ExtractorNode*> vpIniNodes;  //存储初始提取器节点指针的vectorvpIniNodes.resize(nIni);//创建节点链表准备工作
//创建初始提取节点For(int i=0;i<nIni;++i){ ExtractorNode ni; //初始节点 ni.UL = cv::Point2f(hx*i, 0); ni.UR = cv::Point2f(hx*(i+1), 0); ni.BL = cv::Point2f(ni.UL.x, maxY-minY); ni.BR = cv::Point2f(ni.UR.x, maxY-minY); //初始节点四个角点坐标 ni.vKeys.reserve(vToDistributeKeys.size()); lNodes.push_back(ni); //初始节点加入节点总链表; vpIniNodes[i] = &lNodes.back();}//创建初始提取节点
//确定初始节点与特征点匹配关系for(size_t i=0;i<vToDistributeKeys.size();++i){ const cv::KeyPoint &kp = vToDistributeKeys[i]; //获取节点引用 vpIniNodes[kp.pt.x/hx] ->vKeys.push_back(kp); //将特征点分配对应初始节点}//确定初始节点与特征点对应关系
//确定初始节点是否可分裂状态List<ExtractorNode>::iterator lit =lNodes.begin();While(lit!=lNodes.end()){ if(lit->keys.size()==1) { lit->bNoMore = true; ++lit;} //节点中特征数为1不可分 else if(lit->vKeys.empty) lit = lNodes.erase(lit); //节点空则删除返回下一节点 else ++lit; //该节点中特征数>1,bNoMore保持初始化时false状态}//确定初始节点分裂状态
bool bFinish = false; //结束标志位 vector<pair<int, ExtractorNode*>> vSizeAndPointerToNode; //记录分裂中,可以继续分裂节点中特征数和指针vSizeAndPointerToNode.reserve(lNodes.size()*4);

2、节点分裂,并关联节点链接关系,跟踪节点链表

节点5-->节点4-->节点3-->节点2-->节点1;

节点5-->节点4-->节点3-->节点2;  //删除被分裂节点

节点9->节点8-->节点7-->节点6-->节点5-->节点4-->节点3

依序进行;

//四叉树进行划分分配特征点While(!bFinish){    int prevSize = lNodes.size();       //保存当前节点数      lit =lNodes.begin();                      //指针重新定位链表头           int nToExpand =0;                      //需要展开的节点计数          vSizeAndPointerToNode.clear();       //统计某一个循环中的点,记录可在分裂点     //分解所有母节点得到新的四叉节点,然后删除母节点,遍历整个节点     while(lit!=lNodes.end())      {   if(lit->bNoMore){  lit++;continue; }//没有要在细分,进入下一节点          else  {  ExtractorNode n1,n2,n3,n4;                     lit->DivideNode(n1,n2,n3,n4); //创建四节点,进行分裂                      if(n1.vKeys.size()>0)             //节点n2 n3 n4采取相同操作                      {   lNodes.push_front(n1);//添加到链表前;                           if(n1.vKeys.size()>1)                           {  nToExpand++; //即待分裂的节点数加1                                     vSizeAndPointerToNode.push_back(pair<n1.vKeys.size(),&lNodes.front()>);                                       //保存这个特征点数目和节点指针信息                                      lNodes.front().lit=lNodes.begin();                                  }                         }                         lit = lNodes.erase(lit);//母节点分裂完后被删除,返回下一个节点                         continue;                    }         }

注:该图片源于https://zhuanlan.zhihu.com/p/61738607,侵权删;

05

计算描述子

旋转不变性:为保证旋转不变性,计算描述子所用的pattern[提前设计好的点对]都默认为是以质心在坐标系x-y 的x轴上,所以需要将x-y坐标系下的pattern旋转到图像坐标系x’-y’中

static int bit_pattern[256*4]=

{ 8,-3,9,5

  4,2,7,-12

   。。。

}

一行四个数:

一个点两个数,两个点四个数,

对比两个点的灰度值,确定1bit

256行产生256bit描述子

       pattern已经将pattern转化成512个点;

图像金字塔各层特征点描述子形式如下:

描述子计算如下

Static void computeOrbDescriptor(  const KeyPoint& kpt, //特征点坐标  const Mat& img, //特征对应图像  const Point* pattern, //orb定义点位置  uchar* desc)//描述子存放位置{  float angle = (float)kpt.angle*factorPI;   //特征点角度用弧度制表示   float a =(float)cos(angle), b=(float)sin(angle);
//图像中心指针const uchar* center = &img.at<uchar>(cvRound(kpt.pt.y), cvRound(kpt.pt.x)); //图像每行字节数const int step = (int)img.step; //旋转去坐标(x,y),旋转后(x’,y,);//对应关系:x’=xcosθ-ysinθ,y’=xsin(θ)+ycos(θ)//下面表示y’*step+x’,//y代表行,x表示列,根据地址定位位置#define GET_VALUES(idx) center[cvRound(pattern[idx].x*b + pattern[idx].y*a)*step + cvRound(pattern[idx].x*a - pattern[idx].y*b)] ;//描述子是由32*8位构成,每一位是由两个像素点灰度值直接比较得到for(int i;i<32;i++,pattern+=16){ int t0, t1, val; //第一第二个特征点的灰度值,val是比较结果 t0 = GET_VALUES(0); t1=GET_VALUES(1); val=t0<t1; t0 = GET_VALUES(2); t1=GET_VALUES(3); val | = ( t0<t1) << 1; t0 = GET_VALUES(4); t1=GET_VALUES(5); val | = ( t0<t1) << 2; //以此类推 t0 = GET_VALUES(14); t1=GET_VALUES(15); val | = ( t0<t1) << 7; desc[i] = (uchar)val } #undef GET_VALUES

06

特征点网格化

将图像划分为若干个网格,每个网格存储其内包含的观点点集合,便于后续跟踪搜索;

Void Frame::AssignFeaturesToGrid(){    //给网格数组预分配空间   int nReserve =0.5f*N/(FRAME_GRID_COLS*FRAME_GRID_ROWS);   for(unsigned int i=0;i<FRAME_GRID_COLS;i++)       for(unsigned int j=0;j<FRAME_GRID_ROWS;j++)               mGrid[i][j].reserve(nReserve);
   //遍历特征点,将特征点在mvKeysUn中的索引值放到网格mGrid中     for(int i=0;i<N;i++)     {    const cv::KeyPoint &kp = mvKeysUn[i];           int nGridPosX, nGridPoxy;           if(PosInGrid(kp, nGridPosX, nGridPosY))                  mGrid[nGridPosX][nGridPosY].push_back(i);      }|

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多