Skip to content

使用 OpenCV 创建视频

当你处理视频源时,最终可能希望将图像处理结果保存为一个新的视频文件。对于简单的视频输出,你可以使用 OpenCV 内置的、专为此设计的 cv::VideoWriter 类。

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

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

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

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

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

videoFileStructure.png

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

代码见下方示例

首先,你应当了解视频文件大致是什么样的。每个视频文件本身就是一个容器(container)。容器的类型由文件的扩展名表示(例如 avi、mov 或 mkv)。它包含多个元素,例如:视频流、音频流或其他轨道(例如字幕)。这些流如何存储取决于为每种流使用的编解码器。对于音频轨道,常用的编解码器是 mp3 或 aac。对于视频文件,列表要长一些,包括诸如 XVID、DIVX、H264 或 LAGS(Lagarith 无损编解码器)等名称。你在系统上可以使用的编解码器完整列表取决于你安装了哪些。

如你所见,视频的事情可能变得相当复杂。然而,OpenCV 主要是一个计算机视觉库,而不是一个视频流、编解码和写入库。因此,开发者试图让这部分尽可能简单。正因如此,OpenCV 对视频容器仅支持 avi 扩展名,即其第一个版本。这一点的直接限制是你无法保存大于 2 GB 的视频文件。此外,你只能在容器内创建并扩展单个视频轨道。这里不支持音频或其他轨道的编辑。尽管如此,你系统上存在的任何视频编解码器都可能工作。如果你遇到这些限制中的某一个,将需要研究更专门的视频写入库,例如 FFmpeg,或编解码器如 HuffYUV、CorePNG 和 LCL。作为替代方案,可以使用 OpenCV 创建视频轨道,然后通过使用诸如 VirtualDub 或 AviSynth 等视频处理程序,用声轨对其进行扩展或将其转换为其他格式。

这里所写的内容建立在这样一个假设之上:你已经阅读了 tutorial_video_input_psnr_ssim 教程,并且知道如何读取视频文件。要创建视频文件,你只需创建一个 cv::VideoWriter 类的实例。你可以通过构造函数中的参数或稍后通过 cv::VideoWriter::open 函数来指定其属性。无论哪种方式,参数都是相同的:1. 输出的名称,其扩展名中包含容器类型。目前仅支持 avi。我们从输入文件构造它,在其上加上要使用的通道名称,并以容器扩展名结尾。

const string source = argv[1]; // the source file name
string::size_type pAt = source.find_last_of('.'); // Find extension point
const string NAME = source.substr(0, pAt) + argv[2][0] + ".avi"; // Form the new name with container
  1. 用于视频轨道的编解码器。现在所有的视频编解码器都有一个最多四个字符的唯一短名称。因此有 XVID、DIVX 或 H264 这些名称。这被称为四字符代码(four character code)。你也可以通过输入视频的 get 函数来获取它。因为 get 函数是一个通用函数,它总是返回 double 值。一个 double 值存储在 64 位上。四个字符即四个字节,也就是 32 位。这四个字符被编码在 double 的低 32 位中。一种丢弃高 32 位的简单方法是直接将此值转换为 int:
VideoCapture inputVideo(source); // Open input
int ex = static_cast<int>(inputVideo.get(CAP_PROP_FOURCC)); // Get Codec Type- Int form
OpenCV 内部使用此整数类型,并期望将其作为第二个参数。现在要从整数形式转换为字符串,我们可以使用两种方法:按位运算符方法和联合(union)方法。第一种从 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; // From Int to char via union
uEx.c[4]='\0';
这样做的好处是赋值后转换会自动完成,而对于按位运算符,每当更改编解码器类型时你都需要执行这些运算。如果你事先知道编解码器的四字符代码,可以使用 *CV_FOURCC* 宏来构造该整数:
CV_FOURCC('P','I','M,'1') // this is an MPEG1 codec from the characters to integer

videoCompressSelect.png

如果你为此参数传入负一,则在运行时会弹出一个窗口,其中包含系统上安装的所有编解码器,并要求你选择要使用的一个:
  1. 输出视频的每秒帧数。同样,这里我使用 get 函数保持输入视频的每秒帧数。
  2. 输出视频的帧大小。这里我也使用 get 函数保持输入视频的帧大小。
  3. 最后一个参数是可选的。默认为 true,表示输出将是彩色的(因此对于写入,你将发送三通道图像)。要创建灰度视频,请在此处传入 false 参数。

下面是我在示例中的用法:

VideoWriter outputVideo;
Size S = Size((int) inputVideo.get(CAP_PROP_FRAME_WIDTH), //Acquire input size
(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); //or
outputVideo << res;

从 BGR 图像中提取某个颜色通道意味着将其他通道的 BGR 值置为零。你可以通过图像扫描操作或使用 split 和 merge 操作来完成。首先将通道拆分到不同的图像中,将其他通道设置为相同大小和类型的零图像,最后将它们合并回来:

split(src, spl); // process - extract only the correct channel
for( int i =0; i < 3; ++i)
if (i != channel)
spl[i] = Mat::zeros(S, spl[0].type());
merge(spl, res);

resultOutputWideoWrite.png

将所有这些组合在一起,你就得到了上面的源代码,其运行结果将展示类似下面的内容:

你可以在这里的 YouTube上观看一个运行实例。