Bootstrap

OpenCV进阶(2)OpenCV中的光流

在这篇文章中,我们将学习在视频或帧序列中计算光流的各种算法。我们将讨论稀疏和密集光流算法的相关理论和在OpenCV中的实现。

1.什么是光流?

光流是一个视频中两个连续帧之间的逐像素运动估计任务。基本上,光流任务意味着计算像素的移动向量作为物体在两个相邻图像之间的位移差。光流的主要思想是估计物体运动或摄像机运动引起的物体的位移矢量。

在这里插入图片描述

2.理论基础

让我们假设我们有一个灰度图像。我们定义函数 I ( x , y , t ) I(x,y,t) I(x,y,t),其中x,y为像素坐标,t为帧数。函数 I ( x , y , t ) I(x,y,t) I(x,y,t)定义了t帧处的像素强度。

首先,我们假设对象的位移不会改变对象的像素强度,这意味着 I ( x , y , t ) = I ( x + Δ x , y + Δ y , t + Δ t ) I(x,y,t) = I(x+ \Delta x,y + \Delta y,t + \Delta t) I(x,y,t)=I(x+Δx,y+Δy,t+Δt)。在我们的例子中, Δ t = 1 \Delta t = 1 Δt=1。主要关注的是找到运动矢量 ( Δ x , Δ y ) ( \Delta x, \Delta y) (Δx,Δy)。让我们看看图形表示:
在这里插入图片描述
使用泰勒级数展开,我们可以重写 I ( x , y , t ) − I ( x + Δ x , y + Δ y , t + Δ t ) = 0 I(x,y,t) - I_(x+ \Delta x,y + \Delta y,t + \Delta t) = 0 I(x,y,t)I(x+Δx,y+Δy,t+Δt)=0 I x ′ u + I y ′ v = − I t ′ I'_x u + I'_y v = - I'_t Ixu+Iyv=It,其中 u = d x d t , v = d y d t , I x ′ , I y ′ u = \frac{dx}{dt}, v = \frac{dy}{dt}, I'_x, I'_y u=dtdxv=dtdyIx,Iy是图像梯度。重要的是,我们假设高阶泰勒级数的部分可以忽略,所以这是一个函数近似,只用一阶泰勒展开式。帧 I 1 I_1 I1 I 2 I_2 I2之间的像素运动差可表示为 I 1 − I 2 ≈ I x ′ u + I y ′ v + I t ′ I_1 - I_2 \approx I'_x u + I'_y v + I'_t I1I2Ixu+Iyv+It。现在,我们有两个变量 u u u v v v,只有一个方程,所以我们现在不能解这个方程,但是我们可以使用一些技巧,这些技巧会在接下来的算法中被揭示。

3.光流的应用

光流可以应用于许多对目标运动信息至关重要的领域。光流通常在视频编辑压缩,稳定,慢动作等被发现。此外,光流在动作识别任务和实时跟踪系统中也有应用。

4.稀疏和密集光流

光流有两种类型,第一种称为稀疏光流。它计算特定对象集合的运动向量(例如,图像上检测到的角)。因此,需要对图像进行预处理以提取特征,这是光流计算的基础。OpenCV提供了一些算法实现来解决稀疏光流任务:(1)Pyramid Lucas-Kanade(2)Sparse RLOF

仅使用稀疏特征集意味着我们将不会有不包含在其中的像素的运动信息。使用密集光流算法可以消除这一限制,该算法假定为图像中的每个像素计算一个运动向量。OpenCV中已经实现了一些密集光流算法:
(1)Dense Pyramid Lucas-Kanade
(2)Farneback
(3)PCAFlow
(4)SimpleFlow
(5)RLOF
(6)DeepFlow
(7)DualTVL1

在这篇文章中,我们将看看其中一些算法的理论方面以及它们在OpenCV中的使用。

5.稀疏光流

Lucas-Kanade算法

Lucas-Kanade方法是计算稀疏特征集光流的常用方法。该方法的主要思想是基于局部运动不变的假设,即附近像素具有相同的位移方向。这个假设有助于求出二元方程的近似解。

Lucas-Kanade算法改进

由于算法的局限性,光流算法确实会受到突然移动的影响。实践中常用的方法是使用多重缩放技巧。我们需要创建一个所谓的图像金字塔,其中每一张图像都将比前一张图像大一些(例如,比例因子是2)。在固定尺寸窗口中,小尺寸图像上的突然移动比大尺寸图像上更明显。在小图像中建立的位移矢量将用于下一个更大的金字塔阶段,以获得更好的结果。

如前所述,密集光流算法计算稀疏特征集的运动向量,所以这里常用的方法是使用Shi-Tomasi角点检测器。该算法用于寻找图像中的角点,然后计算连续两帧之间的角点运动矢量。

使用OpenCV实现Lucas-Kanade

OpenCV实现了基于Shi-Tomasi的Pyramid Lucas & Kanade算法来计算光流。让我们看看基于官方文档的OpenCV算法。
(1)Python

# lucas_kanade.py
import cv2
import numpy as np


def lucas_kanade_method(video_path):
    cap = cv2.VideoCapture(video_path)
    # ShiTomasi角点检测的参数
    feature_params = dict(maxCorners=100, qualityLevel=0.3, minDistance=7, blockSize=7)
    # lucas kanade光流算法的参数
    lk_params = dict(
        winSize=(15, 15),
        maxLevel=2,
        criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03),
    )
    # 创建一些随机的颜色
    color = np.random.randint(0, 255, (100, 3))
    # 取第一帧并在其中找到角点
    ret, old_frame = cap.read()
    old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
    p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)
    # 创建用于绘图的掩模图像
    mask = np.zeros_like(old_frame)
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        # 计算光流
        # calcOpticalFlowPyrLK(prevImg, nextImg, prevPts, nextPts[, status[, err[, \\
        # winSize[, maxLevel[, criteria[, flags[, minEigThreshold]]]]]]]) -> nextPts, status, err
        p1, st, err = cv2.calcOpticalFlowPyrLK(
            old_gray, frame_gray, p0, None, **lk_params
        )
        # 选择比较好的点
        good_new = p1[st == 1]
        good_old = p0[st == 1]
        # 画出轨迹
        for i, (new, old) in enumerate(zip(good_new, good_old)):
            a, b = new.ravel()
            c, d = old.ravel()
            mask = cv2.line(mask, (a, b), (c, d), color[i].tolist(), 2)
            frame = cv2.circle(frame, (a, b), 5, color[i].tolist(), -1)
        img = cv2.add(frame, mask)
        cv2.imshow("frame", img)
        k = cv2.waitKey(25) & 0xFF
        if k == 27:
            break
        if k == ord("c"):
            mask = np.zeros_like(old_frame)
        # 现在更新之前的帧和之前的点
        old_gray = frame_gray.copy()
        p0 = good_new.reshape(-1, 1, 2)
if __name__ == "__main__":
	video_path = "videos//people.mp4"
	lucas_kanade_method(video_path)
# python lucas_kanade.py

(2)C++

// lucas_kanade.cpp
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/video.hpp>
#include <opencv2/optflow.hpp>
#include <sys/stat.h>
using namespace cv;
using namespace std;

int lucas_kanade(const string& filename, bool save)
{
    VideoCapture capture(filename);
    if (!capture.isOpened()){
        //打开视频输入错误
        cerr << "Unable to open file!" << endl;
        return 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());
    int counter = 0;
    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);
        if (save) {
            string save_path = "./optical_flow_frames/frame_" + to_string(counter) + ".jpg";
            imwrite(save_path, img);
        }
        imshow("flow", img);
        int keyboard = waitKey(25);
        if (keyboard == 'q' || keyboard == 27)
            break;
        // 创建用于绘图的掩模图像
        old_gray = frame_gray.clone();
        p0 = good_new;
        counter++;
    }
}

int main(int argc, char** argv)
{
    const string keys =
            "{ h help |      | print this help message }"
            "{ @video |  | path to image file }"
            "{ @method | | method to OF calcualtion }"
            "{ save | | save video frames }";
    CommandLineParser parser(argc, argv, keys);

    string filename = samples::findFile(parser.get<string>("@video"));
    if (!parser.check())
    {
        parser.printErrors();
        return 0;
    }

    string method = parser.get<string>("@method");
    printf("%s %s", method.c_str(), "method is now working!");
    bool save = false;
    if (parser.has("save")){
        save = true;
        mkdir("optical_flow_frames", 0777);
    }
    bool to_gray = true;
    if (method == "lucaskanade")
    {
        lucas_kanade(filename, save);
    }
    return 0;
}
//./OpticalFlow ../videos/car.mp4 lucaskanade

代码解析

首先,我们需要加载我们的视频,并从第一帧得到Shi-Tomasi算法的特征。此外,这里还需要一些算法和可视化的预处理。

之后,我们可以开始我们的演示。这是一个循环过程,我们读取一个新的视频帧,并在循环中计算Shi-Tomasi特征和光流。计算出的光流显示为彩色曲线。

简而言之,这个脚本取两个连续的帧,并使用cv2.goodFeaturesToTrack()函数查找第一个帧的角点。然后根据角点位置信息,利用Lucas-Kanade算法计算光流。这是一个循环的过程,对每一对连续的图像做同样的事情。

6.稠密光流

在本节中,我们将看一看稠密光流算法,它可以计算图像中每个像素的运动矢量。

Farneback算法

该方法的主要思想是用一个多项式逼近每个像素的一些邻域: I ( x ) ∼ x T  ⁣ A x + b T  ⁣ x + c I(x) \sim x^T \!Ax + b^T \!x + c I(x)xTAx+bTx+c。一般来说,在Lucas-Kanade方法中,由于只有一阶泰勒展开式,我们使用了 I = b T x + c I = b^Tx + c I=bTx+c的线性逼近。现在,我们要提高二阶近似的精度。在这里,这个想法导致观察由物体位移引起的近似多项式的差异。我们的目标是用多项式近似计算 I 2 ( x ) = I 1 ( x − d ) I_2(x)=I_1(x-d) I2(x)=I1(xd)方程中的位移 d d d

RLOF算法(Robust Local Optical Flow algorithm)

这项工作的主要观点是,强度不变性假设并不能完全反映真实世界的行为。也有阴影、反射、天气状况、移动的光源,简而言之,不同的亮度。
RLOF算法基于Gennert和Negahdaripour在1995年提出的光照模型: I ( x , y , t ) + m ⋅ I ( x , y , t ) + c = I ( x + u , y + v , t + 1 ) I(x,y,t) + m \cdot I(x,y,t) + c = I(x + u, y + v, t + 1) I(x,y,t)+mI(x,y,t)+c=I(x+u,y+v,t+1),其中 m , c m,c m,c为光照模型参数。与之前的算法一样,有一个局部运动恒常性假设,并辅以光照恒常性。数学上,这意味着向量 [ d    m    c ] [d\;m\;c] [dmc]对于每个局部图像区域都是常数。

基于OpenCV的实现

由于OpenCV密集光流算法具有相同的使用模式,我们创建了封装函数以方便和避免代码重复。
(1)Python

# dense_optical_flow.py
import cv2
import numpy as np


def dense_optical_flow(method, video_path, params=[], to_gray=False):
    # 读取视频
    cap = cv2.VideoCapture(video_path)
    # 读取第一帧
    ret, old_frame = cap.read()

    # 创建HSV并使Value为常量
    hsv = np.zeros_like(old_frame)
    hsv[..., 1] = 255

    # 精确方法的预处理
    if to_gray:
        old_frame = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)

    while True:
        # 读取下一帧
        ret, new_frame = cap.read()
        frame_copy = new_frame
        if not ret:
            break
        # 精确方法的预处理
        if to_gray:
            new_frame = cv2.cvtColor(new_frame, cv2.COLOR_BGR2GRAY)
        # 计算光流
        flow = method(old_frame, new_frame, None, *params)

        # 编码:将算法的输出转换为极坐标
        mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1])
        # 使用色相和饱和度来编码光流
        hsv[..., 0] = ang * 180 / np.pi / 2
        hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
        # 转换HSV图像为BGR
        bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
        cv2.imshow("frame", frame_copy)
        cv2.imshow("optical flow", bgr)
        k = cv2.waitKey(25) & 0xFF
        if k == 27:
            break
        old_frame = new_frame
def main():
    parser = ArgumentParser()
    parser.add_argument(
        "--algorithm",
        choices=["farneback", "lucaskanade_dense", "rlof"],
        required=True,
        help="Optical flow algorithm to use",
    )
    parser.add_argument(
        "--video_path", default="videos/people.mp4", help="Path to the video",
    )

    args = parser.parse_args()
    video_path = args.video_path
    if args.algorithm == "lucaskanade_dense":
        method = cv2.optflow.calcOpticalFlowSparseToDense
        dense_optical_flow(method, video_path, to_gray=True)
    elif args.algorithm == "farneback":
        # OpenCV Farneback算法需要一个单通道的输入图像,因此我们将BRG图像转换为灰度。
        method = cv2.calcOpticalFlowFarneback
        params = [0.5, 3, 15, 3, 5, 1.2, 0]  # Farneback的算法参数
        dense_optical_flow(method, video_path, params, to_gray=True)
    elif args.algorithm == "rlof":
    	# 与Farneback算法相比,RLOF算法需要3通道图像,所以这里没有预处理。
        method = cv2.optflow.calcOpticalFlowDenseRLOF
        dense_optical_flow(method, video_path)


if __name__ == "__main__":
    main()
# python dense_optical_flow.py

(2)C++

// dense_optical_flow.cpp
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/optflow.hpp>
#include <sys/stat.h>

using namespace cv;
using namespace std;

template <typename Method, typename... Args>
void dense_optical_flow(string filename, bool save, Method method, bool to_gray, Args&&... args)
{
    VideoCapture capture(samples::findFile(filename));
    if (!capture.isOpened()) {
        //打开视频错误
        cerr << "Unable to open file!" << endl;
    }
    Mat frame1, prvs;
    capture >> frame1;
    if (to_gray)
        cvtColor(frame1, prvs, COLOR_BGR2GRAY);
    else
        prvs = frame1;
    int counter = 0;
    while (true) {
        Mat frame2, next;
        capture >> frame2;
        if (frame2.empty())
            break;
        if (to_gray)
            cvtColor(frame2, next, COLOR_BGR2GRAY);
        else
            next = frame2;
        Mat flow(prvs.size(), CV_32FC2);
        method(prvs, next, flow, std::forward<Args>(args)...);
        // 可视化
        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);
        if (save) {
            string save_path = "./optical_flow_frames/frame_" + to_string(counter) + ".jpg";
            imwrite(save_path, bgr);
        }
        imshow("frame", frame2);
        imshow("flow", bgr);
        int keyboard = waitKey(30);
        if (keyboard == 'q' || keyboard == 27)
            break;
        prvs = next;
        counter++;
    }
}
int main(int argc, char** argv)
{
    const string keys =
            "{ h help |      | print this help message }"
            "{ @video |  | path to image file }"
            "{ @method | | method to OF calcualtion }"
            "{ save | | save video frames }";
    CommandLineParser parser(argc, argv, keys);

    string filename = samples::findFile(parser.get<string>("@video"));
    if (!parser.check())
    {
        parser.printErrors();
        return 0;
    }

    string method = parser.get<string>("@method");
    printf("%s %s", method.c_str(), "method is now working!");
    bool save = false;
    if (parser.has("save")){
        save = true;
        mkdir("optical_flow_frames", 0777);
    }
    bool to_gray = true;
    if (method == "lucaskanade_dense"){
        dense_optical_flow(filename, save, optflow::calcOpticalFlowSparseToDense, to_gray, 8, 128, 0.05f, true, 500.0f, 1.5f);
    }
    else if (method == "farneback"){
        dense_optical_flow(filename, save, calcOpticalFlowFarneback, to_gray, 0.5, 3, 15, 3, 5, 1.2, 0);
    }
    else if (method == "rlof"){
        to_gray = false;
        dense_optical_flow(
                filename, save, optflow::calcOpticalFlowDenseRLOF, to_gray,
                Ptr<cv::optflow::RLOFOpticalFlowParameter>(), 1.f, Size(6,6),
                cv::optflow::InterpolationType::INTERP_EPIC, 128, 0.05f, 999.0f, 15, 100, true, 500.0f, 1.5f, false
                );
    }
    return 0;
}

代码解析

首先,我们需要读取第一个视频帧,并在必要时进行图像预处理。演示的主要部分是一个循环,我们在其中为每对新的连续图像计算光流。之后,我们将结果编码为 HSV 格式以进行可视化。因此,method()函数读取两个连续的帧作为输入。在某些情况下,需要图像灰度化,所以to_gray参数应该设置为True。在得到算法输出后,我们对其进行编码,使用HSV颜色格式进行适当的可视化。

总结

在这篇文章中,我们考虑了光流任务,这是我们需要物体运动信息时不可缺少的任务。我们看了一些经典的算法,它们的理论思想,以及OpenCV库的实际使用。实际上,光流估计并不局限于算法方法,基于深度学习的新方法提高了光流估计的质量。

参考目录

https://learnopencv.com/optical-flow-in-opencv/

;