OpenCV 
开源计算机视觉库
正在加载...
正在搜索...
无匹配结果
光流

上一篇教程:均值漂移和CamShift
下一篇教程:级联分类器

目标

在本节中,

光流

光流是图像物体在两帧之间出现的运动模式,由物体或摄像头的运动引起。它是一个二维向量场,每个向量都是一个位移向量,显示了点从第一帧到第二帧的运动。考虑下面的图像(图片来源:维基百科关于光流的文章)。

image

它显示了一个球在 5 帧连续画面中的移动。箭头显示了它的位移向量。光流在以下领域有许多应用:

  • 运动结构
  • 视频压缩
  • 视频稳定...

光流基于以下几个假设:

  1. 物体的像素强度在连续帧之间不会发生变化。
  2. 相邻像素具有相似的运动。

考虑第一帧中的一个像素 I(x,y,t)(注意这里增加了一个新的维度,即时间。之前我们只处理图像,所以不需要时间)。它在下一帧中移动了 (dx,dy) 的距离,下一帧是在 dt 时间后拍摄的。由于这些像素是相同的,并且强度不会发生变化,我们可以说:

I(x,y,t)=I(x+dx,y+dy,t+dt)

然后对等式右侧进行泰勒级数展开,去除共同项,并除以 dt,得到以下等式:

fxu+fyv+ft=0

其中

fx=fx;fy=fy

u=dxdt;v=dydt

上面的等式称为光流方程。其中,我们可以找到 fxfy,它们是图像梯度。类似地,ft 是沿时间的梯度。但 (u,v) 是未知的。我们不能用一个方程求解两个未知变量。因此,提供了几种方法来解决这个问题,其中一种是 Lucas-Kanade 方法。

Lucas-Kanade 方法

我们之前已经看到过一个假设,即所有相邻像素都具有相似的运动。Lucas-Kanade 方法在点周围取一个 3x3 的块。因此,所有 9 个点都具有相同的运动。我们可以找到这 9 个点的 (fx,fy,ft)。因此,我们的问题就变成了用两个未知变量求解 9 个方程,这是一个超定问题。可以使用最小二乘拟合方法得到更好的解。以下是最终解,这是一个二元二次方程组,可以解出该解。

[uv]=[ifxi2ifxifyiifxifyiifyi2]1[ifxiftiifyifti]

(注意,逆矩阵与 Harris 角点检测器类似。它表明角点是更好的跟踪点。)

因此,从用户的角度来看,这个想法很简单,我们提供一些要跟踪的点,我们得到这些点的光流向量。但再次出现了一些问题。到目前为止,我们一直处理的是小运动,因此在出现大运动时它就会失效。为了解决这个问题,我们使用金字塔。当我们向上移动金字塔时,小运动会被消除,而大运动会变成小运动。因此,通过在那里应用 Lucas-Kanade 方法,我们得到了带有尺度信息的光流。

OpenCV 中的 Lucas-Kanade 光流

OpenCV 在一个函数中提供了所有这些功能,即 cv.calcOpticalFlowPyrLK()。在这里,我们将创建一个简单的应用程序,它跟踪视频中的某些点。为了确定这些点,我们使用 cv.goodFeaturesToTrack()。我们取第一帧,在其中检测一些 Shi-Tomasi 角点,然后我们使用 Lucas-Kanade 光流迭代地跟踪这些点。对于函数 cv.calcOpticalFlowPyrLK(),我们传递上一帧、上一帧的点和下一帧。它返回下一帧的点,以及一些状态号,如果找到下一帧的点,则值为 1,否则为零。我们迭代地将这些下一帧的点作为上一帧的点传递到下一步。请查看下面的代码。

  • 可下载代码:点击这里
  • 代码概览
    #include <iostream>
    #include <opencv2/core.hpp>
    using namespace cv;
    using namespace std;
    int main(int argc, char **argv)
    {
    const string about =
    "本示例演示了 Lucas-Kanade 光流的计算。\n"
    "示例文件可以从以下地址下载:\n"
    " https://www.bogotobogo.com/python/OpenCV_Python/images/mean_shift_tracking/slow_traffic_small.mp4";
    const string keys =
    "{ h help | | 打印此帮助信息 }"
    "{ @image | vtest.avi | 图像文件路径 }";
    CommandLineParser parser(argc, argv, keys);
    parser.about(about);
    if (parser.has("help"))
    {
    parser.printMessage();
    return 0; 0;
    }
    string filename = samples::findFile(parser.get<string>("@image"));
    if (!parser.check())
    {
    parser.printErrors();
    return 0; 0;
    }
    VideoCapture capture(filename);
    if (!capture.isOpened()){
    // 打开视频输入时出错
    cerr << "无法打开文件!" << endl;
    return 0; 0;
    }
    // 创建一些随机颜色
    vector<Scalar> colors;
    RNG rng;
    for(int i = 0; i < 100; i++)
    {
    int r = rng.uniform(0, 256);
    int g = rng.uniform(0, 256);
    int b = rng.uniform(0, 256);
    colors.push_back(Scalar(r,g,b));
    }
    Mat old_frame, old_gray;
    vector<Point2f> p0, p1;
    // 取第一帧,并在其中找到角点
    capture >> old_frame;
    cvtColor(old_frame, old_gray, COLOR_BGR2GRAY);
    goodFeaturesToTrack(old_gray, p0, 100, 0.3, 7, Mat(), 7, false, 0.04);
    // 创建一个掩码图像,用于绘图目的
    Mat mask = Mat::zeros(old_frame.size(), old_frame.type());
    while(true){
    Mat frame, frame_gray;
    capture >> frame;
    if (frame.empty())
    break;;
    cvtColor(frame, frame_gray, COLOR_BGR2GRAY);
    // 计算光流
    vector<uchar> status;
    vector<float> err;
    TermCriteria criteria = TermCriteria((TermCriteria::COUNT) + (TermCriteria::EPS), 10, 0.03);
    calcOpticalFlowPyrLK(old_gray, frame_gray, p0, p1, status, err, Size(15,15), 2, criteria);
    vector<Point2f> good_new;
    for(uint i = 0; i < p0.size(); i++)
    {
    // 选择好的点
    if(status[i] == 1) {
    good_new.push_back(p1[i]);
    // 绘制轨迹
    line(mask,p1[i], p0[i], colors[i], 2);
    circle(frame, p1[i], 5, colors[i], -1);
    }
    }
    Mat img;
    add(frame, mask, img);
    imshow("Frame", img);
    int keyboard = waitKey(30);
    if (keyboard == 'q' || keyboard == 27)
    break;;
    break;
    // 现在更新上一帧和上一帧的点
    old_gray = frame_gray.clone();
    }
    }
    cv::CommandLineParser
    用于命令行解析的类。
    cv::Mat
    n 维稠密数组类。
    cv::Mat::clone
    CV_NODISCARD_STD Mat clone() const
    cv::Mat::empty
    bool empty() const
    cv::RNG
    随机数生成器。
    cv::RNG::uniform
    int uniform(int a, int b)
    cv::Size_
    用于指定图像或矩形大小的模板类。
    cv::TermCriteria
    定义迭代算法终止条件的类。
    cv::VideoCapture
    用于从视频文件、图像序列或摄像头捕获视频的类。
    cv::add
    计算两个数组或一个数组和一个标量的逐元素和。
    无符号 32 位整型 无符号整型
    定义 interface.h:42
    @ circle
    定义 gr_skig.hpp:62
    GMat mask(const GMat &src, const GMat &mask)
    将掩码应用于矩阵。
    void imshow(const String &winname, InputArray mat)
    在指定窗口中显示图像。
    int waitKey(int delay=0)
    等待按键按下。
    void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0)
    将图像从一种颜色空间转换为另一种颜色空间。
    void line(InputOutputArray img, Point pt1, Point pt2, const Scalar &color, int thickness=1, int lineType=LINE_8, int shift=0)
    绘制连接两个点的线段。
    void goodFeaturesToTrack(InputArray image, OutputArray corners, int maxCorners, double qualityLevel, double minDistance, InputArray mask=noArray(), int blockSize=3, bool useHarrisDetector=false, double k=0.04)
    确定图像上的强角点。
    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)
    使用带金字塔的迭代 Lucas-Kanade 方法计算稀疏特征集的光流。
    int main(int argc, char *argv[])
    定义 highgui_qt.cpp:3
    磁盘上与文件关联的文件存储的“黑盒”表示。
    定义 core.hpp:102
    STL 命名空间。

(此代码未检查下一个关键点的正确性。因此,即使任何特征点在图像中消失,光流也可能找到看起来接近它的下一个点。所以实际上,对于鲁棒跟踪,应该在特定间隔检测角点。OpenCV 示例提供了一个这样的示例,它每隔 5 帧查找特征点。它还运行获得的光流点的反向检查,以仅选择好的点。查看 samples/python/lk_track.py)。

查看我们获得的结果

image

OpenCV 中的稠密光流

Lucas-Kanade 方法计算稀疏特征集的光流(在我们的示例中,使用 Shi-Tomasi 算法检测到的角点)。OpenCV 提供了另一种算法来查找稠密光流。它计算帧中所有点的光流。它基于 Gunnar Farneback 的算法,该算法在 2003 年由 Gunnar Farneback 在“基于多项式扩展的两帧运动估计”中进行了解释。

以下示例展示了如何使用上述算法查找稠密光流。我们得到一个包含光流向量的二维数组,(u,v)。我们找到它们的幅度和方向。为了更好地可视化,我们对结果进行颜色编码。方向对应于图像的色调值。幅度对应于值平面。查看下面的代码

  • 可下载代码: 点击 这里
  • 代码概览
    #include <iostream>
    #include <opencv2/core.hpp>
    using namespace cv;
    using namespace std;
    int main()
    {
    VideoCapture capture(samples::findFile("vtest.avi"));
    if (!capture.isOpened()){
    // 打开视频输入时出错
    cerr << "无法打开文件!" << endl;
    return 0; 0;
    }
    Mat frame1, prvs;
    capture >> frame1;
    cvtColor(frame1, prvs, COLOR_BGR2GRAY);
    while(true){
    Mat frame2, next;
    capture >> frame2;
    if (frame2.empty())
    break;;
    cvtColor(frame2, next, COLOR_BGR2GRAY);
    Mat flow(prvs.size(), CV_32FC2);
    calcOpticalFlowFarneback(prvs, next, flow, 0.5, 3, 15, 3, 5, 1.2, 0);
    // 可视化
    Mat flow_parts[2];
    split(flow, flow_parts);
    Mat magnitude, angle, magn_norm;
    cartToPolar(flow_parts[0], flow_parts[1], magnitude, angle, true);
    normalize(magnitude, magn_norm, 0.0f, 1.0f, NORM_MINMAX);
    angle *= ((1.f / 360.f) * (180.f / 255.f));
    // 构建 HSV 图像
    Mat _hsv[3], hsv, hsv8, bgr;
    _hsv[0] = angle;
    _hsv[1] = Mat::ones(angle.size(), CV_32F);
    _hsv[2] = magn_norm;
    merge(_hsv, 3, hsv);
    hsv.convertTo(hsv8, CV_8U, 255.0);
    cvtColor(hsv8, bgr, COLOR_HSV2BGR);
    imshow("frame2", bgr);
    int keyboard = waitKey(30);
    if (keyboard == 'q' || keyboard == 27)
    break;;
    prvs = next;
    }
    }
    void convertTo(OutputArray m, int rtype, double alpha=1, double beta=0) const
    将数组转换为其他数据类型,并可选地进行缩放。
    #define CV_32FC2
    定义 interface.h:119
    #define CV_8U
    定义 interface.h:73
    #define CV_32F
    定义 interface.h:78

请查看下面的结果

image