百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

Python计算机视觉-OpenCV中的光流

toyiye 2024-07-05 01:27 15 浏览 0 评论

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



1.什么是光流?

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

2.理论基础

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

首先,我们假设对象的位移不会改变对象的像素强度,这意味着I ( x , y , t ) = I ( x + Δ x , y + Δ y , t + Δ t )。在我们的例子中,Δ t = 1 。主要关注的是找到运动矢量( Δ x , Δ y )。让我们看看图形表示:

使用泰勒级数展开,我们可以重写I ( x , y , t ) ? I ( x + Δ x , y + Δ y , t + Δ t ) = 0为I x ′ u + I y ′ v = ? I t ′,其中u = d x d t , v = d y d t , I x ′ , I y ′ 是图像梯度。重要的是,我们假设高阶泰勒级数的部分可以忽略,所以这是一个函数近似,只用一阶泰勒展开式。帧I 1 和I 2 之间的像素运动差可表示为I 1 ? I 2 ≈ I x ′ u + I y ′ v + I t ′ 。现在,我们有两个变量u 和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 。一般来说,在Lucas-Kanade方法中,由于只有一阶泰勒展开式,我们使用了I = b T x + c 的线性逼近。现在,我们要提高二阶近似的精度。在这里,这个想法导致观察由物体位移引起的近似多项式的差异。我们的目标是用多项式近似计算I 2 ( x ) = I 1 ( x ? 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 ),其中m , c m,cm,c为光照模型参数。与之前的算法一样,有一个局部运动恒常性假设,并辅以光照恒常性。数学上,这意味着向量[ d ?? m ?? c ] 对于每个局部图像区域都是常数。

基于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/

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码