cmakelists:
cmake_minimum_required(VERSION 3.7)
project(LKFlow)
set(CMAKE_CXX_STANDARD 11)
find_package(OpenCV)
include_directories(${OpenCV_INCLUDE_DIRS})
set(SOURCE_FILES main.cpp)
add_executable(LKFlow ${SOURCE_FILES})
target_link_libraries(LKFlow ${OpenCV_LIBS})
source:
#include <iostream>
#include <fstream>
#include <list>//这里用到了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_data_folder"<<endl;
return 1;
}
//将命令行转存一下,转存至path_to_data_folder。
//这里注意,这里的argv[1]是一个文件夹,而不是像以往用的都直接是文件,
//因为这个程序需要用到data文件夹中好多个文件,所以这里直接通过命令行引入文件夹,然后再在文件夹中寻找文件
string path_to_data_folder=argv[1];
//这里就是寻址文件的一种方法,由于path_to_data_folder是文件夹路径,在文件夹中选文件,中间要有/,不要忘记。
string associate_file=path_to_data_folder+"/associate.txt";
//通过文件输入,将associate_file文件读进来。
ifstream fin(associate_file);
//读入防呆,如果没有读取成功直接跳出。
if(!fin)
{
cerr<<"I cann't find associate.txt!"<<endl;
return 1;
}
//这里定义用于承接associate.txt文件中的输入,依次是:rgb图的时间,rgb图名称,深度图时间,深度图
string time_rgb, rgb_file, time_depth, depth_file;
list<cv::Point2f> keypoints;//因为要删除跟踪失败的点,所以使用list
//定义彩色图和深度图,以及用于逐帧操作的将color承接为last_color,然后color再去读取新的图,依次循环
cv::Mat color, depth, last_color;
//循环,这里index指的是每一帧对应一个index。为什么是100,后面解释。
for(int index=0; index<100; index++)
{
//这里可以看出,虽然程序中并不需要time_rgb和time_depth这两个数据,
//但是由于文件输入流associate_file中有这两个信息,所以还是将其从流中读出来,保证顺次能够正确读取到rgb_file和depth_file
fin>>time_rgb>>rgb_file>>time_depth>>depth_file;
//到这里,我们才真正开始读到一张color图和一张depth图,path_to_data_folder寻址到data文件夹,
//rgb_file变量格式是这样:rgb/1305031453.359684.png,自带了进入rgb文件夹中寻址彩色图
color=cv::imread(path_to_data_folder+"/"+rgb_file);
//此程序中好像没有用到depth做什么事,因为本来LK光流就没深度什么事,纯粹是玩两张平面图
depth=cv::imread(path_to_data_folder+"/"+depth_file, -1);
//单独处理第一帧,对第一帧提取FAST特征点。第一帧不需要其他工作。
if(index==0)
{
//用于承接关键点的数组
vector<cv::KeyPoint> kps;
//创建detector指针,注意这里是指针,注意创建格式
cv::Ptr<cv::FastFeatureDetector> detector = cv::FastFeatureDetector::create();
//调用detector的detect方法,对color进行角点检测,检测结果存储在kps关键点数组中
detector->detect(color,kps);
//遍历所有关键点,并把关键点的坐标放入keypoints列表中,
//注意keypoints类型为list,元素类型为cv::Point2f,即是坐标
//这里还是要注意注意注意,kp类型是KeyPoint,KeyPoint是KeyPoint,跟KeyPoint的坐标是两码事,
//要访问坐标,用.pt成员进行访问!!!
for (auto kp:kps)
keypoints.push_back(kp.pt);
//由于是第一帧,所以last_color也就是color了。
last_color = color;
//第一帧的工作就是检测角点,并把角点坐标存储在keypoints列表中,没有其他工作,所以处理完这些就直接continue跳出
continue;
}
//这里就是上方index为100的原因。在不是第一帧(即index不为0)时候,上一句就是读取彩色图和深度图,
//cv::imread的参数为一个图片名称字符串,即使没有那个文件时并不会报错,所以这里来检测。
//associate.txt文件中提供了很多时间对齐的color和depth图,但是我们的color和depth文件夹中只有前9帧,其他都没有。
//所以运行操作时,index=1,2,3,4,5,6,7,8,9.当在第十帧时,这一句就起作用了,因为imread去读了一个不存在的文件,
//所以color.data和depth.data都为空指针(这里为了以防万一用了或),用continue将这一帧跳过,
//由于后面都没有了,所以一直continue到index=99,然后程序结束
//猜测一下这里为什么这么用,这样设置index其实就是设置了总帧数,
//用这个if进行continue跳过仅是跳过这一帧,因为实际工况中极有可能某一帧丢失,
//后面又有画面帧时还会继续工作,而不会丢失一帧就直接停掉
if ( color.data==nullptr || depth.data==nullptr )
{
//加上这一句就会发现index=1,2,3,4,5,6,7,8,9时会有角点跟踪图,
//后面就是直接秒输出到index直到99,然后循环结束,程序结束
//也就是后面在没有帧画面时还是会一直imread,只是因为没有data就被一直continue掉了,很快index到99,程序结束了
cout<<"index="<<index<<endl;
continue;
}
//在index不为0时,对其他帧用LK跟踪特征点
//首先定义前一帧和后一帧的关键点坐标数组,这里是在循环里面直接创建,所以每次出循环,这俩就没了,进循环到这里就被定义创建
vector<cv::Point2f> prev_keypoints;
vector<cv::Point2f> next_keypoints;
//将从第一帧检测得到的关键点坐标列表,赋值给prev_keypoints数组,遍历并逐个元素push_back
for (auto kp:keypoints)
prev_keypoints.push_back(kp);
//匹配状态承接数组,第一幅图中的关键点,在第二幅图中匹配到就给status赋1,没匹配到就赋0.
vector<unsigned char> status;
//匹配误差承接数组,在匹配到的关键点中,都会有一个匹配误差,用error承接出来。
//想想LK光流原理,每一个像素带入都会得到一个方程,所以最后求解的超定方程解会让每个像素(这里就是关键点)的方程有一点误差。
vector<float> error;
//这就是LK光流跟踪函数,说一下参数:
/*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 );*/
//InputArray prevImg 前一张图
//InputArray nextImg 后一张要被跟踪的图
//InputArray prevPts 前一张图中需要跟踪的角点坐标数组
//InputOutputArray nextPts 后一张图中角点被跟踪出来后,承接放到此数组中,大小跟prevPts一样
//OutputArray status 角点跟踪状态承接数组
//OutputArray err 误差承接数组
cv::calcOpticalFlowPyrLK( last_color, color, prev_keypoints, next_keypoints, status, error );
//这一步是对keypoints列表进行更新,从前面的prev_keypoints更新到next_keypoints。
int i=0;
//这个循环中其实有两条遍历线,
// 一条就是keypoints列表,通过iter指针控制,从keypoints.begin()开始到keypoints.end()结束。
//遍历过程中通过判断status[]值是否为0,如果为0,则说明在跟踪过程中丢了,需要删掉。用.erase(iter)擦除
//另外一条就是i,来遍历每两帧跟踪后得到的next_keypoints
for ( auto iter=keypoints.begin(); iter!=keypoints.end(); i++)
{
if ( status[i] == 0 )
{
//用这一句看看那些跟丢的点,很明显在next_keypoints数组中也是有值的,只不过这里看输出发现好多负值的坐标。这就要在后面跳过了
cout<<next_keypoints[i]<<endl;
iter = keypoints.erase(iter);
//keypoints.erase(iter);为什么这样不行??
//这个continue,continue后下一次循环时,iter不会++,因为iter++为下面的语句,continue给跳过了。
//所以在continue跳过跟丢的点时,iter还是保持当前的位置,不会++到下一个,
//这是对的,keypoints列表保存的是跟踪到的关键点。跟丢了会等着一直到后面没跟丢的数据填充进来,而不会跳过
//但是i就会被++了,因为i在for循环的()中,运行continue后就会被++。
//也就是说跟踪失败的话,next_keypoints中的数据就被跳过了,至于为什么在最后面解释
continue;
}
*iter = next_keypoints[i];
iter++;
}
cout<<"tracked keypoints: "<<keypoints.size()<<endl;
//所有跟踪都没了,就直接break了
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, 3, cv::Scalar(250, 0, 0));
//显示画出跟踪点的图像
cv::imshow("corners", img_show);
cv::waitKey(500);
//将color赋值给last_color,下一次循环将会用color读入新的帧
last_color = color;
}
return 0;
}
//说一下程序中的list<cv::Point2f> keypoints、vector<cv::Point2f> prev_keypoints和vector<cv::Point2f> next_keypoints
//keypoints为list类型,定义在每帧跟踪循环之外,一直存在。被初始化是第一帧检测角点后,将所有角点坐标存入。他的更新只有减少,没有增加(只有跟丢的点进行删除)
//vector<cv::Point2f> prev_keypoints和vector<cv::Point2f> next_keypoints是在每帧跟踪循环内被定义的,也就是每次进循环被定义,出循环被释放
//循环中两两帧跟踪时,先将keypoints中点copy给prev_keypoints,然后调用追踪函数,生成next_keypoints。
//两两帧跟踪这个过程中,肯定是有关键点丢失的(因为视角变了,一些点可能出了视野范围),比如前帧100关键点,后帧98个了。
//但是丢失归丢失,整个跟踪过程没关系,prev_keypoints和next_keypoints大小是一样的,包括status数组,都是100。
//那两个跟丢的点在next_keypoints对应位置上依然存在,依然是个坐标值,只不过区分上是在status数组对应位置上,他们的状态是0,其他跟踪到的都是1。
//这样就好解释上面的i为什么被continue跳过了,next_keypoints数组中是有坐标的,只是这个坐标没啥用,因为它的状态已经被标志为0,所以就需要跳过。
//千万不要默认为跟丢之后,next_keypoints数组就自动将其舍掉不保存了。