ORB-SLAM2学习笔记:ComputeKeyPointsOctTree分析过程记录
ComputeKeyPointsOctTree是ORB特征提取器中计算关键点的部分,特别是使用八叉树(OctTree)方法进行关键点分布。
首先,函数参数是vector<vector的引用allKeypoints,用来存储各层的关键点。代码开头调整了allKeypoints的大小为nlevels,即金字塔层数。接下来,循环处理每一层图像金字塔。
在循环内部,首先定义了边界minBorderX和minBorderY,可能是为了避开边缘区域,因为边缘附近的关键点可能不完整。然后计算了maxBorderX和maxBorderY,确定当前层的有效区域。接着初始化vToDistributeKeys,预留空间用于存储待分配的关键点。
然后根据宽度和高度划分网格,每个网格大小为W(30)。这里计算了nCols和nRows,即网格的行列数。wCell和hCell是每个网格的实际大小,可能因为不能整除而向上取整。
接下来是双重循环遍历每个网格。对于每个网格,计算初始坐标iniY和iniX,以及最大坐标maxY和maxX。这里有一些条件判断,确保网格不超过最大边界。然后在每个网格内使用FAST算法检测关键点,首先使用iniThFAST阈值,如果没检测到则改用minThFAST阈值。
检测到关键点后,调整它们的坐标,加上网格的偏移量,并存入vToDistributeKeys。这一步是因为FAST检测是在网格的子图像上进行的,所以需要将坐标转换回原图的坐标。
之后,调用DistributeOctTree函数,将关键点按八叉树分布,筛选出符合数量要求的关键点,并存入当前层的keypoints。这里可能涉及到关键点的均匀分布和响应值排序,确保每层的关键点数量符合mnFeaturesPerLevel的设置。
接着处理每个关键点的坐标,加上minBorderX和minBorderY的偏移,设置octave(金字塔层)和size(根据缩放因子调整的尺寸)。最后,循环结束后,计算每个关键点的方向,使用computeOrientation函数。
需要注意的是,代码中的EDGE_THRESHOLD、mvImagePyramid、PATCH_SIZE、mvScaleFactor等变量可能在类的其他部分定义。此外,DistributeOctTree函数的具体实现细节会影响关键点的分布方式,可能涉及四叉树分割和响应值筛选。
void ORBextractor::ComputeKeyPointsOctTree(vector<vector<KeyPoint> >& 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(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(vector<cv::KeyPoint>::iterator 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];// Add border to coordinates and scale informationconst 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;}}// compute orientationsfor (int level = 0; level < nlevels; ++level)computeOrientation(mvImagePyramid[level], allKeypoints[level], umax);
}
以下是 ORBextractor::ComputeKeyPointsOctTree
函数的逐行解析及原理说明,结合 ORB 特征提取的算法设计与实现细节:
功能概述
该函数用于在图像金字塔各层级上提取并均匀分布 ORB 特征点,通过分块检测 FAST 角点并结合四叉树筛选策略,保证特征点分布的均匀性和尺度不变性。这是 ORB-SLAM 等视觉系统的核心步骤之一。
输入与输出
- 输入:图像金字塔
mvImagePyramid
(通过高斯模糊生成的多尺度图像集合)。 - 输出:分层的关键点集合
allKeypoints
,包含每个金字塔层级的关键点信息。
代码解析
1. 分层处理图像金字塔
allKeypoints.resize(nlevels);
for (int level = 0; level < nlevels; ++level) {// 各层参数初始化const int minBorderX = EDGE_THRESHOLD - 3;const int maxBorderX = mvImagePyramid[level].cols - EDGE_THRESHOLD + 3;// 类似处理Y方向
}
以下是代码的逐行解析,结合ORB特征提取及图像分块处理逻辑:
代码功能概述
该代码段用于在图像金字塔的指定层级上进行分块FAST特征点检测,并通过动态阈值调整和特征点再分配策略实现关键点的均匀分布,属于ORB特征提取流程中的核心步骤。
行级解析
for(int i=0; i<levelRows; i++)
{// 计算当前行的起始Y坐标(考虑边界扩展3像素)const float iniY = minBorderY + i*cellH - 3;iniYRow[i] = iniY; // 保存当前行的起始Y坐标// 处理最后一行的高度计算if(i == levelRows-1) {hY = maxBorderY + 3 - iniY; // 计算实际高度if(hY <= 0) continue; // 跳过无效行}float hX = cellW + 6; // 列的默认宽度(包含左右各3像素扩展)// 遍历每一列for(int j=0; j<levelCols; j++) {float iniX;// 第一行初始化X坐标if(i == 0) {iniX = minBorderX + j*cellW - 3;iniXCol[j] = iniX; // 保存列的起始X坐标} else {iniX = iniXCol[j]; // 复用已保存的X坐标}// 处理最后一列的宽度计算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++; // 统计无法分配的单元格数}}
}
关键设计原理
-
分块策略
- 将图像划分为
levelRows×levelCols
的网格,每个单元格独立检测特征点,确保空间均匀性。 - 边界扩展3像素(
cellW+6
和cellH+6
),避免边缘特征遗漏。
- 将图像划分为
-
动态阈值调整
- 初始阈值
iniThFAST
检测失败时,改用更低阈值minThFAST
,适应低纹理区域。
- 初始阈值
-
特征点再分配
- 通过
nToDistribute
累计需要补充的特征点数,后续步骤可能通过非极大值抑制或四叉树分割重新分配。
- 通过
-
内存优化
reserve(nfeaturesCell*5)
预分配内存,减少动态扩容开销。
应用场景
- ORB特征提取:用于SLAM(如ORB-SLAM3)、目标跟踪等需要旋转不变性和实时性的场景。
- 多尺度处理:
mvImagePyramid
支持图像金字塔,实现多尺度特征检测。
性能优化点
- 复用坐标:首行初始化
iniXCol[j]
,后续行直接复用,减少重复计算。 - 提前终止:
hY<=0
或hX<=0
时跳过无效区域,减少冗余计算。 - 并行潜力:分块结构天然适合多线程处理(需注意线程安全)。
- 目的:遍历金字塔的每一层(
nlevels
),处理不同尺度的图像。 - 边界处理:
EDGE_THRESHOLD
是特征点距图像边缘的最小距离,±3
的调整用于扩展有效检测区域,避免边缘特征不稳定。
2. 网格分块与FAST角点检测
const float W = 30;
const int nCols = width / W; // 列方向分块数
const int nRows = height / W; // 行方向分块数for (int i = 0; i < nRows; i++) {for (int j = 0; j < nCols; j++) {// 定义当前网格的坐标范围const float iniY = minBorderY + i * hCell;const float iniX = minBorderX + j * wCell;// 执行FAST角点检测FAST(mvImagePyramid[level].rowRange(iniY, maxY).colRange(iniX, maxX),vKeysCell, iniThFAST, true);}
}
- 网格划分:将当前金字塔层级的有效区域划分为
30x30
的网格,每个网格独立检测 FAST 角点,确保初步均匀性。 - FAST检测优化:
- 使用两种阈值(
iniThFAST
和minThFAST
),优先用高阈值检测强角点,若失败则改用低阈值,平衡特征点数量与质量。 - 检测到的角点坐标需根据网格位置偏移,转换为全局坐标(
(*vit).pt.x += j * wCell
)。
- 使用两种阈值(
3. 四叉树筛选关键点
keypoints = DistributeOctTree(vToDistributeKeys, minBorderX, maxBorderX,minBorderY, maxBorderY, mnFeaturesPerLevel[level], level);
- 四叉树算法:将初步检测的角点(
vToDistributeKeys
)按空间分布均匀性筛选,最终保留mnFeaturesPerLevel[level]
个关键点。- 原理:递归将区域划分为四个子节点,保留响应最强的角点,避免特征点聚集。
- 终止条件:当子节点中仅剩一个角点或达到预设特征数量时停止分割。
4. 关键点属性设置
const int scaledPatchSize = PATCH_SIZE * mvScaleFactor[level];
keypoints[i].octave = level; // 所属金字塔层级
keypoints[i].size = scaledPatchSize; // 特征点邻域直径(尺度自适应)
- 尺度不变性:
scaledPatchSize
根据金字塔层级缩放,高层级特征点对应更大的邻域,用于后续计算旋转和描述子。 - 坐标修正:将筛选后的关键点坐标还原到原始图像坐标系(
minBorderX/Y
偏移)。
5. 计算关键点方向
computeOrientation(mvImagePyramid[level], allKeypoints[level], umax);
- 灰度质心法:通过计算图像块的一阶矩(
m10
和m01
),确定关键点的主方向,实现旋转不变性。 - 预计算参数
umax
:用于加速圆形区域的像素采样,确保方向计算的效率。
关键设计原理
-
多尺度处理:
- 通过图像金字塔(
mvImagePyramid
)实现尺度不变性,高层级检测大尺度特征,低层级检测细节。 - 特征点邻域大小(
PATCH_SIZE
)随层级缩放,匹配图像分辨率变化。
- 通过图像金字塔(
-
均匀分布策略:
- 网格分块:初步分块检测避免特征点过度集中。
- 四叉树筛选:二次优化分布,保证每层特征数量与质量。
-
效率优化:
- FAST检测加速:网格化检测减少无效区域遍历,双阈值策略平衡速度与鲁棒性。
- 预计算参数:如
umax
和金字塔图像,减少运行时计算量。
应用场景
- ORB-SLAM:用于实时特征提取与匹配,支撑相机位姿估计和地图构建。
- 目标跟踪:均匀分布的特征点提升匹配稳定性,减少误匹配。
扩展说明
- 浮点坐标问题:尽管代码中关键点坐标为整数,但在高层级金字塔中,坐标经缩放后可能为浮点数(如
mvScaleFactor
作用)。 - 边界与异常处理:依赖
EDGE_THRESHOLD
避免越界,但未显式处理极端情况(如全图无角点),需前置步骤保证鲁棒性。
以下是ORBextractor::ComputeKeyPointsOctTree函数的逐行注释分析:
- 初始化多尺度关键点容器
allKeypoints.resize(nlevels); // 根据金字塔层数调整容器维度
const float W = 30; // 网格划分基准宽度
- 金字塔层级遍历
for (int level = 0; level < nlevels; ++level) {// 计算有效检测区域边界(EDGE_THRESHOLD通常取19像素)const int minBorderX = EDGE_THRESHOLD-3; // 左边界留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); // 预分配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); // 实际网格高度
- 网格遍历与FAST检测
for(int i=0; i<nRows; i++) {const float iniY = minBorderY + i*hCell; // 网格起始Y坐标float maxY = iniY + hCell + 6; // 网格终止Y坐标(+6扩展检测范围)// 边界溢出处理if(iniY >= maxBorderY-3) continue;if(maxY > maxBorderY) maxY = maxBorderY;for(int j=0; j<nCols; j++) {const float iniX = minBorderX + j*wCell; // 网格起始X坐标float maxX = iniX + wCell + 6; // 网格终止X坐标// 边界溢出处理if(iniX >= maxBorderX-6) continue;if(maxX > maxBorderX) maxX = maxBorderX;
- 双阈值FAST检测策略
vector<cv::KeyPoint> vKeysCell;// 先使用初始阈值iniThFAST检测FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),vKeysCell, iniThFAST, true); // true表示使用非极大值抑制// 若未检测到,改用更低阈值minThFASTif(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; // X坐标恢复全局坐标系(*vit).pt.y += i*hCell; // Y坐标恢复全局坐标系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];for(int i=0; i<keypoints.size(); 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);
}
核心设计解析:
- 网格化检测:通过30x30网格划分实现特征点初步均匀分布,避免特征聚集
- 双阈值策略:先使用较高阈值(iniThFAST)确保质量,失败后降阈值(minThFAST)保证数量
- 边界缓冲:±3像素的边界处理既避免越界,又保证边缘特征的有效提取
- 坐标校正:将网格局部坐标转换为金字塔层全局坐标,保持空间一致性
- 八叉树分配:通过递归区域分割和响应值排序,实现特征点最优空间分布
- 尺度自适应:scaledPatchSize根据金字塔缩放系数调整邻域大小,实现尺度不变性
注:EDGE_THRESHOLD的取值(通常为19)与ORB描述子计算相关,保证特征点周围有足够区域计算描述子。computeOrientation使用灰度质心法计算特征方向,umax为模式匹配预计算表。