目录
(10)ORBextractor::operator() 函数
(11)ORBextractor::ComputePyramid 函数
ORBextractor.h
ExtractorNode 类
方法名 | 作用 | 传入参数 |
DivideNode | 将当前节点分成四个子节点 |
|
ORBextractor 类
方法名 | 作用 | 传入参数 |
ORBextractor | 构造函数,初始化ORB提取器 |
|
~ORBextractor | 析构函数,销毁ORB提取器 | 无 |
operator() | 计算图像的ORB特征和描述子 |
|
GetLevels | 获取金字塔的层数 | 无 |
GetScaleFactor | 获取金字塔的尺度因子 | 无 |
GetScaleFactors | 获取每一层金字塔的尺度因子 | 无 |
GetInverseScaleFactors | 获取每一层金字塔的尺度因子的倒数 | 无 |
GetScaleSigmaSquares | 获取每一层金字塔的尺度方差 | 无 |
GetInverseScaleSigmaSquares | 获取每一层金字塔的尺度方差的倒数 | 无 |
ComputePyramid | 构建图像金字塔 |
|
ComputeKeyPointsOctTree | 在Octree中计算关键点 |
|
ComputeKeyPointsOld | 旧方法中计算关键点 |
|
DistributeOctTree | 在Octree中分布关键点 |
|
构造函数和析构函数
方法名 | 作用 | 传入参数 |
ORBextractor | 构造函数,初始化ORB提取器 |
|
~ORBextractor | 析构函数,销毁ORB提取器 | 无 |
ORBextractor.cc
(1) static float IC_Angle+
// 计算关键点的主方向
static float IC_Angle(const Mat& image, Point2f pt, const vector<int> & u_max)
{
// m_01 和 m_10 是图像的矩
int m_01 = 0, m_10 = 0;
// 获取图像中心点的指针
const uchar* center = &image.at<uchar>(cvRound(pt.y), cvRound(pt.x));
// 特别处理中心线,v = 0
for (int u = -HALF_PATCH_SIZE; u <= HALF_PATCH_SIZE; ++u)
m_10 += u * center[u];
// 按行处理圆形区域
int step = (int)image.step1();
for (int v = 1; v <= HALF_PATCH_SIZE; ++v)
{
// 处理两行
int v_sum = 0;
int d = u_max[v];
for (int u = -d; u <= d; ++u)
{
int val_plus = center[u + v*step], val_minus = center[u - v*step];
v_sum += (val_plus - val_minus);
m_10 += u * (val_plus + val_minus);
}
m_01 += v * v_sum;
}
// 计算并返回关键点的方向角度
return fastAtan2((float)m_01, (float)m_10);
}
方法名 | 作用 | 传入参数 |
IC_Angle | 计算关键点的主方向 |
|
详细参数说明
参数 | 类型 | 说明 |
|
| 输入的图像 |
|
| 关键点的坐标 |
|
| 限制环形区域的最大 u 值,用于优化计算 |
函数内部变量和步骤
变量/步骤 | 类型 | 说明 |
|
| 图像的矩之一,初始化为0 |
|
| 图像的矩之一,初始化为0 |
|
| 指向关键点位置的图像像素 |
|
| 图像每行的步长 |
|
| 当前行的像素值和 |
|
| 当前行的最大 u 值 |
循环中心线 |
| 遍历中心线上的像素值并计算 m_10 |
循环圆形区域 |
| 遍历圆形区域的每一行,内层循环计算每行像素的值并更新 m_10 和 m_01 |
返回值 |
| 关键点的方向角度 |
(2) computeOrbDescriptor 函数
const float factorPI = (float)(CV_PI / 180.f); // 将角度转换为弧度的因子
// 计算ORB特征描述子
static void computeOrbDescriptor(const KeyPoint& kpt,
const Mat& img, const Point* pattern,
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;
// 宏定义,用于获取旋转后的图像块的像素值
#define GET_VALUE(idx) \
center[cvRound(pattern[idx].x * b + pattern[idx].y * a) * step + \
cvRound(pattern[idx].x * a - pattern[idx].y * b)]
// 计算特征描述子
for (int i = 0; i < 32; ++i, pattern += 16)
{
int t0, t1, val;
t0 = GET_VALUE(0); t1 = GET_VALUE(1);
val = t0 < t1;
t0 = GET_VALUE(2); t1 = GET_VALUE(3);
val |= (t0 < t1) << 1;
t0 = GET_VALUE(4); t1 = GET_VALUE(5);
val |= (t0 < t1) << 2;
t0 = GET_VALUE(6); t1 = GET_VALUE(7);
val |= (t0 < t1) << 3;
t0 = GET_VALUE(8); t1 = GET_VALUE(9);
val |= (t0 < t1) << 4;
t0 = GET_VALUE(10); t1 = GET_VALUE(11);
val |= (t0 < t1) << 5;
t0 = GET_VALUE(12); t1 = GET_VALUE(13);
val |= (t0 < t1) << 6;
t0 = GET_VALUE(14); t1 = GET_VALUE(15);
val |= (t0 < t1) << 7;
desc[i] = (uchar)val;
}
#undef GET_VALUE
}
详细参数说明
参数 | 类型 | 说明 |
|
| 输入的关键点 |
|
| 输入的图像 |
|
| 用于描述子计算的点模式 |
|
| 输出的特征描述子 |
函数内部变量和步骤
变量/步骤 | 类型 | 说明 |
|
| 关键点的角度(以弧度表示) |
|
| 计算方向角度的余弦和正弦值 |
|
| 图像中关键点的中心像素指针 |
|
| 图像每行的步长 |
宏定义 | 宏 | 获取旋转后的图像块的像素值 |
| 循环 | 计算特征描述子,每个描述子包含32个字节,每字节比较16对像素 |
详细说明
computeOrbDescriptor
函数:用于计算给定关键点的ORB特征描述子。
-
- 参数:
-
-
const KeyPoint& kpt
:输入的关键点。const Mat& img
:输入图像。const Point* pattern
:用于描述子计算的点模式。uchar* desc
:输出的特征描述子。
-
- 变量初始化:
-
const float factorPI
:将角度转换为弧度的因子。float angle
:关键点的角度(以弧度表示)。float a, b
:角度的余弦和正弦值。const uchar* center
:指向图像中关键点位置的像素。int step
:图像每行的步长。
- 宏定义
GET_VALUE(idx)
:用于获取旋转后的图像块的像素值。 - 计算描述子:
-
- 使用
for
循环计算描述子,每个描述子包含32个字节,每个字节比较16对像素。 - 通过比较两个像素值,生成二进制值,并将其组合成一个字节,存储在
desc
数组中。
- 使用
(3)ORBextractor 构造函数
ORBextractor::ORBextractor(int _nfeatures, float _scaleFactor, int _nlevels,
int _iniThFAST, int _minThFAST):
nfeatures(_nfeatures), scaleFactor(_scaleFactor), nlevels(_nlevels),
iniThFAST(_iniThFAST), minThFAST(_minThFAST)
{
// 初始化尺度因子和方差
mvScaleFactor.resize(nlevels);
mvLevelSigma2.resize(nlevels);
mvScaleFactor[0] = 1.0f;
mvLevelSigma2[0] = 1.0f;
for(int i = 1; i < nlevels; i++)
{
mvScaleFactor[i] = mvScaleFactor[i-1] * scaleFactor;
mvLevelSigma2[i] = mvScaleFactor[i] * mvScaleFactor[i];
}
// 计算尺度因子的倒数和方差的倒数
mvInvScaleFactor.resize(nlevels);
mvInvLevelSigma2.resize(nlevels);
for(int i = 0; i < nlevels; i++)
{
mvInvScaleFactor[i] = 1.0f / mvScaleFactor[i];
mvInvLevelSigma2[i] = 1.0f / mvLevelSigma2[i];
}
// 初始化图像金字塔
mvImagePyramid.resize(nlevels);
// 计算每个尺度上的特征点数量
mnFeaturesPerLevel.resize(nlevels);
float factor = 1.0f / scaleFactor;
float nDesiredFeaturesPerScale = nfeatures * (1 - factor) / (1 - (float)pow((double)factor, (double)nlevels));
int sumFeatures = 0;
for(int level = 0; level < nlevels - 1; level++)
{
mnFeaturesPerLevel[level] = cvRound(nDesiredFeaturesPerScale);
sumFeatures += mnFeaturesPerLevel[level];
nDesiredFeaturesPerScale *= factor;
}
mnFeaturesPerLevel[nlevels - 1] = std::max(nfeatures - sumFeatures, 0);
// 复制模式点
const int npoints = 512;
const Point* pattern0 = (const Point*)bit_pattern_31_;
std::copy(pattern0, pattern0 + npoints, std::back_inserter(pattern));
// 预计算圆形补丁中的行结束点
umax.resize(HALF_PATCH_SIZE + 1);
int v, v0, vmax = cvFloor(HALF_PATCH_SIZE * sqrt(2.f) / 2 + 1);
int vmin = cvCeil(HALF_PATCH_SIZE * sqrt(2.f) / 2);
const double hp2 = HALF_PATCH_SIZE * HALF_PATCH_SIZE;
for(v = 0; v <= vmax; ++v)
umax[v] = cvRound(sqrt(hp2 - v * v));
// 确保对称性
for(v = HALF_PATCH_SIZE, v0 = 0; v >= vmin; --v)
{
while(umax[v0] == umax[v0 + 1])
++v0;
umax[v] = v0;
++v0;
}
}
详细参数说明
参数 | 类型 | 说明 |
|
| 所需特征点数量 |
|
| 尺度因子,用于图像金字塔 |
|
| 图像金字塔的层数 |
|
| FAST特征检测初始阈值 |
|
| FAST特征检测的最小阈值 |
构造函数内部变量和步骤
变量/步骤 | 类型 | 说明 |
|
| 尺度因子 |
|
| 尺度方差 |
|
| 尺度因子的倒数 |
|
| 尺度方差的倒数 |
|
| 图像金字塔 |
|
| 每层的特征点数量 |
|
| 特征点描述子的模式点 |
|
| 预计算圆形补丁中的行结束点 |
计算尺度因子和方差 |
| 计算每层的尺度因子和方差 |
计算尺度因子的倒数和方差的倒数 |
| 计算每层的尺度因子倒数和方差倒数 |
初始化图像金字塔 |
| 初始化图像金字塔的层数 |
计算每层的特征点数量 |
| 计算每层所需的特征点数量 |
复制模式点 |
| 复制用于描述子计算的模式点 |
预计算圆形补丁中的行结束点 |
| 预计算圆形补丁中的行结束点,并确保对称性 |
详细说明
- 初始化:
-
- 构造函数接受五个参数:
_nfeatures
、_scaleFactor
、_nlevels
、_iniThFAST
和_minThFAST
。 - 使用这些参数初始化对应的成员变量。
- 构造函数接受五个参数:
- 计算尺度因子和方差:
-
mvScaleFactor
和mvLevelSigma2
用于存储每层的尺度因子和方差。mvInvScaleFactor
和mvInvLevelSigma2
存储尺度因子和方差的倒数。
- 初始化图像金字塔:
-
mvImagePyramid
存储图像金字塔的每层。
- 计算每层的特征点数量:
-
- 根据尺度因子和所需的总特征点数量,计算每层的特征点数量并存储在
mnFeaturesPerLevel
中。
- 根据尺度因子和所需的总特征点数量,计算每层的特征点数量并存储在
- 复制模式点:
-
- 将
bit_pattern_31_
中的模式点复制到pattern
中,用于描述子计算。
- 将
- 预计算圆形补丁中的行结束点:
-
umax
存储圆形补丁中每行的结束点,用于计算关键点的方向角度。- 确保对称性,使得圆形补丁在计算时更加准确。
通过这些步骤,ORBextractor
构造函数初始化了一个ORB特征提取器实例,准备好在后续步骤中提取特征点和计算描述子。
(4)computeOrientation
函数
详细注释代码
/**
* @brief 计算所有关键点的方向角度
*
* 该函数对给定的图像中的所有关键点计算其方向角度。计算方法是基于图像块的灰度质心。
*
* @param image 输入图像
* @param keypoints 关键点列表,包含关键点的位置和其他信息
* @param umax 预计算的行结束点,用于加速计算
*/
static void computeOrientation(const Mat& image, vector<KeyPoint>& keypoints, const vector<int>& umax)
{
// 遍历所有关键点
for (vector<KeyPoint>::iterator keypoint = keypoints.begin(),
keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
{
// 计算每个关键点的方向角度,并存储在关键点的angle属性中
keypoint->angle = IC_Angle(image, keypoint->pt, umax);
}
}
这个函数的作用是在给定图像中计算所有关键点的方向角度,以便在特征描述时能够更好地对特征进行匹配和识别。具体实现是通过调用 IC_Angle
函数,计算图像块的灰度质心来确定方向角度。
以下是 DivideNode
函数的表格:
(5)DivideNode 函数
详细注释代码
/**
* @brief 将当前节点分割成四个子节点
*
* 该函数将当前节点分割成四个子节点,每个子节点代表当前节点的一个象限。函数首先计算当前节点的水平和垂直中点,然后定义每个子节点的边界。接着,将当前节点的关键点分配给对应的子节点。如果某个子节点只有一个关键点,则标记该子节点为 bNoMore。
*
* @param n1 子节点 1,用于存储当前节点的左上部分。
* @param n2 子节点 2,用于存储当前节点的右上部分。
* @param n3 子节点 3,用于存储当前节点的左下部分。
* @param n4 子节点 4,用于存储当前节点的右下部分。
*/
void ExtractorNode::DivideNode(ExtractorNode &n1, ExtractorNode &n2, ExtractorNode &n3, ExtractorNode &n4)
{
// 计算当前节点的水平和垂直中点
const int halfX = ceil(static_cast<float>(UR.x-UL.x)/2);
const int halfY = ceil(static_cast<float>(BR.y-UL.y)/2);
// 定义子节点 1 的边界并预分配内存
n1.UL = UL;
n1.UR = cv::Point2i(UL.x+halfX, UL.y);
n1.BL = cv::Point2i(UL.x, UL.y+halfY);
n1.BR = cv::Point2i(UL.x+halfX, UL.y+halfY);
n1.vKeys.reserve(vKeys.size());
// 定义子节点 2 的边界并预分配内存
n2.UL = n1.UR;
n2.UR = UR;
n2.BL = n1.BR;
n2.BR = cv::Point2i(UR.x, UL.y+halfY);
n2.vKeys.reserve(vKeys.size());
// 定义子节点 3 的边界并预分配内存
n3.UL = n1.BL;
n3.UR = n1.BR;
n3.BL = BL;
n3.BR = cv::Point2i(n1.BR.x, BL.y);
n3.vKeys.reserve(vKeys.size());
// 定义子节点 4 的边界并预分配内存
n4.UL = n3.UR;
n4.UR = n2.BR;
n4.BL = n3.BR;
n4.BR = BR;
n4.vKeys.reserve(vKeys.size());
// 将当前节点的关键点分配给对应的子节点
for(size_t i = 0; i < vKeys.size(); i++)
{
const cv::KeyPoint &kp = vKeys[i];
if (kp.pt.x < n1.UR.x)
{
if (kp.pt.y < n1.BR.y)
n1.vKeys.push_back(kp);
else
n3.vKeys.push_back(kp);
}
else if (kp.pt.y < n1.BR.y)
n2.vKeys.push_back(kp);
else
n4.vKeys.push_back(kp);
}
// 如果子节点只有一个关键点,则标记该子节点为 bNoMore
if (n1.vKeys.size() == 1)
n1.bNoMore = true;
if (n2.vKeys.size() == 1)
n2.bNoMore = true;
if (n3.vKeys.size() == 1)
n3.bNoMore = true;
if (n4.vKeys.size() == 1)
n4.bNoMore = true;
}
方法名 | 作用 | 传入参数 |
| 将当前节点分割成四个子节点 |
|
详细说明
传入参数名称 | 类型 | 描述 |
|
| 子节点 1,用于存储当前节点的左上部分。 |
|
| 子节点 2,用于存储当前节点的右上部分。 |
|
| 子节点 3,用于存储当前节点的左下部分。 |
|
| 子节点 4,用于存储当前节点的右下部分。 |
函数作用
DivideNode
函数将当前节点分割成四个子节点。每个子节点代表当前节点的一个象限。函数首先计算当前节点的水平和垂直中点,然后定义每个子节点的边界。接着,将当前节点的关键点分配给对应的子节点。如果某个子节点只有一个关键点,则标记该子节点为 bNoMore
。
相关方法
方法名 | 作用 | 传入参数 |
| 预分配内存以避免重复分配 |
|
(6)DistributeOctTree 函数
方法名 | 作用 | 传入参数 |
| 使用四叉树算法将关键点分配到图像的不同区域中,然后选择响应值最大的关键点 |
|
详细说明
传入参数名称 | 类型 | 描述 |
|
| 需要分配的关键点向量。 |
|
| 图像的最小 X 坐标。 |
|
| 图像的最大 X 坐标。 |
|
| 图像的最小 Y 坐标。 |
|
| 图像的最大 Y 坐标。 |
|
| 需要的关键点数量。 |
|
| 当前图像金字塔的层级。 |
函数作用
DistributeOctTree
函数通过四叉树算法将关键点分配到图像的不同区域中,并从每个区域中选择响应值最大的关键点,以确保选出的关键点在图像上均匀分布。
相关方法
方法名 | 作用 | 传入参数 |
| 将当前节点分割成四个子节点 |
|
详细注释代码
/**
* @brief 使用四叉树算法将关键点分配到图像的不同区域中,然后选择响应值最大的关键点
*
* 该函数通过四叉树算法将关键点分配到图像的不同区域中,并从每个区域中选择响应值最大的关键点,以确保选出的关键点在图像上均匀分布。
*
* @param vToDistributeKeys 需要分配的关键点向量。
* @param minX 图像的最小 X 坐标。
* @param maxX 图像的最大 X 坐标。
* @param minY 图像的最小 Y 坐标。
* @param maxY 图像的最大 Y 坐标。
* @param N 需要的关键点数量。
* @param level 当前图像金字塔的层级。
* @return vector<cv::KeyPoint> 均匀分布且响应值最大的关键点向量。
*/
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)
{
// 计算初始节点数量
const int nIni = round(static_cast<float>(maxX - minX) / (maxY - minY));
const float hX = static_cast<float>(maxX - minX) / nIni;
// 初始化节点列表
list<ExtractorNode> lNodes;
vector<ExtractorNode*> vpIniNodes(nIni);
// 创建初始节点并分配关键点
for (int i = 0; i < nIni; i++)
{
ExtractorNode ni;
ni.UL = cv::Point2i(hX * static_cast<float>(i), 0);
ni.UR = cv::Point2i(hX * static_cast<float>(i + 1), 0);
ni.BL = cv::Point2i(ni.UL.x, maxY - minY);
ni.BR = cv::Point2i(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);
}
// 移除没有关键点的节点或仅包含一个关键点的节点
for (auto lit = lNodes.begin(); lit != lNodes.end();)
{
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<pair<int, ExtractorNode*>> vSizeAndPointerToNode;
vSizeAndPointerToNode.reserve(lNodes.size() * 4);
// 迭代分割节点直到满足条件
while (!bFinish)
{
iteration++;
int prevSize = lNodes.size();
int nToExpand = 0;
vSizeAndPointerToNode.clear();
for (auto lit = lNodes.begin(); lit != lNodes.end();)
{
if (lit->bNoMore)
{
++lit;
continue;
}
// 分割节点
ExtractorNode n1, n2, n3, n4;
lit->DivideNode(n1, n2, n3, n4);
// 添加子节点
if (n1.vKeys.size() > 0)
{
lNodes.push_front(n1);
if (n1.vKeys.size() > 1)
{
nToExpand++;
vSizeAndPointerToNode.emplace_back(n1.vKeys.size(), &lNodes.front());
lNodes.front().lit = lNodes.begin();
}
}
if (n2.vKeys.size() > 0)
{
lNodes.push_front(n2);
if (n2.vKeys.size() > 1)
{
nToExpand++;
vSizeAndPointerToNode.emplace_back(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.emplace_back(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.emplace_back(n4.vKeys.size(), &lNodes.front());
lNodes.front().lit = lNodes.begin();
}
}
lit = lNodes.erase(lit);
}
// 判断是否完成
if (lNodes.size() >= N || lNodes.size() == prevSize)
{
bFinish = true;
}
else if (lNodes.size() + nToExpand * 3 > N)
{
// 进一步分割
while (!bFinish)
{
prevSize = lNodes.size();
auto vPrevSizeAndPointerToNode = vSizeAndPointerToNode;
vSizeAndPointerToNode.clear();
sort(vPrevSizeAndPointerToNode.begin(), vPrevSizeAndPointerToNode.end());
for (int j = vPrevSizeAndPointerToNode.size() - 1; j >= 0; j--)
{
ExtractorNode n1, n2, n3, n4;
vPrevSizeAndPointerToNode[j].second->DivideNode(n1, n2, n3, n4);
// 添加子节点
if (n1.vKeys.size() > 0)
{
lNodes.push_front(n1
);
if (n1.vKeys.size() > 1)
{
vSizeAndPointerToNode.emplace_back(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.emplace_back(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.emplace_back(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.emplace_back(n4.vKeys.size(), &lNodes.front());
lNodes.front().lit = lNodes.begin();
}
}
lNodes.erase(vPrevSizeAndPointerToNode[j].second->lit);
if (lNodes.size() >= N)
break;
}
if (lNodes.size() >= N || lNodes.size() == prevSize)
bFinish = true;
}
}
}
// 保留每个节点中响应值最大的关键点
vector<cv::KeyPoint> vResultKeys;
vResultKeys.reserve(nfeatures);
for (auto lit = lNodes.begin(); lit != lNodes.end(); lit++)
{
auto &vNodeKeys = lit->vKeys;
auto pKP = &vNodeKeys[0];
float maxResponse = pKP->response;
for (size_t k = 1; k < vNodeKeys.size(); k++)
{
if (vNodeKeys[k].response > maxResponse)
{
pKP = &vNodeKeys[k];
maxResponse = vNodeKeys[k].response;
}
}
vResultKeys.push_back(*pKP);
}
return vResultKeys;
}
(7)ComputeKeyPointsOctTree 函数
函数作用
ComputeKeyPointsOctTree
函数通过金字塔图像层次的方式,在每一层中检测并分配关键点,使用四叉树算法确保关键点在图像上均匀分布,然后计算每个关键点的方向。
传入参数
参数名称 | 类型 | 描述 |
|
| 存储每一层的所有关键点的向量。 |
详细注释代码
/**
* @brief 通过金字塔图像层次检测并分配关键点,使用四叉树算法均匀分布关键点,最后计算每个关键点的方向。
*
* @param allKeypoints 存储每一层的所有关键点的向量。
*/
void ORBextractor::ComputeKeyPointsOctTree(vector<vector<KeyPoint>>& allKeypoints)
{
// 调整 allKeypoints 的大小为金字塔层数
allKeypoints.resize(nlevels);
const float W = 30; // 分块大小
// 遍历每一层金字塔
for (int level = 0; level < nlevels; ++level)
{
const int minBorderX = EDGE_THRESHOLD - 3;
const int minBorderY = minBorderX;
const int maxBorderX = mvImagePyramid[level].cols - EDGE_THRESHOLD + 3;
const int maxBorderY = mvImagePyramid[level].rows - EDGE_THRESHOLD + 3;
vector<cv::KeyPoint> vToDistributeKeys;
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;
float maxY = iniY + hCell + 6;
if (iniY >= maxBorderY - 3)
continue;
if (maxY > maxBorderY)
maxY = maxBorderY;
for (int j = 0; j < nCols; j++)
{
const float iniX = minBorderX + j * wCell;
float maxX = iniX + wCell + 6;
if (iniX >= maxBorderX - 6)
continue;
if (maxX > maxBorderX)
maxX = maxBorderX;
vector<cv::KeyPoint> vKeysCell;
// 在当前块中检测 FAST 关键点
FAST(mvImagePyramid[level].rowRange(iniY, maxY).colRange(iniX, maxX),
vKeysCell, iniThFAST, true);
// 如果当前块中没有检测到关键点,则降低阈值再次检测
if (vKeysCell.empty())
{
FAST(mvImagePyramid[level].rowRange(iniY, maxY).colRange(iniX, maxX),
vKeysCell, minThFAST, true);
}
// 如果检测到关键点,则将其加入到待分配关键点向量中
if (!vKeysCell.empty())
{
for (auto vit = vKeysCell.begin(); vit != vKeysCell.end(); ++vit)
{
(*vit).pt.x += j * wCell;
(*vit).pt.y += i * hCell;
vToDistributeKeys.push_back(*vit);
}
}
}
}
// 获取当前层的关键点向量引用
vector<KeyPoint>& keypoints = allKeypoints[level];
keypoints.reserve(nfeatures);
// 使用四叉树算法分配关键点
keypoints = DistributeOctTree(vToDistributeKeys, minBorderX, maxBorderX,
minBorderY, maxBorderY, mnFeaturesPerLevel[level], level);
const int scaledPatchSize = PATCH_SIZE * mvScaleFactor[level];
// 为每个关键点添加边界和尺度信息
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;
keypoints[i].size = scaledPatchSize;
}
}
// 计算每个关键点的方向
for (int level = 0; level < nlevels; ++level)
computeOrientation(mvImagePyramid[level], allKeypoints[level], umax);
}
关键步骤和说明
- 初始化和边界计算:
-
- 函数开始时,初始化了一些变量,用于定义每一层金字塔图像的边界和分块大小。
- 遍历金字塔图像的每一层:
-
- 遍历每一层金字塔图像,计算该层的边界和分块大小。
- 遍历每一块并检测 FAST 关键点:
-
- 在每一块中使用 FAST 算法检测关键点。如果检测不到关键点,则降低阈值再次检测。
- 将检测到的关键点加入到待分配关键点的向量中。
- 使用四叉树算法分配关键点:
-
- 调用
DistributeOctTree
函数,使用四叉树算法分配关键点,确保关键点在图像上均匀分布。
- 调用
- 为每个关键点添加边界和尺度信息:
-
- 调整关键点的位置,添加边界和尺度信息。
- 计算关键点的方向:
-
- 调用
computeOrientation
函数,为每个关键点计算方向。
- 调用
变量名称 | 类型 | 描述 |
|
| 存储每一层的所有关键点的向量。 |
|
| 用于划分单元块的宽度,固定值为30。 |
|
| 当前处理的金字塔层级。 |
|
| 当前层图像的最小 X 边界。 |
|
| 当前层图像的最小 Y 边界。 |
|
| 当前层图像的最大 X 边界。 |
|
| 当前层图像的最大 Y 边界。 |
|
| 用于存储当前层需要分配的所有关键点。 |
|
| 当前层图像的宽度(maxBorderX - minBorderX)。 |
|
| 当前层图像的高度(maxBorderY - minBorderY)。 |
|
| 图像划分的列数。 |
|
| 图像划分的行数。 |
|
| 每个单元块的宽度。 |
|
| 每个单元块的高度。 |
|
| 当前处理的单元块的起始 Y 坐标。 |
|
| 当前处理的单元块的结束 Y 坐标。 |
|
| 当前处理的单元块的起始 X 坐标。 |
|
| 当前处理的单元块的结束 X 坐标。 |
|
| 存储当前单元块检测到的关键点。 |
|
| 存储当前层的所有关键点。 |
|
| 当前层的关键点补丁大小,按层级缩放。 |
|
| 当前层的关键点数量。 |
|
| 当前处理的行索引。 |
|
| 当前处理的列索引。 |
|
| 当前关键点的迭代器。 |
|
| 用于计算关键点方向的辅助变量,存储最大半径。 |
|
| 每一层的期望关键点数量。 |
|
| 关键点补丁大小的固定值。 |
|
| 存储每一层的缩放因子。 |
|
| 存储图像金字塔的每一层。 |
|
| 初始 FAST 角点检测阈值。 |
|
| 最小 FAST 角点检测阈值。 |
(8)ComputeKeyPointsOld 函数
函数作用
ComputeKeyPointsOld
函数在每一层金字塔图像中检测关键点,并使用分块策略确保关键点均匀分布。然后,根据得分保留最好的关键点,并计算每个关键点的方向。
传入参数
参数名称 | 类型 | 描述 |
|
| 存储每一层的所有关键点的向量。 |
详细注释代码
/**
* @brief 在每一层金字塔图像中检测并均匀分配关键点,根据得分保留最佳关键点,最后计算每个关键点的方向。
*
* @param allKeypoints 存储每一层的所有关键点的向量。
*/
void ORBextractor::ComputeKeyPointsOld(vector<vector<KeyPoint>>& allKeypoints)
{
// 调整 allKeypoints 的大小为金字塔层数
allKeypoints.resize(nlevels);
float imageRatio = (float)mvImagePyramid[0].cols / mvImagePyramid[0].rows;
// 遍历每一层金字塔
for (int level = 0; level < nlevels; ++level)
{
const int nDesiredFeatures = mnFeaturesPerLevel[level];
// 计算当前层的列数和行数
const int levelCols = sqrt((float)nDesiredFeatures / (5 * imageRatio));
const int levelRows = imageRatio * levelCols;
const int minBorderX = EDGE_THRESHOLD;
const int minBorderY = minBorderX;
const int maxBorderX = mvImagePyramid[level].cols - EDGE_THRESHOLD;
const int maxBorderY = mvImagePyramid[level].rows - EDGE_THRESHOLD;
const int W = maxBorderX - minBorderX;
const int H = maxBorderY - minBorderY;
const int cellW = ceil((float)W / levelCols);
const int cellH = ceil((float)H / levelRows);
const int nCells = levelRows * levelCols;
const int nfeaturesCell = ceil((float)nDesiredFeatures / nCells);
vector<vector<vector<KeyPoint>>> cellKeyPoints(levelRows, vector<vector<KeyPoint>>(levelCols));
vector<vector<int>> nToRetain(levelRows, vector<int>(levelCols, 0));
vector<vector<int>> nTotal(levelRows, vector<int>(levelCols, 0));
vector<vector<bool>> bNoMore(levelRows, vector<bool>(levelCols, false));
vector<int> iniXCol(levelCols);
vector<int> iniYRow(levelRows);
int nNoMore = 0;
int nToDistribute = 0;
float hY = cellH + 6;
// 遍历每一行
for (int i = 0; i < levelRows; i++)
{
const float iniY = minBorderY + i * cellH - 3;
iniYRow[i] = iniY;
if (i == levelRows - 1)
{
hY = maxBorderY + 3 - iniY;
if (hY <= 0)
continue;
}
float hX = cellW + 6;
// 遍历每一列
for (int j = 0; j < levelCols; j++)
{
float iniX;
if (i == 0)
{
iniX = minBorderX + j * cellW - 3;
iniXCol[j] = iniX;
}
else
{
iniX = iniXCol[j];
}
if (j == levelCols - 1)
{
hX = maxBorderX + 3 - iniX;
if (hX <= 0)
continue;
}
Mat cellImage = mvImagePyramid[level].rowRange(iniY, iniY + hY).colRange(iniX, iniX + hX);
cellKeyPoints[i][j].reserve(nfeaturesCell * 5);
// 在当前块中检测 FAST 关键点
FAST(cellImage, cellKeyPoints[i][j], iniThFAST, true);
// 如果当前块中没有检测到关键点,则降低阈值再次检测
if (cellKeyPoints[i][j].size() <= 3)
{
cellKeyPoints[i][j].clear();
FAST(cellImage, cellKeyPoints[i][j], minThFAST, true);
}
const int nKeys = cellKeyPoints[i][j].size();
nTotal[i][j] = nKeys;
if (nKeys > nfeaturesCell)
{
nToRetain[i][j] = nfeaturesCell;
bNoMore[i][j] = false;
}
else
{
nToRetain[i][j] = nKeys;
nToDistribute += nfeaturesCell - nKeys;
bNoMore[i][j] = true;
nNoMore++;
}
}
}
// 根据得分保留最佳关键点
while (nToDistribute > 0 && nNoMore < nCells)
{
int nNewFeaturesCell = nfeaturesCell + ceil((float)nToDistribute / (nCells - nNoMore));
nToDistribute = 0;
for (int i = 0; i < levelRows; i++)
{
for (int j = 0; j < levelCols; j++)
{
if (!bNoMore[i][j])
{
if (nTotal[i][j] > nNewFeaturesCell)
{
nToRetain[i][j] = nNewFeaturesCell;
bNoMore[i][j] = false;
}
else
{
nToRetain[i][j] = nTotal[i][j];
nToDistribute += nNewFeaturesCell - nTotal[i][j];
bNoMore[i][j] = true;
nNoMore++;
}
}
}
}
}
vector<KeyPoint>& keypoints = allKeypoints[level];
keypoints.reserve(nDesiredFeatures * 2);
const int scaledPatchSize = PATCH_SIZE * mvScaleFactor[level];
// 根据得分保留最佳关键点并转换坐标
for (int i = 0; i < levelRows; i++)
{
for (int j = 0; j < levelCols; j++)
{
vector<KeyPoint>& keysCell = cellKeyPoints[i][j];
KeyPointsFilter::retainBest(keysCell, nToRetain[i][j]);
if ((int)keysCell.size() > nToRetain[i][j])
keysCell.resize(nToRetain[i][j]);
for (size_t k = 0, kend = keysCell.size(); k < kend; k++)
{
keysCell[k].pt.x += iniXCol[j];
keysCell[k].pt.y += iniYRow[i];
keysCell[k].octave = level;
keysCell[k].size = scaledPatchSize;
keypoints.push_back(keysCell[k]);
}
}
}
if ((int)keypoints.size() > nDesiredFeatures)
{
KeyPointsFilter::retainBest(keypoints, nDesiredFeatures);
keypoints.resize(nDesiredFeatures);
}
}
// 计算每个关键点的方向
for (int level = 0; level < nlevels; ++level)
computeOrientation(mvImagePyramid[level], allKeypoints[level], umax);
}
关键步骤和说明
- 初始化和边界计算:
-
- 函数开始时,初始化了一些变量,用于定义每一层金字塔图像的边界和分块大小。
- 遍历金字塔图像的每一层:
-
- 遍历每一层金字塔图像,计算该层的边界和分块大小。
- 遍历每一块并检测 FAST 关键点:
-
- 在每一块中使用 FAST 算法检测关键点。如果检测不到关键点,则降低阈值再次检测。
- 将检测到的关键点加入到待分配关键点的向量中。
- 根据得分保留最佳关键点:
-
- 调用
KeyPointsFilter::retainBest
函数,根据得分保留最佳关键点。
- 调用
- 为每个关键点添加边界和尺度信息:
-
- 调整关键点的位置,添加边界和尺度信息。
- 计算关键点的方向:
-
- 调用
computeOrientation
函数,为每个关键点计算方向。
- 调用
这个函数通过分块策略确保关键点均匀分布,然后根据得分保留最佳关键点,并计算每个关键点的方向信息。与 ComputeKeyPointsOctTree
不同的是,这个方法更注重在每个块内检测和分配关键点。
变量名称 | 类型 | 描述 |
|
| 存储每一层的所有关键点的向量。 |
|
| 图像的宽高比。 |
|
| 当前处理的金字塔层级。 |
|
| 当前层期望检测到的关键点数量。 |
|
| 当前层划分的列数。 |
|
| 当前层划分的行数。 |
|
| 当前层图像的最小 X 边界。 |
|
| 当前层图像的最小 Y 边界。 |
|
| 当前层图像的最大 X 边界。 |
|
| 当前层图像的最大 Y 边界。 |
|
| 当前层图像的宽度。 |
|
| 当前层图像的高度。 |
|
| 每个单元块的宽度。 |
|
| 每个单元块的高度。 |
|
| 当前层图像划分的单元块总数。 |
|
| 每个单元块期望检测到的关键点数量。 |
|
| 存储每个单元块检测到的关键点。 |
|
| 存储每个单元块中保留的关键点数量。 |
|
| 存储每个单元块中检测到的总关键点数量。 |
|
| 存储每个单元块是否需要继续分配关键点的标志。 |
|
| 存储每个单元块的起始 X 坐标。 |
|
| 存储每个单元块的起始 Y 坐标。 |
|
| 需要停止分配关键点的单元块数量。 |
|
| 需要分配的关键点数量。 |
|
| 当前处理的单元块的高度(包含边界调整)。 |
|
| 当前单元块的起始 Y 坐标。 |
|
| 当前处理的单元块的宽度(包含边界调整)。 |
|
| 当前单元块的起始 X 坐标。 |
|
| 当前单元块的图像块。 |
|
| 当前单元块中检测到的关键点数量。 |
|
| 存储当前层的所有关键点。 |
|
| 当前层的关键点补丁大小,按层级缩放。 |
|
| 存储当前单元块的所有关键点。 |
|
| 当前关键点的索引。 |
|
| 当前单元块中关键点的数量。 |
|
| 用于计算关键点方向的辅助变量,存储最大半径。 |
(9)computeDescriptors
函数
/**
* @brief 计算输入关键点的描述符
*
* @param image 输入图像,用于计算描述符
* @param keypoints 输入的关键点向量
* @param descriptors 输出的描述符矩阵,每个关键点对应一个 32 维的描述符
* @param pattern ORB 描述符计算中使用的采样点模式
*/
static void computeDescriptors(const Mat& image, vector<KeyPoint>& keypoints, Mat& descriptors,
const vector<Point>& pattern)
{
// 初始化描述符矩阵,大小为关键点数量 x 32,类型为 8 位无符号整型
descriptors = Mat::zeros((int)keypoints.size(), 32, CV_8UC1);
// 遍历所有关键点
for (size_t i = 0; i < keypoints.size(); i++)
// 计算每个关键点的 ORB 描述符,并存储到描述符矩阵中对应的行
computeOrbDescriptor(keypoints[i], image, &pattern[0], descriptors.ptr((int)i));
}
变量名称 | 类型 | 描述 |
|
| 输入图像,用于计算描述符。 |
|
| 输入的关键点向量。 |
|
| 输出的描述符矩阵,每个关键点对应一个 32 维的描述符。 |
|
| ORB 描述符计算中使用的采样点模式。 |
|
| 循环变量,用于遍历关键点。 |
(10)ORBextractor::operator()
函数
/**
* @brief ORB特征点检测和描述符计算运算符重载函数
*
* @param _image 输入图像
* @param _mask 输入掩码(未使用)
* @param _keypoints 输出关键点向量
* @param _descriptors 输出描述符矩阵
*/
void ORBextractor::operator()( InputArray _image, InputArray _mask, vector<KeyPoint>& _keypoints,
OutputArray _descriptors)
{
// 如果输入图像为空,则返回
if(_image.empty())
return;
// 获取输入图像矩阵
Mat image = _image.getMat();
// 确保输入图像为8位单通道
assert(image.type() == CV_8UC1 );
// 预先计算尺度金字塔
ComputePyramid(image);
// 存储所有金字塔层的关键点
vector < vector<KeyPoint> > allKeypoints;
ComputeKeyPointsOctTree(allKeypoints); // 采用八叉树分布方法计算关键点
//ComputeKeyPointsOld(allKeypoints); // 采用旧方法计算关键点(未使用)
Mat descriptors;
// 计算所有层的关键点总数
int nkeypoints = 0;
for (int level = 0; level < nlevels; ++level)
nkeypoints += (int)allKeypoints[level].size();
// 如果没有检测到关键点,则释放描述符矩阵
if( nkeypoints == 0 )
_descriptors.release();
else
{
// 创建描述符矩阵,行数为关键点总数,列数为32(每个描述符的字节数),类型为8位无符号整型
_descriptors.create(nkeypoints, 32, CV_8U);
descriptors = _descriptors.getMat();
}
// 清空并预留关键点向量的空间
_keypoints.clear();
_keypoints.reserve(nkeypoints);
// 偏移量初始化为0
int offset = 0;
// 遍历每一层
for (int level = 0; level < nlevels; ++level)
{
// 获取当前层的关键点
vector<KeyPoint>& keypoints = allKeypoints[level];
int nkeypointsLevel = (int)keypoints.size();
// 如果当前层没有关键点,继续下一层
if(nkeypointsLevel==0)
continue;
// 预处理调整后的图像
Mat workingMat = mvImagePyramid[level].clone();
GaussianBlur(workingMat, workingMat, Size(7, 7), 2, 2, BORDER_REFLECT_101);
// 计算描述符
Mat desc = descriptors.rowRange(offset, offset + nkeypointsLevel);
computeDescriptors(workingMat, keypoints, desc, pattern);
// 更新偏移量
offset += nkeypointsLevel;
// 缩放关键点坐标
if (level != 0)
{
float scale = mvScaleFactor[level]; // 获取当前层的缩放因子
for (vector<KeyPoint>::iterator keypoint = keypoints.begin(),
keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
keypoint->pt *= scale;
}
// 将当前层的关键点添加到输出关键点向量中
_keypoints.insert(_keypoints.end(), keypoints.begin(), keypoints.end());
}
}
这个函数的主要流程如下:
- 检查输入图像是否为空,若为空则返回。
- 获取输入图像并检查其类型是否为8位单通道。
- 计算尺度金字塔。
- 使用八叉树方法计算所有层的关键点。
- 如果没有检测到任何关键点,释放描述符矩阵。否则,创建描述符矩阵。
- 清空并预留输出关键点向量的空间。
- 遍历每一层,预处理图像,计算描述符,缩放关键点坐标,并将关键点添加到输出向量中。
(11)ORBextractor::ComputePyramid
函数
/**
* @brief 计算图像金字塔的各个层
*
* @param image 输入图像
*/
void ORBextractor::ComputePyramid(cv::Mat image)
{
// 遍历每一层金字塔
for (int level = 0; level < nlevels; ++level)
{
// 获取当前层的缩放因子
float scale = mvInvScaleFactor[level];
// 计算当前层的大小
Size sz(cvRound((float)image.cols*scale), cvRound((float)image.rows*scale));
// 计算带边界的完整大小
Size wholeSize(sz.width + EDGE_THRESHOLD*2, sz.height + EDGE_THRESHOLD*2);
// 创建带边界的临时图像
Mat temp(wholeSize, image.type()), masktemp;
// 存储当前层的图像(去除边界部分)
mvImagePyramid[level] = temp(Rect(EDGE_THRESHOLD, EDGE_THRESHOLD, sz.width, sz.height));
// 计算缩放后的图像
if (level != 0)
{
// 将上一层的图像缩放至当前层大小
resize(mvImagePyramid[level-1], mvImagePyramid[level], sz, 0, 0, INTER_LINEAR);
// 添加边界
copyMakeBorder(mvImagePyramid[level], temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD,
BORDER_REFLECT_101 + BORDER_ISOLATED);
}
else
{
// 对第一层图像添加边界
copyMakeBorder(image, temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD,
BORDER_REFLECT_101);
}
}
}
这个函数的主要流程如下:
- 遍历金字塔的每一层。
- 获取当前层的缩放因子并计算当前层的图像大小。
- 计算带有边界的完整图像大小,并创建一个临时图像来存储带边界的图像。
- 存储去除边界后的当前层图像。
- 如果不是第一层,则将上一层图像缩放到当前层大小,并添加边界;否则,直接对输入图像添加边界。