前言
直方圖是計算機視覺中一個很重要的工具,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下載。
參考資料:
http://blog.csdn.net/caiqi1123/article/details/8146858