《OpenCV3和Qt5计算机视觉应用开发》学习笔记第9章(2)

 本篇学习OpenCV的MeanShift算法和CamShift算法

9.3 MeanShift算法和CamShift算法

除了已经看到的用例之外,在本章所学的内容都是为了能够正确地使用MeanShift和CamShift算法,因为这两个算法完全受益于直方图和反投影图像。但什么是MeanShift算法和CAMShift算法呢?

让我们先从MeanShift开始,然后再学习CamShift,后者基本上是同一个算法的增强版本。因此,MeanShift的一个非常实用的定义(正如当前OpenCV文档所述那样)如下:

查找反投影图像上的对象

这是一个十分简单但却实用的MeanShift算法定义,也是我们使用时一直坚持使用的定义。但是,值得注意的是底层算法,因为底层算法有助于更轻松、更有效地对其进行使用。为描述MeanShift的工作原理,首先,我们需要把一个反投影图像中的白色像素(或一般的二值图像)看成二维平面上的散点,这应该是很简单的。有了这个作为前提条件,我们可以说,实际上MeanShift是一个迭代方法,用来寻找在平面上点分布的最密集位置。这个算法提供了一个初始窗口(一个指定整个图像中部分区域的矩形),用来搜索质量中心,然后将窗口的中心移到新发现的质量中心。这个过程重复地寻找质量中心并移动窗口中心,直到需要的移动小于给定的阈值(极小值)或达到迭代的最大次数。图9-15显示了MeanShift算法每次迭代之后的窗口移动方式,直到到达最密集的位置(或者甚至是在达到迭代计数之前)。

基于此,MeanShift算法可以用来跟踪视频中的对象,方法是确保对象在每一帧的反投影中是可区分的。当然,需要使用一个与之前使用的方法类似的阈值方法。最常见的方法就是应用一个已经准备好的直方图,并用其计算反投影(在前面的例子中,只修改了输入直方图)。让我们用一个例子来逐步学习。出于这个原因,我们将创建一个QThread的子类,可以在任意一个独立的Qt应用程序中创建,或者在一个DLL内使用,也可以从一个插件中使用,在computer_vision工程项目中将使用这个插件。无论如何,这个线程对于所有的工程项目类型都是完全相同的。

正如第8章中所讨论的那样,应该在一个单独的线程中完成视频的处理,这样就不会阻碍GUI线程,并让其自由地响应用户的操作。注意,同样的线程也可以用作创建任何其他(类似)视频处理线程的模板。

1.我们将创建一个Qt控件应用程序,用于跟踪一个对象(在本例中,可以是任意颜色,但不是全白或全黑),该对象一开始使用鼠标在实时直播的相机中被选择,在此过程中使用MeanShift算法。在实时直播的相机中进行初始选择之后,能够切换到场景中的另外一个对象。第一次选择对象之后每次选择对象发生变化时,都会提取视频帧的色调通道,并使用直方图和反投影图像对其进行计算并提供给MeanShift算法,从而对对象进行跟踪。因此,我们需要首先创建一个Qt控件应用程序,并对其进行命名,如MeanShiftTracker,然后继续执行实际的跟踪器实现。

2.创建一个QThread子类,与在第8章中学习到的内容一样。请将其命名为QCvMean-ShiftThread,并确保在私有和公共成员区域包含了下列代码。我们将使用setTrack-Rect函数设置初始MeanShift跟踪窗口,而且还将使用该函数将跟踪对象更改为另一个对象。会在处理每一帧之后发出一个newFrame,这样GUI就可以显示它。私有区域中的成员和GUI将会在随后使用时再进行介绍,但它们包含了我们迄今为止学习过的一些最重要的主题。

public:
    void setTrackRect(QRect rect);

signals:
    void newFrame(QPixmap pix);

public slots:

private:
    void run() override;
    cv::Rect trackRect;
    QMutex rectMutex;
    bool updateHistogram;

3.setTrackRect函数是setter函数,用来更新我们希望MeanShift算法跟踪的矩形(初始窗口)。下面是实现该函数的代码:

void QCvMeanShiftThread::setTrackRect(QRect rect)
{
    QMutexLocker locker(&rectMutex);
    if((rect.width()>2) && (rect.height()>2))
    {
        trackRect.x = rect.left();
        trackRect.y = rect.top();
        trackRect.width = rect.width();
        trackRect.height = rect.height();
        updateHistogram = true;
    }
}

QMutexLocker和rectMutex一起使用,为trackRect提供访问序列化。因为我们还将以一种实时的方式实现跟踪方法,所以需要确保trackRect在处理过程中不会被更新。还要确保其大小是合理的,否则就将其忽略。

4.至于跟踪器线程的run函数,需要使用VideoCapture打开电脑上的默认摄像头,并传送帧。注意,如果帧是空的(断开),或摄像头已关闭,或者从线程外部请求线程中断,那么将退出循环:

VideoCapture video;
Mat hist;
video.open(0);

while(video.isOpened() && !this->isInterruptionRequested())
{
    //this->msleep(100);

    Mat frame;
    video >> frame;
    if(frame.empty())
        break; // or continue if this should be tolerated
        ...
}

在循环中标记为“rest of the process”的地方,首先将使用cv::Rect类中的area函数来看是否已经设置了trackRect。如果是,那么将锁定访问并继续跟踪操作:

if(trackRect.size().area() > 0)
{
	QMutexLocker locker(&rectMutex);
	//tracking code 
	...
}

至于MeanShift算法和实际跟踪,可以使用下列源代码:

Mat hsv, hue, hist;
cvtColor(frame, hsv, CV_BGR2HSV);
hue.create(hsv.size(), hsv.depth());
float hrange[] = {0, 179};
const float* ranges[] = {hrange};
int bins[] = {24};
int fromto[] = {0, 0};
mixChannels(&hsv, 1, &hue, 1, fromto, 1);

if(updateHistogram)
{
	Mat roi(hue, trackRect);
	calcHist(&roi, 1, 0, Mat(), hist, 1, bins, ranges);

	normalize(hist,
			  hist,
			  0,
			  255,
			  NORM_MINMAX);

	updateHistogram = false;
}

Mat backProj;
calcBackProject(&hue,
				1,
				0,
				hist,
				backProj,
				ranges);

TermCriteria criteria;
criteria.maxCount = 5;
criteria.epsilon = 3;
criteria.type = TermCriteria::EPS;
RotatedRect rotRec = CamShift(backProj, trackRect, criteria);

rectangle(frame, trackRect, Scalar(0,0,255), 2);

上述代码按照以下顺序执行下列操作:

❑ 使用cvtColor函数,将输入帧从BGR转换到HSV颜色空间。

❑ 使用mixChannels函数,只提取色调通道。

❑ 如果需要的话,使用calcHist函数和normalize函数计算和正则化直方图。

❑ 使用calcBackproject函数,计算反投影图像。

❑ 通过提供迭代条件,对反投影图像运行MeanShift算法。这是使用TermCriteria类和meanShift函数来完成的。meanShift将简单地更新所提供的矩形(在每一帧中的新trackRect)。

❑ 在原始图像上绘制检索到的矩形。

除了TermCriteria类和meanShift函数自身之外,刚才看到的任何代码都没有什么新的内容。正如前面介绍的那样,MeanShift算法是一个迭代方法,需要一些停止条件,这些条件基于移动的数量(极小值)和迭代次数。简单地说,增加迭代的次数可以降低算法的速度,但是也会让算法更加准确。另一方面,提供更小的极小值意味着更敏感的行为。

在处理完每一帧之后,线程仍然需要使用专用的信号将其发送给另一个类。如下所示:

emit newFrame(
                    QPixmap::fromImage(
                        QImage(
                            frame.data,
                            frame.cols,
                            frame.rows,
                            frame.step,
                            QImage::Format_RGB888)
                        .rgbSwapped()));

请注意,除了发送QPixmap或QImage之外,还可以发送不是QObject子类的类。为了能够通过Qt信号发送非Qt的类,它必须有一个公共的默认构造函数,和一个公共副本构造函数,以及一个公共析构函数。还必须首先将它注册。例如,Mat类包含了所需的方法,但不是注册类型,因此可以这样注册它:

qRegisterMetaType<Mat>("Mat");

之后,可以在Qt信号和槽中使用这个Mat类。

5.除非完成该线程所需的用户界面,否则仍然看不到任何结果。我们用QGraphics-View来完成该任务,只需使用设计器将它拖放到mainwindow.ui上,然后将下列内容添加到mainwindow.h。我们将使用QGraphicsView类的橡皮筋功能,来轻松地实现对象选择:

private slots:
    void onRubberBandChanged(QRect rect, QPointF, QPointF);
    void onNewFrame(QPixmap newFrm);

private:
    QCvMeanShiftThread *meanshift;
    QGraphicsPixmapItem pixmap;

6.在mainwindow.cpp文件以及MainWindow类的构造函数中,确保添加下列代码:

ui->graphicsView->setScene(new QGraphicsScene(this));
ui->graphicsView->setDragMode(QGraphicsView::RubberBandDrag);
connect(ui->graphicsView, SIGNAL(rubberBandChanged(QRect,QPointF,QPointF)),
            this, SLOT(onRubberBandChanged(QRect,QPointF,QPointF)));

meanshift = new QCvMeanShiftThread();
connect(meanshift, SIGNAL(newFrame(QPixmap)), this, SLOT(onNewFrame(QPixmap)));
meanshift->start();

ui->graphicsView->scene()->addItem(&pixmap);

第5章中详细介绍过如何使用Qt图形视图框架。

7.还要确保关闭应用程序时处理好线程,如下所示:

meanshift->requestInterruption();
meanshift->wait();
delete meanshift;

8.剩下唯一的一件事情就是在GUI上设置传入的QPixmap,并传递在更新被跟踪的对象时所需的矩形:

void MainWindow::onRubberBandChanged(QRect rect, QPointF, QPointF)
{
    meanshift->setTrackRect(rect);
}

void MainWindow::onNewFrame(QPixmap newFrm)
{
    pixmap.setPixmap(newFrm);
}

请尝试运行应用程序,并选择一个在相机中可见的对象。无论用鼠标选择的对象在屏幕上的什么地方,在图形视图上用鼠标绘制的矩形都会一直跟着所选择的对象。图9-16是从视图中选择要跟踪的Qt图标的几个截图:

可视化反投影图像并看看后台的神奇之处,也是一个好主意。记住,正如前面介绍过的那样,MeanShift算法正在搜索质量中心,如果在反投影图像中观察,这是十分容易看到的。要这样做,只需在线程内用下列代码替换用来可视化图像的最后几行:

cvtColor(backProj, backProj, CV_GRAY2BGR);       
frame = backProj;                                
rectangle(frame, trackRect, Scalar(0,0,255), 2); 

现在再试一次,在图形视图中应该有一个反投影图像,如图9-17所示。

从图9-17的结果中可以看出,只要为其提供一个灰度图像,就可以很容易地使用MeanShift算法即meanShift函数,因为灰度图像可以使用任何阈值方法分离岀感兴趣对象。所以,反投影也类似于阈值化,它可以基于颜色、强度或其他条件允许某些像素通过,或者某些其他的像素不能通过。现在,如果回到MeanShift算法的初始描述,那么基于反投影图像可以找到并追踪对象的说法就是完全有意义的。

尽管meanShift函数的使用很简单,但是仍然缺少一些非常重要的功能,即对被跟踪对象的缩放和方向变化的容忍度。无论对象的大小或方向是什么,camShift函数都将提供大小和旋转完全相同的窗口,并尝试将中心放到感兴趣的对象上。在MeanShift算法的增强版本中解决了这些问题,该算法称为连续自适应MeanShift算法,或简称为CamShift。

CamShift函数是CamShift算法在OpenCV中的实现,与MeanShift算法有很大的关系,出于同样的原因,使用方式几乎相同。为了证明这一点,可以简单地用CamShift替换前面代码中的meanShift算法,如下所示:

CamShift(backProj, trackRect, criteria);

如果再次运行这个程序,将会发现什么都没有发生改变。但是该函数还提供了Rotated-Rect类型的一个返回值,这是一个有中心、大小和角度属性的矩形。可以保存返回的Rotated-Rect,并将它绘制在原始图像上,如下所示:

RotatedRect rotRec = CamShift(backProj, trackRect, criteria);
rectangle(frame, trackRect, Scalar(0,0,255), 2);
ellipse(frame, rotRec, Scalar(0,255,0), 2);

注意,在该代码片段中,实际上绘制了一个满足RotatedRect类属性的一个椭圆形。为了与旋转的矩形进行比较,还绘制了之前存在的矩形。图9-18是再次运行这个程序的结果。

注意绿色椭圆形的旋转,与红色矩形形成对比,这是CamShift函数的结果。试着移动追踪的彩色对象,远离或靠近相机,看看CamShift如何适应这些变化。另外,试一下非正方形的对象,观察由CamShift提供的旋转不变性的跟踪。

当然,如果对象与周围环境有区别的话,Cam-Shift函数还可以基于对象的颜色来检测对象。要这样做,需要设置一个预先准备好的直方图,而不是像在例子中那样在运行时对其进行设置。还需要将初始窗口的大小设置为足够大,比如整张图像的大小,或在图像中期望出现对象的最大区域。运行同样的代码,你将注意到每执行一帧之后,窗口都会变得越来越小,直到只能覆盖所提供的直方图中的感兴趣对象。

完整的源码如下:

BackgroundDetect.pro


QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = BackgroundDetect
TEMPLATE = app

# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0


SOURCES += \
        main.cpp \
        mainwindow.cpp \
    qcvbackgrounddetect.cpp

HEADERS += \
        mainwindow.h \
    qcvbackgrounddetect.h

FORMS += \
        mainwindow.ui

win32: {
    include("./opencv.pri")
}

unix: !macx{
    CONFIG += link_pkgconfig
    PKGCONFIG += opencv
}

unix: macx{
INCLUDEPATH += "/usr/local/include"
LIBS += -L"/usr/local/lib" \
    -lopencv_world
}
qcvbackgrounddetect.h文件
#ifndef QCVBACKGROUNDDETECT_H
#define QCVBACKGROUNDDETECT_H

#include <QThread>
#include <QImage>
#include <QPixmap>
#include <QMutex>
#include "opencv2/opencv.hpp"

class QCvBackgroundDetect : public QThread
{
    Q_OBJECT
public:
    explicit QCvBackgroundDetect(QObject *parent = nullptr);

signals:
    void newFrame(QPixmap pix);

public slots:

private:
    void run() override;

};

#endif // QCVBACKGROUNDDETECT_H

qcvbackgrounddetect.cpp文件

#include "qcvbackgrounddetect.h"
#include <QDebug>

QCvBackgroundDetect::QCvBackgroundDetect(QObject *parent) : QThread(parent)
{

}

void QCvBackgroundDetect::run()
{
    using namespace cv;

    Mat foreground;
    VideoCapture video;
    video.open("D:\\evVideo\\test1.mp4", CAP_ANY);
    //video.open(0);

    //Ptr<BackgroundSubtractorMOG2> subtractor = createBackgroundSubtractorMOG2();
    Ptr<BackgroundSubtractorKNN> subtractor = createBackgroundSubtractorKNN();
    qDebug() << "QCvBackgroundDetect::run===============================" << video.isOpened() << this->isInterruptionRequested();
    while(video.isOpened() && !this->isInterruptionRequested())
    {
        //this->msleep(100);

        Mat frame;
        video >> frame;
        if(frame.empty())
            break; // or continue if this should be tolerated

        subtractor->apply(frame, foreground);

        Mat foregroundBgr;
        //bitwise_and(frame, foreground, foreground);
        frame.copyTo(foregroundBgr, foreground);

        emit newFrame(
                    QPixmap::fromImage(
                        QImage(
                            foregroundBgr.data,
                            foregroundBgr.cols,
                            foregroundBgr.rows,
                            foregroundBgr.step,
                            QImage::Format_RGB888)
                        .rgbSwapped()));

    }
}
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QGraphicsScene>
#include <QGraphicsItem>
#include <QGraphicsView>
#include <QDebug>
#include "qcvbackgrounddetect.h"

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void onNewFrame(QPixmap newFrm);

private:
    Ui::MainWindow *ui;

    QCvBackgroundDetect *backDetect;
    QGraphicsPixmapItem pixmap;

};

#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    ui->graphicsView->setScene(new QGraphicsScene(this));
    backDetect = new QCvBackgroundDetect();
    connect(backDetect, SIGNAL(newFrame(QPixmap)), this, SLOT(onNewFrame(QPixmap)));
    backDetect->start();

    ui->graphicsView->scene()->addItem(&pixmap);
}

MainWindow::~MainWindow()
{
    backDetect->requestInterruption();
    backDetect->wait();
    delete backDetect;

    delete ui;
}

void MainWindow::onNewFrame(QPixmap newFrm)
{
    pixmap.setPixmap(newFrm);
}

main.cpp

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

运行结果:

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值