OpenCV  4.10.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> // 用于标准输入/输出
#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)); // 获取编解码器类型 - 整数形式
// 通过位运算符从 int 转换为 char
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
<< “ 的 nr#: " << 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; // 读取
if (src.empty()) break; // 检查是否在末尾
split(src, spl); // 处理 - 仅提取正确的通道
for (int i =0; i < 3; ++i)
if (i != channel)
spl[i] = Mat::zeros(S, spl[0].type());
merge(spl, res);
//outputVideo.write(res); //保存或
outputVideo << res;
}
cout << "完成写入" << endl;
return 0;
}
n 维密集阵类
定义 mat.hpp:812
用于指定图像或矩形大小的模板类。
定义 types.hpp:335
_Tp height
高度
定义 types.hpp:363
_Tp width
宽度
定义 types.hpp:362
用于从视频文件、图像序列或摄像头捕获视频的类。
定义 videoio.hpp:731
视频编写器类。
定义 videoio.hpp:1009
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:102
STL 名称空间。

视频结构

首先,你应了解视频文件的外观。每个视频文件本身都是一个容器。容器类型在文件扩展名中表示(例如avimovmkv)。其中包含多种元素,如:视频流、音频流或其他音轨(例如字幕)。这些流的存储方式由所使用的编解码器确定。对于音频流,常用的编解码器是mp3aac。对于视频文件而言,列表会更长一些,其中包括XVIDDIVXH264LAGSLagarith Lossless Codec)等名称。系统上可用的编解码器完整列表取决于安装的编解码器。

正如你所见,视频中会变得非常复杂。然而,OpenCV 主要是一个计算机视觉库,而不是视频流,是编解码器和撰写一个视频流。因此,开发者尽量使该部分尽可能简单。由于此原因,用于视频容器的 OpenCV 仅支持avi扩展的第一个版本。其直接限制是你无法保存大于 2 GB 的视频文件。此外,你只能在容器内创建和扩展单个视频音轨。不提供音频或其他音轨编辑支持。但系统上的任何视频编解码器都可以正常工作。如果你遇到其中的一些限制,则需要查看更专业的视频编写库,如FFmpegHuffYUVCorePNGLCL等编解码器。作为替代,使用 OpenCV 创建视频音轨并使用声音音轨对其进行扩展,或使用视频处理程序(例如VirtualDubAviSynth)将其转换为其他格式。

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. 用于视频轨道的编解码器。现在所有视频编解码器都有一个长度最长为四个字符的唯一短名称。因此,XVIDDIVXH264名称称为四字符代码。您还可以使用其get函数从输入视频中索取此代码。因为get函数是一个通用函数,它始终返回双精度值。64 位中存储一个双精度值。四个字符是四个字节,即 32 位。这四个字符编码在double的低 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 编解码器
    如果您从此参数传递减一,则在运行时会出现一个窗口,其中包含系统上安装的所有编解码器并要求您选择要使用的编解码器
  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 和 merge 操作来执行此操作。首先将通道拆分为不同的图像,将其他通道设为相同大小和类型的零图像,最后将它们重新合并

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

将所有内容组合在一起,您将获得上面的源代码,其运行时结果将显示有关创意的一些内容

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