OpenCV 4.11.0
开源计算机视觉库
加载中…
搜索中…
无匹配项
使用OpenCV创建视频

上一教程: 使用OpenCV进行视频输入和相似性测量
下一教程: 使用Kinect和其他兼容OpenNI的深度传感器

原作者Bernát Gábor
兼容性OpenCV >= 3.0

目标

在处理视频流时,您最终可能希望将图像处理结果保存为新的视频文件。对于简单的视频输出,您可以使用OpenCV内置的cv::VideoWriter类,该类为此而设计。

  • 如何使用OpenCV创建视频文件
  • 您可以使用OpenCV创建哪些类型的视频文件
  • 如何从视频中提取给定的颜色通道

作为一个简单的演示,我将只将输入视频文件的BGR颜色通道之一提取到一个新的视频中。您可以通过其控制台命令行参数来控制应用程序的流程。

  • 第一个参数指向要处理的视频文件。
  • 第二个参数可以是以下字符之一:R G B。这将指定要提取的通道。
  • 最后一个参数是字符Y(是)或N(否)。如果为否,则输出视频使用的编解码器将与输入视频相同。否则,将弹出一个窗口,允许您自己选择要使用的编解码器。

例如,有效的命令行如下所示:

video-write.exe video/Megamind.avi R Y

源代码

您也可以在OpenCV源代码库的samples/cpp/tutorial_code/videoio/video-write/文件夹中找到源代码和这些视频文件,或者从这里下载

#include <iostream> // 用于标准I/O
#include <string> // 用于字符串
#include <opencv2/core.hpp> // 基本OpenCV结构(cv::Mat)
#include <opencv2/videoio.hpp> // 视频写入
using namespace std;
using namespace cv;
static void help()
{
cout
<< "------------------------------------------------------------------------------" << endl
<< "此程序演示如何写入视频文件。" << endl
<< "您可以提取输入视频的R、G或B颜色通道。" << endl
<< "用法:" << endl
<< "./video-write <input_video_name> [ R | G | B] [Y | N]" << endl
<< "------------------------------------------------------------------------------" << endl
<< endl;
}
int main(int argc, char *argv[])
{
help();
if (argc != 4)
{
cout << "参数不足" << endl;
return -1;
}
const string source = argv[1]; // 源文件名
const bool askOutputType = argv[3][0] =='Y'; // 如果为false,则使用输入编解码器类型
VideoCapture inputVideo(source); // 打开输入
if (!inputVideo.isOpened())
{
cout << "无法打开输入视频: " << source << endl;
return -1;
}
string::size_type pAt = source.find_last_of('.'); // 查找扩展名点
const string NAME = source.substr(0, pAt) + argv[2][0] + ".avi"; // 使用容器形成新名称
int ex = static_cast<int>(inputVideo.get(CAP_PROP_FOURCC)); // 获取编解码器类型-整数形式
// 通过按位运算符将整数转换为字符
char EXT[] = {(char)(ex & 0XFF) , (char)((ex & 0XFF00) >> 8),(char)((ex & 0XFF0000) >> 16),(char)((ex & 0XFF000000) >> 24), 0};
Size S = Size((int) inputVideo.get(CAP_PROP_FRAME_WIDTH), // 获取输入大小
(int) inputVideo.get(CAP_PROP_FRAME_HEIGHT));
VideoWriter outputVideo; // 打开输出
if (askOutputType)
outputVideo.open(NAME, ex=-1, inputVideo.get(CAP_PROP_FPS), S, true);
else
outputVideo.open(NAME, ex, inputVideo.get(CAP_PROP_FPS), S, true);
if (!outputVideo.isOpened())
{
cout << "无法打开输出视频进行写入: " << source << endl;
return -1;
}
cout << "输入帧分辨率: 宽度=" << S.width << " 高度=" << S.height
<< " 编号: " << inputVideo.get(CAP_PROP_FRAME_COUNT) << endl;
cout << "输入编解码器类型: " << EXT << endl;
int channel = 2; // 选择要保存的通道
switch(argv[2][0])
{
case 'R' : channel = 2; break;
case 'G' : channel = 1; break;
case 'B' : channel = 0; break;
}
Mat src, res;
vector<Mat> spl;
for(;;) // 在窗口中显示捕获的图像并重复
{
inputVideo >> src; // 读取
如果 (src.empty()) break; // 检查是否到达结尾
split(src, spl); // 处理 - 只提取正确的通道
for (int i = 0; i < 3; ++i)
如果 (i != channel)
spl[i] = Mat::zeros(S, spl[0].type());
merge(spl, res);
//outputVideo.write(res); //保存或
outputVideo << res;
}
cout << "Finished writing" << endl;
return 0;
}
n维密集数组类
**定义** mat.hpp:829
用于指定图像或矩形大小的模板类。
**定义** types.hpp:335
_Tp height
高度
**定义** types.hpp:363
_Tp width
宽度
**定义** types.hpp:362
用于从视频文件、图像序列或摄像头捕获视频的类。
**定义** videoio.hpp:766
视频写入类。
**定义** videoio.hpp:1065
virtual bool open(const String &filename, int fourcc, double fps, Size frameSize, bool isColor=true)
初始化或重新初始化视频写入器。
virtual bool isOpened() const
如果视频写入器已成功初始化,则返回 true。
void split(const Mat &src, Mat *mvbegin)
将多通道数组分割成多个单通道数组。
void merge(const Mat *mv, size_t count, OutputArray dst)
从多个单通道数组创建一个多通道数组。
int main(int argc, char *argv[])
**定义** highgui_qt.cpp:3
**定义** core.hpp:107
STL 命名空间。

视频结构

首先,您应该了解视频文件的结构。每个视频文件本身就是一个容器。容器的类型由文件的扩展名表示(例如 *avi*、*mov* 或 *mkv*)。这包含多个元素,例如:视频流、音频流或其他轨道(例如字幕)。这些流的存储方式由为每个流使用的编解码器决定。对于音频轨道,常用的编解码器是 *mp3* 或 *aac*。对于视频文件,列表较长,包括 *XVID*、*DIVX*、*H264* 或 *LAGS*(*Lagarith Lossless Codec*)等名称。系统上可以使用编解码器的完整列表取决于您安装的编解码器。

如您所见,视频可能会非常复杂。但是,OpenCV 主要是一个计算机视觉库,而不是视频流、编解码器和写入器。因此,开发人员试图使这部分尽可能简单。因此,OpenCV 对于视频容器只支持 *avi* 扩展名及其第一个版本。这直接限制了您无法保存大于 2 GB 的视频文件。此外,您只能在容器内创建和扩展单个视频轨道。这里不支持音频或其他轨道编辑。但是,系统上存在的任何视频编解码器都可以工作。如果您遇到这些限制,则需要查看更专业的视频写入库,例如 *FFmpeg* 或编解码器,例如 *HuffYUV*、*CorePNG* 和 *LCL*。或者,您可以使用 OpenCV 创建视频轨道,并使用 *VirtualDub* 或 *AviSynth* 等视频处理程序为其添加音轨或将其转换为其他格式。

VideoWriter 类

此处编写的内容基于您已阅读使用 OpenCV 进行视频输入和相似性测量 教程并了解如何读取视频文件的假设。要创建视频文件,您只需创建一个 cv::VideoWriter 类的实例即可。您可以通过构造函数中的参数或稍后通过 cv::VideoWriter::open 函数来指定其属性。无论哪种方式,参数都是相同的:1. 包含容器类型扩展名的输出名称。目前仅支持 *avi*。我们从输入文件中构造它,向其中添加要使用的通道的名称,并以容器扩展名结尾。

const string source = argv[1]; // 源文件名
string::size_type pAt = source.find_last_of('.'); // 查找扩展名点
const string NAME = source.substr(0, pAt) + argv[2][0] + ".avi"; // 使用容器形成新名称
  1. 要用于视频轨道的编解码器。现在所有视频编解码器都具有最多四个字符的唯一短名称。因此,*XVID*、*DIVX* 或 *H264* 名称。这称为四个字符代码。您也可以使用其 *get* 函数从输入视频中查询此信息。因为 *get* 函数是一个通用函数,所以它总是返回双精度值。双精度值存储在 64 位上。四个字符是四个字节,即 32 位。这四个字符编码在双精度值的低 32 位中。丢弃高 32 位的一种简单方法是将此值转换为 *int*
    VideoCapture inputVideo(source); // 打开输入
    int ex = static_cast<int>(inputVideo.get(CAP_PROP_FOURCC)); // 获取编解码器类型-整数形式
    OpenCV 在内部使用这种整数类型,并将其作为第二个参数。现在,要从整数形式转换为字符串,我们可以使用两种方法:按位运算符和联合方法。第一个从 int 中提取字符看起来像(“与”运算、一些移位和在末尾添加 0 以关闭字符串)
    char EXT[] = {ex & 0XFF , (ex & 0XFF00) >> 8,(ex & 0XFF0000) >> 16,(ex & 0XFF000000) >> 24, 0};
    您可以使用联合体执行相同的操作,如下所示:
    union { int v; char c[5];} uEx ;
    uEx.v = ex; // 通过联合体将 Int 转换为 char
    uEx.c[4] = '\0';
    这样做的好处是赋值后会自动进行转换,而对于按位运算符,您需要在每次更改编解码器类型时执行操作。如果您事先知道编解码器的四个字符代码,则可以使用 *CV_FOURCC* 宏来构建整数
    CV_FOURCC('P','I','M','1') // 这是从字符到整数的 MPEG1 编解码器
    int CV_FOURCC(char c1, char c2, char c3, char c4)
    构建“fourcc”代码,用于视频编解码器和其他许多地方。只需用4个字符调用它……
    定义 cvdef.h:934
    如果将-1作为此参数传递,则会在运行时弹出一个窗口,其中包含系统上安装的所有编解码器,并要求您选择要使用的编解码器。
  1. 输出视频的帧速率。同样,这里我使用 _get_ 函数保留输入视频的帧速率。
  2. 输出视频的帧大小。这里我也使用 _get_ 函数保留输入视频的帧大小。
  3. 最后一个参数是可选的。默认值为true,表示输出将是彩色的(因此对于写入,您将发送三通道图像)。要创建灰度视频,请在此处传递false参数。

这是我在示例中如何使用它的方法

VideoWriter outputVideo;
Size S = Size((int) inputVideo.get(CAP_PROP_FRAME_WIDTH), //获取输入大小
(int) inputVideo.get(CAP_PROP_FRAME_HEIGHT));
outputVideo.open(NAME , ex, inputVideo.get(CAP_PROP_FPS),S, true);

之后,您可以使用 cv::VideoWriter::isOpened() 函数来确定打开操作是否成功。当 VideoWriter 对象被销毁时,视频文件会自动关闭。成功打开对象后,您可以使用该类的 cv::VideoWriter::write 函数按顺序发送视频帧。或者,您可以使用其重载运算符 <<

outputVideo.write(res); //或者
outputVideo << res;
virtual void write(InputArray image)
写入下一个视频帧。

从BGR图像中提取颜色通道意味着将其他通道的BGR值设置为零。您可以使用图像扫描操作或使用分割和合并操作来实现此目的。首先将通道分割成不同的图像,将其他通道设置为相同大小和类型的零图像,最后将它们合并。

split(src, spl); // 处理 - 只提取正确的通道
for( int i =0; i < 3; ++i)
如果 (i != channel)
spl[i] = Mat::zeros(S, spl[0].type());
merge(spl, res);

将所有这些放在一起,您将得到上面的源代码,其运行时结果将显示一些想法。

您可以在 YouTube 上观看此运行时实例