前言
在当今的计算机视觉领域,视频处理技术有着广泛的应用,从安防监控到自动驾驶,从视频编辑到人工智能交互,都离不开对视频的有效处理。 OpenCV 作为一个开源的计算机视觉库,包含了大量用于图像处理和视频分析的函数和算法。能够高效地实现各种复杂的视频处理功能。
本文将详细介绍在 Qt C++ 中使用 OpenCV 库实现视频处理技术,包括视频 I/O 类 VideoCapture/VideoWriter(编解码设置)、运动分析中的背景减除(MOG2/KNN)以及实时目标跟踪中的 KCF 算法(核相关滤波)等内容,旨在为初学者和有一定基础的开发者提供全面且易懂的指导。
一、Qt 与 OpenCV 环境搭建
在开始进行视频处理之前,我们首先需要搭建好 Qt 与 OpenCV 的开发环境。这是后续所有开发工作的基础,只有环境配置正确,才能顺利地调用相关库函数进行开发。
1.1 OpenCV 的安装
OpenCV 的安装和配置相对复杂一些,需要将其库文件和头文件正确地集成到 Qt 项目中。
下载 OpenCV:访问 OpenCV 官方网站(https://opencv.org/),下载适合自己操作系统的 OpenCV 版本。对于 Windows 系统,通常下载.exe 安装文件,运行后会解压出 OpenCV 的库文件和头文件。
1.2 配置环境变量
将 OpenCV 的 bin 目录添加到系统环境变量中。例如,如果 OpenCV 解压到了 “D:\opencv” 目录,那么需要将 “D:\opencv\build\x64\vc15\bin” 添加到系统的 PATH 环境变量中。这样,在运行程序时,系统才能找到 OpenCV 的动态链接库。
在 Qt 项目中配置 OpenCV:打开 Qt Creator,创建一个新的 Qt C++ 项目。然后,在项目的.pro 文件中添加以下内容,指定 OpenCV 的头文件和库文件路径:
INCLUDEPATH += D:\opencv\build\include
LIBS += -LD:\opencv\build\x64\vc15\lib \
-lopencv_core455d \
-lopencv_highgui455d \
-lopencv_imgproc455d \
-lopencv_video455d \
-lopencv_videoio455d
其中,“D:\opencv” 是 OpenCV 的安装路径,“455d” 是 OpenCV 的版本号,根据实际情况进行修改。“d” 表示 debug 版本,如果需要发布程序,还需要链接 release 版本的库文件(去掉 “d”)。
配置完成后,可以编写一个简单的程序测试 OpenCV 是否配置成功。例如,读取一张图片并显示:
#include <opencv2/opencv.hpp>
#include <QApplication>
#include <QLabel>
#include <QImage>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// 读取图片
cv::Mat image = cv::imread("test.jpg");
if (image.empty()) {
qDebug() << "无法读取图片";
return -1;
}
// 将OpenCV的Mat格式转换为Qt的QImage格式
cv::cvtColor(image, image, cv::COLOR_BGR2RGB);
QImage qImage(image.data, image.cols, image.rows, image.step, QImage::Format_RGB888);
// 显示图片
QLabel label;
label.setPixmap(QPixmap::fromImage(qImage));
label.show();
return a.exec();
}
如果程序能够成功编译并显示图片,说明 OpenCV 配置成功。
二、视频 I/O 类 VideoCapture/VideoWriter 及编解码设置
视频的输入输出是视频处理的基础,OpenCV 提供了 VideoCapture 和 VideoWriter 两个类来实现视频的读取和写入功能。同时,编解码设置对于视频的处理和存储也非常重要,不同的编解码器会影响视频的质量、大小和兼容性。
2.1 VideoCapture 类
VideoCapture 类用于从视频文件、摄像头或其他视频源中读取视频帧。它支持多种视频格式和设备,使用起来非常灵活。
2.1.1 打开视频源:
可以通过构造函数或 open () 方法打开视频源。例如:
// 打开视频文件
cv::VideoCapture cap("test.mp4");
// 打开摄像头(0表示默认摄像头)
cv::VideoCapture cap(0);
如果打开成功,isOpened () 方法会返回 true;否则,返回 false。
2.1.2 读取视频帧:
使用 read () 方法或>> 运算符读取视频帧,读取到的视频帧存储在 Mat 对象中。例如:
cv::Mat frame;
cap.read(frame); // 读取一帧视频
// 或者
cap >> frame;
如果读取失败(例如到达视频末尾),read () 方法会返回 false。
2.1.3 获取视频属性:
可以使用 get () 方法获取视频的各种属性,如帧率、宽度、高度等。例如:
double fps = cap.get(cv::CAP_PROP_FPS); // 获取帧率
int frameWidth = cap.get(cv::CAP_PROP_FRAME_WIDTH); // 获取帧宽度
int frameHeight = cap.get(cv::CAP_PROP_FRAME_HEIGHT); // 获取帧高度
int totalFrames = cap.get(cv::CAP_PROP_FRAME_COUNT); // 获取总帧数
2.1.4 设置视频属性:
使用 set () 方法可以设置视频的一些属性,如当前播放位置等。例如:
cap.set(cv::CAP_PROP_POS_FRAMES, 100); // 设置当前播放位置为第100帧
2.2 VideoWriter 类
VideoWriter 类用于将处理后的视频帧写入到视频文件中。它需要指定输出视频的文件名、编解码器、帧率、帧大小等参数。
2.2.1 构造 VideoWriter 对象:
在构造 VideoWriter 对象时,需要指定输出视频的文件名、编解码器、帧率、帧大小等参数。例如:
cv::VideoWriter writer;
int fourcc = cv::VideoWriter::fourcc('M', 'J', 'P', 'G'); // 指定编解码器为MJPG
double fps = 30.0;
cv::Size frameSize(640, 480);
writer.open("output.avi", fourcc, fps, frameSize, true); // true表示彩色视频
其中,fourcc 是一个 4 字节的代码,用于指定视频的编解码器。不同的编解码器有不同的 fourcc 代码,例如:
MJPG:‘M’, ‘J’, ‘P’, 'G’
XVID:‘X’, ‘V’, ‘I’, 'D’
H.264:需要安装相应的编码器,fourcc 代码为 ‘AVC1’ 或 'H264’
2.2.2 写入视频帧:
使用 write () 方法或 << 运算符将视频帧写入到输出文件中。例如:
cv::Mat frame;
// 处理视频帧...
writer.write(frame); // 写入一帧视频
// 或者
writer << frame;
2.2.3 释放资源:
在完成视频写入后,需要调用 release () 方法释放资源。例如:
writer.release();
2.3 编解码设置
编解码器是视频处理中非常重要的一部分,它决定了视频的压缩方式和存储格式。不同的编解码器有不同的特点,在选择时需要根据实际需求进行权衡。
2.3.1 常见的编解码器:
- MJPG(Motion JPEG):一种基于 JPEG 的视频编解码器,压缩率较低,但兼容性好,适合存储高质量视频。
- XVID:一种基于 MPEG-4 的编解码器,压缩率较高,视频质量较好,广泛应用于各种视频格式。
- H.264(AVC):一种高效的视频编解码器,压缩率高,视频质量好,是目前主流的视频编解码标准之一,广泛应用于网络视频、高清电视等领域。
- H.265(HEVC):H.264 的升级版,压缩率更高,但解码复杂度也更高,适合存储超高清视频。
2.3.2 在 OpenCV 中设置编解码器:
如前所述,在构造 VideoWriter 对象时,需要通过 fourcc 代码指定编解码器。如果指定的编解码器不可用,OpenCV 会尝试使用默认的编解码器。在实际开发中,可能需要根据系统中安装的编解码器来选择合适的 fourcc 代码。
2.3.3 编解码器的安装:
有些编解码器可能需要单独安装,例如 H.264 编解码器。在 Windows 系统中,可以安装 K-Lite Codec Pack 等编解码器包来获得更多的编解码器支持。
2.4 Qt 中使用 VideoCapture 和 VideoWriter 的示例
下面是一个在 Qt 中使用 VideoCapture 读取视频并使用 VideoWriter 写入处理后视频的示例程序:
#include <QApplication>
#include <QMainWindow>
#include <QLabel>
#include <opencv2/opencv.hpp>
class VideoProcessor : public QMainWindow
{
Q_OBJECT
public:
VideoProcessor(QWidget *parent = nullptr) : QMainWindow(parent)
{
// 创建显示标签
label = new QLabel(this);
setCentralWidget(label);
// 打开视频文件
cap.open("input.mp4");
if (!cap.isOpened()) {
qDebug() << "无法打开视频文件";
return;
}
// 获取视频属性
double fps = cap.get(cv::CAP_PROP_FPS);
int frameWidth = cap.get(cv::CAP_PROP_FRAME_WIDTH);
int frameHeight = cap.get(cv::CAP_PROP_FRAME_HEIGHT);
// 创建VideoWriter对象
int fourcc = cv::VideoWriter::fourcc('M', 'J', 'P', 'G');
writer.open("output.avi", fourcc, fps, cv::Size(frameWidth, frameHeight), true);
// 启动定时器,定时读取视频帧
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &VideoProcessor::processFrame);
timer->start(1000 / fps); // 根据帧率设置定时器间隔
}
~VideoProcessor()
{
cap.release();
writer.release();
}
private slots:
void processFrame()
{
cv::Mat frame;
cap >> frame;
if (frame.empty()) {
timer->stop();
return;
}
// 对视频帧进行简单处理(例如转为灰度图)
cv::Mat grayFrame;
cv::cvtColor(frame, grayFrame, cv::COLOR_BGR2GRAY);
cv::cvtColor(grayFrame, frame, cv::COLOR_GRAY2BGR); // 转回BGR格式以便写入
// 写入处理后的视频帧
writer << frame;
// 显示视频帧
cv::cvtColor(frame, frame, cv::COLOR_BGR2RGB);
QImage qImage(frame.data, frame.cols, frame.rows, frame.step, QImage::Format_RGB888);
label->setPixmap(QPixmap::fromImage(qImage.scaled(label->size(), Qt::KeepAspectRatio)));
}
private:
cv::VideoCapture cap;
cv::VideoWriter writer;
QLabel *label;
QTimer *timer;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
VideoProcessor w;
w.show();
return a.exec();
}
#include "main.moc"
2.5 编解码设置
编解码器是视频处理中非常重要的一部分,它决定了视频的压缩方式和存储格式。不同的编解码器有不同的特点,在选择时需要根据实际需求进行权衡。
常见的编解码器:
- MJPG(Motion JPEG):一种基于 JPEG 的视频编解码器,压缩率较低,但兼容性好,适合存储高质量视频。
- XVID:一种基于 MPEG-4 的编解码器,压缩率较高,视频质量较好,广泛应用于各种视频格式。
- H.264(AVC):一种高效的视频编解码器,压缩率高,视频质量好,是目前主流的视频编解码标准之一,广泛应用于网络视频、高清电视等领域。
- H.265(HEVC):H.264 的升级版,压缩率更高,但解码复杂度也更高,适合存储超高清视频。
在 OpenCV 中设置编解码器:如前所述,在构造 VideoWriter 对象时,需要通过 fourcc 代码指定编解码器。如果指定的编解码器不可用,OpenCV 会尝试使用默认的编解码器。在实际开发中,可能需要根据系统中安装的编解码器来选择合适的 fourcc 代码。
编解码器的安装:有些编解码器可能需要单独安装,例如 H.264 编解码器。在 Windows 系统中,可以安装 K-Lite Codec Pack 等编解码器包来获得更多的编解码器支持。
2.6 Qt 中使用 VideoCapture 和 VideoWriter 的示例
下面是一个在 Qt 中使用 VideoCapture 读取视频并使用 VideoWriter 写入处理后视频的示例程序:
#include <QApplication>
#include <QMainWindow>
#include <QLabel>
#include <opencv2/opencv.hpp>
class VideoProcessor : public QMainWindow
{
Q_OBJECT
public:
VideoProcessor(QWidget *parent = nullptr) : QMainWindow(parent)
{
// 创建显示标签
label = new QLabel(this);
setCentralWidget(label);
// 打开视频文件
cap.open("input.mp4");
if (!cap.isOpened()) {
qDebug() << "无法打开视频文件";
return;
}
// 获取视频属性
double fps = cap.get(cv::CAP_PROP_FPS);
int frameWidth = cap.get(cv::CAP_PROP_FRAME_WIDTH);
int frameHeight = cap.get(cv::CAP_PROP_FRAME_HEIGHT);
// 创建VideoWriter对象
int fourcc = cv::VideoWriter::fourcc('M', 'J', 'P', 'G');
writer.open("output.avi", fourcc, fps, cv::Size(frameWidth, frameHeight), true);
// 启动定时器,定时读取视频帧
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &VideoProcessor::processFrame);
timer->start(1000 / fps); // 根据帧率设置定时器间隔
}
~VideoProcessor()
{
cap.release();
writer.release();
}
private slots:
void processFrame()
{
cv::Mat frame;
cap >> frame;
if (frame.empty()) {
timer->stop();
return;
}
// 对视频帧进行简单处理(例如转为灰度图)
cv::Mat grayFrame;
cv::cvtColor(frame, grayFrame, cv::COLOR_BGR2GRAY);
cv::cvtColor(grayFrame, frame, cv::COLOR_GRAY2BGR); // 转回BGR格式以便写入
// 写入处理后的视频帧
writer << frame;
// 显示视频帧
cv::cvtColor(frame, frame, cv::COLOR_BGR2RGB);
QImage qImage(frame.data, frame.cols, frame.rows, frame.step, QImage::Format_RGB888);
label->setPixmap(QPixmap::fromImage(qImage.scaled(label->size(), Qt::KeepAspectRatio)));
}
private:
cv::VideoCapture cap;
cv::VideoWriter writer;
QLabel *label;
QTimer *timer;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
VideoProcessor w;
w.show();
return a.exec();
}
#include "main.moc"
在这个示例中,我们创建了一个 VideoProcessor 类,它继承自 QMainWindow。在构造函数中,我们打开视频文件,获取视频属性,创建 VideoWriter 对象,并启动定时器定时读取和处理视频帧。在 processFrame 槽函数中,我们读取视频帧,将其转为灰度图,然后写入到输出文件中,并在界面上显示处理后的视频帧。
三、运动分析:背景减除(MOG2/KNN)
背景减除是一种常用的运动分析技术,它通过从视频序列中减去背景模型,来检测出前景目标(即运动的物体)。OpenCV 提供了多种背景减除算法,其中 MOG2 和 KNN 是两种比较常用的算法。
3.1 背景减除的基本原理
背景减除的基本思想是:在视频序列中,背景通常是相对稳定的,而前景目标是运动的。因此,可以通过建立一个背景模型,然后将当前帧与背景模型进行比较,差异部分即为前景目标。
具体来说,背景减除的步骤如下:
- 建立背景模型:通过对视频序列的初始帧或多帧进行分析,建立一个背景模型。背景模型可以是一个单帧图像,也可以是一个统计模型。
- 前景检测:将当前帧与背景模型进行比较,计算它们之间的差异。通常使用阈值化的方法来判断哪些像素属于前景,哪些属于背景。
- 背景更新:由于背景可能会随着时间的推移而发生缓慢变化(如光照变化、物体移动等),因此需要对背景模型进行动态更新,以适应背景的变化。
3.2 MOG2 算法
MOG2(Mixture of Gaussians)算法是一种基于高斯混合模型的背景减除算法。它假设每个像素的颜色值在背景中服从多个高斯分布的混合,通过对这些高斯分布的参数进行估计和更新,来建立背景模型。
MOG2 算法的特点:
- 能够适应背景的动态变化,如光照变化、缓慢移动的背景物体等。
- 对噪声有一定的抑制能力。
- 可以检测出阴影,并将阴影从前景中分离出来。
在 OpenCV 中使用 MOG2 算法:OpenCV 提供了 cv::createBackgroundSubtractorMOG2 () 函数来创建 MOG2 背景减除器。例如:
cv::Ptr<cv::BackgroundSubtractor> pMOG2 = cv::createBackgroundSubtractorMOG2();
然后,使用 apply () 方法对当前帧进行前景检测:
cv::Mat frame, fgMask;
cap >> frame;
pMOG2->apply(frame, fgMask);
其中,fgMask 是输出的前景掩码,掩码中前景像素的值为 255,背景像素的值为 0,阴影像素的值为 127(可以通过设置参数来关闭阴影检测)。
3.3 KNN 算法
KNN(K-Nearest Neighbors)背景减除算法是一种基于 K 近邻分类的背景减除算法。它将每个像素的历史颜色值作为样本,通过计算当前像素与这些样本的距离,来判断当前像素是属于前景还是背景。
3.3.1 KNN 算法的特点:
- 对复杂背景的适应能力较强。
- 能够较好地处理光照变化和动态背景。
- 前景检测的精度较高。
3.3.2 在 OpenCV 中使用 KNN 算法:
OpenCV 提供了 cv::createBackgroundSubtractorKNN () 函数来创建 KNN 背景减除器。例如:
cv::Ptr<cv::BackgroundSubtractor> pKNN = cv::createBackgroundSubtractorKNN();
同样,使用 apply () 方法进行前景检测:
cv::Mat frame, fgMask;
cap >> frame;
pKNN->apply(frame, fgMask);
KNN 算法的前景掩码与 MOG2 类似,前景像素为 255,背景像素为 0,阴影像素为 127。
3.4 Qt 中使用背景减除算法的示例
下面是一个在 Qt 中使用 MOG2 和 KNN 算法进行背景减除的示例程序:
#include <QApplication>
#include <QMainWindow>
#include <QTabWidget>
#include <QLabel>
#include <opencv2/opencv.hpp>
class BackgroundSubtractorDemo : public QMainWindow
{
Q_OBJECT
public:
BackgroundSubtractorDemo(QWidget *parent = nullptr) : QMainWindow(parent)
{
// 创建标签页控件
QTabWidget *tabWidget = new QTabWidget(this);
setCentralWidget(tabWidget);
// 创建显示标签
originalLabel = new QLabel(tabWidget);
mog2Label = new QLabel(tabWidget);
knnLabel = new QLabel(tabWidget);
tabWidget->addTab(originalLabel, "原始视频");
tabWidget->addTab(mog2Label, "MOG2前景");
tabWidget->addTab(knnLabel, "KNN前景");
// 打开摄像头
cap.open(0);
if (!cap.isOpened()) {
qDebug() << "无法打开摄像头";
return;
}
// 创建背景减除器
pMOG2 = cv::createBackgroundSubtractorMOG2();
pKNN = cv::createBackgroundSubtractorKNN();
// 启动定时器
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &BackgroundSubtractorDemo::processFrame);
timer->start(30); // 大约33fps
}
~BackgroundSubtractorDemo()
{
cap.release();
}
private slots:
void processFrame()
{
cv::Mat frame;
cap >> frame;
if (frame.empty()) {
timer->stop();
return;
}
// 显示原始视频
cv::Mat originalFrame;
cv::cvtColor(frame, originalFrame, cv::COLOR_BGR2RGB);
QImage originalQImage(originalFrame.data, originalFrame.cols, originalFrame.rows, originalFrame.step, QImage::Format_RGB888);
originalLabel->setPixmap(QPixmap::fromImage(originalQImage.scaled(originalLabel->size(), Qt::KeepAspectRatio)));
// MOG2背景减除
cv::Mat mog2FgMask;
pMOG2->apply(frame, mog2FgMask);
// 对前景掩码进行后处理(如腐蚀和膨胀,去除噪声)
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(3, 3));
cv::erode(mog2FgMask, mog2FgMask, kernel);
cv::dilate(mog2FgMask, mog2FgMask, kernel);
// 显示前景掩码
QImage mog2QImage(mog2FgMask.data, mog2FgMask.cols, mog2FgMask.rows, mog2FgMask.step, QImage::Format_Grayscale8);
mog2Label->setPixmap(QPixmap::fromImage(mog2QImage.scaled(mog2Label->size(), Qt::KeepAspectRatio)));
// KNN背景减除
cv::Mat knnFgMask;
pKNN->apply(frame, knnFgMask);
// 后处理
cv::erode(knnFgMask, knnFgMask, kernel);
cv::dilate(knnFgMask, knnFgMask, kernel);
// 显示前景掩码
QImage knnQImage(knnFgMask.data, knnFgMask.cols, knnFgMask.rows, knnFgMask.step, QImage::Format_Grayscale8);
knnLabel->setPixmap(QPixmap::fromImage(knnQImage.scaled(knnLabel->size(), Qt::KeepAspectRatio)));
}
private:
cv::VideoCapture cap;
cv::Ptr<cv::BackgroundSubtractor> pMOG2;
cv::Ptr<cv::BackgroundSubtractor> pKNN;
QLabel *originalLabel;
QLabel *mog2Label;
QLabel *knnLabel;
QTimer *timer;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
BackgroundSubtractorDemo w;
w.show();
return a.exec();
}
#include "main.moc"
在这个示例中,我们创建了一个 BackgroundSubtractorDemo 类,它使用两个背景减除器(MOG2 和 KNN)对摄像头捕获的视频进行前景检测。程序界面使用 QTabWidget 分为三个标签页,分别显示原始视频、MOG2 算法检测到的前景和 KNN 算法检测到的前景。为了去除前景掩码中的噪声,我们对掩码进行了腐蚀和膨胀的后处理操作。
四、实时目标跟踪:KCF 算法(核相关滤波)
实时目标跟踪是计算机视觉中的一个重要研究方向,它旨在在视频序列中实时地跟踪指定的目标。KCF(Kernelized Correlation Filters)算法是一种高效的实时目标跟踪算法,它基于相关滤波和核方法,具有跟踪速度快、精度高的特点。
4.1 KCF 算法的基本原理
KCF 算法的核心思想是利用相关滤波来学习一个目标的外观模型,然后在后续帧中通过计算与该模型的相关性来找到目标的位置。
- 相关滤波:相关滤波是一种基于模板匹配的方法,它通过计算目标模板与图像区域的相关性来确定目标的位置。在频域中,相关性可以通过快速傅里叶变换(FFT)来高效计算,从而提高跟踪速度。
- 核方法:KCF 算法引入了核方法,将线性相关滤波扩展到非线性情况。通过核函数,可以将低维特征映射到高维特征空间,从而更好地处理目标的非线性变化。
- 循环移位:KCF 算法利用循环移位生成大量的训练样本,这些样本可以通过快速傅里叶变换高效地进行处理,从而提高模型的学习效率和跟踪精度。
4.2 KCF 算法的特点
- 实时性好:KCF 算法在频域中进行计算,利用了 FFT 的高效性,使得跟踪速度可以达到每秒数百帧,能够满足实时跟踪的需求。
- 跟踪精度高:通过核方法和相关滤波的结合,KCF 算法能够较好地处理目标的尺度变化、旋转和部分遮挡等情况。
- 计算量小:相比其他复杂的跟踪算法,KCF 算法的计算量较小,适合在嵌入式设备等资源受限的平台上运行。
4.3 在 OpenCV 中使用 KCF 算法
OpenCV 的 contrib 模块中提供了 KCF 跟踪算法的实现,我们可以通过 cv::TrackerKCF::create () 函数来创建 KCF 跟踪器。
4.3.1 初始化跟踪器:
首先需要在第一帧中指定目标的初始位置,然后初始化跟踪器。例如:
cv::Mat frame;
cap >> frame;
cv::Rect2d bbox(100, 100, 200, 200); // 目标初始位置(x, y, width, height)
cv::Ptr<cv::Tracker> tracker = cv::TrackerKCF::create();
tracker->init(frame, bbox);
4.3.2 更新跟踪器:
在后续帧中,使用 update () 方法更新跟踪器,获取目标的新位置。例如:
cap >> frame;
bool ok = tracker->update(frame, bbox);
if (ok) {
// 目标跟踪成功,绘制目标框
cv::rectangle(frame, bbox, cv::Scalar(255, 0, 0), 2, 1);
} else {
// 目标跟踪失败
cv::putText(frame, "Tracking failure detected", cv::Point(100, 80), cv::FONT_HERSHEY_SIMPLEX, 0.75, cv::Scalar(0, 0, 255), 2);
}
4.4 Qt 中使用 KCF 算法进行实时目标跟踪的示例
下面是一个在 Qt 中使用 KCF 算法进行实时目标跟踪的示例程序:
#include <QApplication>
#include <QMainWindow>
#include <QLabel>
#include <QMouseEvent>
#include <opencv2/opencv.hpp>
#include <opencv2/tracking.hpp>
class KCFTrackerDemo : public QMainWindow
{
Q_OBJECT
public:
KCFTrackerDemo(QWidget *parent = nullptr) : QMainWindow(parent), isSelecting(false)
{
// 创建显示标签
label = new QLabel(this);
setCentralWidget(label);
// 打开摄像头
cap.open(0);
if (!cap.isOpened()) {
qDebug() << "无法打开摄像头";
return;
}
// 创建KCF跟踪器
tracker = cv::TrackerKCF::create();
// 启动定时器
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &KCFTrackerDemo::processFrame);
timer->start(30);
}
~KCFTrackerDemo()
{
cap.release();
}
protected:
void mousePressEvent(QMouseEvent *event) override
{
if (event->button() == Qt::LeftButton) {
// 开始选择目标
isSelecting = true;
startPoint = event->pos();
}
}
void mouseReleaseEvent(QMouseEvent *event) override
{
if (event->button() == Qt::LeftButton && isSelecting) {
// 结束选择目标
isSelecting = false;
endPoint = event->pos();
// 计算目标框在图像中的位置
QRect qRect = QRect(startPoint, endPoint).normalized();
cv::Rect2d bbox(qRect.x(), qRect.y(), qRect.width(), qRect.height());
// 初始化跟踪器
if (!frame.empty()) {
tracker->init(frame, bbox);
currentBbox = bbox;
isTracking = true;
}
}
}
void mouseMoveEvent(QMouseEvent *event) override
{
if (isSelecting) {
// 更新选择框
endPoint = event->pos();
}
}
private slots:
void processFrame()
{
cap >> frame;
if (frame.empty()) {
timer->stop();
return;
}
// 如果正在跟踪,更新跟踪器
if (isTracking) {
bool ok = tracker->update(frame, currentBbox);
if (ok) {
// 绘制目标框
cv::rectangle(frame, currentBbox, cv::Scalar(255, 0, 0), 2, 1);
} else {
// 跟踪失败
cv::putText(frame, "Tracking failure", cv::Point(50, 50), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 0, 255), 2);
isTracking = false;
}
}
// 如果正在选择目标,绘制选择框
if (isSelecting) {
QRect qRect = QRect(startPoint, endPoint).normalized();
cv::rectangle(frame, cv::Rect(qRect.x(), qRect.y(), qRect.width(), qRect.height()), cv::Scalar(0, 255, 0), 2, 1);
}
// 显示视频帧
cv::cvtColor(frame, frame, cv::COLOR_BGR2RGB);
QImage qImage(frame.data, frame.cols, frame.rows, frame.step, QImage::Format_RGB888);
label->setPixmap(QPixmap::fromImage(qImage.scaled(label->size(), Qt::KeepAspectRatio)));
}
private:
cv::VideoCapture cap;
cv::Mat frame;
cv::Ptr<cv::Tracker> tracker;
cv::Rect2d currentBbox;
bool isTracking = false;
QLabel *label;
QTimer *timer;
// 目标选择相关变量
bool isSelecting;
QPoint startPoint;
QPoint endPoint;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
KCFTrackerDemo w;
w.show();
return a.exec();
}
#include "main.moc"
在这个示例中,我们创建了一个 KCFTrackerDemo 类,它使用 KCF 算法对摄像头捕获的视频进行实时目标跟踪。用户可以通过鼠标拖动来选择要跟踪的目标,然后跟踪器会自动在后续帧中跟踪该目标。如果跟踪失败,程序会显示 “Tracking failure” 信息。
五、综合应用案例
为了更好地理解和运用前面介绍的视频处理技术,我们可以将它们结合起来,实现一个综合的视频处理应用。例如,一个基于 Qt 和 OpenCV 的视频监控系统,该系统能够实现视频的采集、显示、运动目标检测和跟踪等功能。
5.1 功能设计
- 视频采集:从摄像头或视频文件中采集视频帧。
- 视频显示:在 Qt 界面上实时显示采集到的视频。
- 运动目标检测:使用背景减除算法(如 MOG2 或 KNN)检测视频中的运动目标。
- 目标跟踪:对检测到的运动目标使用 KCF 算法进行实时跟踪。
- 视频录制:将处理后的视频(包含运动目标检测和跟踪结果)录制到文件中。
5.2 实现代码
#include <QApplication>
#include <QMainWindow>
#include <QTabWidget>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QWidget>
#include <opencv2/opencv.hpp>
#include <opencv2/tracking.hpp>
#include <vector>
class VideoMonitoringSystem : public QMainWindow
{
Q_OBJECT
public:
VideoMonitoringSystem(QWidget *parent = nullptr) : QMainWindow(parent)
{
// 创建主窗口部件
QWidget *centralWidget = new QWidget(this);
setCentralWidget(centralWidget);
// 创建布局
QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
QHBoxLayout *buttonLayout = new QHBoxLayout();
// 创建按钮
startBtn = new QPushButton("开始");
stopBtn = new QPushButton("停止");
recordBtn = new QPushButton("录制");
buttonLayout->addWidget(startBtn);
buttonLayout->addWidget(stopBtn);
buttonLayout->addWidget(recordBtn);
// 创建标签页控件
QTabWidget *tabWidget = new QTabWidget(this);
originalLabel = new QLabel(tabWidget);
fgLabel = new QLabel(tabWidget);
trackingLabel = new QLabel(tabWidget);
tabWidget->addTab(originalLabel, "原始视频");
tabWidget->addTab(fgLabel, "运动目标");
tabWidget->addTab(trackingLabel, "目标跟踪");
mainLayout->addLayout(buttonLayout);
mainLayout->addWidget(tabWidget);
// 初始化变量
isRunning = false;
isRecording = false;
// 打开摄像头
cap.open(0);
if (!cap.isOpened()) {
qDebug() << "无法打开摄像头";
return;
}
// 创建背景减除器和跟踪器
pMOG2 = cv::createBackgroundSubtractorMOG2();
tracker = cv::TrackerKCF::create();
// 连接信号和槽
connect(startBtn, &QPushButton::clicked, this, &VideoMonitoringSystem::startProcessing);
connect(stopBtn, &QPushButton::clicked, this, &VideoMonitoringSystem::stopProcessing);
connect(recordBtn, &QPushButton::clicked, this, &VideoMonitoringSystem::toggleRecording);
// 启动定时器
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &VideoMonitoringSystem::processFrame);
}
~VideoMonitoringSystem()
{
cap.release();
if (isRecording) {
writer.release();
}
}
private slots:
void startProcessing()
{
if (!isRunning) {
isRunning = true;
timer->start(30);
}
}
void stopProcessing()
{
if (isRunning) {
isRunning = false;
timer->stop();
trackers.clear();
bboxes.clear();
}
}
void toggleRecording()
{
if (isRecording) {
// 停止录制
isRecording = false;
writer.release();
recordBtn->setText("录制");
} else {
// 开始录制
if (!frame.empty()) {
int fourcc = cv::VideoWriter::fourcc('M', 'J', 'P', 'G');
double fps = 30.0;
cv::Size frameSize(frame.cols, frame.rows);
writer.open("monitoring.avi", fourcc, fps, frameSize, true);
if (writer.isOpened()) {
isRecording = true;
recordBtn->setText("停止录制");
}
}
}
}
void processFrame()
{
cap >> frame;
if (frame.empty()) {
stopProcessing();
return;
}
cv::Mat originalFrame = frame.clone();
cv::Mat fgMask;
// 运动目标检测
pMOG2->apply(frame, fgMask);
// 后处理
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5, 5));
cv::erode(fgMask, fgMask, kernel);
cv::dilate(fgMask, fgMask, kernel);
// 查找轮廓,获取运动目标
std::vector<std::vector<cv::Point>> contours;
cv::findContours(fgMask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
std::vector<cv::Rect2d> newBboxes;
for (const auto &contour : contours) {
// 过滤小轮廓
if (cv::contourArea(contour) < 500) {
continue;
}
// 获取目标边界框
cv::Rect2d bbox = cv::boundingRect(contour);
newBboxes.push_back(bbox);
}
// 更新跟踪器
std::vector<cv::Ptr<cv::Tracker>> newTrackers;
std::vector<cv::Rect2d> trackedBboxes;
for (const auto &newBbox : newBboxes) {
bool matched = false;
// 与现有跟踪器匹配
for (size_t i = 0; i < trackers.size(); ++i) {
cv::Rect2d trackedBbox;
if (trackers[i]->update(frame, trackedBbox)) {
// 计算IOU(交并比)
double iou = calculateIOU(newBbox, trackedBbox);
if (iou > 0.3) { // IOU阈值
// 匹配成功,更新跟踪器
trackers[i]->init(frame, newBbox);
newTrackers.push_back(trackers[i]);
trackedBboxes.push_back(newBbox);
matched = true;
break;
}
}
}
if (!matched) {
// 未匹配,创建新的跟踪器
cv::Ptr<cv::Tracker> newTracker = cv::TrackerKCF::create();
newTracker->init(frame, newBbox);
newTrackers.push_back(newTracker);
trackedBboxes.push_back(newBbox);
}
}
trackers = newTrackers;
bboxes = trackedBboxes;
// 绘制跟踪框
cv::Mat trackingFrame = originalFrame.clone();
for (const auto &bbox : bboxes) {
cv::rectangle(trackingFrame, bbox, cv::Scalar(0, 255, 0), 2, 1);
}
// 录制视频
if (isRecording) {
writer << trackingFrame;
}
// 显示视频
showImage(originalFrame, originalLabel);
showImage(fgMask, fgLabel);
showImage(trackingFrame, trackingLabel);
}
private:
// 计算两个边界框的交并比
double calculateIOU(const cv::Rect2d &bbox1, const cv::Rect2d &bbox2)
{
double x1 = std::max(bbox1.x, bbox2.x);
double y1 = std::max(bbox1.y, bbox2.y);
double x2 = std::min(bbox1.x + bbox1.width, bbox2.x + bbox2.width);
double y2 = std::min(bbox1.y + bbox1.height, bbox2.y + bbox2.height);
double intersectionArea = std::max(0.0, x2 - x1) * std::max(0.0, y2 - y1);
double area1 = bbox1.width * bbox1.height;
double area2 = bbox2.width * bbox2.height;
double unionArea = area1 + area2 - intersectionArea;
return unionArea > 0 ? intersectionArea / unionArea : 0;
}
// 显示图像
void showImage(const cv::Mat &image, QLabel *label)
{
cv::Mat displayImage;
if (image.channels() == 1) {
cv::cvtColor(image, displayImage, cv::COLOR_GRAY2RGB);
} else {
cv::cvtColor(image, displayImage, cv::COLOR_BGR2RGB);
}
QImage qImage(displayImage.data, displayImage.cols, displayImage.rows, displayImage.step, QImage::Format_RGB888);
label->setPixmap(QPixmap::fromImage(qImage.scaled(label->size(), Qt::KeepAspectRatio)));
}
cv::VideoCapture cap;
cv::VideoWriter writer;
cv::Mat frame;
cv::Ptr<cv::BackgroundSubtractor> pMOG2;
std::vector<cv::Ptr<cv::Tracker>> trackers;
std::vector<cv::Rect2d> bboxes;
QLabel *originalLabel;
QLabel *fgLabel;
QLabel *trackingLabel;
QPushButton *startBtn;
QPushButton *stopBtn;
QPushButton *recordBtn;
QTimer *timer;
bool isRunning;
bool isRecording;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
VideoMonitoringSystem w;
w.show();
return a.exec();
}
#include "main.moc"
在这个综合应用案例中,我们实现了一个视频监控系统。该系统通过摄像头采集视频,使用 MOG2 算法检测运动目标,然后对每个运动目标创建 KCF 跟踪器进行跟踪。用户可以通过按钮控制系统的开始、停止和录制功能。系统界面分为三个标签页,分别显示原始视频、运动目标检测结果和目标跟踪结果。
六、总结
本文详细介绍了在 Qt C++ 中使用 OpenCV 库实现视频处理技术的相关内容,包括 Qt 与 OpenCV 环境搭建、视频 I/O 类 VideoCapture/VideoWriter 及编解码设置、运动分析中的背景减除(MOG2/KNN)、实时目标跟踪中的 KCF 算法以及一个综合应用案例。通过这些内容的学习,了解基本的视频处理技术,并能够将它们应用到实际的项目开发中。
同时,在实际开发中,还需要注意视频处理的实时性、稳定性和兼容性等问题。根据不同的应用场景和需求,选择合适的算法和技术,进行优化和改进,以提高系统的性能和用户体验。