Opencv计算机视觉编程攻略(第二版)在第4.5节介绍了反向投影直方图检测图像中的特定内容。本文为学习笔记、示例程序的实现方法以及自己的一些体会。本例的工程共包含3个头文件和一个源文件。3个头文件分别定义了1维直方图Histogram1D和3维直方图ColorHistogram操作方法,以及直方图检测方法ContentFinder的类。源文件为主程序。

本例中使用的图像如下:

1.直方图操作方法

(1)1维灰度直方图操作方法

头文件histogram.h中定义了操作1维灰度直方图的方法类class Histogram1D。histogram.h代码如下:

if !defined HISTOGRAM
#define HISTOGRAM
 
#include <opencv2\core\core.hpp>
#include <opencv2\imgproc\imgproc.hpp>
 
// To create histograms of gray-level images
class Histogram1D { //将算法封装进类
 
  private:
 
    int histSize[1];         // number of bins in histogram直方图中箱子(bin)个数,[1]表示只有1维
    float hranges[2];    // range of values值范围,min和max共2个值,因此定义2维浮点数组
    const float* ranges[1];  // pointer to the different value ranges值范围的指针
    int channels[1];         // channel number to be examined要检查的通道数量
 
  public:
 
    Histogram1D() {
 
        // Prepare default arguments for 1D histogram
        histSize[0]= 256;   // 256 bins,只有1维,因此通过[0]来设置该维的箱子数
        hranges[0]= 0.0;    // from 0 (inclusive)直方图取值范围的min
        hranges[1]= 256.0;  // to 256 (exclusive)直方图取值范围的max
        ranges[0]= hranges; 
        channels[0]= 0;     // we look at channel 0,1维直方图暂时只看0通道
    }
 
    // Sets the channel on which histogram will be calculated.
    // By default it is channel 0.设置通道的方法
    void setChannel(int c) {
 
        channels[0]= c;
    }
 
    // Gets the channel used.获取通道的方法
    int getChannel() {     
 
        return channels[0];
    }
 
    // Sets the range for the pixel values.设置直方图值的范围
    // By default it is [0,256]
    void setRange(float minValue, float maxValue) {
 
        hranges[0]= minValue;
        hranges[1]= maxValue;
    }
 
    // Gets the min pixel value.
    float getMinValue() {
 
        return hranges[0];
    }
 
    // Gets the max pixel value.
    float getMaxValue() {
 
        return hranges[1];
    }
 
    // Sets the number of bins in histogram.设置直方图箱子数(统计多少个灰度级)
    // By default it is 256.构造函数中默认设置为256
    void setNBins(int nbins) {
 
        histSize[0]= nbins;
    }
 
    // Gets the number of bins in histogram.
    int getNBins() {
 
        return histSize[0];
    }
 
    // Computes the 1D histogram.自编函数计算1维直方图
    cv::Mat getHistogram(const cv::Mat &image) {//输入图像image
 
        cv::Mat hist;
 
        // Compute histogram
        cv::calcHist(&image, 
            1,        // histogram of 1 image only
            channels,    // the channel used
            cv::Mat(),    // no mask is used,不使用掩码
            hist,        // the resulting histogram
            1,        // it is a 1D histogram
            histSize,    // number of bins
            ranges        // pixel value range
        );
 
        return hist;
    }
 
    // Computes the 1D histogram and returns an image of it.直方图图像
    cv::Mat getHistogramImage(const cv::Mat &image, int zoom = 1){
 
        // Compute histogram first
        cv::Mat hist = getHistogram(image);
 
        // Creates image
        return Histogram1D::getImageOfHistogram(hist, zoom);
    }
 
    // Stretches the source image using min number of count in bins.
    cv::Mat stretch(const cv::Mat &image, int minValue = 0) {
 
        // Compute histogram first
        cv::Mat hist = getHistogram(image);
 
        // find left extremity of the histogram
        int imin = 0;
        for (; imin < histSize[0]; imin++) {
            // ignore bins with less than minValue entries
            if (hist.at<float>(imin) > minValue)
                break;
        }
 
        // find right extremity of the histogram
        int imax = histSize[0] - 1;
        for (; imax >= 0; imax--) {
 
            // ignore bins with less than minValue entries
            if (hist.at<float>(imax) > minValue)
                break;
        }
 
        // Create lookup table
        int dims[1] = { 256 };
        cv::Mat lookup(1, dims, CV_8U);
 
        for (int i = 0; i<256; i++) {
 
            if (i < imin) lookup.at<uchar>(i) = 0;
            else if (i > imax) lookup.at<uchar>(i) = 255;
            else lookup.at<uchar>(i) = cvRound(255.0*(i - imin) / (imax - imin));
        }
 
        // Apply lookup table
        cv::Mat result;
        result = applyLookUp(image, lookup);
 
        return result;
    }
 
    // Stretches the source image using percentile.
    cv::Mat stretch(const cv::Mat &image, float percentile) {
 
        // number of pixels in percentile
        float number= image.total()*percentile;
 
        // Compute histogram first
        cv::Mat hist = getHistogram(image);
 
        // find left extremity of the histogram
        int imin = 0;
        for (float count=0.0; imin < histSize[0]; imin++) {
            // number of pixel at imin and below must be > number
            if ((count+=hist.at<float>(imin)) >= number)
                break;
        }
 
        // find right extremity of the histogram
        int imax = histSize[0] - 1;
        for (float count=0.0; imax >= 0; imax--) {
            // number of pixel at imax and below must be > number
            if ((count += hist.at<float>(imax)) >= number)
                break;
        }
 
        // Create lookup table
        int dims[1] = { 256 };
        cv::Mat lookup(1, dims, CV_8U);
 
        for (int i = 0; i<256; i++) {
 
            if (i < imin) lookup.at<uchar>(i) = 0;
            else if (i > imax) lookup.at<uchar>(i) = 255;
            else lookup.at<uchar>(i) = cvRound(255.0*(i - imin) / (imax - imin));
        }
 
        // Apply lookup table
        cv::Mat result;
        result = applyLookUp(image, lookup);
 
        return result;
    }
 
    // static methods
 
    // Create an image representing a histogram
    static cv::Mat getImageOfHistogram(const cv::Mat &hist, int zoom) {
 
        // Get min and max bin values
        double maxVal = 0;
        double minVal = 0;
        cv::minMaxLoc(hist, &minVal, &maxVal, 0, 0);
 
        // get histogram size
        int histSize = hist.rows;
 
        // Square image on which to display histogram
        cv::Mat histImg(histSize*zoom, histSize*zoom, CV_8U, cv::Scalar(255));
 
        // set highest point at 90% of nbins (i.e. image height)
        int hpt = static_cast<int>(0.9*histSize);
 
        // Draw vertical line for each bin
        for (int h = 0; h < histSize; h++) {
 
            float binVal = hist.at<float>(h);
            if (binVal>0) {
                int intensity = static_cast<int>(binVal*hpt / maxVal);
                cv::line(histImg, cv::Point(h*zoom, histSize*zoom),
                    cv::Point(h*zoom, (histSize - intensity)*zoom), cv::Scalar(0), zoom);
            }
        }
 
        return histImg;
    }
 
    // Equalizes the source image.
    static cv::Mat equalize(const cv::Mat &image) {
 
        cv::Mat result;
        cv::equalizeHist(image,result);
 
        return result;
    }
 
    // Applies a lookup table transforming an input image into a 1-channel image
    static cv::Mat applyLookUp(const cv::Mat& image, // input image
      const cv::Mat& lookup) { // 1x256 uchar matrix
 
      // the output image
      cv::Mat result;
 
      // apply lookup table
      cv::LUT(image,lookup,result);
 
      return result;
    }
};
 
#endif
(2)3维彩色直方图操作方法
头文件ColorHistogram.h中定义了操作彩色直方图的方法类class ColorHistogram。colorhistogram.h代码如下:

#if !defined COLHISTOGRAM
#define COLHISTOGRAM
 
#include <opencv2\core\core.hpp>
#include <opencv2\imgproc\imgproc.hpp>
 
class ColorHistogram {
 
  private:
 
    int histSize[3];         // size of each dimension
    float hranges[2];    // range of values
    const float* ranges[3];  // array of ranges for each dimension
    int channels[3];         // channel to be considered
 
  public:
 
    ColorHistogram() {
 
        // Prepare default arguments for a color histogram
        // each dimension has equal size and range
        histSize[0]= histSize[1]= histSize[2]= 256;
        hranges[0]= 0.0;    // BRG range from 0 to 256
        hranges[1]= 256.0;
        ranges[0]= hranges; // in this class,  
        ranges[1]= hranges; // all channels have the same range
        ranges[2]= hranges; 
        channels[0]= 0;        // the three channels 
        channels[1]= 1; 
        channels[2]= 2; 
    }
 
    // set histogram size for each dimension
    void setSize(int size) {
 
        // each dimension has equal size 
        histSize[0]= histSize[1]= histSize[2]= size;
    }
 
    // Computes the histogram.
    cv::Mat getHistogram(const cv::Mat &image) {
 
        cv::Mat hist;
 
        // BGR color histogram
        hranges[0]= 0.0;    // BRG range
        hranges[1]= 256.0;
        channels[0]= 0;        // the three channels 
        channels[1]= 1; 
        channels[2]= 2; 
 
        // Compute histogram
        cv::calcHist(&image, 
            1,        // histogram of 1 image only
            channels,    // the channel used
            cv::Mat(),    // no mask is used
            hist,        // the resulting histogram
            3,        // it is a 3D histogram
            histSize,    // number of bins
            ranges        // pixel value range
        );
        return hist;
    }
 
    // Computes the histogram.
    cv::SparseMat getSparseHistogram(const cv::Mat &image) {
 
        cv::SparseMat hist(3,        // number of dimensions
                       histSize, // size of each dimension
                       CV_32F);
 
        // BGR color histogram
        hranges[0]= 0.0;    // BRG range
        hranges[1]= 256.0;
        channels[0]= 0;        // the three channels 
        channels[1]= 1; 
        channels[2]= 2; 
 
        // Compute histogram
        cv::calcHist(&image, 
            1,        // histogram of 1 image only
            channels,    // the channel used
            cv::Mat(),    // no mask is used
            hist,        // the resulting histogram
            3,        // it is a 3D histogram
            histSize,    // number of bins
            ranges        // pixel value range
        );
        return hist;
    }
 
    // Computes the 1D Hue histogram with a mask.
    // BGR source image is converted to HSV
    // Pixels with low saturation are ignored
    cv::Mat getHueHistogram(const cv::Mat &image, 
                             int minSaturation=0) {
        cv::Mat hist;
 
        // Convert to HSV colour space
        cv::Mat hsv;
        cv::cvtColor(image, hsv, CV_BGR2HSV);
 
        // Mask to be used (or not)
        cv::Mat mask;
 
        if (minSaturation>0) {
            // Spliting the 3 channels into 3 images
            std::vector<cv::Mat> v;
            cv::split(hsv,v);
 
            // Mask out the low saturated pixels
            cv::threshold(v[1],mask,minSaturation,255,
                                 cv::THRESH_BINARY);
        }
 
        // Prepare arguments for a 1D hue histogram
        hranges[0]= 0.0;    // range is from 0 to 180
        hranges[1]= 180.0;
        channels[0]= 0;    // the hue channel 
 
        // Compute histogram
        cv::calcHist(&hsv, 
            1,        // histogram of 1 image only
            channels,    // the channel used
            mask,        // binary mask
            hist,        // the resulting histogram
            1,        // it is a 1D histogram
            histSize,    // number of bins
            ranges        // pixel value range
        );
        return hist;
    }
 
    // Computes the 2D ab histogram.
    // BGR source image is converted to Lab
    cv::Mat getabHistogram(const cv::Mat &image) {
 
        cv::Mat hist;
 
        // Convert to Lab color space
        cv::Mat lab;
        cv::cvtColor(image, lab, CV_BGR2Lab);
 
        // Prepare arguments for a 2D color histogram
        hranges[0]= 0;
        hranges[1]= 256.0;
        channels[0]= 1; // the two channels used are ab 
        channels[1]= 2; 
 
        // Compute histogram
        cv::calcHist(&lab, 
            1,            // histogram of 1 image only
            channels,        // the channel used
            cv::Mat(),        // no mask is used
            hist,            // the resulting histogram
            2,            // it is a 2D histogram
            histSize,        // number of bins
            ranges            // pixel value range
        );
        return hist;
    }
};
#endif

2.计算反向投影直方图的方法
计算反向投影直方图的过程:

从ROI区域的归一化的直方图中读取概率值
把输入图像(代检测图)中的每一个像素替换成与之对应的归一化中方图中的概率值
把替换成概率值(0~1)的像素值再从0~1映射到0~255
灰度值越大的像素越有可能是ROI的成分。
本例中,将反向投影直方图封装成一个类ContentFinder,contentfinder.h代码如下:

#if !defined OFINDER
#define OFINDER
 
#include <opencv2\core\core.hpp>
#include <opencv2\imgproc\imgproc.hpp>
 
class ContentFinder {
 
  private:
 
    // histogram parameters
    float hranges[2];
    const float* ranges[3];
    int channels[3];
 
    float threshold;           // decision threshold
    cv::Mat histogram;         // histogram can be sparse 输入直方图
    cv::SparseMat shistogram;  // or not
    bool isSparse;
 
  public:
 
    ContentFinder() : threshold(0.1f), isSparse(false) {
 
        // in this class,
        // all channels have the same range
        ranges[0]= hranges;  
        ranges[1]= hranges; 
        ranges[2]= hranges; 
    }
   
    // Sets the threshold on histogram values [0,1]
    void setThreshold(float t) {
 
        threshold= t;
    }
 
    // Gets the threshold
    float getThreshold() {
 
        return threshold;
    }
 
    // Sets the reference histogram
    void setHistogram(const cv::Mat& h) {
 
        isSparse= false;
        cv::normalize(h,histogram,1.0);
    }
 
    // Sets the reference histogram
    void setHistogram(const cv::SparseMat& h) {
 
        isSparse= true;
        cv::normalize(h,shistogram,1.0,cv::NORM_L2);
    }
 
    // All channels used, with range [0,256]
    cv::Mat find(const cv::Mat& image) {
 
        cv::Mat result;
 
        hranges[0]= 0.0;    // default range [0,256]
        hranges[1]= 256.0;
        channels[0]= 0;        // the three channels 
        channels[1]= 1; 
        channels[2]= 2; 
 
        return find(image, hranges[0], hranges[1], channels);
    }
 
    // Finds the pixels belonging to the histogram
    cv::Mat find(const cv::Mat& image, float minValue, float maxValue, int *channels) {
 
        cv::Mat result;
 
        hranges[0]= minValue;
        hranges[1]= maxValue;
 
        if (isSparse) { // call the right function based on histogram type
 
           for (int i=0; i<shistogram.dims(); i++)
              this->channels[i]= channels[i];
 
           cv::calcBackProject(&image,
                      1,            // we only use one image at a time
                      channels,     // vector specifying what histogram dimensions belong to what image channels
                      shistogram,   // the histogram we are using
                      result,       // the resulting back projection image
                      ranges,       // the range of values, for each dimension
                      255.0         // the scaling factor is chosen such that a histogram value of 1 maps to 255
           );
 
        } else {
 
           for (int i=0; i<histogram.dims; i++)
              this->channels[i]= channels[i];
//某对象的this指针,指向被调用函数所在的对象,此处对象为ContentFinder类
           //this->channels[i]即ContentFinder类的私有成员channels[3]
           //对ContentFinder类各成员的访问均通过this进行
           cv::calcBackProject(&image,
                      1,            // we only use one image at a time
                      channels,     // 向量表示哪个直方图维度属于哪个图像通道
                      histogram,    // 用到的直方图
                      result,       // 反向投影的图像
                      ranges,       // 每个维度值的范围
                      255.0         // 选用的换算系数
           );
        }
        // Threshold back projection to obtain a binary image阈值分割反向投影图像得到二值图
        if (threshold>0.0)// 设置的阈值>0时,才进行阈值分割
            cv::threshold(result, result, 255.0*threshold, 255.0, cv::THRESH_BINARY);
        return result;
    }
};
#endif

3.提取感兴趣区域ROI的直方图
以上定义了直方图操作方法和反向投影直方图检测方法。下面将在main函数中调用上述方法来做反向投影直方图检测的实验。

要查找图像中特定的内容(例如在下图中检测出天空中的云彩),首先要选择一个包括所需样本的兴趣区域,即画一个矩形框选出ROI。

(1)读取图像
补充:imread(const string& filename, int flags=1)函数读取图像色彩空间参数flags:

enum
{
    // 8bit, color or not
    IMREAD_UNCHANGED  =-1,
    // 8bit, gray
    IMREAD_GRAYSCALE  =0,
    // color
    IMREAD_COLOR      =1,
    // any depth,
    IMREAD_ANYDEPTH   =2,
    // any color
    IMREAD_ANYCOLOR   =4
};
在main函数中运行程序主干。以灰度图的形式读取图像。

// Read input image
    cv::Mat image= cv::imread("f:\\images\\waves.jpg",0);// /灰度图方式读取图像
    if (!image.data)
        return 0; 
(2)设置ROI
设置读取图像中的云层区域为ROI。

    // define image ROI
    cv::Mat imageROI;
    imageROI= image(cv::Rect(406,146,30,24)); // Cloud  region

                                                                          以灰度图方式读取原图与设置的ROI云彩区域
(3)获取ROI的直方图
调用Histogram1D类中的.getHistogram( )方法获取ROI的1维直方图

// Find histogram of reference
    Histogram1D h;
    cv::Mat hist= h.getHistogram(imageROI);
    cv::namedWindow("Reference Hist");
    cv::imshow("Reference Hist",h.getHistogramImage(imageROI));
    waitKey(0);

                                                                                                                      ROI区域直方图

(5)归一化ROI区域直方图
通过归一化直方图得到一个函数,该函数为特定强度的像素属于这个区域的概率:

cv::normalize(histogram,histogram,1.0);

实际上,归一化在反向计算投影类ContentFinder中进行,此处单独说明一下。归一化直方图后,直方图每个bin位置的值之和为1。因此,可以将直方图在灰度级n位置的值视为灰度级为n的像素属于此ROI区域的概率。例如,假设下图为某一区域的归一化的直方图,当进行直方图反向投影时,如果图像中有一个像素灰度值为31,那么这个像素是该感兴趣区域的概率为0.1;如果图像中有一个像素灰度值为33,那么这个像素是该感兴趣区域的概率为0.5;如果图像中有像素灰度值为31~34以外的值,那么这些像素是该感兴趣区域的概率为0,因为该ROI区域的归一化直方图只在31~34位置有不为0的值。

                                                                                                                    归一化直方图示例
4.调用反向投影直方图检测灰度图像
前文定义了1维直方图的操作方法Histogram1D类,定义了计算反向投影直方图的方法ContentFinder类,在main函数中读取了图像、设置好ROI,获取了ROI直方图后,就可以调用ContentFinder类来计算反向投影了。

注意为了能将处理过程的中间结果显示出来,设置阈值为-0.1<0,用ContentFinder类在find()方法中当设置的阈值<0时不进行阈值分割,因此输出的结果为灰度图像,便于显示观察。下面的代码先使用find()获取反向投影结果result1,然后使用result1.convertTo(tmp,CV_8U,-1.0,255.0);将result1转换为反向投影图像tmp。

// Create the content finder
    ContentFinder finder;
 
    // set histogram to be back-projected
    finder.setHistogram(hist);//将ROI直方图hist传入反向投影计算类ContentFinder finder
    finder.setThreshold(-1.0f);// /设置阈值为-0.1,float型。阈值<0,不使用find方法中的阈值分割,输出result为灰度图 
 
    // Get back-projection
    cv::Mat result1;
    result1= finder.find(image);
 
    // Create negative image and display result
    cv::Mat tmp;
    result1.convertTo(tmp,CV_8U,-1.0,255.0);
    cv::namedWindow("Backprojection result");
    cv::imshow("Backprojection result",tmp);

为了搞清楚convertTo()的操作,我们用如下代码扫描图像像素的方法读取result1中一个15×15矩形区域的值在屏幕上输出:

if(1)//for debug
    {
        cv::namedWindow("Backprojection result1");
        cv::imshow("Backprojection result1",result1);
        cout<<"pixel vaule of result1(421,141)~(436,156),15x15 region"<<endl<<endl;
        for ( int j = 140;j<155;j++)//从第j=101行开始
        {
            uchar* data= result1.ptr<uchar>(j); 
            for (int i=420;i<435;i++)
            {
                cout<<(int)data[i]<<" ";//打印输出第j行的所有数据
            }
            cout<<endl;//输出1行后换行
        }
    }

result1图像该区域的像素值输出结果如下图所示

                                                                                   图像result1从点(421,141)到点(436,156)的矩形区域的像素值

使用使用上述方法输出tmp图像同样位置的15×15矩形区域的像素值如下图所示。比较转换前后的图像,发现对源图中的像素逐点I(x,y)做了反向操作I(x,y)=255-I(x,y).

                                                                                    图像tmp从点(421,141)到点(436,156)的矩形区域的像素值
显示出反向前后的全图如下2图所示。

                                                                                                          反向前的result1图像

                                                                                                         反向后的tmp图像
刚才我们观察了反向投影的处理过程中的图像,对上述图像进行阈值分割即可得到检测结果,如下图所示。

阈值分割后的反向投影检测结果

5.调用反向投影直方图检测彩色图像
从上图检测结果中可以看到,本来是检测云彩区域,但得到的检测结果还包括了底部许多沙滩区域和部分海水区域。一个改进的方法就是使用彩色图像的直方图来进行反向投影检测。使用彩色信息检测需要用到前面提到的3维彩色直方图操作ColorHistogram类。在main函数中通过如下代码来进行彩色图像的反向投影直方图检测。

// Load color image
    ColorHistogram hc; //声明一个彩色直方图类hc用于下面获取ROI直方图的操作
    cv::Mat color= cv::imread("f:\\images\\waves.jpg");//这里要使用本机的图像路径
 
    // extract region of interest
    imageROI= color(cv::Rect(406,154,30,24)); //设置检测云彩区域
 
    // Get 3D colour histogram (8 bins per channel)
    hc.setSize(8); // 8x8x8 降低bin的数量使用8,16,64分别计算,结果如下图所示。
    cv::Mat shist= hc.getHistogram(imageROI);
 
    // set histogram to be back-projected
    finder.setHistogram(shist);
    finder.setThreshold(0.05f);
 
    // Get back-projection of color histogram
    result1= finder.find(color);
    cv::namedWindow("Color Detection Result");
    cv::imshow("Color Detection Result",result1);

下图是输入的彩色图像与ROI检测区域

                                                                                                           彩色图像与选择的ROI区域

在程序中,可以通过hc.setSize()来设置直方图中每个通道bin个数,本实验中,选择了8/16/64三个参数,检测结果如下图所示。从检测结果可以看出来,使用彩色直方图后,检测效果大大优于灰度直方图,并且连水中云的倒影都能检测出来。而设置的bin个数越多,检测精度越高。并且本例中使用16个bin得到的检测结果优于8和64个bin,因为精度过高将排除掉部分与ROI区域类似的云彩区域,所以bin不是越多越好,合适的bin的个数需要根据实际情况而定。

                                                                                彩色图检测结果(8个bin,出现了少部分沙滩)

                                                                                       彩色图检测结果(16个bin,基本检测出了全部的云彩)

                                                                                          彩色图检测结果(64个bin,有部分云彩漏检)

计算稀疏直方图可以减少内存使用量。可以使用cv::SparseMat重做本实验。

如果将RGB色彩空间转换为Lab色彩空间或HSV色彩空间。如果寻找色彩鲜艳的物体,使用HSV色彩空间的色调通道可能会更有效。在其他情况下,最好使用感知上均匀的Lab色彩空间的色度组件。色彩空间转换代码如下:

// Convert to Lab space
    cv::Mat lab;
    cv::cvtColor(color, lab, CV_BGR2Lab);
 
    // Convert to HSV space
    cv::Mat hsv;
    cv::cvtColor(color, hsv, CV_BGR2HSV);
检测结果如下图所示。

                                                                                         LAB彩色模式检测结果(8个bin)

                                                                                         HSV彩色模式检测结果(8个bin)

————————————————
版权声明:本文为CSDN博主「iracer」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/iracer/article/details/48845677