图像帧是整个系统运行基础,是跟踪的输入信息;
图像帧包括主要成员有:
特征点提取器、时间戳、内参矩阵、畸变参数、特征点数量、特征点集合、特征点对应的词袋向量和特征向量、描述子、特征点对应地图点集合、网格中特征点分布集合、相机姿态、帧编号、参考帧等等;
图像帧计算整体流程如下:
第1步
收集图像金字塔构造参数;
第2步
构造图像金字塔;
第3步
计算fast关键点
第4步
特征点均云化
第5步
计算描述子
第6步
特征点网格化
收集金字塔构造参数
图像金字塔的构建主要依赖于参数:
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();
构造图像金字塔
图像金字塔的基本构造如图所示;
对于每层金字塔图像,由三部分组成:
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);
计算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);
}
}
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; //存储初始提取器节点指针的vector
vpIniNodes.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,侵权删;
计算描述子
旋转不变性:为保证旋转不变性,计算描述子所用的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
特征点网格化
将图像划分为若干个网格,每个网格存储其内包含的观点点集合,便于后续跟踪搜索;
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);
}
|
联系客服