前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SLAM程序阅读(第8讲 LK光流法)

SLAM程序阅读(第8讲 LK光流法)

作者头像
小白学视觉
发布2019-10-24 14:23:31
1.2K0
发布2019-10-24 14:23:31
举报

本期,小绿带大家阅读高翔Slambook第8讲中LK光流法程序。

细心的同学已经发现,小绿换了文章的封皮,因为有一些同学都觉得原来那张图比较捞,不沉稳也不正经…而更细心的同学也会发现,小绿连题目都改了,原来叫“解读”,现在叫“阅读”,这也是因为一些热心的同学在后台积极提问,然而小绿作为一个门徒,实在是有些束手无策,没法很透彻的解答同学们的问题…

确实,比方说在第7讲中的几个.cpp,求E矩阵需要使用findEssentialMat函数,求F矩阵需要使用findFundamentalMat函数,这两个函数虽由OpenCV提供,而且原理使用对极约束,但具体求取E、F时构造的是如何的一个最小二乘问题?求解时又使用何种方式求解?再比如说三角测量中,使用的triangulatePoint函数,其1、2号形参是两个projection matrix,也叫投影矩阵,那么这两个投影矩阵是怎么求得?又为什么简单地把R、t做一个增广就叫做projection matrix而不叫function matrix….等等诸如此类的问题,小绿确实由于没有深入阅读OpenCV源码,直接当做封装好的函数,当做一个工具去使用,却并没有深入其原理。

所以从本期开始,小绿没法再带着大家去“解读”程序啦o(╥﹏╥)o…小绿只能带着大家去“阅读”程序~~

好了,闲话到此为止,现在咱们来看一下Slambook第8讲的第一个程序:useLK.cpp

首先,来了解一下程序的用途:useLK.cpp这个程序是一个演示使用LK光流法跟踪特征点运动轨迹的实例,通过从数据库截取9张RGB图像(这里虽然data数据集里包含了9张深度图,然而只是为了读取RGB图像方便,为了使用associate.txt中排好序的图像名称,而在之后使用直接法求解位姿时才使用深度信息),在第一张图像中寻找FAST角点作为特征点,进而在后续的图像中使用LK光流法对这些角点进行跟踪。本程序只进行特征点的跟踪,并没有涉及帧与帧之间的位姿变换运算,可以说是光流法的一个基础例程。这里可以先展示一下程序的运行结果:

下面我们来看代码。由于本程序没有子函数,在这里就直接把主函数贴在这里:

#include <iostream>
#include <fstream>
#include <list>
#include <vector>
#include <chrono>
using namespace std; 

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/video/tracking.hpp>

int main( int argc, char** argv )
{
    if ( argc != 2 )
    {
        cout<<"usage: useLK path_to_dataset"<<endl;
        return 1;
    }
    string path_to_dataset = argv[1];
    string associate_file = path_to_dataset + "/associate.txt";
    
    ifstream fin( associate_file );
    if ( !fin ) 
    {
        cerr<<"I cann't find associate.txt!"<<endl;
        return 1;
    }
    
    string rgb_file, depth_file, time_rgb, time_depth;
    list< cv::Point2f > keypoints;      // 因为要删除跟踪失败的点,使用list
    cv::Mat color, depth, last_color;
    
    for ( int index=0; index<100; index++ )
    {
        fin>>time_rgb>>rgb_file>>time_depth>>depth_file;
        color = cv::imread( path_to_dataset+"/"+rgb_file );
        depth = cv::imread( path_to_dataset+"/"+depth_file, -1 );
        if (index ==0 )
        {
            // 对第一帧提取FAST特征点
            vector<cv::KeyPoint> kps;
            cv::Ptr<cv::FastFeatureDetector> detector = cv::FastFeatureDetector::create();
            detector->detect( color, kps );
            for ( auto kp:kps )
                keypoints.push_back( kp.pt );
            last_color = color;
            continue;
        }
        if ( color.data==nullptr || depth.data==nullptr )
            continue;
        // 对其他帧用LK跟踪特征点
        vector<cv::Point2f> next_keypoints; 
        vector<cv::Point2f> prev_keypoints;
        for ( auto kp:keypoints )
            prev_keypoints.push_back(kp);
        vector<unsigned char> status;
        vector<float> error; 
        chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
        cv::calcOpticalFlowPyrLK( last_color, color, prev_keypoints, next_keypoints, status, error );
        chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
        chrono::duration<double> time_used = chrono::duration_cast<chrono::duration<double>>( t2-t1 );
        cout<<"LK Flow use time:"<<time_used.count()<<" seconds."<<endl;
        // 把跟丢的点删掉
        int i=0; 
        for ( auto iter=keypoints.begin(); iter!=keypoints.end(); i++)
        {
            if ( status[i] == 0 )
            {
                iter = keypoints.erase(iter);
                continue;
            }
            *iter = next_keypoints[i];
            iter++;
        }
        cout<<"tracked keypoints: "<<keypoints.size()<<endl;
        if (keypoints.size() == 0)
        {
            cout<<"all keypoints are lost."<<endl;
            break; 
        }
        // 画出 keypoints
        cv::Mat img_show = color.clone();
        for ( auto kp:keypoints )
            cv::circle(img_show, kp, 10, cv::Scalar(0, 240, 0), 1);
        cv::imshow("corners", img_show);
        cv::waitKey(0);
        last_color = color;
    }
    return 0;
}

首先是对输入参数个数的判断。这里我们只需传入“数据集排序文件”associate.txt所在的文件夹就可以,因而argc的判别值为2。

string path_to_dataset = argv[1];
string associate_file = path_to_dataset + "/associate.txt";

这里定义了两个string类变量,即两个字符串,分别存储associate.txt所在文件夹的绝对路径,与associate.txt的结对路径。

ifstream fin( associate_file );
    if ( !fin ) 
    {
        cerr<<"I cann't find associate.txt!"<<endl;
        return 1;
    }

这里实例化了一个ifstream输入文件流类的变量fin,并直接初始化为associate_file所存储的字符串。随后判断是否能够打开fin所存储路径下的文件,而在判断语句中“!fin”并不是说判断fin是否为0或者为空,而是ifstream类重载了“!”操作符,所以当我们如此使用的时候,是“!”操作符函数返回一个bool类变量来标记是否成功,成功则为1。

list< cv::Point2f > keypoints;

还记得之前用于存储特征点的keypoints是Point2f类的容器,而现在使用list链表是为了方便插入与删除某个元素,这里是为了方便在后续光流法跟踪时删除跟丢的点。

for ( int index=0; index<100; index++ )
    {
       ...
    }

在每次循环中,输入流fin输入associate.txt每行的数据,因为associate文件的每一行分别是time_color、color、time_depth、depth,所以分别将其赋值给存储文件名称或文件产生时间的变量:

fin>>time_rgb>>rgb_file>>time_depth>>depth_file;

此后,针对第一张图像,按照FAST角点寻找特征点并存入keypoints中;进而在后续帧之间使用LK进行特征点的跟踪。

if ( color.data==nullptr || depth.data==nullptr )
            continue;

这里,通过判断color与depth两个Mat类变量中数据存储区data是否为空指针,来判断是否成功找到了本帧所对应的彩色图与深度图。如果有一项为空,则continue进行下一次循环。

vector<cv::Point2f> next_keypoints; 
vector<cv::Point2f> prev_keypoints;
for ( auto kp:keypoints )
      prev_keypoints.push_back(kp);

这里定义了两个存储Point2f类的容器next_keypoints与prev_keypoints,分别用来存储当前帧(下一帧)通过光流跟踪得到的特征点的像素坐标,与前一帧特征点的像素坐标。其中,前一帧的特征点需要将存储特征点的list进行遍历(每次光流跟踪后会有坏点剔除),分别存入prev_keypoints。

cv::calcOpticalFlowPyrLK( last_color, color, prev_keypoints, next_keypoints, status, error );

这里调用OpenCV提供的光流法计算函数calcOpticalFlowPyrLK,通过金字塔LK光流法计算当前帧跟踪得到的特征点的像素坐标并存入next_keypoints,同时会将每一个特征点的跟踪情况存入同维度的容器status与error,用来判断该特征点是否被成功跟踪。

// 把跟丢的点删掉
        int i=0; 
        for ( auto iter=keypoints.begin(); iter!=keypoints.end(); i++)
        {
            if ( status[i] == 0 )
            {
                iter = keypoints.erase(iter);
                continue;
            }
            *iter = next_keypoints[i];
            iter++;
        }

这里创建一个链表keypoints的迭代器iter,依次访问内部元素,通过判断status容器内同位置的标志量是否为0,来选择是否在链表内部删除该特征点。若未跟丢,则使用当前帧该特征点运动到的像素位置替换keypoints中该特征点存储的像素位置(即在前一帧的位置)。

// 画出 keypoints
        cv::Mat img_show = color.clone();
        for ( auto kp:keypoints )
            cv::circle(img_show, kp, 10, cv::Scalar(0, 240, 0), 1);
        cv::imshow("corners", img_show);

最后,深拷贝当前帧(用“=”浅拷贝会修改原图),并使用CV提供的特征点圈画函数circle画出特征点,并将圈画过的图像输出到屏幕上。

程序的运行结果在文首已经展示过了。如果大家没看过瘾,第九章Project部分提供了一个RGBD数据集,我们在包含数百张RGBD-深度图像的数据集中再次运行本程序进行LK光流跟踪,结果如下(由于上传的gif不能超过2m,小绿只截取了其中的一些帧):

本期程序useLK.cpp小绿就带领大家阅读到这里,水平有限,难免有疏漏之处。如有问题或者疑问,尽管在后台向小绿指出,在此表示感谢。

相关阅读

图像特征点|moravec特征点

入门学习SLAM(Ubuntu16.04安装ROS kinetic)

高翔Slambook第七讲代码解读(特征点提取)

高翔Slambook第七讲代码解读(三角测量)

高翔Slambook第七讲代码解读(2d-2d位姿估计)

高翔Slambook第七讲代码解读(3d-2d位姿估计)

高翔Slambook第七讲代码解读(3d-3d位姿估计)

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-11-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小白学视觉 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档