个人技术分享

 返回:OpenCV系列文章目录(持续更新中......)
上一篇:OpenCV 库来捕获和处理视频输入和相似度测量(73)
下一篇:OpenCV使用 Kinect 和其他兼容 OpenNI 的深度传感器(75)

目标

每当您使用视频源时,您最终可能希望将图像处理结果保存为新视频文件的形式。对于简单的视频输出,您可以使用专为此设计的 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> // for standard I/O
#include <string> // for strings
 
#include <opencv2/core.hpp> // Basic OpenCV structures (cv::Mat)
#include <opencv2/videoio.hpp> // Video write
 
using namespace std;
using namespace cv;
 
static void help()
{
 cout
 << "------------------------------------------------------------------------------" << endl
 << "This program shows how to write video files." << endl
 << "You can extract the R or G or B color channel of the input video." << endl
 << "Usage:" << 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 << "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(CAP_PROP_FOURCC)); // Get Codec Type- Int form
 
 // Transform from int to char via Bitwise operators
 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), // Acquire input size
 (int) inputVideo.get(CAP_PROP_FRAME_HEIGHT));
 
 VideoWriter outputVideo; // Open the output
 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 << "Could not open the output video for write: " << source << endl;
 return -1;
 }
 
 cout << "Input frame resolution: Width=" << S.width << " Height=" << S.height
 << " of nr#: " << inputVideo.get(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;
 
 for(;;) //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)。这包含多个元素,例如:视频源、音频源或其他轨道(例如字幕)。这些源的存储方式由用于每个源的编解码器决定。对于音轨,常用的编解码器是mp3aac。对于视频文件,列表在某种程度上更长,包括 XVID、DIVXH264 或 LAGSLagarith 无损编解码器)等名称。您可以在系统上使用的编解码器的完整列表仅取决于您安装了什么编解码器。

如您所见,视频可能会变得非常复杂。但是,OpenCV 主要是一个计算机视觉库,而不是视频流、编解码器和写入库。因此,开发人员试图使这部分尽可能简单。因此,用于视频容器的 OpenCV 仅支持 avi 扩展,即其第一个版本。这样做的直接限制是您不能保存大于 2 GB 的视频文件。此外,您只能在容器内创建和扩展单个视频轨道。此处不支持音频或其他轨道编辑。尽管如此,您系统上存在的任何视频编解码器都可能有效。如果您遇到其中一些限制,您将需要研究更专业的视频编写库,例如 FFmpeg 或编解码器,如 HuffYUVCorePNG 和 LCL。或者,使用 OpenCV 创建视频轨道并使用音轨扩展它,或者使用 VirtualDub 或 AviSynth 等视频处理程序将其转换为其他格式。

VideoWriter 类

此处编写的内容基于以下假设:您已经阅读了使用 OpenCV 的视频输入和相似度测量教程,并且您知道如何阅读视频文件。要创建视频文件,您只需要创建 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 名称。这称为四字符代码。您也可以使用其 get 函数从输入视频中询问此问题。由于 get 函数是通用函数,因此它始终返回双精度值。双精度值存储在 64 位上。四个字符是四个字节,即 32 位。这四个字符以双精度的下 32 位编码。丢弃上面 32 位的一种简单方法是将此值转换为 int
    VideoCapture inputVideo(source); // Open input
    int ex = static_cast<int>(inputVideo.get(CAP_PROP_FOURCC)); // Get Codec Type- Int form

    OpenCV 在内部使用此整数类型,并期望将其作为其第二个参数。现在,要从整数形式转换为字符串,我们可以使用两种方法:按位运算符和并集方法。第一个从 int 中提取字符看起来像(一个“and”操作,一些移动并在末尾添加一个 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; // 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
    如果通过此参数减去 1,则在运行时会弹出一个窗口,其中包含系统上安装的所有编解码器,并要求您选择要使用的编解码器:

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

把所有这些放在一起,你会得到上面的源代码,其运行时结果将显示围绕这个想法的东西:

您可以在 YouTube 上观察此操作时实例。

参考文献:

1、《Creating a video with OpenCV》------Bernát Gábor