ORB-SLAM2算法7详细了解了System
主类和多线程、ORB-SLAM2学习笔记8详细了解了图像特征点提取和描述子的生成、ORB-SLAM2算法9详细了解了图像帧及ORB-SLAM2算法10详细了解了图像关键帧,本文继续学习ORB-SLAM2
中的地图点MapPoint
类,该类中主要包含增、删、替换地图点及地图点观测关系,计算描述子,更新法向量和深度值等围绕地图点操作的函数。
MapPoint
类有两个构造函数,分别对应关键帧和普通帧。
/**
* @brief 给定坐标和关键帧构造MapPoint
*
* @param[in] Pos MapPoint的坐标(世界坐标系)
* @param[in] pRefKF 关键帧
* @param[in] pMap 地图
*/
MapPoint::MapPoint(const cv::Mat &Pos, //地图点的世界坐标
KeyFrame *pRefKF, //生成地图点的关键帧
Map* pMap): //地图点所存在的地图
mnFirstKFid(pRefKF->mnId), //第一次观测/生成它的关键帧 id
mnFirstFrame(pRefKF->mnFrameId), //创建该地图点的帧ID(因为关键帧也是帧啊)
nObs(0), //被观测次数
mnTrackReferenceForFrame(0), //放置被重复添加到局部地图点的标记
mnLastFrameSeen(0), //是否决定判断在某个帧视野中的变量
mnBALocalForKF(0), //
mnFuseCandidateForKF(0), //
mnLoopPointForKF(0), //
mnCorrectedByKF(0), //
mnCorrectedReference(0), //
mnBAGlobalForKF(0), //
mpRefKF(pRefKF), //
mnVisible(1), //在帧中的可视次数
mnFound(1), //被找到的次数 和上面的相比要求能够匹配上
mbBad(false), //坏点标记
mpReplaced(static_cast<MapPoint*>(NULL)), //替换掉当前地图点的点
mfMinDistance(0), //当前地图点在某帧下,可信赖的被找到时其到关键帧光心距离的下界
mfMaxDistance(0), //上界
mpMap(pMap) //从属地图
{
Pos.copyTo(mWorldPos);
//平均观测方向初始化为0
mNormalVector = cv::Mat::zeros(3,1,CV_32F);
// MapPoints can be created from Tracking and Local Mapping. This mutex avoid conflicts with id.
unique_lock<mutex> lock(mpMap->mMutexPointCreation);
mnId=nNextId++;
}
/*
* @brief 给定坐标与普通帧构造MapPoint
*
* 双目:UpdateLastFrame()
* @param Pos MapPoint的坐标(世界坐标系)
* @param pMap Map
* @param pFrame Frame
* @param idxF MapPoint在Frame中的索引,即对应的特征点的编号
*/
MapPoint::MapPoint(const cv::Mat &Pos, Map* pMap, Frame* pFrame, const int &idxF):
mnFirstKFid(-1), mnFirstFrame(pFrame->mnId), nObs(0), mnTrackReferenceForFrame(0), mnLastFrameSeen(0),
mnBALocalForKF(0), mnFuseCandidateForKF(0),mnLoopPointForKF(0), mnCorrectedByKF(0),
mnCorrectedReference(0), mnBAGlobalForKF(0), mpRefKF(static_cast<KeyFrame*>(NULL)), mnVisible(1),
mnFound(1), mbBad(false), mpReplaced(NULL), mpMap(pMap)
{
Pos.copyTo(mWorldPos);
cv::Mat Ow = pFrame->GetCameraCenter();
mNormalVector = mWorldPos - Ow;// 世界坐标系下相机到3D点的向量 (当前关键帧的观测方向)
mNormalVector = mNormalVector/cv::norm(mNormalVector);// 单位化
//这个算重了吧
cv::Mat PC = Pos - Ow;
const float dist = cv::norm(PC); //到相机的距离
const int level = pFrame->mvKeysUn[idxF].octave;
const float levelScaleFactor = pFrame->mvScaleFactors[level];
const int nLevels = pFrame->mnScaleLevels;
// 另见 PredictScale 函数前的注释
/* 因为在提取特征点的时候, 考虑到了图像的尺度问题,因此在不同图层上提取得到的特征点,对应着特征点距离相机的远近
不同, 所以在这里生成地图点的时候,也要再对其进行确认
虽然我们拿不到每个图层之间确定的尺度信息,但是我们有缩放比例这个相对的信息哇
*/
mfMaxDistance = dist*levelScaleFactor; //当前图层的"深度"
mfMinDistance = mfMaxDistance/pFrame->mvScaleFactors[nLevels-1]; //该特征点上一个图层的"深度""
// 见 mDescriptor 在MapPoint.h中的注释 ==> 其实就是获取这个地图点的描述子
pFrame->mDescriptors.row(idxF).copyTo(mDescriptor);
// MapPoints can be created from Tracking and Local Mapping. This mutex avoid conflicts with id.
// TODO 不太懂,怎么个冲突法?
unique_lock<mutex> lock(mpMap->mMutexPointCreation);
mnId=nNextId++;
}
MapPoint
类中的成员函数一览表:
成员函数 | 类型 | 定义 |
---|---|---|
void MapPoint::SetWorldPos(const cv::Mat &Pos) |
public |
设置地图点在世界坐标系下的坐标 |
cv::Mat MapPoint::GetWorldPos() |
public |
获取地图点在世界坐标系下的坐标 |
cv::Mat MapPoint::GetNormal() |
public |
世界坐标系下地图点被多个相机观测的平均观测方向 |
KeyFrame* MapPoint::GetReferenceKeyFrame() |
public |
获取地图点的参考关键帧 |
void MapPoint::AddObservation(KeyFrame* pKF, size_t idx) |
public |
给地图点添加观测 |
void MapPoint::EraseObservation(KeyFrame* pKF) |
public |
删除某个关键帧对当前地图点的观测 |
map |
public |
能够观测到当前地图点的所有关键帧及该地图点在KF中的索引 |
int MapPoint::Observations() |
public |
被观测到的相机数目,单目+1 ,双目或RGB-D 则+2 |
void MapPoint::SetBadFlag() |
public |
告知可以观测到该MapPoint 的Frame ,该MapPoint 已被删除 |
void MapPoint::Replace(MapPoint* pMP) |
public |
替换地图点,更新观测关系 |
bool MapPoint::isBad() |
public |
没有经过 MapPointCulling 检测的MapPoints , 认为是坏掉的点 |
void MapPoint::IncreaseVisible(int n);void MapPoint::IncreaseFound(int n) |
public |
找到地图点和帧的特征点能匹配后+1 |
float MapPoint::GetFoundRatio() |
public |
计算被找到的比例 |
void MapPoint::ComputeDistinctiveDescriptors() |
public |
计算地图点最具代表性的描述子 |
cv::Mat MapPoint::GetDescriptor() |
public |
获取当前地图点的描述子 |
int MapPoint::GetIndexInKeyFrame(KeyFrame *pKF) |
public |
获取当前地图点在某个关键帧的观测中,对应的特征点的ID |
bool MapPoint::IsInKeyFrame(KeyFrame *pKF) |
public |
检查该地图点是否在关键帧中(有对应的二维特征点) |
void MapPoint::UpdateNormalAndDepth() |
public |
更新地图点的平均观测方向、观测距离范围 |
float MapPoint::GetMinDistanceInvariance() |
public |
获得最小的平均观测距离 |
float MapPoint::GetMinDistanceInvariance() |
public |
获得最大的平均观测距离 |
int MapPoint::PredictScale(const float ¤tDist, KeyFrame* pKF) |
public |
预测地图点对应特征点所在的图像金字塔尺度层数 |
int MapPoint::PredictScale(const float ¤tDist, Frame* pF) |
public |
根据地图点到光心的距离来预测一个类似特征金字塔的尺度 |
以下对一些重点的成员函数进行详细介绍:
该函数主要功能是新增地图点的观测关系,当然,首先判断是否已存在观测关系,如果不存在就新增,而且分成单目和双目两种情况,单目时观测次数加1
,双目时观测次数加2
。
// 新增地图点的观测关系
void MapPoint::AddObservation(KeyFrame* pKF, size_t idx)
{
unique_lock<mutex> lock(mMutexFeatures);
// mObservations:观测到该MapPoint的关键帧KF和该MapPoint在KF中的索引
// 如果已经添加过观测,返回
if(mObservations.count(pKF))
return;
// 如果没有添加过观测,记录下能观测到该MapPoint的KF和该MapPoint在KF中的索引
mObservations[pKF]=idx;
if(pKF->mvuRight[idx]>=0)
nObs+=2; // 双目或者rgbd
else
nObs++; // 单目
}
该函数主要功能是删除地图点观测关系,首先判断关键帧是否在观测中,如果在,就先从容器mObservations
中移除该关键帧,然后判断该帧是否是参考关键帧,如果是,参考关键帧换成观测的第一帧,删除以后,如果该地图点被观测次数小于2,就删除该地图点。
// 删除某个关键帧对当前地图点的观测
void MapPoint::EraseObservation(KeyFrame* pKF)
{
bool bBad=false;
{
unique_lock<mutex> lock(mMutexFeatures);
// 查找这个要删除的观测,根据单目和双目类型的不同从其中删除当前地图点的被观测次数
if(mObservations.count(pKF))
{
int idx = mObservations[pKF];
if(pKF->mvuRight[idx]>=0)
nObs-=2;
else
nObs--;
mObservations.erase(pKF);
// 如果该keyFrame是参考帧,该Frame被删除后重新指定RefFrame
if(mpRefKF==pKF)
mpRefKF=mObservations.begin()->first;
// If only 2 observations or less, discard point
// 当观测到该点的相机数目少于2时,丢弃该点
if(nObs<=2)
bBad=true;
}
}
if(bBad)
// 告知可以观测到该MapPoint的Frame,该MapPoint已被删除
SetBadFlag();
}
该函数的主要功能是删除地图点,并清除关键帧和地图中所有和该地图点对应的关联关系。该函数在1.2.2``EraseObservation
函数中的最后一步调用。
// 告知可以观测到该MapPoint的Frame,该MapPoint已被删除
void MapPoint::SetBadFlag()
{
map<KeyFrame*,size_t> obs;
{
unique_lock<mutex> lock1(mMutexFeatures);
unique_lock<mutex> lock2(mMutexPos);
mbBad=true;
// 把mObservations转存到obs,obs和mObservations里存的是指针,赋值过程为浅拷贝
obs = mObservations;
// 把mObservations指向的内存释放,obs作为局部变量之后自动删除
mObservations.clear();
}
for(map<KeyFrame*,size_t>::iterator mit=obs.begin(), mend=obs.end(); mit!=mend; mit++)
{
KeyFrame* pKF = mit->first;
// 告诉可以观测到该MapPoint的KeyFrame,该MapPoint被删了
pKF->EraseMapPointMatch(mit->second);
}
// 擦除该MapPoint申请的内存
mpMap->EraseMapPoint(this);
}
该函数的主要功能是替换掉当前地图点,函数输入是待替换的地图点,首先判断是否是同一个地图点,如果是直接跳过,然后清除当前地图点的信息,和上一个SetBadFlag
函数差不多,然后将当前地图点的观测数据等都替换到新的地图点上,最后将观测到当前地图点的关键帧的信息进行更新。
// 替换地图点,更新观测关系
void MapPoint::Replace(MapPoint* pMP)
{
// 同一个地图点则跳过
if(pMP->mnId==this->mnId)
return;
//要替换当前地图点,有两个工作:
// 1. 将当前地图点的观测数据等其他数据都"叠加"到新的地图点上
// 2. 将观测到当前地图点的关键帧的信息进行更新
// 清除当前地图点的信息,这一段和SetBadFlag函数相同
int nvisible, nfound;
map<KeyFrame*,size_t> obs;
{
unique_lock<mutex> lock1(mMutexFeatures);
unique_lock<mutex> lock2(mMutexPos);
obs=mObservations;
//清除当前地图点的原有观测
mObservations.clear();
//当前的地图点被删除了
mbBad=true;
//暂存当前地图点的可视次数和被找到的次数
nvisible = mnVisible;
nfound = mnFound;
//指明当前地图点已经被指定的地图点替换了
mpReplaced = pMP;
}
// 所有能观测到原地图点的关键帧都要复制到替换的地图点上
//- 将观测到当前地图的的关键帧的信息进行更新
for(map<KeyFrame*,size_t>::iterator mit=obs.begin(), mend=obs.end(); mit!=mend; mit++)
{
// Replace measurement in keyframe
KeyFrame* pKF = mit->first;
if(!pMP->IsInKeyFrame(pKF))
{
// 该关键帧中没有对"要替换本地图点的地图点"的观测
pKF->ReplaceMapPointMatch(mit->second, pMP);// 让KeyFrame用pMP替换掉原来的MapPoint
pMP->AddObservation(pKF,mit->second);// 让MapPoint替换掉对应的KeyFrame
}
else
{
// 这个关键帧对当前的地图点和"要替换本地图点的地图点"都具有观测
// 产生冲突,即pKF中有两个特征点a,b(这两个特征点的描述子是近似相同的),这两个特征点对应两个 MapPoint 为this,pMP
// 然而在fuse的过程中pMP的观测更多,需要替换this,因此保留b与pMP的联系,去掉a与this的联系
//说白了,既然是让对方的那个地图点来代替当前的地图点,就是说明对方更好,所以删除这个关键帧对当前帧的观测
pKF->EraseMapPointMatch(mit->second);
}
}
//- 将当前地图点的观测数据等其他数据都"叠加"到新的地图点上
pMP->IncreaseFound(nfound);
pMP->IncreaseVisible(nvisible);
//描述子更新
pMP->ComputeDistinctiveDescriptors();
//告知地图,删掉我
mpMap->EraseMapPoint(this);
}
该函数主要功能是计算最匹配的描述子,由于一个地图点会被多个相机观测到,在插入关键帧后,需判断是否更新当前点的最适合的描述子,最好的描述子与其他描述子应该具有最小的平均距离。
vDescriptors
中;// 计算地图点最具代表性的描述子
void MapPoint::ComputeDistinctiveDescriptors()
{
// Retrieve all observed descriptors
vector<cv::Mat> vDescriptors;
map<KeyFrame*,size_t> observations;
// Step 1 获取该地图点所有有效的观测关键帧信息
{
unique_lock<mutex> lock1(mMutexFeatures);
if(mbBad)
return;
observations=mObservations;
}
if(observations.empty())
return;
vDescriptors.reserve(observations.size());
// Step 2 遍历观测到该地图点的所有关键帧,对应的orb描述子,放到向量vDescriptors中
for(map<KeyFrame*,size_t>::iterator mit=observations.begin(), mend=observations.end(); mit!=mend; mit++)
{
// mit->first取观测到该地图点的关键帧
// mit->second取该地图点在关键帧中的索引
KeyFrame* pKF = mit->first;
if(!pKF->isBad())
// 取对应的描述子向量
vDescriptors.push_back(pKF->mDescriptors.row(mit->second));
}
if(vDescriptors.empty())
return;
// Compute distances between them
// Step 3 计算这些描述子两两之间的距离
// N表示为一共多少个描述子
const size_t N = vDescriptors.size();
// 将Distances表述成一个对称的矩阵
// float Distances[N][N];
std::vector<std::vector<float> > Distances;
Distances.resize(N, vector<float>(N, 0));
for (size_t i = 0; i<N; i++)
{
// 和自己的距离当然是0
Distances[i][i]=0;
// 计算并记录不同描述子距离
for(size_t j=i+1;j<N;j++)
{
int distij = ORBmatcher::DescriptorDistance(vDescriptors[i],vDescriptors[j]);
Distances[i][j]=distij;
Distances[j][i]=distij;
}
}
// Take the descriptor with least median distance to the rest
// Step 4 选择最有代表性的描述子,它与其他描述子应该具有最小的距离中值
int BestMedian = INT_MAX; // 记录最小的中值
int BestIdx = 0; // 最小中值对应的索引
for(size_t i=0;i<N;i++)
{
// 第i个描述子到其它所有描述子之间的距离
// vector vDists(Distances[i],Distances[i]+N);
vector<int> vDists(Distances[i].begin(), Distances[i].end());
sort(vDists.begin(), vDists.end());
// 获得中值
int median = vDists[0.5*(N-1)];
// 寻找最小的中值
if(median<BestMedian)
{
BestMedian = median;
BestIdx = i;
}
}
{
unique_lock<mutex> lock(mMutexFeatures);
mDescriptor = vDescriptors[BestIdx].clone();
}
}
该函数主要功能是更新法向量和深度值,图像提取描述子是使用金字塔分层提取,那计算法向量和深度后,可知MapPoint
在对应的关键帧的金字塔哪一层可以提取到。
法向量:相机光心指向地图点的方向,计算公式是地图点的三维坐标减去相机光心的三维坐标。
// 更新地图点的平均观测方向、观测距离范围
void MapPoint::UpdateNormalAndDepth()
{
// Step 1 获得观测到该地图点的所有关键帧、坐标等信息
map<KeyFrame*,size_t> observations;
KeyFrame* pRefKF;
cv::Mat Pos;
{
unique_lock<mutex> lock1(mMutexFeatures);
unique_lock<mutex> lock2(mMutexPos);
if(mbBad)
return;
observations=mObservations; // 获得观测到该地图点的所有关键帧
pRefKF=mpRefKF; // 观测到该点的参考关键帧(第一次创建时的关键帧)
Pos = mWorldPos.clone(); // 地图点在世界坐标系中的位置
}
if(observations.empty())
return;
// Step 2 计算该地图点的平均观测方向
// 能观测到该地图点的所有关键帧,对该点的观测方向归一化为单位向量,然后进行求和得到该地图点的朝向
// 初始值为0向量,累加为归一化向量,最后除以总数n
cv::Mat normal = cv::Mat::zeros(3,1,CV_32F);
int n=0;
for(map<KeyFrame*,size_t>::iterator mit=observations.begin(), mend=observations.end(); mit!=mend; mit++)
{
KeyFrame* pKF = mit->first;
cv::Mat Owi = pKF->GetCameraCenter();
// 获得地图点和观测到它关键帧的向量并归一化
cv::Mat normali = mWorldPos - Owi;
normal = normal + normali/cv::norm(normali);
n++;
}
cv::Mat PC = Pos - pRefKF->GetCameraCenter(); // 参考关键帧相机指向地图点的向量(在世界坐标系下的表示)
const float dist = cv::norm(PC); // 该点到参考关键帧相机的距离
const int level = pRefKF->mvKeysUn[observations[pRefKF]].octave; // 观测到该地图点的当前帧的特征点在金字塔的第几层
const float levelScaleFactor = pRefKF->mvScaleFactors[level]; // 当前金字塔层对应的尺度因子,scale^n,scale=1.2,n为层数
const int nLevels = pRefKF->mnScaleLevels; // 金字塔总层数,默认为8
{
unique_lock<mutex> lock3(mMutexPos);
// 使用方法见PredictScale函数前的注释
mfMaxDistance = dist*levelScaleFactor; // 观测到该点的距离上限
mfMinDistance = mfMaxDistance/pRefKF->mvScaleFactors[nLevels-1]; // 观测到该点的距离下限
mNormalVector = normal/n; // 获得地图点平均的观测方向
}
}
// 下图中横线的大小表示不同图层图像上的一个像素表示的真实物理空间中的大小
// ____
// Nearer /____\ level:n-1 --> dmin
// /______\ d/dmin = 1.2^(n-1-m)
// /________\ level:m --> d
// /__________\ dmax/d = 1.2^m
// Farther /____________\ level:0 --> dmax
//
// log(dmax/d)
// m = ceil(------------)
// log(1.2)
该函数的主要功能是预测特征点在金字塔哪一层可以找到,如上所示,在进行投影匹配的时候会给定特征点的搜索范围,考虑到处于不同尺度(也就是距离相机远近,位于图像金字塔中不同图层)的特征点受到相机旋转的影响不同,因此会希望距离相机近的点的搜索范围更大一点,距离相机更远的点的搜索范围更小一点,所以要在这里,根据点到关键帧/帧的距离来估计它在当前的关键帧/帧中。
/**
* @brief 预测地图点对应特征点所在的图像金字塔尺度层数
*
* @param[in] currentDist 相机光心距离地图点距离
* @param[in] pKF 关键帧
* @return int 预测的金字塔尺度
*/
int MapPoint::PredictScale(const float ¤tDist, KeyFrame* pKF)
{
float ratio;
{
unique_lock<mutex> lock(mMutexPos);
// mfMaxDistance = ref_dist*levelScaleFactor 为参考帧考虑上尺度后的距离
// ratio = mfMaxDistance/currentDist = ref_dist/cur_dist
ratio = mfMaxDistance/currentDist;
}
// 取对数
int nScale = ceil(log(ratio)/pKF->mfLogScaleFactor);
if(nScale<0)
nScale = 0;
else if(nScale>=pKF->mnScaleLevels)
nScale = pKF->mnScaleLevels-1;
return nScale;
}
/**
* @brief 根据地图点到光心的距离来预测一个类似特征金字塔的尺度
*
* @param[in] currentDist 地图点到光心的距离
* @param[in] pF 当前帧
* @return int 尺度
*/
int MapPoint::PredictScale(const float ¤tDist, Frame* pF)
{
float ratio;
{
unique_lock<mutex> lock(mMutexPos);
ratio = mfMaxDistance/currentDist;
}
int nScale = ceil(log(ratio)/pF->mfLogScaleFactor);
if(nScale<0)
nScale = 0;
else if(nScale>=pF->mnScaleLevels)
nScale = pF->mnScaleLevels-1;
return nScale;
}
在ORB-SLAM2
中,MapPoint
类用于表示地图中的一个特征点或地图点。每个MapPoint
对象表示场景中的一个3D
点,该点由多个帧中的特征描述子观测到。以下是MapPoint
类的一些主要作用和用途:
特征点表示:MapPoint
对象保存了一个特征点的位置信息(3D
坐标),以及它在多个帧中的观测信息(例如,特征描述子、观测帧等)。
地图构建:MapPoint
对象用于构建ORB-SLAM2
的稀疏地图。通过观测到的特征点,ORB-SLAM2
可以估计相机的轨迹并构建场景的3D
模型。
相机姿态估计:MapPoint
对象与关键帧(KeyFrame
)之间的观测关系可用于优化相机的姿态估计。通过三角化算法,ORB-SLAM2
可以从多个观测到同一MapPoint
的关键帧中估计出相机和地图点的相对姿态。
重定位和回环检测:MapPoint
对象用于重定位和回环检测。在重定位时,ORB-SLAM2
可以根据当前帧与地图中的MapPoint
的匹配关系来估计相机的姿态。在回环检测中,ORB-SLAM2
可以使用地图点的描述子与历史帧进行匹配,以判断是否遇到了先前观测过的场景。
总之,MapPoint
类在ORB-SLAM2
中扮演着重要的角色,用于表示地图中的3D
点以及与之相关的观测信息,为场景恢复、姿态估计、重定位和回环检测等任务提供支持。
还要重点关注下地图点MapPoint
的三个生命周期:
MapPoint
的时机:Tracking
线程中初始化过程Tracking::MonocularInitialization()
和Tracking::StereoInitialization()
;Tracking
线程中创建新的关键帧Tracking::CreateNewKeyFrame()
;Tracking
线程中恒速运动模型跟踪Tracking::TrackWithMotionModel()
也会产生临时地图点,但这些临时地图点在跟踪成功后会被马上删除。如果跟踪失败,则不会产生关键帧,这些地图点也不会被注册进地图;LocalMapping
线程中创建新地图点的步骤LocalMapping::CreateNewMapPoints()
会将当前关键帧与前一关键帧进行匹配,生成新地图点。MapPoint
的时机:LocalMapping
线程中删除恶劣地图点的步骤LocalMapping::MapPointCulling()
;KeyFrame::SetBadFlag()
会调用函数MapPoint::EraseObservation()
删除地图点对关键帧的观测,若地图点对关键帧的观测少于2
,则地图点无法被三角化,就删除该地图点。MapPoint
的时机:LoopClosing
线程中闭环矫正LoopClosing::CorrectLoop()
时当前关键帧和闭环关键帧上的地图点发生冲突时,会使用闭环关键帧的地图点替换当前关键帧的地图点;LoopClosing
线程中闭环矫正函数LoopClosing::CorrectLoop()
会调用LoopClosing::SearchAndFuse()
将闭环关键帧的共视关键帧组中所有地图点投影到当前关键帧的共视关键帧组中,发生冲突时就会替换。Reference:
⭐️