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

除了到目前为止在本书中看到的所有内容以外,计算机视觉技术还有另一项任务,那就是处理视频,即对输入帧进行基本上实时的处理。我们有充分的理由认为这是计算机视觉最受欢迎的主题之一,因为这可以为动态的机器或设备提供动力,让这些设备能够监视周围的环境,寻找感兴趣的对象、运动、图案、颜色等。我们已经学习过的所有算法和类(尤其是在第6章和第7章)是用来处理单个图像的,出于同样原因,可以简单地以完全相同的方式将其应用于单个视频帧。我们只需确保正确地将每一帧都读取到cv::Mat类实例(例如,使用cv::VideoCapture类),然后作为一个又一个图像传递到这些函数中。但是在处理视频(是指来自网络、相机、视频文件等方式的视频馈入)时,有时候需要对一个特定时间段内连续的视频帧进行处理,才能获得我们需要的结果。这就意味着结果不仅取决于从视频中获取的当前图像,还取决于在此之前获取的帧。

在本章中,我们将学习OpenCV中用来处理连续帧(即视频)的一些最重要的算法和类。我们将从这些算法所使用的某些概念开始学习,例如,直方图和反向投影图像,然后将通过使用示例以及获得的亲身体验来深入研究每一个算法。我们将学习如何使用MeanShift算法和CamShift算法来实现实时对象跟踪,而且将继续对视频进行运动分析。本章将学习的大部分内容都与OpenCV框架内的视频分析模块相关,但是同时也会确保梳理一遍其他所需模块的所有相关主题,以便有效地跟进本章的主题,尤其是直方图和反投影图像对于理解本章介绍的视频分析主题是至关重要的。背景/前景检测也是本章将要学习的最重要的主题之一。通过使用这些方法组合,你将能够有效地处理视频以检测和分析运动,或者在视频帧中根据颜色分离出部分和片段,并使用现有的OpenCV图像处理算法以各种方式对其进行处理。

另外,根据从第8章中学到的知识,我们将使用线程来实现在本章中学习到算法。这些线程将独立于任意一种工程项目类型;无论工程项目类型是否是一个独立的应用程序、库、插件等等,都可以简单地包含和使用它们。
本章将介绍以下主题:
❑ 直方图及其提取、使用与可视化
❑ 反投影图像
❑ MeanShift算法和CamShift算法
❑ 背景/前景检测及运动分析

9.1 理解直方图

正如本章引言中介绍的那样,在计算机视觉中有几个概念在进行视频处理以及在涉及本章后面会讲到的算法时尤为重要。其中一个概念是直方图。因为直方图的概念对于理解大多数视频分析主题是至关重要的,所以将在本节详细介绍与直方图相关的内容,然后再讨论下一个主题。通常,直方图是指表示数据分布的方式。这是一个非常简单和完整的描述,但是让我们再来介绍一下直方图在计算机视觉方面的意义。在计算机视觉中,直方图是图像中像素值分布的图形表示。例如,在灰度图像中,直方图图形就表示在灰度图中包含每个可能强度(0和255之间的一个值)的像素数量。在RGB彩色图像中,直方图可以是三个图,每个图表示包含所有可能的红色、绿色或者蓝色强度的像素数量。请注意,像素值并不一定表示颜色或强度值。例如,在转换为HSV颜色空间的颜色图像中,直方图将包含色调、饱和度以及数值数据。

OpenCV中的直方图使用calcHist函数进行计算,并存储在Mat类中,因为可以将其存储为一组数字,并且可能有多个通道。calcHist函数需要下面的参数来计算直方图:
❑ images或input images,是要用来计算直方图的图像,它是cv::Mat类的数组。
❑ nimages是第一个参数中的图像数量。注意,也可以为第一个参数传递cv::Mat类的std::vector,在这种情况下,可以忽略这个参数。
❑ channels是一个数组,包含用于计算直方图的通道的索引号。
❑ mask可以用来掩模图像,以便只用输入图像的一部分计算直方图。如果不需要掩模,则可以传递一个空Mat类,否则,需要提供一个单通道的Mat类,它包含的零值表示在计算直方图时应该掩模的所有像素,而非零值是应该考虑的所有像素。

❑ hist是输出直方图,是Mat类,函数返回时,用计算的直方图填充它。
❑ dims是直方图的维度,在1到32之间取值(在当前的OpenCV 3实现中),需要根据用来计算直方图的通道数设置该值。
❑ histSize是包含的直方图每一维大小的数组,或者称为组(bin)的大小。 直方图中的组指的是在计算直方图时将相似值视为相同。稍后,将用一个例子来看看这到底表示什么,但是现在,可以认为直方图的大小和组数是一样的。
❑ ranges是数组的数组,包含每通道值的范围。简单地说,这个数组包含通道的一对最小和最大可能值。
❑ uniform是布尔标志,用于标记直方图是否均匀。
❑ accumulate是布尔标志,用于标记在计算之前是否应该清除直方图。如果要更新之前计算过的直方图,这将是非常有用的。

现在,用两个例子来看看这个函数是如何使用的。首先,作为一个更简单的用例,我们将计算一个灰度图像的直方图:

在上面的代码中,grayImg是一个Mat类的灰度图像。图像数量为1,channels索引数组参数只包含一个值(0,是第一个通道),因为输入的图像是单通道和灰度级的。dimensionality也是1,其余的参数与它们的默认值相同(如果省略的话)。

在执行前面的代码之后,将在histogram变量内得到灰度图像的结果直方图。这是一个单通道、单列、256行的Mat类,每一行表示像素值与行号相同的像素数。可以用下列代码绘制存储在这个Mat类中的每个值,输出将是柱状图中的直方图可视化:

 

乍看起来,这段代码有点复杂,但是实际上十分简单,它基于这样一个事实,即需要将直方图中的每一个值绘制为一个矩形。对每个矩形来说,左上角的点用图像的value变量和宽除以组(bin)数(即histSize)来计算。在示例代码中,简单地将最高可能值分配给bins(即256),得到高分辨率的直方图可视化,因为柱状图中的每一个柱将表示灰度中的一个像素强度。
请注意,这个意义上的分辨率不是指图像的分辨率或者质量,而是指构成我们的柱状图的最小块数的分辨率。
我们还假设输出的可视化高与直方图的峰值(最高点)相同。如果对图9-1左边的灰度图运行这些代码,那么将得到图9-1右边看到的直方图结果。

图9-1 左边是一个灰度图像,右边是该灰度图像所对应的直方图

让我们来解释一下输出直方图的可视化,并进一步说明在代码中使用的参数通常会有什么效果。首先,从左到右,每个竖条指的是具有一种特定灰度强度值的像素数量。最左边的竖条(相当低)指的是纯黑色(0密度值),右边的竖条是纯白色(255),所有的竖条指的是不同深浅的灰色。实际上这是由输入图像中最浅部分(左上角)形成的。每个竖条的高除以最高竖条值,然后缩放以适应图像的高度。
让我们再来看看bins变量的影响。降低bins将导致分组强度的聚集,从而导致计算并可视化一个较低的分辨率直方图。如果用值为20的bins运行同样的代码,将得到图9-2的直方图。

图9-2 值为20的bins对应的直方图

如果需要简单的图表而不是柱状图视图的话,可以在上面代码的末尾使用下面的代码:

如果使用值为256的bins,则会产生图9-3的输出。

图9-3 值为256的bins产生的输出

类似地,可以计算并可视化一个彩色(RGB)图像的直方图,这只需为三个单独的通道修改相同的代码。为达该目的,首先需要将输入图像分割成底层通道,然后计算每个通道的直方图,就和单个通道的图像是一样的。下面代码展示如何分割图像得到三个Mat类,每个类代表一个单独的通道:

vector<Mat> planes;
split(inputImage, planes);

现在可以在一个循环中使用planes[i]或类似的手段,并将每个通道当作一个图像,然后使用上面的代码示例计算并可视化直方图。如果使用它自己的颜色可视化每一个直方图,将得到如图9-4所示的结果(生成这个直方图的图像是这本书中使用过的之前例子的彩色图像)。

图9-4 生成的三通道的彩色直方图

同样,结果的内容可以用与之前几乎一样的方式来解释。图9-4的直方图图像显示在一个RGB图像的不同通道上颜色是如何分布的。但是如何才能真正使用直方图,而不仅仅是获取有关像素值分布的信息呢?下一节将介绍使用直方图来修改图像的方法。

下面是完整的插件代码

Histogram_Plugin.pro

QT       += widgets

TARGET = Histogram_Plugin
TEMPLATE = lib

CONFIG += plugin

DEFINES += HISTOGRAM_PLUGIN_LIBRARY

# 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

INCLUDEPATH += ../cvplugininterface

SOURCES += \
        histogram_plugin.cpp

HEADERS += \
        histogram_plugin.h \
        histogram_plugin_global.h

unix {
    target.path = /usr/lib
    INSTALLS += target
}

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

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

unix: macx{
INCLUDEPATH += "/usr/local/include"
LIBS += -L"/usr/local/lib" \
    -lopencv_world
}

FORMS += \
    plugin.ui

opencv.pri

#win环境配置
win32:{
    CONFIG(debug, debug|release){
        DESTDIR += $$PWD/../../bin/win64d/cvplugins
        LIBS += -L$$PWD/../../lib/win64d -lopencv_world480d
    }else{
        DESTDIR += $$PWD/../../bin/win64/cvplugins
        LIBS += -L$$PWD/../../lib/win64 -lopencv_world480
    }
}

INCLUDEPATH += ../../Include
histogram_plugin_global.h
#ifndef HISTOGRAM_PLUGIN_GLOBAL_H
#define HISTOGRAM_PLUGIN_GLOBAL_H

#include <QtCore/qglobal.h>

#if defined(HISTOGRAM_PLUGIN_LIBRARY)
#  define HISTOGRAM_PLUGIN_SHARED_EXPORT Q_DECL_EXPORT
#else
#  define HISTOGRAM_PLUGIN_SHARED_EXPORT Q_DECL_IMPORT
#endif

#endif // HISTOGRAM_PLUGIN_GLOBAL_H

histogram_plugin.h

#ifndef HISTOGRAM_PLUGIN_H
#define HISTOGRAM_PLUGIN_H

#include "histogram_plugin_global.h"
#include "cvplugininterface.h"

namespace Ui {
    class PluginGui;
}

class HISTOGRAM_PLUGIN_SHARED_EXPORT Histogram_Plugin: public QObject, public CvPluginInterface
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "com.computervision.cvplugininterface")
    Q_INTERFACES(CvPluginInterface)
public:
    Histogram_Plugin();
    ~Histogram_Plugin();

    QString title();
    QString version();
    QString description();
    QString help();
    void setupUi(QWidget *parent);
    void processImage(const cv::Mat &inputImage, cv::Mat &outputImage);

signals:
    void updateNeeded();
    void errorMessage(QString msg);
    void infoMessage(QString msg);

private slots:
    void on_grayRadio_toggled(bool);

    void on_rgbRadio_toggled(bool checked);

    void on_hsvRadio_toggled(bool checked);

    void on_binsSpin_valueChanged(int arg1);

    void on_widthSpin_valueChanged(int arg1);

    void on_heightSpin_valueChanged(int arg1);

    void on_uniformCheck_toggled(bool checked);

private:
    Ui::PluginGui *ui;

    void grayScaleHist(const cv::Mat &inputImage, cv::Mat &outputImage);
    void rgbHist(const cv::Mat &inputImage, cv::Mat &outputImage);
    void hsvHist(const cv::Mat &inputImage, cv::Mat &outputImage);

};

#endif // HISTOGRAM_PLUGIN_H

histogram_plugin.cpp

#include "histogram_plugin.h"
#include "ui_plugin.h"
#include <opencv2\imgproc\types_c.h>
#include <opencv2/imgproc/imgproc_c.h>
#include <QDebug>

Histogram_Plugin::Histogram_Plugin()
{
    // Insert initialization codes here ...
}

Histogram_Plugin::~Histogram_Plugin()
{
    // Insert cleanup codes here ...
}

QString Histogram_Plugin::title()
{
    return this->metaObject()->className();
}

QString Histogram_Plugin::version()
{
    return "1.0.0";
}

QString Histogram_Plugin::description()
{
    return "Histogram plugin";
}

QString Histogram_Plugin::help()
{
    return "A plugin for playing around with histograms.";
}

void Histogram_Plugin::setupUi(QWidget *parent)
{
    ui = new Ui::PluginGui;
    ui->setupUi(parent);

    connect(ui->grayRadio, SIGNAL(toggled(bool)), this, SLOT(on_grayRadio_toggled(bool)));
    connect(ui->rgbRadio, SIGNAL(toggled(bool)), this, SLOT(on_rgbRadio_toggled(bool)));
    connect(ui->hsvRadio, SIGNAL(toggled(bool)), this, SLOT(on_hsvRadio_toggled(bool)));
    connect(ui->binsSpin, SIGNAL(valueChanged(int)), this, SLOT(on_binsSpin_valueChanged(int)));
    connect(ui->widthSpin, SIGNAL(valueChanged(int)), this, SLOT(on_widthSpin_valueChanged(int)));
    connect(ui->heightSpin, SIGNAL(valueChanged(int)), this, SLOT(on_heightSpin_valueChanged(int)));
    connect(ui->uniformCheck, SIGNAL(toggled(bool)), this, SLOT(on_uniformCheck_toggled(bool)));
}

void Histogram_Plugin::processImage(const cv::Mat &inputImage, cv::Mat &outputImage)
{
    if(ui->grayRadio->isChecked())
    {
        grayScaleHist(inputImage, outputImage);
    }
    else if(ui->rgbRadio->isChecked())
    {
        rgbHist(inputImage, outputImage);
    }
    else if(ui->hsvRadio->isChecked())
    {
        hsvHist(inputImage, outputImage);
    }
}

void Histogram_Plugin::on_grayRadio_toggled(bool)
{
    emit updateNeeded();
}

void Histogram_Plugin::on_rgbRadio_toggled(bool)
{
    emit updateNeeded();
}

void Histogram_Plugin::on_hsvRadio_toggled(bool)
{
    emit updateNeeded();
}

void Histogram_Plugin::on_binsSpin_valueChanged(int)
{
    emit updateNeeded();
}

void Histogram_Plugin::grayScaleHist(const cv::Mat &inputImage, cv::Mat &outputImage)
{
    using namespace cv;
    Mat grayImg;
    cvtColor(inputImage, grayImg, CV_BGR2GRAY);

    int channels[] = {0}; // only the first channel
    int histSize[] = {ui->binsSpin->value()}; // number of bins

    float rangeGray[] = {0,255}; // range of grayscale
    const float* ranges[] = { rangeGray };

    Mat histogram;

    calcHist(&grayImg,
             1, // number of images
             channels,
             Mat(), // no masks, an empty Mat
             histogram,
             1, // dimensionality
             histSize,
             ranges);

    /*
    for(int i=0; i<histogram.rows; i++)
    {
        if(i < 40) // threshold
            histogram.at<float>(i,0) = 255;
        else
            histogram.at<float>(i,0) = 0;
    }

    Mat backprojection;
    calcBackProject(&grayImg,
                    1,
                    channels,
                    histogram,
                    backprojection,
                    ranges);
    */

    double maxVal = 0;
    minMaxLoc(histogram,
              Q_NULLPTR, // don't need min
              &maxVal,
              Q_NULLPTR, // don't need index min
              Q_NULLPTR // don't need index max
              );

    outputImage.create(ui->heightSpin->value(), // any image width
                       ui->widthSpin->value(), // any image height
                       CV_8UC(3));

    outputImage = Scalar::all(128); // empty grayish image

    //imshow("grayImg", grayImg);

    Point p1(0,0), p2(0,0);
    for(int i=0; i<ui->binsSpin->value(); i++)
    {
        float value = histogram.at<float>(i,0);
        value = maxVal - value; // invert
        value = value / maxVal * outputImage.rows;
        line(outputImage,
             p1,
             Point(p1.x,value),
             Scalar(0,0,0));
        p1.y = p2.y = value;
        p2.x = float(i+1) * float(outputImage.cols) / float(ui->binsSpin->value());
        line(outputImage,
             p1, p2,
             Scalar(0,0,0));
        p1.x = p2.x;
    }
}

void Histogram_Plugin::rgbHist(const cv::Mat &inputImage, cv::Mat &outputImage)
{
    using namespace cv;
    using namespace std;

    int channels[] = {0};
    int histSize[] = {ui->binsSpin->value()}; // number of bins

    float range[] = {0,255}; // range of colors
    const float* ranges[] = { range };

    Mat histograms[3];

    vector<Mat> planes;
    split(inputImage, planes);

    double maxVal[3] = {0,0,0};

    for(int i=0; i<3; i++)
    {
        calcHist(&planes[i],
                 1, // number of images
                 channels,
                 Mat(), // no masks, an empty Mat
                 histograms[i],
                 1, // dimensionality
                 histSize,
                 ranges);

        minMaxLoc(histograms[i],
                  Q_NULLPTR, // don't need min
                  &maxVal[i],
                  Q_NULLPTR, // don't need index min
                  Q_NULLPTR // don't need index max
                  );
    }

    outputImage.create(ui->heightSpin->value(), // any image width
                       ui->widthSpin->value(), // any image height
                       CV_8UC(3));

    outputImage = Scalar::all(0); // empty black image

    Point p1[3], p2[3];
    for(int i=0; i<ui->binsSpin->value(); i++)
    {
        for(int j=0; j<3; j++)
        {
            float value = histograms[j].at<float>(i,0);
            value = maxVal[j] - value; // invert
            value = value / maxVal[j] * outputImage.rows;
            line(outputImage,
                 p1[j],
                 Point(p1[j].x,value),
                 Scalar(j==0 ? 255:0,
                        j==1 ? 255:0,
                        j==2 ? 255:0),
                 2);
            p1[j].y = p2[j].y = value;
            p2[j].x = float(i+1) * float(outputImage.cols) / float(ui->binsSpin->value());
            line(outputImage,
                 p1[j], p2[j],
                 Scalar(j==0 ? 255:0,
                        j==1 ? 255:0,
                        j==2 ? 255:0),
                 2);
            p1[j].x = p2[j].x;
        }
    }
}

void Histogram_Plugin::hsvHist(const cv::Mat &inputImage, cv::Mat &outputImage)
{
    using namespace cv;
    Mat hsvImg;
    cvtColor(inputImage, hsvImg, CV_BGR2HSV);

    int channels[] = {0}; // only the first channel
    int histSize[] = {ui->binsSpin->value()}; // number of bins

    float rangeHue[] = {0,179}; // range of Hue channel
    const float* ranges[] = { rangeHue };

    Mat histogram;

    calcHist(&hsvImg,
             1, // number of images
             channels,
             Mat(), // no masks, an empty Mat
             histogram,
             1, // dimensionality
             histSize,
             ranges);

    double maxVal = 0;
    minMaxLoc(histogram,
              Q_NULLPTR, // don't need min
              &maxVal,
              Q_NULLPTR, // don't need index min
              Q_NULLPTR // don't need index max
              );

    outputImage.create(ui->heightSpin->value(), // any image width
                       ui->widthSpin->value(), // any image height
                       CV_8UC(3));

    outputImage = Scalar::all(0); // empty black image

    Mat colors(1, ui->binsSpin->value(), CV_8UC3);
    for(int i=0; i<ui->binsSpin->value(); i++)
    {
        colors.at<Vec3b>(i) = Vec3b(saturate_cast<uchar>((i+1)*180.0/ui->binsSpin->value()), 255, 255);
    }
    cvtColor(colors, colors, COLOR_HSV2BGR);

    Point p1(0,0), p2(0,outputImage.rows-1);
    for(int i=0; i<ui->binsSpin->value(); i++)
    {
        float value = histogram.at<float>(i,0);
        value = maxVal - value; // invert
        value = value / maxVal * outputImage.rows; // scale
        p1.y = value;
        p2.x = float(i+1) * float(outputImage.cols) / float(ui->binsSpin->value());
        rectangle(outputImage,
                  p1,
                  p2,
                  Scalar(colors.at<Vec3b>(i)),
                  CV_FILLED);
        p1.x = p2.x;
    }

}

void Histogram_Plugin::on_widthSpin_valueChanged(int)
{
    emit updateNeeded();
}

void Histogram_Plugin::on_heightSpin_valueChanged(int)
{
    emit updateNeeded();
}

void Histogram_Plugin::on_uniformCheck_toggled(bool)
{
    emit updateNeeded();
}

运行结果:

RGB模式

HSV

9.2 理解反投影图像

除了直方图中的可视信息之外,直方图还有一个更重要的用途,即直方图的反投影,可以使用它的直方图对图像进行修改,或者如稍后将在本章中看到的那样,通过它在图像内定位感兴趣的对象。让我们进一步详细分析,正如在上一节中学习到的,直方图反映的是图像上像素数据的分布情况,因此如果以某种方式修改生成的直方图,然后将其重新应用到原图像(就好像它是像素值的查找表),生成的图像将被看作反投影图像。需要重点注意的是,反投影图像总是单通道图像,其中每一个像素的值都是从直方图中其对应的bin获取的。

让我们把这当作另一个例子。首先,下面展示在OpenCV中如何计算反投影:

calcBackProject函数与calcHist函数的用法非常相似。只需确保传递一个额外的Mat类实例,即可获得图像的反投影。因为在反投影图像中,像素值是从直方图获取的,所以很容易溢出标准的0到255(包括)之间的灰度数值范围。这就是为什么在反投影计算之前,需要对直方图的结果进行标准化,如下例所示:

normalize函数将直方图中的所有值缩放到从最小值0到最大值255的范围内。再重复一次,必须在calcBackProject之前调用该函数,否则,在反投影图像中,将得到溢出的数据,如果尝试使用imshow函数查看图像,那么它很可能包含所有的白色像素。

如果没有对生成图像的直方图进行任何修改而只是查看反投影图像,在我们的例子中将得到图9-5所示的输出图像。

图9-5 在对生成的直方图不做任何修改的情况下输出的反投影图像

图9-5中每个像素的强度与包含该特定值的图像中的像素数量有关。例如,注意反投影图像的右上角最暗的部分。与较亮区域相比,这个区域包含的像素并不是很多。也就是说,在图像及其不同区域中,明亮区域包含更多的像素值。那么在处理图像和视频帧时如何使用反投影图像呢?

从本质上说,反投影图像可以用来为计算机视觉操作获取有用的掩模图像。到目前为止,我们并没有真正使用OpenCV函数中的掩模参数(掩模参数存在于大部分OpenCV函数中)。下面用一个例子说明如何使用图9-5中的反投影图像。可以用一个简单阈值修改直方图得到一个掩模,以滤除不需要的图像部分。假设想要一个可以用来获取包含最暗值(例如,从0-39的像素值)的像素的掩模。为了能够完成这一任务,首先可以通过将前40个元素(只是最暗值的一个阈值,可以设置为任意其他值或范围)设置为灰度范围中最大的可能值(255),并将其余设置为最小的可能值(0),来修改直方图并计算反投影图像。下面是举例:

 运行上述示例代码后,将在backprojection变量中得到图9-6的输出图像。实际上,这是一种阈值化技术,通过它可以得到一个合适的掩模,用于将图像中最暗的区域隔离出来,以便使用OpenCV进行任意计算机视觉处理。可以将使用该示例代码获得的掩模传递给任意一个OpenCV函数,该函数接受掩模并对那些与掩模中白色位置相对应的像素执行操作,而忽略与黑色位置相对应的像素。

图9-6 运行上述代码后,在backprojection变量中得到的输出图像

另一个类似于刚刚学过的阈值方法的技术,可以用来屏蔽图像中包含特定颜色的区域,因此可以将其用来处理图像的某些部分(例如修改颜色),甚至追踪有特定颜色的对象,这将在本章后面学习。但是在此之前,先来学习HSV颜色空间的直方图(使用色调通道),以及如何隔离具有特定颜色的图像部分。让我们再用一个例子来说明这个问题。假设需要找到包含一个特定颜色的图像部分,例如图9-7中的红玫瑰。

图9-7 包含红玫瑰的图像

不能只根据一个阈值滤除红色通道(在RGB图像中),因为它可能太亮或太暗,但它仍然可能是红色的不同阴影。另外,可能想要考虑与红色极其相似的颜色,以确保尽可能精确地得到玫瑰。在这种情况下,以及在需要处理颜色的类似情况下,最好使用色调、饱和度、值(三者简称HSV)颜色空间,其中颜色保存在单个通道(hue或h通道)中。这可以使用OpenCV的一个示例实验来演示。只需在一个新应用程序中尝试运行下列代码片段,它可以是一个控制台应用程序或一个控件,但这并不重要:

注意,这里只更改了我们的三通道图像中的第一个通道,其值从0变化到179。这将产生图9-8的输出。

图9-8 三通道图像中的第一个通道的值在0到179之间变化时产生的输出

正如前面介绍的,其原因是色调单独负责每个像素的颜色。另一方面,饱和度和值通道可以用来得到相同颜色的更亮(使用饱和度通道)和更暗(使用值通道)的变化。注意,在HSV颜色空间中,与RGB不同,色调是0到360之间的值。这是因为色调被建模为一个圆。因此,只要其值溢出,颜色就会回到起点。很明显,如果看一下上一个图像的开始和结束,这两个位置都是红色,所以在0或360周围的色调值一定是红色的。

但是,在OpenCV中,色调通常除以2以满足8位(除非我们使用16或更多位)的像素数据,所以颜色的值在0到180之间变化。如果返回之前的代码示例,可以注意到,在Mat类的列上,色调值从0变到180,从而产生上述彩色频谱输出图像。

现在,让我们使用之前学过的内容创建一个彩色直方图,并用其得到反投影图像,以分离出红玫瑰。为了给这个操作设一个目的,甚至可以用一段简单的代码把它变成蓝色玫瑰,但是我们会在后面的章节中学习到,同样的方法还要与MeanShift算法以及CamShift算法相结合使用,才能追踪具有特定颜色的对象。我们的直方图将基于图像的HSV版本中的颜色分布或色调通道。因此,需要首先使用下列代码将它转换成HSV颜色空间。

Mat hsvImg;
cvtColor(inputImage, hsvImg, CV_BGR2HSV);

然后用与之前例子中完全相同的方法来计算直方图。这次主要的不同(在可视化方面)是:直方图还需要显示每个bin的颜色,因为它是一个颜色分布,否则,输出将会很难解释。为了产生正确的输出,这次将使用HSV到BGR的转换来创建一个包含所有bin的颜色值的缓冲区,然后相应地填入输出柱状图中的每一个竖条。下面是计算之后正确地可视化一个色调通道直方图(即颜色分布图)的源代码:

如前面的代码所示,maxVal是使用minMaxLoc函数通过计算直方图数据得到的。bins只是竖条的个数(或直方图的大小),在本例中不能高于180。就像我们知道的那样,色调只能在0-179之间变化。剩下的几乎是一样的,除了在图中设置每个竖条的填充颜色值。在示例玫瑰图像中,如果使用最大的bin大小(即180)来执行上述代码,将得到图9-9的输出。

在图9-9的直方图中,基本上考虑了直方图中色调精度(8位)下的所有可能颜色,但在这里大幅降低bin的大小来简化该直方图。一个大小为24的bin足够低,可以简化并将相似的颜色组合在一起,同时提供足够的精度。如果将bin的大小变为24,那么将得到图9-10的输出。

通过查看直方图,很明显,直方图的24个竖条中第一个(从左向右)和最后两个竖条是最红的颜色。与之前一样,我们只对除此之外的其他像素进行阈值化。方法如下:

一个好的做法是创建一个用户界面,允许在一个直方图中选择bin,并将其滤除。可以根据目前所学过的知识来完成这项工作,这需要使用QGraphicsScene和QGraphicsRectItem绘制一个柱状图和一个直方图。然后,可以启用项目选择,并确保按下“Delete”按钮时删除竖条,从而将其过滤掉。

 简单的阈值之后,可以使用下列代码计算反投影。请注意,因为我们的直方图是一个单维直方图,所以只有当输入图像也是单通道时,才能使用反投影重新应用它。这就是为什么需要首先从图像中提取色调通道。mixChannels函数可以用来将通道从一个Mat类复制到另一个Mat类。因此,可以使用这个函数将色调通道从HSV图像复制到单通道的Mat类。只需为mixChannels函数提供源和目标Mat类(只要有相同的深度,而不一定是相同通道),以及源和目标图像的数量,还有用来确定源通道索引以及目标通道索引的一对整数(下列代码中的fromto数组):

 使用原样imshow或者Qt控件在输出中显示反投影图像,在将其转换为RGB颜色空间之后,将看到示例玫瑰图像中红色的完美掩模:

现在,如果用正确的数量来改变色调通道中的值,那么可以从红色玫瑰得到一朵蓝色玫瑰。它不仅有相同的静态蓝色,而且在所有相应的像素中都有正确的阴影和亮度值。如果返回到之前在本章中创建的彩色光谱图像输出,你会注意到红色、绿色、蓝色和红色刚好与色调值0、120、240和360一致。当然,如果考虑除数为2(因为360不能刚好放入一个字节,但是180可以),它们实际上是0、60、120和180。这就意味着,如果想要在色调通道中将红色转换为蓝色,必须将其变换120,同样地,也可以将其转换成其他颜色。因此,可以使用与此类似的办法正确地变换颜色,并且只变换被之前的反投影图像突出显示的像素。请注意,我们还需要处理溢出,因为色调的最高可能值应该是179,而不是更大的值: 

通过执行上述代码,将得到图9-12的蓝玫瑰,这只是从一个图像转换回来的RGB图像,红色像素变成了蓝色: 

请用不同大小bin的直方图尝试相同的操作。而且作为一个练习,可以尝试为颜色转换建立一个合适的GUI。甚至可以尝试编写一个程序,在图像中用一个特定颜色(准确地说是颜色直方图)将对象变换成某些其他颜色。在电影和相机编辑程序中广泛使用一个非常类似的技术,以改变图像或连续视频帧中特定区域的颜色(色调)。

9.2.1 直方图比较

使用calcHist函数计算岀的两个直方图(也可以从硬盘加载并填充到Mat类中,或用任意一种方法创建),可以通过使用compareHist方法进行相互比较,以找岀二者之间的距离或差异(散度)。注意,这需要直方图的Mat结构(即列数、深度以及通道数)与之前看到的一致。

compareHist函数接受存储在Mat类中的两个直方图以及比较方法,后者可以是下列常量之一:

❑ HISTCMP_CORREL

❑ HISTCMP_CHISQR

❑ HISTCMP_INTERSECT

❑ HISTCMP_BHATTACHARYYA

❑ HISTCMP_HELLINGER

❑ HISTCMP_CHISQR_ALT

❑ HISTCMP_KL_DIV

注意,compareHist函数的返回值以及如何对其进行解释完全依赖于比较方法,它们的变种很多,所以一定要查看OpenCV文档页面,以获得每一种方法中使用的底层比较方程的详细列表。下面是一个示例代码,可以使用所有现存的方法来计算两张图像(或两个视频帧)之间的差异:

总之,用直方图差异来比较图像是一种常见的做法。在视频帧中,也可以使用类似的技术来检测一个场景的散度或场景中存在的对象。要这样做,应该准备好之前的直方图,然后与每一个传入的视频帧的直方图进行比较。

9.2.2 直方图均衡化

图像的直方图可以用来调整图像的亮度和对比度。OpenCV提供了称为equalizeHist的函数,它可以在内部计算一个给定图像的直方图,然后正则化此直方图,并计算直方图的积分(所有bin之和),最后使用更新的直方图作为一个查找表来更新输入图像的像素,从而得到输入图像中的标准化亮度和对比度。下面是这个函数的用法:

equalizeHist(image, equalizedImg);

有些图像有不合适的亮度水平或者对比度,如果在这些图像上尝试这个函数,那么它们在亮度和对比度方面将自动地调整到视觉上的最佳水平,这个过程就称为直方图均衡化。图9-14的例子显示两个亮度太低或太高的图像,并显示对应像素值分布的直方图。使用equalizeHist函数产生右边的图像,左边的两个图像看起来大致相同。请注意输出图像直方图的变化,这反过来会产生一个更有视觉吸引力的图像。

在大多数数码相机中,也使用了类似的技术,根据整张图像的分布量来调整像素的黑暗和亮度。你也可以使用任意一个普通智能手机来试试。只要把相机指向一个明亮的区域,智能手机上的软件就开始降低亮度水平,反之亦然。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值