上一篇 双目初始化完成过程中,跟踪状态FrontendStatus变成了TRACKING_GOOD,因此之后,将进入 正常跟踪的Track() 函数:
//正常状态下的跟踪
bool Frontend::Track(){
// 用上一帧位姿获得当前位姿估计的初始值
// 假设匀速运动,即每两帧之间的位姿变换差不多,
// 以【上一帧位姿】乘以【上上帧与上一帧之间的相对运动】作为这次位姿的估计初值
if (last_frame_)
{
current_frame_->SetPose(relative_motion_ * last_frame_->Pose());
}
//用光流法匹配上一帧与当前帧,并返回光流法匹配到的点的数量
int num_track_last= TrackLastFrame();
// 修正当前帧的位姿估计,并返回追踪成功点数量(在光流匹配成功基础上,更信任的点)
tracking_inliers_=EstimateCurrentPose();
// 改变状态,为下次的添加帧做准备
// tracking good
if (tracking_inliers_>num_features_tracking_)
{
status_=FrontendStatus::TRACKING_GOOD;
}
// tracking bad
else if (tracking_inliers_>num_features_tracking_bad_)
{
status_=FrontendStatus::TRACKING_BAD;
}else // lost
{
status_=FrontendStatus::LOST;
}
//将当前帧插入关键帧
InsertKeyframe();
//当前帧位姿 = 上一帧的位姿 左乘 当前帧与上一帧的相对变换
//相对位姿变换 = 当前帧的位姿 * 上一帧位姿的逆
relative_motion_=current_frame_->Pose() * last_frame_->Pose().inverse();
if (viewer_)
{
viewer_->AddCurrentFrame(current_frame_);
return true;
}
}
接下来我们具体看一下函数实现:
用光流法匹配上一帧与当前帧,并返回光流法匹配到的点的数量,这和前面左右目光流追踪FindFeaturesInRight() 函数基本一样,不同的是这里是上一帧和当前帧的左目图像来进行光流追踪,流程如下:
//用光流法匹配上一帧与当前帧,并返回光流法匹配到的点的数量
int Frontend::TrackLastFrame(){
// 赋初值
std::vector kps_last, kps_current;
for(auto &kp : last_frame_->features_left_){
// 如果这个点有对应的路标点,则当前帧的光流法初值为路标点对当前帧位姿(初值)的投影
if (kp->map_point_.lock())
{
auto mp = kp->map_point_.lock();//检查是否上锁
// 将世界坐标系中的坐标 投影 到像素坐标系上
auto px = camera_left_->world2pixel(mp->Pos(),current_frame_->Pose());
//关键帧的上一个容器中推入匹配到的关键点
kps_last.push_back(kp->position_.pt);
//当前帧中推入对应的像素坐标系上的坐标
kps_current.push_back(cv::Point2f(px[0],px[1]));
}
//没有上锁,直接推入对应的地图点
else{
kps_last.push_back(kp->position_.pt);
kps_current.push_back(kp->position_.pt);
}
}
// 光流追踪
std::vector status;// 设置一个状态的容器
Mat error; //设置一个error矩阵
cv::calcOpticalFlowPyrLK(last_frame_->left_img_,
last_frame_->left_img_, kps_last, kps_current,
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 = 0;
for (size_t i = 0; i < status.size(); ++i)
{
//如果匹配到对应的流则个数加一;并指针继续向后指
if (status[i])
{
cv::KeyPoint kp(kps_current[i],7); // 关键点代表的区域直径大小为7
Feature::Ptr feature(new Feature(current_frame_,kp)); // 有一个关键点,就要有一个特征类
feature->map_point_=last_frame_->features_left_[i]->map_point_;// 关键点对应的地图点就是上一帧的点对应的地图点
current_frame_->features_left_.push_back(feature);// 填充当前帧的左图特征点容器
num_good_pts++;// 匹配成功点计数
}
}
// 匹配成功的点数量记录到日志,并返回
LOG(INFO) << "Find " << num_good_pts << " in the last image.";
return num_good_pts;
}
估计当前帧位姿;与backend.cpp中的Optimize中的g2o优化相似,区别在于此处无需优化,只是估计出来,新建一些信息,这里很大一部分内容都是源自于《视觉SLAM14讲》第二版中的 P191g2o的实现过程,参考我之前写的手撕ch7(2)中的g2o的实现流程,
原文代码:
int Frontend::EstimateCurrentPose(){
// setup g2o(g2o过程) 图优化:单节点+多条一元边
// 设定g2o
typedef ::g2o::BlockSolver_6_3 BlockSolverType;//pose 是 6 ,mappoint是 3
typedef g2o::LinearSolverDense LinearSolverType; //线性求解器类型
// 块求解器BlockSolver
auto solver = new g2o::OptimizationAlgorithmLevenberg (
g2o::make_unique( g2o::make_unique())); // 选择梯度下降法
g2o::SparseOptimizer optimizer; //稀疏求解
optimizer.setAlgorithm(solver); //设置求解器
// vertex(优化量 顶点)
VertexPose *vertex_pose= new VertexPose();// camera vertex_pose
vertex_pose->setId(0);// 定义节点编号
vertex_pose->setEstimate(current_frame_->Pose());//设置初值
optimizer.addVertex(vertex_pose);//把节点添加到图中
// K
Mat33 K = camera_left_->K();
// edges(约束 边)(每对匹配点就是一个边)
int index =1;
std::vectoredges;
std::vectorfeatures;
//遍历每一对点
for (size_t i = 0; i < current_frame_->features_left_.size(); ++i)
{
auto mp = current_frame_->features_left_[i]->map_point_.lock();
if (mp)
{
features.push_back(current_frame_->features_left_[i]);
EdgeProjectionPoseOnly *edge = new EdgeProjectionPoseOnly(mp->pos_,K);
edge->setId(index);
edge->setVertex(0,vertex_pose);// 设置连接的顶点
edge->setMeasurement(toVec2(current_frame_->features_left_[i]->position_.pt));//传入观测值
edge->setInformation(Eigen::Matrix2d::Identity());// 信息矩阵
edge->setRobustKernel(new g2o::RobustKernelHuber); //鲁棒核函数
edges.push_back(edge);
optimizer.addEdge(edge);
index++;
}
}
// estimate the Pose the determine the outliers
// 要思路是首先定义误差(误差要归一化,也就是去量纲化),然后选择置信度,一般为95%,如果这个特征点的误差在区间内我们认为是内外,否则是外点
const double chi2_th = 5.991;
int cnt_outlier = 0;
for (int iteration = 0; iteration < 4; ++iteration) {
vertex_pose->setEstimate(current_frame_->Pose());
optimizer.initializeOptimization();// 设置优化初始值
optimizer.optimize(10);// 设置优化次数
cnt_outlier = 0;
// count the outliers
for (size_t i = 0; i < edges.size(); ++i) {
auto e = edges[i];
if (features[i]->is_outlier_) {
e->computeError();
}
if (e->chi2() > chi2_th) {
//设置等级 一般情况下g2o只处理level = 0的边,设置等级为1,下次循环g2o不再优化这个边
features[i]->is_outlier_ = true;
e->setLevel(1);
cnt_outlier++;
} else {
features[i]->is_outlier_ = false;
e->setLevel(0);
};
// 在第三次迭代之后,它会将所有边的鲁棒核函数设置为nullptr,以便在后续的迭代中不再考虑outlier
if (iteration == 2) {
e->setRobustKernel(nullptr);
}
}
}
LOG(INFO) << "Outlier/Inlier in pose estimating: " << cnt_outlier << "/"
<< features.size() - cnt_outlier;
// Set pose and outlier
current_frame_->SetPose(vertex_pose->estimate());
LOG(INFO) << "Current Pose = \n" << current_frame_->Pose().matrix();
for (auto &feat : features) {
if (feat->is_outlier_) {
feat->map_point_.reset();
feat->is_outlier_ = false; // maybe we can still use it in future
}
}
return features.size() - cnt_outlier;
}
其中我们关注其中:自定义的顶点和边
// 位姿顶点
class VertexPose : public g2o::BaseVertex<6,SE3>{//优化量的参数模板
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
// 重置,设定被优化变量的原始值
virtual void setToOriginImpl() override
{
_estimate =SE3();
}
// 更新
virtual void oplusImpl(const double * update) override
{
Vec6 update_eigen;
update_eigen << update[0], update[1], update[2], update[3], update[4], update[5];
_estimate= SE3::exp(update_eigen) * _estimate; // 左乘更新 SE3 - 旋转矩阵R
}
// 存盘、读盘
virtual bool read(std::istream &in) override { return true; } //存盘
virtual bool write(std::ostream &out) const override { return true; } //读盘
};
// 仅估计位姿的一元边
class EdgeProjectionPoseOnly : public g2o::BaseUnaryEdge<2,Vec2,VertexPose>{
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
// 构造函数
EdgeProjectionPoseOnly(const Vec3 &pos, const Mat33 &K ): _pos3d(pos), _K(K) {}
// 误差计算函数
virtual void computeError() override{
const VertexPose *v = static_cast(_vertices[0]);
SE3 T = v->estimate();
Vec3 pos_pixel= _K * (T*_pos3d); //将3D世界坐标转为相机像素坐标
pos_pixel /= pos_pixel[2];
_error=_measurement-pos_pixel.head<2>();
}
virtual void linearizeOplus() override {
const VertexPose *v = static_cast(_vertices[0]);
SE3 T = v->estimate();
Vec3 pos_cam = T * _pos3d; //相机坐标系下空间点的坐标=相机位姿 * 空间点的坐标
double fx = _K(0, 0);
double fy = _K(1, 1);
double X = pos_cam[0];
double Y = pos_cam[1];
double Z = pos_cam[2];
double Zinv = 1.0 / (Z + 1e-18);
double Zinv2 = Zinv * Zinv;
//2*6的雅克比矩阵
_jacobianOplusXi << -fx * Zinv, 0, fx * X * Zinv2, fx * X * Y * Zinv2,
-fx - fx * X * X * Zinv2, fx * Y * Zinv, 0, -fy * Zinv,
fy * Y * Zinv2, fy + fy * Y * Y * Zinv2, -fy * X * Y * Zinv2,
-fy * X * Zinv;
}
// 读操作
virtual bool read(std::istream &in) override { return true; }
// 写操作
virtual bool write(std::ostream &out) const override { return true; }
private:
Vec3 _pos3d;
Mat33 _K;
};
至此EstimateCurrentPose() 函数完成,返回的当前帧追踪到的特征点内点数量(tracking_inliers_),然后判断当前的跟踪状态,为下次的添加帧做准备.如果内点数大于50 ,就是 tracking good ,内点数小于50大于20, 就是tracking bad,如果小于20,就 tracking lost .
更新完跟踪状态后,进入 InsertKeyframe() 函数判断要不要插入关键帧
bool Frontend::InsertKeyframe(){
// 如果追踪到的点很多(说明两帧差异不大),则不需要插入关键帧
if (tracking_inliers_>num_features_needed_for_keyframe_)
{
return false;
}
// 如果追踪到的点很少(说明两帧差异较大),判定该帧为关键帧
// 当前帧为关键帧并分配关键帧id
current_frame_->SetKeyFrame();
// 地图中插入当前帧
map_->InsertKeyFrame(current_frame_);
LOG(INFO) << "Set frame " << current_frame_->id_ << " as keyframe "
<< current_frame_->keyframe_id_;
// 将关键帧中特征点对应的地图点加入到观测容器中
SetObservationsForKeyFrame();
// 关键帧重新提取特征点
DetectFeatures();
// track in right image 在关键帧的右目图像中找与左目对应的特征点
FindFeaturesInRight();
// triangulate map points 三角化新的地图点
TriangulateNewPoints();
// update backend because we have a new keyframe更新地图,触发后端优化
backend_->UpdateMap();
// 可视化模块中更新视图
if (viewer_)
{
viewer_->UpdateMap();
}
}
// triangulate map points 三角化新的地图点
int Frontend::TriangulateNewPoints(){
// 1.左右目位姿(pose)
std::vectorposes{camera_left_->pose(),camera_right_->pose()};
SE3 current_pose_Twc=current_frame_->Pose().inverse();
int cnt_triangulated_pts = 0;
for (size_t i = 0; i < current_frame_->features_left_.size(); ++i)
{
//expired()函数用于检查智能指针指向的对象是否为空 ,expired()==true 存储空指针
if (current_frame_->features_left_[i]->map_point_.expired() && current_frame_->features_right_[i] !=nullptr)
{
// 左图的特征点未关联地图点且存在右图匹配点
// 2.左右目匹配点(point)
std::vectorpoints{
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))
};
// 3 .pworld
Vec3 pworld = Vec3::Zero();
// 三角化成功并且深度为正
if (triangulation(poses,points,pworld) && pworld[2]>0)
{
//创建一个新地图,用于信息更新
auto new_map_point = MapPoint::CreateNewMappoint();
//三角化得到的坐标是左目相机系坐标,这里和初始化中的triangulation区分:
// 这里计算出的pworld是相机系坐标,左乘Twc才是世界系坐标, 而初始化是第一帧,一般以第一帧为世界系,因此得到的pworld直接当世界系坐标使用
pworld = current_pose_Twc * pworld;
new_map_point->SetPos(pworld);
//将观测到的特征点加入新地图
new_map_point->AddObservation(current_frame_->features_left_[i]);
new_map_point->AddObservation(current_frame_->features_right_[i]);
//为特征类Feature对象填写地图点成员
current_frame_->features_left_[i]->map_point_=new_map_point;
current_frame_->features_right_[i]->map_point_=new_map_point;
//对Map类对象来说,地图里面应当多了一个地图点,所以要将这个地图点加到地图中去
map_->InsertMapPoint(new_map_point);
cnt_triangulated_pts++;
}
}
}
LOG(INFO) << "new landmarks: " << cnt_triangulated_pts;
return cnt_triangulated_pts;
}
在InsertKeyframe()函数走完后,最后我们会把当前帧位姿和上一帧的位姿的差给到relative_motion_变量(相对位姿变换 = 当前帧的位姿 * 上一帧位姿的逆),最后在显示线程中添加当前帧,对应Viewer::AddCurrentFrame( )函数