0. 写在前面
项目需要用到光流法找到图像中的点运动方向,想到光流法刚好适用。原理部分参考: 图像处理算法--光流法-原理-CSDN博客
1. Opencv4.5.4稀疏光流函数说明
1.1 稀疏光流API介绍
- prevImg:视频前一帧图像/金字塔,单通道CV_8UC1
- nextImg:视频后一帧图像/金字塔,单通道CV_8UC1
- prevPts:前一帧图像的特征向量(输入)需要找到流的2D点的矢量,点坐标必须是单精度浮点数
- nextPts:后一帧图像的特征向量(输出),输出二维点的矢量(具有单精度浮点坐标),包含第二图像中输入特征点计算新位置;当传递OPTFLOW_USE_INITIAL_FLOW标志时,向量必须与输入中的大小相同
- status:输出状态向量(无符号字符);如果找到相应特征的流,则向量的每个元素设置为1,否则设置为0
- err:输出错误矢量
- winSize:每个金字塔等级的搜索窗口winSize大小
- maxLevel:基于0的最大金字塔等级数,如果设置为0,则不使用金字塔,如果设置为1,则使用两个类别,依次类推,如果将金字塔传递给输入,那么算法将使用与金字塔一样多的级别,但不超过maxLevel
- criteria:停止条件,指定迭代搜索算法的终止条件
- flags:操作标志,OPTFLOW_USE_INITIAL_FLOW使用初始估计,存储在nextPts中;如果未设置标志,则将prevPts复制到nextPts并将其视为初始估计。
- OPTFLOW_LK_GET_MIN_EIGENVALS使用最小特征值作为误差测量(参见minEigThreshold描述);如果没有设置标志,则将原稿周围的色块和移动点之间的L1距离除以窗口中的像素数,用作误差测量
- minEigThreshold:算法计算光流方程的2x2正常矩阵的最小特征值,除以窗口中的像素数;如果此值小于minEigThreshold,则过滤掉相应的功能并且不处理其流程,因此它允许删除坏点并获得性能提升。
1.2 算法实现步骤
- 实例化VideoCapture,循环读取视频数据,视频帧灰度转换;
- 执行角点检测,保存角点检测的特征数据;
- 初始化时如果检测到前一帧为空,把当前帧的灰度图像给前一帧;
- 执行光流跟踪,并输出跟踪后的特征向量;
- 遍历光流跟踪的输出特征向量,并得到距离和状态都符合预期的特征向量;
- 重置集合大小,绘制光流线,交换特征向量的输入和输出;
- 将用于跟踪的角点绘制出来,展示最终的跟踪效果。
1.3 代码如下
void MainWindow::on_pBtn_OpenFile_clicked()
{
//打开图片文件,选择图片
QString filename = QFileDialog::getOpenFileName(this,tr("Open File"),QDir::homePath(),tr("所有文件(*.avi *.mp4 *.h624 *.mkv)\n(*.jpg)\n(*.bmp)\n(*.png)"));
capture.open(filename.toStdString()); //.toStdString()
if(!capture.isOpened())
{
ui->statusBar->showMessage(tr("Open Video Failed!"));
}
else
{
ui->statusBar->showMessage(tr("Open Video Success!"));
}
Mat frame,gray;
vector<Point2f> features;//检测出来的角点集合
vector<Point2f> inPoints;//这个主要是为了画线用的
vector<Point2f> fpts[2];//[0],存入的是是二维特征向量,[1]输出的二维特征向量
Mat pre_frame,pre_gray;
vector<uchar> status;//光流输出状态
vector<float> err;//光流输出错误
//【2】循环读取视频
while(capture.read(frame))
{//循环读取视频中每一帧的图像
//【3】将视频帧图像转为灰度图
cvtColor(frame,gray,COLOR_BGR2GRAY);//ps:角点检测输入要求单通道
//【4】如果特征向量(角点)小于40个我们就重新执行角点检测
if(fpts[0].size()<40){//如果小于40个角点就重新开始执行角点检测
//执行角点检测
goodFeaturesToTrack(gray,features,5000,0.01,10,Mat(),3,false,0.04);
//【5】将检测到的角点放入fpts[0]中作为,光流跟踪的输入特征向量
//将检测到的角点插入vector
fpts[0].insert(fpts[0].begin(),features.begin(),features.end());
inPoints.insert(inPoints.end(),features.begin(),features.end());
qDebug()<<"角点检测执行完成,角点个数为:"<<features.size();
}else{
qDebug()<<"正在跟踪...";
}
//【6】初始化的时候如果检测到前一帧为空,这个把当前帧的灰度图像给前一帧
if(pre_gray.empty()){//如果前一帧为空就给前一帧赋值一次
gray.copyTo(pre_gray);
}
//执行光流跟踪
qDebug()<<"开始执行光流跟踪";
//【7】执行光流跟踪,并将输出的特征向量放入fpts[1]中
calcOpticalFlowPyrLK(pre_gray,gray,fpts[0],fpts[1],status,err);
qDebug()<<"光流跟踪执行结束";
//【8】遍历光流跟踪的输出特征向量,并得到距离和状态都符合预期的特征向量。让后将其重新填充到fpts[1]中备用
int k =0;
for(size_t i=0;i<fpts[1].size();i++){//循环遍历二维输出向量
double dist = abs(fpts[0][i].x - fpts[1][i].x) + abs(fpts[0][i].y - fpts[1][i].y);//特征向量移动距离
if(dist>2&&status[i]){//如果距离大于2,status=true(正常)
inPoints[k] = inPoints[i];
fpts[1][k++] = fpts[1][i];
}
}
//【9】重置集合大小(由于有错误/不符合条件的输出特征向量),只拿状态正确的
//重新设置集合大小
inPoints.resize(k);
fpts[1].resize(k);
//【10】绘制光流线,这一步要不要都行
//绘制光流线
if(true){
for(size_t i = 0;i<fpts[1].size();i++){
line(frame,inPoints[i],fpts[1][i],Scalar(0,255,0),1,8,0);
circle(frame, fpts[1][i], 2, Scalar(0, 0, 255), 2, 8, 0);
}
}
qDebug()<<"特征向量的输入输出交换数据";
//【11】交换特征向量的输入和输出,(循环往复/进入下一个循环),此时特征向量的值会递减
std::swap(fpts[1],fpts[0]);//交换特征向量的输入和输出,此处焦点的总数量会递减
//【12】将用于跟踪的角点绘制出来
//将角点绘制出来
for(size_t i = 0;i<fpts[0].size();i++){
circle(frame,fpts[0][i],2,Scalar(0,0,255),2,8,0);
}
//【13】重置前一帧图像(每一个循环都要刷新)
gray.copyTo(pre_gray);
frame.copyTo(pre_frame);
//【14】展示最终的效果
imshow("frame",frame);
int keyValue = waitKey(100);
if(keyValue==27){//如果用户按ese键退出播放
break;
}
}
}