orb_slam3_details/src/Sim3Solver.cc

620 lines
22 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* This file is part of ORB-SLAM3
*
* Copyright (C) 2017-2020 Carlos Campos, Richard Elvira, Juan J. Gómez Rodríguez, José M.M. Montiel and Juan D. Tardós, University of Zaragoza.
* Copyright (C) 2014-2016 Raúl Mur-Artal, José M.M. Montiel and Juan D. Tardós, University of Zaragoza.
*
* ORB-SLAM3 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ORB-SLAM3 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with ORB-SLAM3.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include "Sim3Solver.h"
#include <vector>
#include <cmath>
#include <opencv2/core/core.hpp>
#include "KeyFrame.h"
#include "ORBmatcher.h"
#include "Thirdparty/DBoW2/DUtils/Random.h"
namespace ORB_SLAM3
{
Sim3Solver::Sim3Solver(KeyFrame *pKF1, KeyFrame *pKF2, const vector<MapPoint *> &vpMatched12, const bool bFixScale,
vector<KeyFrame*> vpKeyFrameMatchedMP):
mnIterations(0), mnBestInliers(0), mbFixScale(bFixScale),
pCamera1(pKF1->mpCamera), pCamera2(pKF2->mpCamera)
{
bool bDifferentKFs = false;
if(vpKeyFrameMatchedMP.empty())
{
bDifferentKFs = true;
vpKeyFrameMatchedMP = vector<KeyFrame*>(vpMatched12.size(), pKF2);
}
mpKF1 = pKF1;
mpKF2 = pKF2;
// Step 1 取出当前关键帧中的所有地图点
vector<MapPoint*> vpKeyFrameMP1 = pKF1->GetMapPointMatches();
// 最多匹配的地图点数目
mN1 = vpMatched12.size();
// 预分配空间,在后面使用
mvpMapPoints1.reserve(mN1);
mvpMapPoints2.reserve(mN1);
mvpMatches12 = vpMatched12;
mvnIndices1.reserve(mN1);
mvX3Dc1.reserve(mN1);
mvX3Dc2.reserve(mN1);
// 获取两个关键帧的位姿
cv::Mat Rcw1 = pKF1->GetRotation();
cv::Mat tcw1 = pKF1->GetTranslation();
cv::Mat Rcw2 = pKF2->GetRotation();
cv::Mat tcw2 = pKF2->GetTranslation();
mvAllIndices.reserve(mN1);
size_t idx=0;
// Step 2 记录匹配地图点的各种信息
KeyFrame* pKFm = pKF2; //Default variable
for(int i1=0; i1<mN1; i1++)
{
// 如果该特征点在pKF1中有匹配
if(vpMatched12[i1])
{
// pMP1和pMP2是匹配的MapPoint
MapPoint* pMP1 = vpKeyFrameMP1[i1]; //i1 是匹配地图点在KF1的索引
MapPoint* pMP2 = vpMatched12[i1]; //vpMatched12[i1] 是在KF2中对应的匹配地图点
if(!pMP1)
continue;
if(pMP1->isBad() || pMP2->isBad())
continue;
if(bDifferentKFs)
pKFm = vpKeyFrameMatchedMP[i1];
// indexKF1和indexKF2是匹配特征点的索引
int indexKF1 = get<0>(pMP1->GetIndexInKeyFrame(pKF1));
int indexKF2 = get<0>(pMP2->GetIndexInKeyFrame(pKFm));
if(indexKF1<0 || indexKF2<0)
continue;
// kp1和kp2是匹配的图像特征点
const cv::KeyPoint &kp1 = pKF1->mvKeysUn[indexKF1];
const cv::KeyPoint &kp2 = pKFm->mvKeysUn[indexKF2];
const float sigmaSquare1 = pKF1->mvLevelSigma2[kp1.octave];
const float sigmaSquare2 = pKFm->mvLevelSigma2[kp2.octave];
// 自由度为2的卡方分布显著性水平为0.01对应的临界阈值为9.21
mvnMaxError1.push_back(9.210*sigmaSquare1);
mvnMaxError2.push_back(9.210*sigmaSquare2);
// mvpMapPoints1和mvpMapPoints2是匹配的MapPoints容器
mvpMapPoints1.push_back(pMP1);
mvpMapPoints2.push_back(pMP2);
mvnIndices1.push_back(i1);
// 计算这对匹配地图点分别在各自相机坐标系下的坐标
cv::Mat X3D1w = pMP1->GetWorldPos();
mvX3Dc1.push_back(Rcw1*X3D1w+tcw1);
cv::Mat X3D2w = pMP2->GetWorldPos();
mvX3Dc2.push_back(Rcw2*X3D2w+tcw2);
// 所有有效三维点的索引
mvAllIndices.push_back(idx);
idx++;
}
}
mK1 = pKF1->mK;
mK2 = pKF2->mK;
// Step 3 将相机坐标系下的三维地图点分别投影到各自相机的二维图像坐标用于后面和Sim3投影的比较筛选内点
FromCameraToImage(mvX3Dc1,mvP1im1,pCamera1);
FromCameraToImage(mvX3Dc2,mvP2im2,pCamera2);
// Step 4 设置默认的RANSAC参数,避免在调用的时候因为忘记设置导致崩溃
SetRansacParameters();
}
/**
* @brief 设置进行RANSAC时的参数
*
* @param[in] probability 当前这些匹配点的置信度,也就是一次采样恰好都是内点的概率
* @param[in] minInliers 完成RANSAC所需要的最少内点个数
* @param[in] maxIterations 设定的最大迭代次数
*/
void Sim3Solver::SetRansacParameters(double probability, int minInliers, int maxIterations)
{
mRansacProb = probability; // 0.99
mRansacMinInliers = minInliers; // 20
mRansacMaxIts = maxIterations; // 最大迭代次数 300
// 匹配点的数目
N = mvpMapPoints1.size(); // number of correspondences
// 内点标记向量
mvbInliersi.resize(N);
// Adjust Parameters according to number of correspondences
float epsilon = (float)mRansacMinInliers/N;
// Set RANSAC iterations according to probability, epsilon, and max iterations
// 计算迭代次数的理论值,也就是经过这么多次采样,其中至少有一次采样中,三对点都是内点
// epsilon 表示了在这 N 对匹配点中,我随便抽取一对点是内点的概率;
// 为了计算Sim3,我们需要从这N对匹配点中取三对点;那么如果我有放回的从这些点中抽取三对点,取这三对点均为内点的概率是 p0=epsilon^3
// 相应地,如果取三对点中至少存在一对匹配点是外点, 概率为p1=1-p0
// 当我们进行K次采样的时候,其中每一次采样中三对点中都存在至少一对外点的概率就是p2=p1^k
// K次采样中,至少有一次采样中三对点都是内点的概率是p=1-p2
// 候根据 p2=p1^K 我们就可以导出 K 的公式K=\frac{\log p2}{\log p1}=\frac{\log(1-p)}{\log(1-epsilon^3)}
// 也就是说我们进行K次采样,其中至少有一次采样中,三对点都是内点; 因此我们就得到了RANSAC迭代次数的理论值
int nIterations;
if(mRansacMinInliers==N)
nIterations=1; // 这种情况的时候最后计算得到的迭代次数的确就是一次
else
nIterations = ceil(log(1-mRansacProb)/log(1-pow(epsilon,3)));
// 外层的max保证RANSAC能够最少迭代一次;
// 内层的min的目的是,如果理论值比给定值要小,那么我们优先选择使用较少的理论值来节省时间(其实也有极大概率得到能够达到的最好结果);
// 如果理论值比给定值要大,那么我们也还是有限选择使用较少的给定值来节省时间
mRansacMaxIts = max(1,min(nIterations,mRansacMaxIts));
// 当前正在进行的迭代次数
mnIterations = 0;
}
/**
* @brief Ransac求解mvX3Dc1和mvX3Dc2之间Sim3函数返回mvX3Dc2到mvX3Dc1的Sim3变换
*
* @param[in] nIterations 设置的最大迭代次数
* @param[in] bNoMore 为true表示穷尽迭代还没有找到好的结果说明求解失败
* @param[in] vbInliers 标记是否是内点
* @param[in] nInliers 内点数目
* @return cv::Mat 计算得到的Sim3矩阵
*/
cv::Mat Sim3Solver::iterate(int nIterations, bool &bNoMore, vector<bool> &vbInliers, int &nInliers)
{
bNoMore = false; // 现在还没有达到最好的效果
vbInliers = vector<bool>(mN1,false); // 的确和最初传递给这个解算器的地图点向量是保持一致
nInliers=0; // 存储迭代过程中得到的内点个数
// Step 1 如果匹配点比要求的最少内点数还少不满足Sim3 求解条件,返回空
// mRansacMinInliers 表示RANSAC所需要的最少内点数目
if(N<mRansacMinInliers)
{
bNoMore = true; // 表示求解失败
return cv::Mat();
}
// 可以使用的点对的索引,为了避免重复使用
vector<size_t> vAvailableIndices;
// 随机选择的来自于这两个帧的三对匹配点
cv::Mat P3Dc1i(3,3,CV_32F);
cv::Mat P3Dc2i(3,3,CV_32F);
// nCurrentIterations 当前迭代的次数
// nIterations 理论迭代次数
// mnIterations 总迭代次数
// mRansacMaxIts 最大迭代次数
int nCurrentIterations = 0;
// Step 2 随机选择三个点用于求解后面的Sim3
// 条件1: 已经进行的总迭代次数还没有超过限制的最大总迭代次数
// 条件2: 当前迭代次数还没有超过理论迭代次数
while(mnIterations<mRansacMaxIts && nCurrentIterations<nIterations)
{
nCurrentIterations++;// 这个函数中迭代的次数
mnIterations++; // 总的迭代次数默认为最大为300
// 记录所有有效(可以采样)的候选三维点索引
vAvailableIndices = mvAllIndices;
// Get min set of points
// Step 2.1 随机取三组点,取完后从候选索引中删掉
for(short i = 0; i < 3; ++i)
{
// DBoW3中的随机数生成函数
int randi = DUtils::Random::RandomInt(0, vAvailableIndices.size()-1);
int idx = vAvailableIndices[randi];
// P3Dc1i和P3Dc2i中点的排列顺序
// x1 x2 x3 ...
// y1 y2 y3 ...
// z1 z2 z3 ...
mvX3Dc1[idx].copyTo(P3Dc1i.col(i));
mvX3Dc2[idx].copyTo(P3Dc2i.col(i));
// 从"可用索引列表"中删除这个点的索引
vAvailableIndices[randi] = vAvailableIndices.back();
vAvailableIndices.pop_back();
}
// Step 2.2 根据随机取的两组匹配的3D点计算P3Dc2i 到 P3Dc1i 的Sim3变换
ComputeSim3(P3Dc1i,P3Dc2i);
// Step 2.3 对计算的Sim3变换通过投影误差进行inlier检测
CheckInliers();
// Step 2.4 记录并更新最多的内点数目及对应的参数
if(mnInliersi>=mnBestInliers)
{
mvbBestInliers = mvbInliersi;
mnBestInliers = mnInliersi;
mBestT12 = mT12i.clone();
mBestRotation = mR12i.clone();
mBestTranslation = mt12i.clone();
mBestScale = ms12i;
if(mnInliersi>mRansacMinInliers) // 只要计算得到一次合格的Sim变换就直接返回
{
// 返回值,告知得到的内点数目
nInliers = mnInliersi;
for(int i=0; i<N; i++)
if(mvbInliersi[i])
// 标记为内点
vbInliers[mvnIndices1[i]] = true;
return mBestT12;
} // 如果当前次迭代已经合格了,直接返回
} // 更新最多的内点数目
} // 迭代循环
// Step 3 如果已经达到了最大迭代次数了还没得到满足条件的Sim3说明失败了放弃返回
if(mnIterations>=mRansacMaxIts)
bNoMore=true;
return cv::Mat(); // no more的时候返回的是一个空矩阵
}
cv::Mat Sim3Solver::iterate(int nIterations, bool &bNoMore, vector<bool> &vbInliers, int &nInliers, bool &bConverge)
{
bNoMore = false;
bConverge = false;
vbInliers = vector<bool>(mN1,false);
nInliers=0;
if(N<mRansacMinInliers)
{
bNoMore = true;
return cv::Mat();
}
vector<size_t> vAvailableIndices;
cv::Mat P3Dc1i(3,3,CV_32F);
cv::Mat P3Dc2i(3,3,CV_32F);
int nCurrentIterations = 0;
cv::Mat bestSim3;
while(mnIterations<mRansacMaxIts && nCurrentIterations<nIterations)
{
nCurrentIterations++;
mnIterations++;
vAvailableIndices = mvAllIndices;
// Get min set of points
for(short i = 0; i < 3; ++i)
{
int randi = DUtils::Random::RandomInt(0, vAvailableIndices.size()-1);
int idx = vAvailableIndices[randi];
mvX3Dc1[idx].copyTo(P3Dc1i.col(i));
mvX3Dc2[idx].copyTo(P3Dc2i.col(i));
vAvailableIndices[randi] = vAvailableIndices.back();
vAvailableIndices.pop_back();
}
ComputeSim3(P3Dc1i,P3Dc2i);
CheckInliers();
if(mnInliersi>=mnBestInliers)
{
mvbBestInliers = mvbInliersi;
mnBestInliers = mnInliersi;
mBestT12 = mT12i.clone();
mBestRotation = mR12i.clone();
mBestTranslation = mt12i.clone();
mBestScale = ms12i;
if(mnInliersi>mRansacMinInliers)
{
nInliers = mnInliersi;
for(int i=0; i<N; i++)
if(mvbInliersi[i])
vbInliers[mvnIndices1[i]] = true;
bConverge = true;
return mBestT12;
}
else
{
bestSim3 = mBestT12;
}
}
}
if(mnIterations>=mRansacMaxIts)
bNoMore=true;
return bestSim3;
}
cv::Mat Sim3Solver::find(vector<bool> &vbInliers12, int &nInliers)
{
bool bFlag;
return iterate(mRansacMaxIts,bFlag,vbInliers12,nInliers);
}
/**
* @brief 给出三个点,计算它们的质心以及去质心之后的坐标
*
* @param[in] P 输入的3D点
* @param[in] Pr 去质心后的点
* @param[in] C 质心
*/
void Sim3Solver::ComputeCentroid(cv::Mat &P, cv::Mat &Pr, cv::Mat &C)
{
// 矩阵P每一行求和结果存在C。这两句也可以使用CV_REDUCE_AVG选项来实现
cv::reduce(P,C,1,cv::REDUCE_SUM);
C = C/P.cols;// 求平均
for(int i=0; i<P.cols; i++)
{
Pr.col(i)=P.col(i)-C;//减去质心
}
}
/**
* @brief 根据两组匹配的3D点,计算P2到P1的Sim3变换
* @param[in] P1 匹配的3D点(三个,每个的坐标都是列向量形式,三个点组成了3x3的矩阵)(当前关键帧)
* @param[in] P2 匹配的3D点(闭环关键帧)
*/
void Sim3Solver::ComputeSim3(cv::Mat &P1, cv::Mat &P2)
{
// Sim3计算过程参考论文:
// Horn 1987, Closed-form solution of absolute orientataion using unit quaternions
// Step 1: 定义3D点质心及去质心后的点
// O1和O2分别为P1和P2矩阵中3D点的质心
// Pr1和Pr2为减去质心后的3D点
cv::Mat Pr1(P1.size(),P1.type()); // Relative coordinates to centroid (set 1)
cv::Mat Pr2(P2.size(),P2.type()); // Relative coordinates to centroid (set 2)
cv::Mat O1(3,1,Pr1.type()); // Centroid of P1
cv::Mat O2(3,1,Pr2.type()); // Centroid of P2
ComputeCentroid(P1,Pr1,O1);
ComputeCentroid(P2,Pr2,O2);
// Step 2: 计算论文中三维点数目n>3的 M 矩阵。这里只使用了3个点
// Pr2 对应论文中 r_l,i'Pr1 对应论文中 r_r,i',计算的是P2到P1的Sim3论文中是left 到 right的Sim3
cv::Mat M = Pr2*Pr1.t();
// Step 3: 计算论文中的 N 矩阵
double N11, N12, N13, N14, N22, N23, N24, N33, N34, N44;
cv::Mat N(4,4,P1.type());
N11 = M.at<float>(0,0)+M.at<float>(1,1)+M.at<float>(2,2); // Sxx+Syy+Szz
N12 = M.at<float>(1,2)-M.at<float>(2,1); // Syz-Szy
N13 = M.at<float>(2,0)-M.at<float>(0,2); // Szx-Sxz
N14 = M.at<float>(0,1)-M.at<float>(1,0); // ...
N22 = M.at<float>(0,0)-M.at<float>(1,1)-M.at<float>(2,2);
N23 = M.at<float>(0,1)+M.at<float>(1,0);
N24 = M.at<float>(2,0)+M.at<float>(0,2);
N33 = -M.at<float>(0,0)+M.at<float>(1,1)-M.at<float>(2,2);
N34 = M.at<float>(1,2)+M.at<float>(2,1);
N44 = -M.at<float>(0,0)-M.at<float>(1,1)+M.at<float>(2,2);
N = (cv::Mat_<float>(4,4) << N11, N12, N13, N14,
N12, N22, N23, N24,
N13, N23, N33, N34,
N14, N24, N34, N44);
// Step 4: 特征值分解求最大特征值对应的特征向量,就是我们要求的旋转四元数
cv::Mat eval, evec; // val vec
// 特征值默认是从大到小排列所以evec[0] 是最大值
cv::eigen(N,eval,evec);
// N 矩阵最大特征值第一个特征值对应特征向量就是要求的四元数q0 q1 q2 q3其中q0 是实部
// 将(q1 q2 q3)放入vec四元数的虚部
cv::Mat vec(1,3,evec.type());
(evec.row(0).colRange(1,4)).copyTo(vec); //extract imaginary part of the quaternion (sin*axis)
// Rotation angle. sin is the norm of the imaginary part, cos is the real part
// 四元数虚部模长 norm(vec)=sin(theta/2), 四元数实部 evec.at<float>(0,0)=q0=cos(theta/2)
// 这一步的ang实际是theta/2theta 是旋转向量中旋转角度的 1/2
// ? 这里也可以用 arccos(q0)=angle/2 得到旋转角吧
double ang=atan2(norm(vec),evec.at<float>(0,0));
// vec/norm(vec)归一化得到归一化后的旋转向量,然后乘上角度得到包含了旋转轴和旋转角信息的旋转向量vec
vec = 2*ang*vec/norm(vec); //Angle-axis representation. quaternion angle is the half
mR12i.create(3,3,P1.type());
// 旋转向量(轴角)转换为旋转矩阵
cv::Rodrigues(vec,mR12i); // computes the rotation matrix from angle-axis
// Step 5: Rotate set 2
// 利用刚计算出来的旋转将三维点旋转到同一个坐标系P3对应论文里的 r_l,i', Pr1 对应论文里的r_r,i'
cv::Mat P3 = mR12i*Pr2;
// Step 6: 计算尺度因子 Scale
if(!mbFixScale)
{
// 论文中有2个求尺度方法。一个是p632右中的位置考虑了尺度的对称性
// 代码里实际使用的是另一种方法这个公式对应着论文中p632左中位置的那个
// Pr1 对应论文里的r_r,i',P3对应论文里的 r_l,i',(经过坐标系转换的Pr2), n=3, 剩下的就和论文中都一样了
double nom = Pr1.dot(P3);
// 准备计算分母
cv::Mat aux_P3(P3.size(),P3.type());
aux_P3=P3;
// 先得到平方
cv::pow(P3,2,aux_P3);
double den = 0;
// 然后再累加
for(int i=0; i<aux_P3.rows; i++)
{
for(int j=0; j<aux_P3.cols; j++)
{
den+=aux_P3.at<float>(i,j);
}
}
ms12i = nom/den;
}
else
ms12i = 1.0f;
// Step 7: 计算平移Translation
mt12i.create(1,3,P1.type());
// 论文中平移公式
mt12i = O1 - ms12i*mR12i*O2;
// Step 8: 计算双向变换矩阵,目的是在后面的检查的过程中能够进行双向的投影操作
// Step 8.1 用尺度,旋转,平移构建变换矩阵 T12
mT12i = cv::Mat::eye(4,4,P1.type());
cv::Mat sR = ms12i*mR12i;
// |sR t|
// mT12i = | 0 1|
sR.copyTo(mT12i.rowRange(0,3).colRange(0,3));
mt12i.copyTo(mT12i.rowRange(0,3).col(3));
// Step 8.2 T21
mT21i = cv::Mat::eye(4,4,P1.type());
cv::Mat sRinv = (1.0/ms12i)*mR12i.t();
sRinv.copyTo(mT21i.rowRange(0,3).colRange(0,3));
cv::Mat tinv = -sRinv*mt12i;
tinv.copyTo(mT21i.rowRange(0,3).col(3));
}
/**
* @brief 通过计算的Sim3投影和自身投影的误差比较进行内点检测
*
*/
void Sim3Solver::CheckInliers()
{
// 用计算的Sim3 对所有的地图点投影,得到图像点
vector<cv::Mat> vP1im2, vP2im1;
Project(mvX3Dc2,vP2im1,mT12i,pCamera1);
Project(mvX3Dc1,vP1im2,mT21i,pCamera2);
mnInliersi=0;
// 对于两帧的每一个匹配点
for(size_t i=0; i<mvP1im1.size(); i++)
{
// 当前关键帧中的地图点直接在当前关键帧图像上的投影坐标mvP1im1mvP2im2
// 对于这对匹配关系,在两帧上的投影点距离都要进行计算
cv::Mat dist1 = mvP1im1[i]-vP2im1[i];
cv::Mat dist2 = vP1im2[i]-mvP2im2[i];
// 取距离的平方作为误差
const float err1 = dist1.dot(dist1);
const float err2 = dist2.dot(dist2);
// 根据之前确定的这个最大容许误差来确定这对匹配点是否是外点
if(err1<mvnMaxError1[i] && err2<mvnMaxError2[i])
{
mvbInliersi[i]=true;
mnInliersi++;
}
else
mvbInliersi[i]=false;
}// 遍历其中的每一对匹配点
}
// 得到计算的旋转矩阵
cv::Mat Sim3Solver::GetEstimatedRotation()
{
return mBestRotation.clone();
}
// 得到计算的平移向量
cv::Mat Sim3Solver::GetEstimatedTranslation()
{
return mBestTranslation.clone();
}
// 得到估计的从候选帧到当前帧的变换尺度
float Sim3Solver::GetEstimatedScale()
{
return mBestScale;
}
void Sim3Solver::Project(const vector<cv::Mat> &vP3Dw, vector<cv::Mat> &vP2D, cv::Mat Tcw, GeometricCamera* pCamera)
{
cv::Mat Rcw = Tcw.rowRange(0,3).colRange(0,3);
cv::Mat tcw = Tcw.rowRange(0,3).col(3);
vP2D.clear();
vP2D.reserve(vP3Dw.size());
// 对每个3D地图点进行投影操作
for(size_t i=0, iend=vP3Dw.size(); i<iend; i++)
{
// 首先将对方关键帧的地图点坐标转换到这个关键帧的相机坐标系下
cv::Mat P3Dc = Rcw*vP3Dw[i]+tcw;
// 投影
const float invz = 1/(P3Dc.at<float>(2));
const float x = P3Dc.at<float>(0);
const float y = P3Dc.at<float>(1);
const float z = P3Dc.at<float>(2);
vP2D.push_back(pCamera->projectMat(cv::Point3f(x,y,z)));
}
}
void Sim3Solver::FromCameraToImage(const vector<cv::Mat> &vP3Dc, vector<cv::Mat> &vP2D, GeometricCamera* pCamera)
{
vP2D.clear();
vP2D.reserve(vP3Dc.size());
for(size_t i=0, iend=vP3Dc.size(); i<iend; i++)
{
const float invz = 1/(vP3Dc[i].at<float>(2));
const float x = vP3Dc[i].at<float>(0);
const float y = vP3Dc[i].at<float>(1);
const float z = vP3Dc[i].at<float>(2);
vP2D.push_back(pCamera->projectMat(cv::Point3f(x,y,z)));
}
}
} //namespace ORB_SLAM