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上觀看此程式的執行例項。