使用 OpenCV 播放视频也很简单简单。我们需要用循环来按顺序读取每一帧;也需要某种方法来提前退出播放。
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <string>
int main(int argc, char const *argv[])
{
cv::namedWindow("Example3", cv::WINDOW_AUTOSIZE);
cv::VideoCapture cap;
cap.open(std::string(argv[1]));
cv::Mat frame;
while (true)
{
cap >> frame;
if (frame.empty())
{
break;
}
cv::imshow("Example3", frame);
if (cv::waitKey(33) >= 0) break;
}
return 0;
}
从main()
函数开始,我们创建一个窗口,然后实例化视频捕获对象cv::VideoCapture
。该对象可以打开和关闭视频文件。
cap.open(std::string(argv[1]));
向cv::VideoCapture
对象提供一个字符串参数,该字符串包含要打开的视频的路径和文件名。cv::VideoCapture
对象将包含有关正在读取的视频文件的所有信息,包括状态信息。以这种方式创建时,cv::VideoCapture
对象被初始化为视频的开头。
cap >> frame;
if (frame.empty())
{
break;
}
cv::imshow("Example3", frame);
一旦进入while()
循环,就会从cv::VideoCapture
对象流中逐帧读取视频文件。程序检查是否真的从视频文件中读取了数据,如果是frame.Empty()
,即如果没有数据,则退出。如果视频帧读入成功,则通过cv::imshow()
显示。
if (cv::waitKey(33) >= 0) break;
每当我们显示了每帧图片,我们就等待33毫秒(自己随便设个时间)。如果用户在这段时间内按下某个键,将退出读取循环。否则,33ms过去,再次执行循环。程序退出时,所有分配的内存在超出作用域时自动释放。
来点骚的
我们来装饰一下刚刚的程序——添加一个进度条。我们还可以提供更多功能,比如按S键逐帧播放,按R键连续播放。HighGUI 提供了许多用于处理图像和视频的简单工具,比如前面提到的进度条,它使用户能够轻松地从视频的一个部分跳到另一个部分。要创建进度条,我们调用cv::createTrackbar()
并指定进度条出现在哪个窗口中。
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
#include <fstream>
using namespace std;
int g_slider_position = 0;
int g_run = 1;
int g_dontset = 0;
cv::VideoCapture g_cap;
void onTrackbarSlide(int pos, void *)
{
g_cap.set(cv::CAP_PROP_POS_FRAMES, pos);
if (!g_dontset)
g_run = 1;
g_dontset = 0;
}
int main(int argc, char const *argv[])
{
cv::namedWindow("Example2_4", cv::WINDOW_AUTOSIZE);
g_cap.open(std::string(argv[1]));
int frames = (int)g_cap.get(cv::CAP_PROP_FRAME_COUNT);
int tmpw = (int)g_cap.get(cv::CAP_PROP_FRAME_WIDTH);
int tmph = (int)g_cap.get(cv::CAP_PROP_FRAME_HEIGHT);
cout << "Video has " << frames << " frames of dimensions("
<< tmpw << ", " << tmph << ")." << endl;
cv::createTrackbar("Position", "Example2_4", &g_slider_position, frames,
onTrackbarSlide);
cv::Mat frame;
while (true)
{
if (g_run != 0)
{
g_cap >> frame;
if (frame.empty())
break;
int current_pos = (int)g_cap.get(cv::CAP_PROP_POS_FRAMES);
g_dontset = 1;
cv::setTrackbarPos("Position", "Example2_4", current_pos);
cv::imshow("Example2_4", frame);
g_run -= 1;
}
char c = (char)cv::waitKey(10);
if (c == 's') // single step
{
g_run = 1;
cout << "Single step, run = " << g_run << endl;
}
if (c == 'r') // run mode
{
g_run = -1;
cout << "Run mode, run = " << g_run << endl;
}
if (c == 27)
break;
}
return 0;
}
我们的做法是添加一个全局变量来表示进度条位置,然后添加一个回调函数来更新该变量并重新定位视频中读取的位置。我们创建进度条的时候就附加回调函数。
int g_slider_position = 0;
int g_run = 1;
int g_dontset = 0;
cv::VideoCapture g_cap;
首先,我们定义一个全局变量g_slider_position
,来记录进度条的位置。回调函数需要访问视频捕获对象g_cap
,因此我们也将其设置为全局变量。我们还创建了另一个全局变量g_run
,只要它不等于0,视频就会连续播放,正数表示每次停止前播放几帧;负数表示系统在连续播放模式下运行。
为了避免bug,当单击进度条跳转到视频中的新位置时,我们将通过设置g_run=1
使视频暂停在新位置。随着视频的快进,我们希望进度条在显示窗口中的位置根据我们在视频中的位置前进。我们通过让主程序在每次获得新的视频帧时调用进度条回调函数来更新进度条滑块的位置来实现这一点。但是,我们不希望这些操作对进度条回调函数的将我们置于逐帧播放模式。为了避免这种情况,我们引入了最后一个全局变量g_dontset
,以允许我们在不触发逐帧播放模式的情况下更新进度条的位置。
void onTrackbarSlide(int pos, void *)
{
g_cap.set(cv::CAP_PROP_POS_FRAMES, pos);
if (!g_dontset)
g_run = 1;
g_dontset = 0;
}
现在,我们定义了一个回调函数,当用户调整进度条时将使用该函数,该函数将传递一个参数pos
,它将是新的进度条位置。在此回调函数中,我们使用g_cap.set()
使视频播放快进到新位置。if()
语句将程序设置为在新位置进入单步模式,但前提是回调函数是由用户单击触发的,而不是从main
函数(设置g_dontset
)调用的。
我们以后会经常看到对g_cap.set()
的调用,以及它的对应方法g_cap.get()
。这些方法允许我们配置(或在后一种情况下查询)cv::VideoCapture
对象的各种属性。在本例中,我们传递参数cv::CAP_PROP_POS_FRAMES
,它表示我们希望以帧为单位设置读取位置。
int frames = (int)g_cap.get(cv::CAP_PROP_FRAME_COUNT);
int tmpw = (int)g_cap.get(cv::CAP_PROP_FRAME_WIDTH);
int tmph = (int)g_cap.get(cv::CAP_PROP_FRAME_HEIGHT);
cout << "Video has " << frames << " frames of dimensions("
<< tmpw << ", " << tmph << ")." << endl;
主程序的核心与上面的相同,不再赘述。打开视频后的第一个区别是,我们使用g_cap.get()
来确定视频中的帧数以及视频图像的宽度和高度。我们需要视频的帧数来校准进度条。
cv::createTrackbar("Position", "Example2_4", &g_slider_position, frames,
onTrackbarSlide);
接下来,我们创建进度条。函数cv::createTrackbar()
给进度条一个标签,并指定放置进度条的窗口。然后,我们提供一个将绑定到进度条的变量、进度条的最大值(视频中的帧数),以及移动滑块时的回调(如果我们不想要回调,则为NULL
)。
if (g_run != 0)
{
g_cap >> frame;
if (frame.empty())
break;
int current_pos = (int)g_cap.get(cv::CAP_PROP_POS_FRAMES);
g_dontset = 1;
cv::setTrackbarPos("Position", "Example2_4", current_pos);
cv::imshow("Example2_4", frame);
g_run -= 1;
}
在while
循环中,除了读取和显示视频帧之外,我们还获取视频中的当前位置,设置g_dontset
以使下一个进度条回调不会使我们进入单步模式,然后调用进度条回调来更新显示给用户的滑块进度条的位置。全局g_run
被取消,其效果是使我们保持单步模式,或者让视频根据用户按键设置的先前状态运行,这一点我们将在下面看到。
char c = (char)cv::waitKey(10);
if (c == 's') // single step
{
g_run = 1;
cout << "Single step, run = " << g_run << endl;
}
if (c == 'r') // run mode
{
g_run = -1;
cout << "Run mode, run = " << g_run << endl;
}
if (c == 27)
break;
在while
循环的底部,我们接收来自用户的键盘输入。如果按下S,我们将进入单步模式(g_run设置为1,允许读取单帧)。如果按下R,我们将进入连续视频模式(g_run被设置为-1,并且对于任何可能的视频大小,进一步递减使其为负值)。最后,如果按Esc,程序将终止。再次注意,对于短程序,我们省略了使用cv::destroyWindow()
清理窗口存储的步骤。