前言
直方图是计算机视觉中一个很重要的工具,OpenCV里面提供了不少有关直方图处理的函数。其中最基本的是计算直方图的函数calcHist()。有关直方图在前面的博文中也有所介绍:基础学习笔记之opencv(4):直方图使用学习。不过目前由于本人课题研究上需要计算多张图片的一维直方图特性,且每张图片对应有自己不同的掩膜矩阵,开始以为OpenCV中提供的calcHist()函数能够实现这个功能,因为其中一个重载函数中有一个参数为InputArrayOfArrays,咋一看会觉得这不就是可以处理多张图片的吗?后面仔细研究后发现其实它实现的功能和我的需求是不同的,因此需要自己单独写函数来完成这一功能。
开发环境:OpenCV2.4.3+Qtcreator2.5.1
实验基础
首先来看看OpenCV中计算直方图的3个重载函数calcHist()中最重要的一个,如下所示:
CV_EXPORTS void calcHist( const Mat* images, int nimages, const int* channels, InputArray mask, OutputArray hist, int dims, const int* histSize, const float** ranges, bool uniform=true, bool accumulate=false );
参数1表示需要用来计算直方图的源图像序列,因此可以允许有多张大小一样,数据类型相同的图像被用来统计其直方图特征。
参数2表示的就是使用多少张图像序列中的图像用于计算直方图。
参数3的出现主要是考虑到输入的每一张图像有可能是多通道的,比如说RGB图就是3通道的,那么从统计意义上来讲,一张RGB图其实就是3张单通道的图像,而计算直方图时其本质也是针对单张图像进行的。这里虽然我们输入的图像序列images中有很多图片,但是并不是每一张图片的每一个通道都会被用来计算。所以参数3的功能是指定哪些通道的图像被用来计算(后面的解释都假设图像序列中图像是3通道的,那么有的图像可能有多个通道都被用来计算,有的图像可能连一个通道都没有被采用),这时参数3里面保存的是通道的序号,那么图像序列images中的第一张图片的通道序号(假设图像时3通道的)为0,1,2;images中第二张图片的图像序列接着上一次的,为3,4,5,;依次类推即可。
参数4是mask掩膜操作,即指定每张图片的哪些像素被用于计算直方图,这个掩膜矩阵不能够针对特定图像设定特定的掩膜,因此在这里是一视同仁对待的。
参数5是保存计算的直方图结果的矩阵,有可能是多维矩阵。
参数6是需要计算的直方图的维数。
参数7是所需计算直方图的每一维的大小,即每一维bin的个数。
参数8是所需计算直方图的每一维的范围,如果参数9的uniform为true,这此时的参数8的大小为2,里面的元素值表示的是每一维的上下限这两个数字;如果参数9的uniform为false,则此时的参数8的大小为bin的个数,即参数7的值,参数8里面的元素值需要人为的指定,即每一维的坐标值不一定是均匀的,需要人为指定。
参数9如果为true的话,则说明所需计算的直方图的每一维按照它的范围和尺寸大小均匀取值;如果为false的话,说明直方图的每一维不是均匀分布取值的,参考参数8的解释。
参数10如果为false,则表示直方图输出矩阵hist在使用该函数的时候被清0了,如果为true,则表示hist在使用calcHist()函数时没有被清0,计算的结果会累加到前一次保存的值中。
使用该函数的时候需要注意,如果在默认参数的情况下uniform = true,则此时的ranges大小必须是histSize大小的两倍,并且channels的大小必须等于dims维数。
从上面可以理解,channels里的值已经指定了使用哪些单通道的图像来计算目标直方图,因此一旦channels的尺寸确定,则对应的直方图的维数也就确定了,所以我们不能使用多张图像来计算一个一维的直方图。
另一个重载函数的形式如下:
CV_EXPORTS_W void calcHist( InputArrayOfArrays images, const vector<int>& channels, InputArray mask, OutputArray hist, const vector<int>& histSize, const vector<float>& ranges, bool accumulate=false );
虽然从名字上看第一个参数是一个图像序列,但是我们并不能通过该函数来计算这些图像序列的一个一维的直方图。这个函数中并不像前面的函数那样需要指定一个参数表明有多少图像参与计算,因为在images中已经体现有了。另外这个函数也不需要像上面的函数一样指定直方图的维数,因为使用这个重载函数就表示默认为直方图的维数和channels的尺寸一样。最后本重载函数中的uniform在函数内部设定了为true,表面直方图中每一维都必须是均匀分布的。
总之,上面2个函数是计算多个图像的直方图,直方图可以是多维的,该维数等于最终用于计算直方图的单通道的图像的个数。
自己设计的计算直方图的函数为:
void CalcHist(vector<Mat> image_array, vector<Mat> mask_array, const int *histsize, const float **ranges);
函数实现的内部其实也是调用了OpenCV的calcHist()函数。
参数1是需要被用来计算一维直方图的图像序列。
参数2是对应参数1中图像序列的掩膜序列。
参数3是对应每张图片被计算时的直方图尺寸;
参数4是对应每张图被计算时的直方图横坐标范围。
该函数的功能是计算图像序列image_array的一维直方图,image_array中的像素是否参与计算由掩膜序列mask_array决定。计算得到的一维直方图的横坐标尺寸大小保存在histsize中,范围保存在ranges中(其实这里是没有必要用指针的,因为在该函数的内部是一张一张来计算图像序列中的图像的,且所有图像的直方图计算共享一个最终的直方图,所有histsize只需一个值即可)。
OpenCV知识点总结:
在使用OpenCV内部的判断条件时应该使用CV_Assert()函数,而不是CV_ASSERT()。
通过实验测试发现,虽然经过calcHist()函数计算过后的直方图保存在hist中,这里hist是一个Mat类型,并且如果计算的是一维的直方图的话,则hist是一个列向量。
实验结果
如果我们不适用掩膜矩阵,即每个像素点都参与工作,如果是下面的图的话:
则其直方图分布图如下:
如果掩膜矩阵是图像最中间的一部分,即以图像中心点为中心的一个长和框都是原图像的一半的矩形,也就是掩膜的有效像素为原图像的一半,此时如果需要计算的直方图图像如下:
则它的直方图显示如下:
从上面的4张图可以看出,所计算的两张图片的大体背景类似,且图像中都有一个黑色的区域,只不过在第一次试验过程中,黑色的区域位于图像中的右下角,而此时的实验是计算整幅图像的直方图,所以直方图的结果中黑色占了一大块,但与此同时由于背景中还有其它的像素,所以也站了不少直方图的比例。而在第二次实验过程中,只计算图像的中间区域的直方图,此时由于黑色区域也向中间移动了,所以此时的直方图显示结果是绝大多数的比例是黑色像素。
以上简单的解释表面自己设计的直方图计算应该是可以工作的(虽然该实验并不严格能解释)。
实验主要部分代码即注释(附录有工程code下载地址):
chistogram.h:
#ifndef CHISTOGRAM_H #define CHISTOGRAM_H #include <opencv2/core/core.hpp> #include <vector> using namespace cv; class CHistogram { public: CHistogram(); ~CHistogram(); void Init(); void CalcHist(vector<Mat> image_array, vector<Mat> mask_array, const int *histsize, const float **ranges); Mat getHistogram1DImage(const Mat &hist, Size hist_image_size, Scalar color); Mat cr_hist_trained, cb_hist_trained; private: }; #endif //
chistogram.cpp:
#include "chistogram.h" #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <vector> #define NUMBER_TRAIN_HAND_SKIN_MODEL 20 using namespace cv; CHistogram::CHistogram() { } CHistogram::~CHistogram() { } /*该函数完成的功能是计算多张图像的一维直方图,其中每张图片都有对应的掩膜矩阵*/ void CHistogram::CalcHist(vector<Mat> image_array, vector<Mat> mask_array, const int *histsize, const float **ranges) { int image_array_size = image_array.size(); int channels[] = {0}; for(int i = 0; i < image_array_size; i++) { //最后一个参数给true,是因为我们需要累积hist中的部分 calcHist(&image_array[i], 1, channels, mask_array[i], cr_hist_trained, 1, histsize, ranges, true, true); } //对计算的直方图进行归一化处理 long sum_hist_value = 0; for(int i = 0; i < cr_hist_trained.rows; i++) for(int j = 0; j < cr_hist_trained.cols; j++) sum_hist_value += cr_hist_trained.at<float>(i, j); cr_hist_trained /= sum_hist_value; } /*该函数的作用是显示一张一维直方图*/ Mat CHistogram::getHistogram1DImage(const Mat &hist, Size hist_image_size, Scalar color) { Mat hist_image(hist_image_size, CV_8UC3); //定义需要显示直方图的图像 int padding = 10; int display_width = hist_image_size.width - 2*padding; int display_height = hist_image_size.height - 2*padding; double max_hist_value = 0.0; minMaxLoc(hist,NULL, &max_hist_value); //找到直方图中最大的值 float height_percent = display_height/max_hist_value ; //计算高度显示的合适比例 int bin_width = display_width/(hist.rows); //因为hist是一个列向量 /*画直方图的条形图*/ for(int k = 0; k < hist.rows; k++) { Point up(padding+k*bin_width, hist_image_size.height-padding-height_percent*hist.at<float>(k)); Point bottom(padding+k*bin_width, hist_image_size.height-padding); line(hist_image, bottom, up, color, bin_width); } return hist_image; }
main.cpp:
#include <opencv2/core/core.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/highgui/highgui.hpp> #include <iostream> #include <vector> #include "chistogram.h" #define TRAIN_HAND_COLOR_FRAMES 20 using namespace std; using namespace cv; int histsize[] = {256}; float range[] = {0, 256}; const float *ranges[] = {range}; long test_number = 0; CHistogram histogram; int main() { vector<Mat> image_array, mask_array; Mat image, hist_display_image, mask, test_image; int number = 0; VideoCapture cam(0); if(!cam.isOpened()) return 0; cam >> image; CV_Assert(!image.empty()); mask.create(image.size(), CV_8UC1); mask.setTo(0); Rect rect(image.cols/4, image.rows/4, image.cols/2, image.rows/2); //计算图像中间直方图 // Rect rect(0, 0, image.rows, image.cols); //计算全部直方图 rectangle(mask, rect, 255, -1); while(true) { cam >> image; CV_Assert(!image.empty()); number++; if(number <= TRAIN_HAND_COLOR_FRAMES) { image_array.push_back(image); //获取图像矩阵序列 mask_array.push_back(mask); //获取掩膜矩阵序列 } if(TRAIN_HAND_COLOR_FRAMES == number) { histogram.CalcHist(image_array, mask_array, histsize, ranges); //计算一维直方图 } if(number > TRAIN_HAND_COLOR_FRAMES) number = TRAIN_HAND_COLOR_FRAMES+1; /*显示归一化后的直方图*/ if(number == TRAIN_HAND_COLOR_FRAMES) { //获得画有直方图的图像 hist_display_image = histogram.getHistogram1DImage(histogram.cr_hist_trained, Size(660, 480), Scalar(255,0,0)); imshow("hist", hist_display_image); test_image = image.clone(); imshow("test", test_image); } imshow("image", image); waitKey(30); } return 0; }
实验总结:直方图是个有用的工具,在编程中应该熟练使用OpenCV中有关直方图操作的函数。
附录:实验工程code下载。
参考资料: