OpenCV之光流法运动目标跟踪

[光流Optical Flow]的概念是Gibson在1950年首先提出来的。它是空间运动物体在观察成像平面上的像素运动的瞬时速度,是利用图像序列中像素在时间域上的变化以及相邻帧之间的相关性来找到上一帧跟当前帧之间存在的对应关系,从而计算出相邻帧之间物体的运动信息的一种方法。
一般而言,光流是由于场景中前景目标本身的移动、相机的运动,或者两者的共同运动所产生的。当人的眼睛观察运动物体时,物体的景象在人眼的视网膜上形成一系列连续变化的图像,这一系列连续变化的信息不断流过视网膜(即图像平面),好像一种光的流;,故称之为光流(optical flow)。
光流表达了图像的变化,由于它包含了目标运动的信息,因此可被观察者用来确定目标的运动情况。从图片序列中近似得到不能直接得到的运动场<运动场,其实就是物体在三维真实世界中的运动;光流场,是运动场在二维图像平面上(人的眼睛或者摄像头)的投影。
那通俗的讲就是通过一个图片序列,把每张图像中每个像素的运动速度和运动方向找出来就是光流场。那怎么找呢?咱们直观理解肯定是:第t帧的时候A点的位置是(x1, y1),那么我们在第t+1帧的时候再找到A点,假如它的位置是(x2,y2),那么我们就可以确定A点的运动了:(ux, vy) = (x2, y2) - (x1,y1)。 
那怎么知道第t+1帧的时候A点的位置呢? 这就存在很多的光流计算方法了。


光流计算方法

大致可分为三类:基于匹配的方法、频域的方法和梯度的方法。
1. 基于匹配的光流计算方法包括基于特征和基于区域两种
2. 基于频域的方法,也称为基于能量的方法,利用速度可调的滤波组输出频率或相位信息。
3. 基于梯度的方法利用图像序列亮度的时空微分计算2D速度场(光流)。

当前对于光流法的研究主要有两个方向

一是研究在固有硬件平台基础上实现现有算法 
二是研究新的算法。

光流算法的主要目的就是基于序列图像实现对光流场的可靠、快速、精确以及鲁棒性的估计。然而,由于图像序列目标的特性、场景中照明,光源的变化、运动的速度以及噪声的影响等多种因素影响着光流算法的有效性。

函数详解

1.CalcOpticalFlowPyrLK

计算一个稀疏特征集的光流,使用金字塔中的迭代 Lucas-Kanade 方法 
C++函数代码

   void calcOpticallFlowPyrLK (
         InuputArray prevImg, 
         InputArray prevPts, 
         InputOutputArraynextPts,
         OutputArray err, 
         Size winSize = Size(21,21), 
         int maxLevel = 3, 
         TermCriteriacriteria=TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 0.01), 
         int flags = 0,
         double minEigThreshold = 1e-4
 ); 

函数参数详解

参数 解释
prevImg 深度为8位的前一帧图像或金字塔图像。
nextImg 和prevImg有相同的大小和类型,后一帧图像或金字塔。
prevPts 计算光流所需要的输入2D点矢量,点坐标必须是单精度浮点数。
nextPts 输出2D点矢量(也是单精度浮点数坐标),点矢量中包含的是在后一帧图像上计算得到的输入特征新位置。
status 输出状态矢量(元素是无符号char类型,uchar),如果相应特征的流发现则矢量元素置为1,否则,为0。
err 输出误差矢量。
winSize 每个金字塔层搜索窗大小。
maxLevel 金字塔层的最大数目;如果置0,金字塔不使用(单层);如果置1,金字塔2层,等等以此类推。
criteria 指定搜索算法收敛迭代的类型
minEigTheshold 算法计算的光流等式的2x2常规矩阵的最小特征值。

金字塔Lucas-Kannade算法:

LK算法有三个假设:亮度恒定,即图像场景中目标的像素在帧间运动时外观上保持不变;时间连续或者运动是”小运动“,即图像的运动随时间的变化比较缓慢;空间一致,即一个场景中同一表面上邻近的点具有相似的运动。然而,对于大多数30HZ的摄像机,大而连贯的运动是普遍存在的情况,所以LK光流正因为这个原因在实际中的跟踪效果并不是很好。我们需要大的窗口来捕获大的运动,而大窗口往往违背运动连贯的假设!而图像金字塔可以解决这个问题,于是乎,金字塔Lucas-Kanade就提出来了。 
金字塔Lucas-Kanade跟踪方法是:在图像金字塔的最高层计算光流,用得到的运动估计结果作为下一层金字塔的起始点,重复这个过程直到到达金字塔的最底层。这样就将不满足运动的假设可能性降到最小从而实现对更快和更长的运动的跟踪。


2.cvGoodFeaturesToTrack

函数 cvGoodFeaturesToTrack 在图像中寻找具有大特征值的角点。该函数,首先用cvCornerMinEigenVal 计算输入图像的每一个像素点的最小特征值,并将结果存储到变量 eig_image 中。然后进行非最大值抑制(仅保留3x3邻域中的局部最大值)。下一步将最小特征值小于 quality_level?max(eig_image(x,y)) 排除掉。最后,函数确保所有发现的角点之间具有足够的距离,(最强的角点第一个保留,然后检查新的角点与已有角点之间的距离大于 min_distance )。 
C++函数代码

void cvGoodFeaturesToTrack( 
         const CvArr* image, 
         CvArr* temp_image,
         CvPoint2D32f* corners, 
         int* corner_count,
         double quality_level, 
         double min_distance,
         const CvArr* mask=NULL,
         int block_size = NULL,
         int use_harris = 0,
         double k = 0.4
);

函数参数详解

参数 解释
image 输入图像,8-位或浮点32-比特,单通道
eig_image 临时浮点32-位图像,尺寸与输入图像一致
temp_image 另外一个临时图像,格式与尺寸与 eig_image 一致
corners 输出参数,检测到的角点
corner_count 输出参数,检测到的角点数目
quality_level 最大最小特征值的乘法因子。定义可接受图像角点的最小质量因子。
min_distance 限制因子。得到的角点的最小距离。使用 Euclidian 距离
mask ROI感兴趣区域。函数在ROI中计算角点,如果 mask 为 NULL,则选择整个图像。
block_size 计算导数的自相关矩阵时指定点的领域,采用小窗口计算的结果比单点(也就是block_size为1)计算的结果要好。
use_harris 标志位。当use_harris的值为非0,则函数使用Harris的角点定义;若为0,则使用Shi-Tomasi的定义。
k 当use_harris为k且非0,则k为用于设置Hessian自相关矩阵即对Hessian行列式的相对权重的权重系数

实例代码块

本代码参考@浅墨_毛星云的书籍《OpenCV3编程入门》,代码版权归老师所有,仅供学习借鉴只用。 

——博主

@--751407505@qq.com
// 程序描述:来自OpenCV安装目录下Samples文件夹中的官方示例程序-利用光流法进行运动目标检测
//  描述:包含程序所使用的头文件和命名空间
#include <opencv2/video/video.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/core/core.hpp>
#include <iostream>
#include <cstdio>
using namespace std;
using namespace cv;
//  描述:声明全局函数
void tracking(Mat &frame, Mat &output);
bool addNewPoints();
bool acceptTrackedPoint(int i);
//  描述:声明全局变量
string window_name = "optical flow tracking";
Mat gray;   // 当前图片
Mat gray_prev;  // 预测图片
vector<Point2f> points[2];  // point0为特征点的原来位置,point1为特征点的新位置
vector<Point2f> initial;    // 初始化跟踪点的位置
vector<Point2f> features;   // 检测的特征
int maxCount = 500; // 检测的最大特征数
double qLevel = 0.01;   // 特征检测的等级
double minDist = 10.0;  // 两特征点之间的最小距离
vector<uchar> status;   // 跟踪特征的状态,特征的流发现为1,否则为0
vector<float> err;
//输出相应信息和OpenCV版本-----
static void helpinformation()
{
    cout <<"\n\n\t\t\t 光流法跟踪运动目标检测\n"
         <<"\n\n\t\t\t 当前使用的OpenCV版本为:" << CV_VERSION 
         <<"\n\n" ;
}

//main( )函数,程序入口
int main()
{
    Mat frame;
    Mat result;
    //加载使用的视频文件,放在项目程序运行文件下
    VideoCapture capture("1.avi");
    //显示信息函数
    helpinformation();
    // 摄像头读取文件开关
    if(capture.isOpened())  
    {
        while(true)
        {
            capture >> frame;

            if(!frame.empty())
            { 
                tracking(frame, result);
            }
            else
            { 
                printf(" --(!) No captured frame -- Break!");
                break;
            }
            int c = waitKey(50);
            if( (char)c == 27 )
            {
                break; 
            } 
        }
    }
    return 0;
}

// parameter: frame 输入的视频帧
//            output 有跟踪结果的视频帧
void tracking(Mat &frame, Mat &output)
{
    cvtColor(frame, gray, CV_BGR2GRAY);
    frame.copyTo(output);
    // 添加特征点
    if (addNewPoints())
    {
        goodFeaturesToTrack(gray, features, maxCount, qLevel, minDist);
        points[0].insert(points[0].end(), features.begin(), features.end());
        initial.insert(initial.end(), features.begin(), features.end());
    }

    if (gray_prev.empty())
    {
        gray.copyTo(gray_prev);
    }
    // l-k光流法运动估计
    calcOpticalFlowPyrLK(gray_prev, gray, points[0], points[1], status, err);
    // 去掉一些不好的特征点
    int k = 0;
    for (size_t i=0; i<points[1].size(); i++)
    {
        if (acceptTrackedPoint(i))
        {
            initial[k] = initial[i];
            points[1][k++] = points[1][i];
        }
    }
    points[1].resize(k);
    initial.resize(k);
    // 显示特征点和运动轨迹
    for (size_t i=0; i<points[1].size(); i++)
    {
        line(output, initial[i], points[1][i], Scalar(0, 0, 255));
        circle(output, points[1][i], 3, Scalar(0, 255, 0), -1);
    }

    // 把当前跟踪结果作为下一此参考
    swap(points[1], points[0]);
    swap(gray_prev, gray);  
    imshow(window_name, output);
}

//  检测新点是否应该被添加
// return: 是否被添加标志
bool addNewPoints()
{
    return points[0].size() <= 10;
}

//决定哪些跟踪点被接受
bool acceptTrackedPoint(int i)
{
    return status[i] && ((abs(points[0][i].x - points[1][i].x) + abs(points[0][i].y - points[1][i].y)) > 2);
}

猜你喜欢

转载自blog.csdn.net/liangchunjiang/article/details/79848711