上一篇,我们分析了Frontend::AddFrame()函数,将会根据前端状态变量FrontendStatus,运行不同的三个函数,StereoInit(),Track()和Reset(),首先肯定是双目初始化StereoInit()函数:
- //双目初始化
- //根据左右目之间的光流匹配,寻找可以三角化的地图点,成功时建立初始地图
- bool Frontend::StereoInit(){
- //提取左目的GFTT特征点(数量)
- int num_features_left = DetectFeatures();
- // 右目光流追踪
- int num_coor_features=FindFeaturesInRight();
-
- //如果匹配到的特征点数量小于num_features_init_,默认100个,就返回false
- if (num_coor_features < num_features_init_)
- {
- return false;
- }
-
- //初始化地图
- bool build_map_success= BuildInitMap();
-
- //如果地图初始化成功就改前端状态为TRACKING_GOOD,并把当前帧和地图点传到viewer_线程里
- if (build_map_success)
- {
- status_=FrontendStatus::TRACKING_GOOD;
- //地图在可视化端的操作,添加当前帧并更新整个地图
- if (viewer_)
- {
- viewer_->AddCurrentFrame(current_frame_);
- viewer_->UpdateMap();
- }
- return true; //地图初始化成功,返回ture
- }
- return false; //地图初始化失败,返回false
- }
接下来我们具体看一下函数实现:
- /*提取左目的GFTT特征点(数量)
- //opencv中mask的作用就是创建感兴趣区域,即待处理的区域。
- 通常,mask大小创建分为两步,先创建与原图一致,类型为CV_8UC1或者CV_8UC3的全零图(即黑色图)。如mask = Mat::zeros(image.size(),CV_8UC1);
- 然后用rect类或者fillPoly()函数将原图中待处理的区域(感兴趣区域)置为1。
- */
- int Frontend::DetectFeatures(){
- cv::Mat mask(current_frame_->left_img_.size(),CV_8UC1, 255);
- //循环遍历当前帧上的左侧图像特征,并绘制边框
- for(auto feat : current_frame_->features_left_){
- /*
- void cv::rectangle(cv::InputOutputArray img, cv::Point pt1, cv::Point pt2,
- const cv::Scalar &color, int thickness = 1, int lineType = 8, int shift = 0)
- 绘制一个简单的、厚的或向上填充的矩形。函数cv::rectangle绘制一个矩形轮廓或填充矩形,其两个相对的角分别为pt1和pt2。
- position_.pt————————>关键点坐标
- inline cv::Point2f::Point_(float _x, float _y)------>关键点上下左右10距离的矩形,color为0,就是黑色填充满
- */
- cv::rectangle(mask,feat->position_.pt-cv::Point2f(10,10),feat->position_.pt+cv::Point2f(10,10),0,CV_FILLED);
- }
- //建立一个关键点的vector
- std::vector
keypoints; -
- //提取左图特征点
- /*
- virtual void cv::Feature2D::detect(cv::InputArray image, std::vector
&keypoints, cv::InputArray mask = noArray()) - 重载:
- 检测图像(第一种变体)或图像集(第二种变体)中的关键点。
- 参数:
- image – 图像.
- keypoints – The detected keypoints. In the second variant of the method keypoints[i] is a set of keypoints detected in images[i] .
- 检测到的关键点。在该方法的第二种变体中,keypoints[i]是在图像[i]中检测到的一组关键点。
- mask – Mask specifying where to look for keypoints (optional). It must be a 8-bit integer matrix with non-zero values in the region of interest.
- 指定在何处查找关键点的掩码(可选)。它必须是一个8位整数矩阵,在感兴趣的区域中具有非零值。
- GFTT角点
- */
- gftt_->detect(current_frame_->left_img_,keypoints,mask);
-
- //关联current_frame_和kp,同时统计这两次检测到的特征点数量
- int cnt_detected = 0;
- for(auto &kp : keypoints){
- current_frame_->features_left_.push_back(
- Feature::Ptr(new Feature(current_frame_,kp))
- );
- cnt_detected++;
- }
- LOG(INFO) << "Detect " << cnt_detected << " new features";
- return cnt_detected;
- }
- // 右目光流追踪
- int Frontend::FindFeaturesInRight(){
- //赋初值
- std::vector
kps_left,kps_right; - // 这里把当前帧左目的特征点位置放到 kps_left 这个临时变量里面。
- // 如果当前特征点有在地图上有对应的点,那么将根据特征点的3D POSE和当前帧的位姿反求出特征点在当前帧的像素坐标。如果没有对应特征点,右目的特征点初值就是和左目一样。
- for(auto &kp : current_frame_->features_left_){
- kps_left.push_back(kp->position_.pt);
- auto mp =kp->map_point_.lock();//检查是否上锁
- // 如果当前特征点有在地图上有对应的点
- if (mp)
- {
- // use projected points as initial guess(使用投影点作为初始估计值)//tzy
- auto px=camera_right_->world2pixel(mp->Pos(),current_frame_->Pose());
- //存入右侧图像的关键点
- kps_right.push_back(cv::Point2f(px[0],px[1]));
- }
- //否则,使用左侧相机特征点坐标为初始值
- else{
- kps_right.push_back(kp->position_.pt);
- }
- }
-
- // 开始使用LK流来估计右侧图像中的点
- std::vector
status; - Mat error;
- //opencv自带的光流跟踪函数
- /*
- CV_EXPORTS_W void calcOpticalFlowPyrLK( InputArray prevImg, InputArray nextImg,
- InputArray prevPts, InputOutputArray nextPts,
- OutputArray status, OutputArray err,
- Size winSize = Size(21,21), int maxLevel = 3,
- TermCriteria criteria = TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 0.01),
- int flags = 0, double minEigThreshold = 1e-4 );
- Calculates an optical flow for a sparse feature set using the iterative Lucas-Kanade method with pyramids.
- */
- cv::calcOpticalFlowPyrLK(current_frame_->left_img_,
- current_frame_->right_img_, kps_left, kps_right,
- status, error,
- cv::Size(11, 11), 3,
- cv::TermCriteria(cv::TermCriteria::COUNT + cv::TermCriteria::EPS, 30, 0.01),
- cv::OPTFLOW_USE_INITIAL_FLOW); // 最后一个参数flag,使用初始估计,存储在nextPts中;如果未设置标志,则将prevPts复制到nextPts并将其视为初始估计
-
- // 统计匹配上的特征点个数,并存储
- int num_good_pts;
- for (size_t i = 0; i < status.size(); ++i)
- {
- // 为真表示光流追踪成功
- if (status[i])
- {
- cv::KeyPoint kp(kps_right[i], 7); // 右目关键点的直径为7
- Feature::Ptr feat(new Feature(current_frame_, kp));
- feat->is_on_left_image_ = false;
- current_frame_->features_right_.push_back(feat);
- num_good_pts++;
- }
- else
- { // 光流追踪未成功,就放个空指针
- current_frame_->features_right_.push_back(nullptr);
- }
- }
- // 输出操作日志
- LOG(INFO) << "Find " << num_good_pts << " in the right image.";
- return num_good_pts; // 返回成功匹配的点对数量
- }
首先,如果上一步光流追踪中匹配到的特征点数量大于num_features_init_(默认100个),才进行地图初始化:
- bool Frontend::BuildInitMap(){
- //设置左右两个相机的位置(获取pose)
- std::vector
poses{camera_left_->pose(),camera_right_->pose()}; - size_t cnt_init_landmarks= 0; //设置地图标志初始值
- //循环左侧图像特征点 (获取 特征点归一化坐标)
- for (size_t i = 0; i < current_frame_->features_left_.size(); ++i)
- {
- //判断右侧图像是否有对应特征点,有则继续向下执行,没有就continue
- if (current_frame_->features_right_[i]== nullptr) continue;
- // create map point from triangulation (开始获取三角化所需的 特征点归一化坐标)
- std::vector
points{ - camera_left_->pixel2camera(Vec2(current_frame_->features_left_[i]->position_.pt.x,current_frame_->features_left_[i]->position_.pt.y)),
- camera_right_->pixel2camera( Vec2(current_frame_->features_right_[i]->position_.pt.x,current_frame_->features_right_[i]->position_.pt.y))};
- //新建一个三维0矩阵
- Vec3 pworld =Vec3::Zero();
-
- // 尝试对每一对匹配点进行三角化
- /*
- inline bool myslam::triangulation(const std::vector
&poses, std::vector points, Vec3 &pt_world) - linear triangulation with SVD 线性三角测量
- 参数:
- poses – poses,
- points – points in normalized plane
- pt_world – triangulated point in the world
- 返回:
- true if success
- */
- if (triangulation(poses,points,pworld) &&pworld[2]>0)
- {
- //创建地图存储数据
- //创建一个新地图,用于信息更新
- auto new_map_point = MapPoint::CreateNewMappoint();
- new_map_point->SetPos(pworld);
-
- //将观测到的特征点加入新地图
- new_map_point->AddObservation(current_frame_->features_left_[i]);
- new_map_point->AddObservation(current_frame_->features_right_[i]);
-
- //当前帧的地图点指向新的地图点—>更新当前帧上的地图点
- current_frame_->features_left_[i]->map_point_ = new_map_point;
- current_frame_->features_right_[i]->map_point_ = new_map_point;
-
- //初始化地图点makr+1
- cnt_init_landmarks++;
- //在地图中插入当前新的更新点
- map_->InsertMapPoint(new_map_point);
- }
- }
- //把初始化的这一帧设置为关键帧
- current_frame_->SetKeyFrame();
- //在地图中插入关键帧
- map_->InsertKeyFrame(current_frame_);
-
- //后端更新地图(在后端更新)
- backend_->UpdateMap();
-
- //输出操作日志
- LOG(INFO) << "Initial map created with " << cnt_init_landmarks
- << " map points";
- //返回:单个图像构建初始地图成功
- return true;
- }
其中triangulation() 三角化函数:
实现的推导过程中可以看一下这篇博客里的SVD分解的部分
- // 通过构建线性方程,然后SVD分解来计算特征点的位置。
- /**
- * linear triangulation with SVD
- * @param poses poses,
- * @param points points in normalized plane
- * @param pt_world triangulated point in the world
- * @return true if success
- */
-
- inline bool triangulation(const std::vector
&poses, - const std::vector
points, Vec3 &pt_world) { - MatXX A(2 * poses.size(), 4);
- VecX b(2 * poses.size());
- b.setZero();
- for (size_t i = 0; i < poses.size(); ++i) {
- Mat34 m = poses[i].matrix3x4();
- A.block<1, 4>(2 * i, 0) = points[i][0] * m.row(2) - m.row(0);
- A.block<1, 4>(2 * i + 1, 0) = points[i][1] * m.row(2) - m.row(1);
- }
- // Eigen::ComputeThinU和Eigen::ComputeThinV是两个参数,用于指定只计算矩阵的前k个奇异向量,其中k是矩阵的秩。这样可以加快计算速度并减小内存占用
- auto svd = A.bdcSvd(Eigen::ComputeThinU | Eigen::ComputeThinV);
- /*
- 取SVD分解得到v矩阵的最有一列作为解:svd.matrixV().col(3)
- 深度值是svd.matrixV()(3, 3)
- head<3>() 是取前三个值
- */
- pt_world = (svd.matrixV().col(3) / svd.matrixV()(3, 3)).head<3>();
-
- if (svd.singularValues()[3] / svd.singularValues()[2] < 1e-2) {
- return true;
- }
- return false;// 解质量不好,放弃
- }