由于项目需要,计划实现九路视频拼接,因此必须熟悉OpenCV对视频序列的处理。视频信号处理是图像处理的一个延伸,所谓的视频序列是由按一定顺序进行排放的图像组成,即帧(Frame)。在这里,主要记录下如何使用Qt+OpenCV读取视频中的每一帧,之后,在这基础上将一些图像处理的算法运用到每一帧上(如使用Canny算子检测视频中的边缘)。
一. 读取视频序列
OpenCV提供了一个简便易用的框架以提取视频文件和USB摄像头中的图像帧,如果只是单单想读取某个视频,你只需要创建一个cv::VideoCapture实例,然后在循环中提取每一帧。新建一个Qt控制台项目,直接在main函数添加:
#include <QCoreApplication>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <QDebug>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 读取视频流
cv::VideoCapture capture("e:/BrokeGirls.mkv");
// 检测视频是否读取成功
if (!capture.isOpened())
{
qDebug() << "No Input Image";
return 1;
}
// 获取图像帧率
double rate= capture.get(CV_CAP_PROP_FPS);
bool stop(false);
cv::Mat frame; // 当前视频帧
cv::namedWindow("Extracted Frame");
// 每一帧之间的延迟
int delay= 1000/rate;
// 遍历每一帧
while (!stop)
{
// 尝试读取下一帧
if (!capture.read(frame))
break;
cv::imshow("Extracted Frame",frame);
// 引入延迟
if (cv::waitKey(delay)>=0)
stop= true;
}
return a.exec();
}
(注意:要正确打开视频文件,计算机中必须安装有对应的解码器,否则cv::VideoCapture无法理解视频格式!)运行后,将出现一个窗口,播放选定的视频(需要在创建cv::VideoCapture对象时指定视频的文件名)。
二. 处理视频帧
为了对视频的每一帧进行处理,这里创建自己的类VideoProcessor,其中封装了OpenCV的视频获取框架,该类允许我们指定每帧调用的处理函数。
首先,我们希望指定一个回调处理函数,每一帧中都将调用它。该函数接受一个cv::Mat对象,并输出处理后的cv::Mat对象,其函数签名如下:
void processFrame(cv::Mat& img, cv::Mat& out);
作为这样一个处理函数的例子,以下的Canny函数计算图像的边缘,使用时直接添加在mian文件中即可:
// 对视频的每帧做Canny算子边缘检测
void canny(cv::Mat& img, cv::Mat& out)
{
// 先要把每帧图像转化为灰度图
cv::cvtColor(img,out,CV_BGR2GRAY);
// 调用Canny函数
cv::Canny(out,out,100,200);
// 对像素进行翻转
cv::threshold(out,out,128,255,cv::THRESH_BINARY_INV);
}
现在我们需要创建一个VideoProcessor类,用来部署视频处理模块。而在此之前,需要先另外创建一个类,即VideoProcessor内部使用的帧处理类。这是因为在面向对象的上下文中,更适合使用帧处理类而不是一个帧处理函数,而使用类可以给程序员在涉及算法方面有更多的灵活度(书上介绍的)。将这个内部帧处理类命名为FrameProcessor,其定义如下:
#ifndef FRAMEPROCESSOR_H
#define FRAMEPROCESSOR_H
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
class FrameProcessor {
public:
virtual void process(cv:: Mat &input, cv:: Mat &output)= 0;
};
#endif // FRAMEPROCESSOR_H
现在可以开始定义VideoProcessor类了,以下为videoprocessor.h中的内容:
#ifndef VIDEOPROCESSOR_H
#define VIDEOPROCESSOR_H
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <QDebug>
#include "frameprocessor.h"
class VideoProcessor
{
private:
// 创建视频捕获对象
cv::VideoCapture capture;
// 每帧调用的回调函数
void (*process)(cv::Mat&, cv::Mat&);
// FrameProcessor接口
FrameProcessor *frameProcessor;
// 确定是否调用回调函数的bool信号
bool callIt;
// 输入窗口的名称
std::string windowNameInput;
// 输出窗口的名称
std::string windowNameOutput;
// 延迟
int delay;
// 已处理的帧数
long fnumber;
// 在该帧停止
long frameToStop;
// 是否停止处理
bool stop;
// 当输入图像序列存储在不同文件中时,可使用以下设置
// 把图像文件名的数组作为输入
std::vector<std::string> images;
// 图像向量的迭加器
std::vector<std::string>::const_iterator itImg;
// 得到下一帧
// 可能来自:视频文件或摄像头
bool readNextFrame(cv::Mat &frame)
{
if (images.size()==0)
return capture.read(frame);
else {
if (itImg != images.end())
{
frame= cv::imread(*itImg);
itImg++;
return frame.data != 0;
}
}
}
public:
// 默认设置 digits(0), frameToStop(-1),
VideoProcessor() : callIt(false), delay(-1),
fnumber(0), stop(false),
process(0), frameProcessor(0) {}
// 创建输入窗口
void displayInput(std::string wt);
// 创建输出窗口
void displayOutput(std::string wn);
// 不再显示处理后的帧
void dontDisplay();
// 以下三个函数设置输入的图像向量
bool setInput(std::string filename);
// 若输入为摄像头,设置ID
bool setInput(int id);
// 若输入为一组图像序列时,应用该函数
bool setInput(const std::vector<std::string>& imgs);
// 设置帧之间的延迟
// 0意味着在每一帧都等待按键响应
// 负数意味着没有延迟
void setDelay(int d);
// 返回图像的帧率
double getFrameRate();
// 需要调用回调函数
void callProcess();
// 不需要调用回调函数
void dontCallProcess();
// 设置FrameProcessor实例
void setFrameProcessor(FrameProcessor* frameProcessorPtr);
// 设置回调函数
void setFrameProcessor(void (*frameProcessingCallback)(cv::Mat&, cv::Mat&));
// 停止运行
void stopIt();
// 判断是否已经停止
bool isStopped();
// 是否开始了捕获设备?
bool isOpened();
// 返回下一帧的帧数
long getFrameNumber();
// 该函数获取并处理视频帧
void run();
};
#endif // VIDEOPROCESSOR_H
然后,在videoprocessor.cpp中定义各个函数的功能:
#include "videoprocessor.h"
// 创建输入窗口
void VideoProcessor::displayInput(std::string wt)
{
windowNameInput= wt;
cv::namedWindow(windowNameInput);
}
// 创建输出窗口
void VideoProcessor::displayOutput(std::string wn)
{
windowNameOutput= wn;
cv::namedWindow(windowNameOutput);
}
// 不再显示处理后的帧
void VideoProcessor::dontDisplay()
{
cv::destroyWindow(windowNameInput);
cv::destroyWindow(windowNameOutput);
windowNameInput.clear();
windowNameOutput.clear();
}
// 设置输入的图像向量
bool VideoProcessor::setInput(std::string filename)
{
fnumber= 0;
// 释放之前打开过的视频资源
capture.release();
images.clear();
// 打开视频
return capture.open(filename);
}
// 若输入为摄像头,设置ID
bool VideoProcessor::setInput(int id)
{
fnumber= 0;
// 释放之前打开过的视频资源
capture.release();
images.clear();
// 打开视频文件
return capture.open(id);
}
// 若输入为一组图像序列时,应用该函数
bool VideoProcessor::setInput(const std::vector<std::string>& imgs)
{
fnumber= 0;
// 释放之前打开过的视频资源
capture.release();
// 输入将是该图像的向量
images= imgs;
itImg= images.begin();
return true;
}
// 设置帧之间的延迟
// 0意味着在每一帧都等待按键响应
// 负数意味着没有延迟
void VideoProcessor::setDelay(int d)
{
delay= d;
}
// 返回图像的帧率
double VideoProcessor::getFrameRate()
{
if (images.size()!=0) return 0;
double r= capture.get(CV_CAP_PROP_FPS);
return r;
}
// 需要调用回调函数
void VideoProcessor::callProcess()
{
callIt= true;
}
// 不需要调用回调函数
void VideoProcessor::dontCallProcess()
{
callIt= false;
}
// 设置FrameProcessor实例
void VideoProcessor::setFrameProcessor(FrameProcessor* frameProcessorPtr)
{
// 使回调函数无效化
process= 0;
// 重新设置FrameProcessor实例
frameProcessor= frameProcessorPtr;
callProcess();
}
// 设置回调函数
void VideoProcessor::setFrameProcessor(void (*frameProcessingCallback)(cv::Mat&, cv::Mat&))
{
// 使FrameProcessor实例无效化
frameProcessor= 0;
// 重新设置回调函数
process= frameProcessingCallback;
callProcess();
}
// 以下函数表示视频的读取状态
// 停止运行
void VideoProcessor::stopIt()
{
stop= true;
}
// 判断是否已经停止
bool VideoProcessor::isStopped()
{
return stop;
}
// 是否开始了捕获设备?
bool VideoProcessor::isOpened()
{
return capture.isOpened() || !images.empty();
}
// 返回下一帧的帧数
long VideoProcessor::getFrameNumber()
{
if (images.size()==0)
{
// 得到捕获设备的信息
long f= static_cast<long>(capture.get(CV_CAP_PROP_POS_FRAMES));
return f;
}
else // 当输入来自一组图像序列时的情况
{
return static_cast<long>(itImg-images.begin());
}
}
// 该函数获取并处理视频帧
void VideoProcessor::run()
{
// 当前帧
cv::Mat frame;
// 输出帧
cv::Mat output;
// 打开失败时
if (!isOpened())
{
qDebug() << "Error!";
return;
}
stop= false;
while (!isStopped())
{
// 读取下一帧
if (!readNextFrame(frame))
break;
// 显示输出帧
if (windowNameInput.length()!=0)
cv::imshow(windowNameInput,frame);
// 调用处理函数
if (callIt)
{
// 处理当前帧
if (process)
process(frame, output);
else if (frameProcessor)
frameProcessor->process(frame,output);
// 增加帧数
fnumber++;
}
else
{
output= frame;
}
// 显示输出帧
if (windowNameOutput.length()!=0)
cv::imshow(windowNameOutput,output);
// 引入延迟
if (delay>=0 && cv::waitKey(delay)>=0)
stopIt();
// 检查是否需要停止运行
if (frameToStop>=0 && getFrameNumber()==frameToStop)
stopIt();
}
}
定义好视频处理类,它将与一个回调函数相关联。使用该类,可以创建一个实例,指定输入的视频文件,绑定回调函数,然后开始对每一帧进行处理,要调用这个视频处理类,只需在main函数中添加:
// 定义一个视频处理类处理视频帧
// 首先创建实例
VideoProcessor processor;
// 打开视频文件
processor.setInput("e:/BrokeGirls.mkv");
// 声明显示窗口
// 分别为输入和输出视频
processor.displayInput("Input Video");
processor.displayOutput("Output Video");
// 以原始帧率播放视频
processor.setDelay(1000./processor.getFrameRate());
// 设置处理回调函数
processor.setFrameProcessor(canny);
// 开始帧处理过程
processor.run();
cv::waitKey();
效果: