OpenCV学习笔记-反向投影直方图检测特定图像内容

反向投影直方图检测特定图像内容

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

#include "histogram.h"               //定义1维灰度直方图的获取与绘制方法类:class Histogram1D
#include "colorhistogram.h"          //定义3维彩色直方图的获取与绘制方法类:class ColorHistogram 
#include "contentFinder.h"           //定义反向投影直方图检测方法的类:class 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

OpenCV学习笔记-反向投影直方图检测特定图像内容_第1张图片
以灰度图方式读取原图与设置的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);

OpenCV学习笔记-反向投影直方图检测特定图像内容_第2张图片

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的值。

OpenCV学习笔记-反向投影直方图检测特定图像内容_第3张图片

归一化直方图示例

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图像该区域的像素值输出结果如下图所示

OpenCV学习笔记-反向投影直方图检测特定图像内容_第4张图片

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

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

OpenCV学习笔记-反向投影直方图检测特定图像内容_第5张图片

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

显示出反向前后的全图如下2图所示。


反向前的result1图像

反向后的tmp图像
刚才我们观察了反向投影的处理过程中的图像,对上述图像进行阈值分割即可得到检测结果,如下图所示。
OpenCV学习笔记-反向投影直方图检测特定图像内容_第6张图片

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

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的个数需要根据实际情况而定。

OpenCV学习笔记-反向投影直方图检测特定图像内容_第7张图片

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


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

OpenCV学习笔记-反向投影直方图检测特定图像内容_第8张图片

彩色图检测结果(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)

OpenCV学习笔记-反向投影直方图检测特定图像内容_第9张图片

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

你可能感兴趣的:(OpenCV学习笔记-反向投影直方图检测特定图像内容)