欢迎光临
我们一直在努力

OpenCV计算机视觉开发入门与实践<三十四>:视频播放和录制

摘要:本文系统讲解 OpenCV 视频处理模块的核心用法。首先介绍视频 I/O 架构,说明 VideoCapture 与 VideoWriter 两个核心类的作用;然后详细讲解 VideoCapture 对象的三种构造方式、打开成功判断、视频帧读取方法,以及视频文件与摄像头视频的播放流程;接着介绍视频属性的获取与设置;最后讲解 VideoWriter 类的构造、初始化与 fourcc 编码,并给出完整的录制摄像头视频示例。全文配有多个可直接运行的 C++ 代码示例,适合 OpenCV 初学者快速上手视频处理。

  OpenCV的视频模块是其核心组成部分之一,主要负责视频文件的读取、处理、分析以及视频流的捕获和输出。这一模块使得开发者能够轻松地处理来自摄像头、文件或其他视频源的视频数据,进行实时或离线的图像处理和计算机视觉任务。

1.OpenCV视频处理架构

  OpenCV的视频I/O模块提供了一组用于读写视频或图像序列的类和函数。该模块将cv::VideoCapture和cv::VideoWriter类作为一层接口面向用户。这两个类下面包括很多不同种类的后端视频I/O API,有效地屏蔽了各后端视频I/O的差异性,简化了用户层的编程。整个视频I/O架构如图所示。

在这里插入图片描述
  可以看到,用户层(USER APPLICATION)主要是和VideoCapture和VideoWriter两个类打交道,而无须和底层打交道。在OpenCV中,视频的读操作是通过VideoCapture类来完成的,视频的写操作是通过VideoWriter类来实现的。

2.VideoCapture类

  类VideoCapture是OpenCV中最基本的视频输入和输出接口,它可以读取视频文件或打开摄像头,提取视频帧,并提供多个函数去获取视频的属性信息,比如帧数。类
VideoCapture常用的成员函数如表所示。
在这里插入图片描述

3.构造VideoCapture对象

  类VideoCapture既支持从视频文件(.avi 、*.mp4、.mpg等格式)读取,也支持直接从摄像机(比如计算机自带的摄像头)中读取。要想获取视频,需要先创建一个VideoCapture对象。VideoCapture对象的创建方式有以下3种:

(1)从视频文件中读取视频。如果是从文件(.mpg或.avi格式)中读取视频,则在定义对象的时候,可以把视频文件的路径作为参数传给构造函数。对象创建以后,OpenCV将会打开该视频文件并做好准备读取它,并且VideoCapture提供了成员函数isOpened来判断文件是否打开成功,如果成功将返回true(建议在打开视频或摄像头时都使用该成员函数判断是否打开成功)。用于读取视频文件的构造函数如下:

VideoCapture (const String &filename, int apiPreference=CAP_ANY);

  其中参数filename是视频文件的文件名(可以包含路径),如果不包含路径,则在当前路径下打开文件;apiPreference表示首选使用的后端捕获API,默认是使用任意捕获API,用宏CAP_ANY来表示。比如我们定义一个VideoCapture对象并打开D盘上的test.avi文件:

cv::VideoCapture capture("D:/test.avi"); // 从视频文件读取

(2)从摄像机中读取视频。如果是从摄像机中读取视频,这种情况下,构造函数会给出一个整型索引参数,用于表示想要访问的摄像机。对于摄像机而言,这个索引参数就是一个标志数字,如果只有1个摄像机,那么索引参数就是0;如果系统中有多个摄像机,那么只要将其值向上增加即可。读取摄像头中的视频的构造函数如下:

VideoCapture (int index, int apiPreference=CAP_ANY);

  其中参数index表示要打开的视频捕获设备(摄像头)的ID,如果使用默认后端的默认摄像头,只需传递0;apiPreference表示首选使用的后端捕获API,默认是使用任意捕获API,用宏CAP_ANY来表示,多数情况下,由于我们只有一个摄像机,因此没必要指定摄像机的域,此时使用cv::CAP_ANY是一种高效的方式。参数apiPreference表示摄像机的类型,这个域值可以是下面任意一个预定义常量:

CAP_ANY:自动选择API
CAP_DSHOW:通过视频输入,例如,摄像头输入
CAP_FFMPEG:使用FFMPEG库
CAP_IMAGES:打开一个图像序列(例如,img_%02d.jpg)

(3)不带参数构造一个VideoCapture对象。使用类VideoCapture的不带参数的构造函数来创建一个VideoCapture对象,然后用成员函数open打开一个视频文件或摄像头。open函数声明如下:

virtual bool open (const String &filename, int apiPreference=CAP_ANY);
virtual bool open (int index, int apiPreference=CAP_ANY);

4.判断视频是否打开成功

  当我们打开一个视频文件或摄像头视频后,可以用成员函数isOpened来判断是否打开成功。该函数声明如下:

virtual bool isOpened() const;

若打开成功,则返回true,否则返回false。比如:

VideoCapture capture;
capture.open(0, CAP_DSHOW); // 打开摄像头
if (!capture.isOpened()) // 判断打开是否成功
puts("open failed");

5.读取视频帧

  要播放视频,肯定要把每一帧视频图像都读出来,然后显示出来。读取方式有两种,一种是用运算符>>,直接输出视频帧到Mat对象中,当输出完一帧后,下次就会输出下一帧。。>>是一个重载运算符,定义如下:

virtual VideoCapture& operator >> (CV_OUT Mat& image);

例如:
在这里插入图片描述

  另外一种方式是通过成员函数read,该函数声明如下:

virtual bool read(OutputArray image);

  参数image用来存放读取到的当前视频帧。若读取成功,则返回true,否则返回false。例如:

在这里插入图片描述

6.播放视频文件

  播放视频文件的基本步骤就是先构造VideoCapture对象,再打开视频文件,接着用一个循环逐帧读取并显示读取到的视频帧,然后间隔一段时间读取下一个视频帧并显示,依次循环,直到全部视频帧读取完毕。

【例12.1】播放MP4视频文件

在这里插入图片描述

#include <opencv2/opencv.hpp>
using namespace cv;

void play(const char* filename)
{
Mat frame;
int i = 0;
VideoCapture capture(filename);
if (!capture.isOpened())
{
printf("open %s failed", filename);
return;
}
//获取整个视频帧数
long totalFrameNumber = capture.get(CAP_PROP_FRAME_COUNT);
std::cout << "整个视频帧数:" << totalFrameNumber << std::endl;
while (i < totalFrameNumber)
{
capture >> frame;
if (frame.empty()) break; //out while
imshow("read video", frame);//显示帧
waitKey(1);
i++;
}
std::cout << "播放完成\\n" << std::endl;
}
int main()
{
cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_ERROR);
play("hd.mp4");
}

  播放摄像头视频的主要流程与之类似,只不过在调用open的时候,需要修改一下传入的整型数据。读者可以准备一个USB接口的计算机摄像头,然后把USB口插入计算机的USB插槽中,通常是不需要安装驱动程序的。

7.获取和设置视频属性

  类VideoCapture的成员函数get可以用来获取视频文件的一些属性,该函数声明如下:

double get(int propId) const;

比如帧数CAP_PROP_FRAME_COUNT表示获取视频帧数。

  其中参数propId表示要获取的属性ID,通常其值是一个宏,常用属性用法如下:
在这里插入图片描述
  有获取属性的函数,自然也有设置属性的函数,即set,该函数声明如下:

bool set(int propId, double value);

  其中参数propId表示要设置的属性ID,value表示要设置的属性值。比如设置起始播放的帧:

long frameToStart = 300;
capture.set(CAP_PROP_POS_FRAMES, frameToStart); // 从300帧开始播放

  下面看一个例子,用get获取视频帧数和帧率,并用set设置起始播放帧,而且在播放过程中支持暂停和退出。

【例12.2】获取和设置属性并支持暂停和退出

#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;

int main()
{
cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_ERROR);
//打开视频文件:其实就是建立一个VideoCapture结构
VideoCapture capture("hd.mp4");
//检测是否正常打开:成功打开时,isOpened返回ture
if (!capture.isOpened())
cout << "fail to open!" << endl;
//获取整个帧数
long totalFrameNumber = capture.get(CAP_PROP_FRAME_COUNT);
cout << "整个视频共" << totalFrameNumber << "帧" << endl;

//设置开始帧()
long frameToStart = 300;
capture.set(CAP_PROP_POS_FRAMES, frameToStart);
cout << "从第" << frameToStart << "帧开始读" << endl;

//设置结束帧
int frameToStop = 400;
if (frameToStop < frameToStart)
{
cout << "结束帧小于开始帧,程序错误,即将退出!" << endl;
return 1;
}
else cout << "结束帧为:第" << frameToStop << "帧" << endl;
double rate = capture.get(CAP_PROP_FPS); //获取帧率
cout << "帧率为:" << rate << endl;

//定义一个用来控制读取视频循环结束的变量
bool stop = false;
//承载每一帧的图像
Mat frame;
//显示每一帧的窗口
//namedWindow("Extracted frame");
//两帧间的间隔时间:
//int delay = 1000/rate;
int delay = 1000 / rate;

//利用while循环读取帧
//currentFrame是在循环体中控制读取到指定的帧后循环结束的变量
long currentFrame = frameToStart;

//滤波器的核
int kernel_size = 3;
Mat kernel = Mat::ones(kernel_size, kernel_size, CV_32F) / (float)(kernel_size * kernel_size);

while (!stop)
{
//读取下一帧
if (!capture.read(frame))
{
cout << "读取视频失败" << endl;
return 1;
}

//这里加滤波程序
//imshow("Extracted frame", frame);
filter2D(frame, frame, 1, kernel);

imshow("after filter", frame);
cout << "正在读取第" << currentFrame << "帧" << endl;
//waitKey(int delay=0)当delay ≤ 0时会永远等待;当delay>0时会等待delay毫秒
//当时间结束前没有按键按下时,返回值为-1;否则返回按键

int c = waitKey(delay);
//按下ESC或者到达指定的结束帧后退出读取视频
if ((char)c == 27 || currentFrame > frameToStop)
{
stop = true;
}
//按下按键后会停留在当前帧,等待下一次按键
if (c >= 0)
{
waitKey(0);
}
currentFrame++;
}
//关闭视频文件
capture.release();
waitKey(0);
return 0;
}

  在上述代码中,我们用get函数获取了帧数和帧率,然后用set设置起始播放帧,最后在while循环中开始播放,并且播放过程中支持暂停和退出。按C键视频暂停,按Esc键视频退出。

8.播放摄像头视频

  播放摄像头视频和播放视频文件类似,也是通过类VideoCapture来实现,只不过调用open的时候传入的是摄像头的索引号。如果计算机安装了一个摄像头,则open的第一个参数通常是0,比如:

VideoCapture capture;
capture.open(0);

或者直接用构造函数打开摄像头:

VideoCapture capture(0);

  打开成功后,就可以一帧一帧地读取并一帧一帧地播放了,其实就是在一个循环里,间隔地显示一幅一幅视频帧图片,当间隔时间短了,图片动起来了,就感觉是在看视频了。在编程之前,我们要确保操作系统能打开摄像头并播放摄像头视频。

【例12.3】播放摄像头视频

#include <opencv2/opencv.hpp>
using namespace cv;

int main()
{
VideoCapture capture(0); //创建一个捕获对象并打开它
if (!capture.isOpened()) //判断打开一个捕获视频的设备(也就是摄像头)是否成功
{
puts("open failed");
return 1;
}
Mat frame;
while (capture.read(frame)) //读取视频帧
{
imshow("video-demo", frame); //显示视频帧
if (waitKey(30) >= 0)//延时30ms,按下任何键退出
break; //跳出循环
}
return 0; //结束程序
}

  在上述代码中,首先创建一个捕获对象并打开它,然后判断打开是否成功。如果打开成功,则通过while循环逐帧读取并用imshow显示该帧图片,在while中每隔30毫秒循环读取帧并显示,这样看起来就像视频了。用户按下任何键就退出循环结束程序。

  waitKey(30)表示程序每30毫秒检测一次按键。类VideoCapture既支持视频文件的读取,也支持摄像头视频的读取。如果要打开视频文件,则输入视频文件路径名;如果要打开摄像头,则输入摄像头的ID。

9.录制视频类VideoWriter

  前面我们捕获视频并播放,这相当于一个读取的过程。既然有读,那就有写,写就是把捕获到的视频写到磁盘文件中,也就是录制视频。这也是经常用到的。OpenCV专门提供了一个类VideoWriter,用于写视频到文件中。这个类使用比较简单,只要设置好输出文件路径和一些必需的参数,就可以逐帧保存到文件中了。帧是从VideoCapture对象中捕获而来的。

9.1 构造VideoWriter对象

  通常有两种方式构造VideoWriter对象,一种带参数,另外一种不带参数。带参数的方式相当于一次性把录制所需的参数都设置好,以后就可以直接写视频帧VideoWriter对象中了;不带参数的方式仅定义一个VideoWriter对象,后面还需要open操作才行。open的作用是设置参数到VideoWriter对象中,告诉VideoWriter对象录制的视频文件保存在哪里,视频的分辨率是多少等。

  带参数的构造函数在实例化VideoWriter对象的时候,也把初始化工作一起完成了。带参数的VideoWriter构造函数声明如下:

cv::VideoWriter::VideoWriter (const String & filename, int fourcc, double fps,
Size frameSize, bool isColor = true );

  其中,filename是输出视频文件的名称,可以带路径,也可以不带路径,不带路径就在当前目录下生成文件;fourcc表示用于压缩帧的编解码器的4字符代码,比如
VideoWriter::fourcc(‘P’,‘I’,‘M’,‘1’)是一个MPEG-1编解码,VideoWriter::fourcc(‘M’,‘J’,‘P’,‘G’)是
一个motion-jpeg编解码;fps表示创建的视频流的帧速率;frameSize表示视频帧的大小,也就是分辨率;isColor如果不是0,编码器将编码彩色帧,否则它将使用灰度帧,默认是彩色帧。

不带参数的构造函数就简单了,其声明如下:

cv::VideoWriter::VideoWriter ();

比如下列代码构造了一个无参数的VideoWriter对象,并用有参数的构造函数实例化后赋值。

VideoCapture capture(0);
VideoWriter writer;
int codec = writer.fourcc('M', 'J', 'P', 'G');
Mat frame;
capture >> frame;
writer = cv::VideoWriter("test.avi", codec, 10, Size(frame.cols, frame.rows)); // 实例化后赋值

9.2 初始化或重新初始化

  类VideoWriter提供了函数open来初始化或重新初始化VideoWriter对象,其实就是设置参数给VideoWriter对象。open函数声明如下:

virtual bool cv::VideoWriter::open (const String & filename, int fourcc,
double fps, Size frameSize, bool isColor = true);

  其中参数的含义和带参数的构造函数的参数含义相同,这里不再赘述。如果函数返回true,则VideoWriter对象初始化成功。也可以使用isOpened函数来判断是否初始化成功。

9.3 连接一个fourcc代码

  初始化VideoWriter对象的时候,需要传fourcc代码给VideoWriter对象。fourcc是“four character code”(4字符编码)的缩写,也称为4CC。该编码用于指示一个视频的编码压缩格式等信息。一个字符通常占用一个字节,也就是8位的存储空间,那么4字码通常占用32位的大小。4字符通常都用ASCII字符编码,以方便交流。

  AVI是最早被广泛使用的视频格式,其他的4CC编码包括DIVX、XVID、H264、DX50等,但这些只是数百个被使用的4CC编码中的很小一部分。

  VideoWriter提供了一个成员函数fourcc,可以将4个名称字符连接到一个fourcc代码,该函数声明如下:

static int cv::VideoWriter::fourcc (char c1, char c2, char c3, char c4 );

其中参数c1、c2、c3、c4分别是编码名称的字符。比如:

VideoWriter writer;
int codec = writer.fourcc('M', 'J', 'P', 'G');

【例12.4】录制摄像头视频到文件

#include <opencv2/opencv.hpp>
using namespace cv;

int main()
{
VideoCapture capture(0);
VideoWriter writer; //定义一个无参数的VideoWriter对象
int codec = writer.fourcc('M', 'J', 'P', 'G');
Mat frame;
capture >> frame;
writer = VideoWriter("test.avi", codec, 10, Size(frame.cols, frame.rows)); //实例化后赋值
if (writer.isOpened())
{
while (capture.isOpened())
{
capture >> frame;
writer << frame;
imshow("video", frame);
if (waitKey(20) == 27) //每隔20微秒录制一帧,按ESC键退出
break;
}
}
return 0;
}

在上述代码中,我们分别定义了VideoCapture对象和VideoWriter对象,然后通过函数
fourcc获得fourcc代码,再用带参数的构造函数实例化一个对象并赋值给writer。注意,
VideoWriter构造函数的最后一个参数分辨率必须和源视频帧大小相同。初始化完毕后,
就开始进入循环录制帧,每隔20微秒就把摄像头视频帧保存到Mat对象frame中,再把
frame存入writer中。如此循环,直到用户按Esc键退出循环,结束程序。视频文件将在
VideoWriter析构函数中自动关闭和释放。

赞(0)
未经允许不得转载:171主机测评 » OpenCV计算机视觉开发入门与实践<三十四>:视频播放和录制
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址