在 Qt C++ 中利用 OpenCV 实现视频处理技术详解 - 实践

前言

在当今的计算机视觉领域,视频处理技术有着广泛的应用,从安防监控到自动驾驶,从视频编辑到人工智能交互,都离不开对视频的有效处理。 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 算法以及一个综合应用案例。通过这些内容的学习,了解基本的视频处理技术,并能够将它们应用到实际的项目开发中。​
同时,在实际开发中,还需要注意视频处理的实时性、稳定性和兼容性等问题。根据不同的应用场景和需求,选择合适的算法和技术,进行优化和改进,以提高系统的性能和用户体验。​

posted @ 2025-08-22 10:17  yjbjingcha  阅读(75)  评论(0)    收藏  举报