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

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

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

目标

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

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

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

  • 第一个参数指向要处理的视频文件
  • 第二个参数可以是字符:R G B 中的一个。这将指定要提取哪个通道。
  • 最后一个参数是字符 Y (Yes) 或 N (No)。如果为 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'; // 如果为假,将使用输入编解码器类型
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
<< " 帧数:" << 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:830
用于指定图像或矩形大小的模板类。
Definition types.hpp:335
_Tp height
高度
Definition types.hpp:363
_Tp width
宽度
Definition types.hpp:362
用于从视频文件、图像序列或摄像机捕获视频的类。
Definition videoio.hpp:772
视频写入类。
定义 videoio.hpp:1071
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 位。这四个字符编码在 *double* 的低 32 位中。一个简单的方法是,直接将此值转换为 *int*,以丢弃高 32 位
    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* 实现相同的功能,如下所示
    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)
if (i != channel)
spl[i] = Mat::zeros(S, spl[0].type());
merge(spl, res);

把所有这些放在一起,你就会得到上面的源代码,它的运行时结果将大致展示出这个想法

你可以在YouTube上观看此程序的运行实例。