用OpenCV创建视频

目标

你可能已经不满足于读取视频,还想要将你产生的一系列结果保存到一个新建的视频文件中。使用OpenCV中的 VideoWriter 类就可以简单的完成创建视频的工作。在接下来的教程中,我们将告诉你:

  • 如何用OpenCV创建一个视频文件
  • 用OpenCV能创建什么样的视频文件
  • 如何释放视频文件当中的某个颜色通道

为了使例子简单,我就仅仅释放原始视频RGB通道中的一个,并把它放入新视频文件中。你可以使用命令行参数来控制程序的一些行为:

  • 第一个参数指向你需要操作的视频文件。
  • 第二个参数可以是如下的几个字母之一:R G B。用来指定你需要释放哪一个通道。
  • 最后一个参数是Y(Yes)或者N(No). 如果你选择N, 就直接使用视频输入格式来创建输出文件,否则就会弹出一个对话框来让你选择编码器。

举例来说,可以使用下面这样子的命令行:

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

源代码

你可以在 samples/cpp/tutorial_code/highgui/video-write/ 文件夹中找到源代码和视频文件。或者从 download it from here 这里下载源代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <iostream> // for standard I/O
#include <string>   // for strings

#include <opencv2/core/core.hpp>        // Basic OpenCV structures (cv::Mat)
#include <opencv2/highgui/highgui.hpp>  // Video write

using namespace std;
using namespace cv;
int main(int argc, char *argv[], char *window_name)
{
    if (argc != 4)
    {
        cout << "Not enough parameters" << endl;
        return -1;
    }

    const string source      = argv[1];            // the source file name
    const bool askOutputType = argv[3][0] =='Y';  // If false it will use the inputs codec type
   
    VideoCapture inputVideo(source);        // Open input
    if ( !inputVideo.isOpened())
    {
        cout  << "Could not open the input video." << source << endl;
        return -1;
    }

    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
    int ex = static_cast<int>(inputVideo.get(CV_CAP_PROP_FOURCC));     // Get Codec Type- Int form

    // Transform from int to char via Bitwise operators
    char EXT[] = {ex & 0XFF , (ex & 0XFF00) >> 8,(ex & 0XFF0000) >> 16,(ex & 0XFF000000) >> 24, 0};

    Size S = Size((int) inputVideo.get(CV_CAP_PROP_FRAME_WIDTH),    //Acquire input size
                  (int) inputVideo.get(CV_CAP_PROP_FRAME_HEIGHT));    

    VideoWriter outputVideo;                                        // Open the output
    if (askOutputType)
            outputVideo.open(NAME  , ex=-1, inputVideo.get(CV_CAP_PROP_FPS),S, true);  
    else
        outputVideo.open(NAME , ex, inputVideo.get(CV_CAP_PROP_FPS),S, true);
    
    if (!outputVideo.isOpened())
    {
        cout  << "Could not open the output video for write: " << source << endl;
        return -1;
    }

    union { int v; char c[5];} uEx ;
    uEx.v = ex;                              // From Int to char via union
    uEx.c[4]='\0';

    cout << "Input frame resolution: Width=" << S.width << "  Height=" << S.height
        << " of nr#: " << inputVideo.get(CV_CAP_PROP_FRAME_COUNT) << endl;
    cout << "Input codec type: " << EXT << endl;

    int channel = 2;    // Select the channel to save
    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; 

    while( true) //Show the image captured in the window and repeat
    {
        inputVideo >> src;              // read
        if( src.empty()) break;         // check if at end

       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);

       //outputVideo.write(res); //save or
       outputVideo << res;
    }

    cout << "Finished writing" << endl;
    return 0;
}

视频文件的结构

首先,你需要知道一个视频文件是什么样子的。每一个视频文件本质上都是一个容器,文件的扩展名只是表示容器格式(例如 avimov ,或者 mkv )而不是视频和音频的压缩格式。容器里可能会有很多元素,例如视频流,音频流和一些字幕流等等。这些流的储存方式是由每一个流对应的编解码器(codec)决定的。通常来说,视频流很可能使用 mp3aac 格式来储存。而视频格式就更多些,通常是 XVIDDIVXH264LAGS (Lagarith Lossless Codec)等等。具体你能够使用的编码器种类可以在操作系统的编解码器列表里找到。

The Structure of the video

如你所见,视频文件确实比图像文件要复杂很多。然而OpenCV只是个计算机视觉库而不是一个视频处理编码库。所以开发者们试图将这个部分尽可能地精简,结果就是OpenCV能够处理的视频只剩下 avi 扩展名的了。另外一个限制就是你不能创建超过2GB的单个视频,还有就是每个文件里只能支持一个视频流,不能将音频流和字幕流等其他数据放在里面。尽管如此,任何系统支持的编解码器在这里应该都能工作。如果这些视频处理能力不够你使用的话,我想你应该去找一些专门处理视频的库例如 FFMpeg 或者更多的编解码器例如 HuffYUVCorePNGLCL 。你可以先用OpenCV创建一个原始的视频流然后通过其他编解码器转换成其他格式并用 VirtualDubAviSynth 这样的软件去创建各种格式的视频文件

VideoWriter

在看下面的内容之前,你需要先阅读教程: OpenCV的视频输入和相似度测量 并且确保你知道如何读取视频文件。

要创建一个视频文件,你需要创建一个 VideoWriter 类的对象。可以通过构造函数里的参数或者在其他合适时机使用 open 函数来打开,两者的参数都是一样的:

  1. 输出的文件名中包含了容器的类型,当然在现在仅仅支持 avi 格式。在这个例子中我们会使用输入文件名+通道名+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";   // 创建新的视频文件名
    
  2. 然后决定使用的编解码器,现在所有的视频编解码器都使用最多四个字节的名称来标识,例如 XVID, DIVX, 和 H264 等。这个被称作FourCC(four character code)。你可以通过 get 函数来向视频询问这个编码, get 函数会返回一个double数,仅仅是因为double包含了64位数据而已,由于FourCC编码只占据了其中低位的4个字节,所以可以直接通过强制转换成int型来扔掉高位的四个字节。

    VideoCapture inputVideo(source);                                   // 打开视频输入
    int ex = static_cast<int>(inputVideo.get(CV_CAP_PROP_FOURCC));     // 得到编码器的int表达式
    

    OpenCV内部使用这个int数来当作第二个参数,这里会使用两种方法来将这个整型数转换为字符串:位操作符和联合体。前者可以用&操作符并进行移位操作,以便从int里面释放出字符:

    char EXT[] = {ex & 0XFF , (ex & 0XFF00) >> 8,(ex & 0XFF0000) >> 16,(ex & 0XFF000000) >> 24, 0};
    

也可以使用 联合体 来做到:

union { int v; char c[5];} uEx ;
uEx.v = ex;                              // 通过联合体来分解字符串
uEx.c[4]='\0';

  反过来,当你需要修改视频的格式时,你都需要修改FourCC码,而更改ForCC的时候都要做逆转换来指定新类型。如果你已经知道这个FourCC编码的具体字符的话,可以直接使用 *CV_FOURCC* 宏来构建这个int数:

如果传入的参数是一个负数的话,就会在执行时弹出一个对话框,在里面包括了所有已安装的编码器类型,你可以在里面选择一个并使用之。

Select the codec type to use
  1. 输出视频的帧率,也就是每秒需要绘制的图像数,在这里我让输出视频的帧率与输入视频相同,输入视频的帧率可以由 get 函数来获得。
  2. 输出视频的尺寸,在这里我同样保持和输入视频一样的大小,这个大小同样也可以由 get 函数来获得。
  3. 最后一个参数是一个可选参数。默认下它是true,来表示输出的视频是彩色的(所以你需要传给它三通道的图像),如果想创建一个灰度视频就传入false。

具体实现请看下面的例子:

VideoWriter outputVideo;
Size S = Size((int) inputVideo.get(CV_CAP_PROP_FRAME_WIDTH),    //获取输入尺寸
              (int) inputVideo.get(CV_CAP_PROP_FRAME_HEIGHT));

outputVideo.open(NAME , ex, inputVideo.get(CV_CAP_PROP_FPS),S, true);

然后,最好使用 isOpened() 函数来检查是不是成功打开。在成功打开时视频后,就可以用 write 函数向这个对象按照序列发送一些图像帧了,另外使用重载操作符 << 也可以完成这些操作。最后,视频会在 VideoWriter 对象析构时自动关闭。

outputVideo.write(res);  //或者
outputVideo << res;

要“释放”出某个通道,又要保持视频为彩色,实际上也就意味着要把未选择的通道都设置为全0。这个操作既可以通过手工扫描整幅图像来完成,又可以通过分离通道然后再合并来做到,具体操作时先分离三通道图像为三个不同的单通道图像,然后再将选定的通道与另外两张大小和类型都相同的黑色图像合并起来。

split(src, spl);                 // 分离三个通道
for( int i =0; i < 3; ++i)
   if (i != channel)
      spl[i] = Mat::zeros(S, spl[0].type());         //创建相同大小的黑色图像
merge(spl, res);             //重新合并

上面的代码完成了这个过程,最终运行结果就像下面所显示的那样。

A sample output

当然如果你会翻墙的话,又可以在“不存在”的`YouTube <https://www.youtube.com/watch?v=jpBwHxsl1_0>`_ 上观看到动态视频。

Table Of Contents

Previous topic

OpenCV的视频输入和相似度测量

Next topic

calib3d 模块. 相机定标和三维重建

This Page