引言
在影像處理中,對於直方圖這個概念,肯定不會陌生。但是其原理真的可以信手拈來嗎?
本文篇幅有點長,在此列個目錄,大家可以跳著看:
- 分析影像直方圖的概念,以及opencv函式calcHist()對於RGB影像的直方圖的繪製
- 在其基礎上自已定義函式實現對灰度影像直方圖的簡單繪製
- 直方圖均衡化
- 直方圖的反向投影
影像直方圖分析以及opencv函式實現
(一)直方圖的介紹
直方圖到底可以幹什麼呢?我覺得最明顯的作用就是有利於很直觀的對影像進行分析了,直方圖就像我們常用的統計圖,直方圖可以用來描述各種不同的事情,如物體的色彩分佈、物體邊緣梯度模板,以及表示目標位置的概率分佈。
例如:我們統計了一個有11個學生的班級的身高和體重情況,身高為160cm的有5人,170cm的有4人,180cm的有2人。然後看體重,體重160斤的有3人,170斤的有5人,180斤的有3人。
用直方圖統計就是這樣:
在opencv中,對於影像的直方圖來說。對應上圖資料,也有三個引數:
- dims:需要統計的特徵的數目。如上面例子裡有身高和體重兩個特徵。
- bins:每個特徵空間子區段數目。如身高有160,170,180所以子區段數目為3。
- range:每個特徵空間的取值範圍。例如:身高的取值範圍就是[160,180]
直方圖的意義:
1. 直方圖是影像中畫素強度分佈的圖形表達方式。
2. 直方圖統計了每一個強度值所具有的畫素個數。
任一幅影像,都能唯一地算出一幅與它對應的直方圖。但不同的影像,可能有相同的直方圖。即影像與直方圖之間是多對一的對映關係。
(二)直方圖API
直方圖是對資料的統計,並把統計值顯示到事先設定好的bin(如上表,設定160,170,180)中,bin中的數值是從資料中計算出的特徵的統計量。總之,直方圖獲取的是資料分佈的統計圖,通常直方圖的維數要低於原始資料。
在OpenCV中封裝了直方圖的計算函式calcHist
,為了更為通用,該函式的引數有些複雜,其宣告如下:
calcHist( const Mat* images, //輸入影像的陣列(CV_8U,CV_16U,CV_32F) int nimages, //輸入陣列個數 const int* channels, //通道索引,可以傳一個陣列 {0, 1} 表示計算第0通道與第1通道的直方圖,此陣列長度要與histsize,ranges 陣列長度一致 InputArray mask; //Mat(), //不使用醃膜 OutputArray hist, //輸出的目標直方圖,一個二維陣列 int dims, //需要計算的直方圖的維數 例如:灰度,R,G,B,H,S,V等資料 congst int* histSize, // 在每一維上直方圖的個數。(簡單把直方圖看作一個一個的豎條的話,就是每一維上豎條的個數。) const float** ranges, //每一維陣列的取值範圍陣列 bool uniform=true, bool accumulate = false );
opencv實現:
Mat src = imread("D:/opencv練習圖片/src1.jpg"); imshow("原圖片", src); // //步驟一:分通道顯示 vector<Mat> bgr_plane; split(src, bgr_plane); //split//把多通道影像分為多個單通道影像 (const Mat &src, //輸入影像 Mat* mvbegin //輸出的通道影像陣列) //步驟二:計算直方圖 // 定義引數變數 const int channels[1] = { 0 }; const int bins[1] = { 256 }; float hranges[2] = { 0,255 }; const float* ranges[1] = { hranges }; Mat b_hist; Mat g_hist; Mat r_hist; // 計算Blue, Green, Red通道的直方圖 calcHist(&bgr_plane[0], 1, 0, Mat(), b_hist, 1, bins, ranges); calcHist(&bgr_plane[1], 1, 0, Mat(), g_hist, 1, bins, ranges); calcHist(&bgr_plane[2], 1, 0, Mat(), r_hist, 1, bins, ranges); // 顯示直方圖 int hist_w = 512; int hist_h = 400; int bin_w = cvRound((double)hist_w / bins[0]);//直方圖的等級 Mat histImage = Mat::zeros(hist_h, hist_w, CV_8UC3); // 歸一化直方圖資料(範圍在0-400) normalize(b_hist, b_hist, 0, hist_h, NORM_MINMAX, -1, Mat()); normalize(g_hist, g_hist, 0, hist_h, NORM_MINMAX, -1, Mat()); normalize(r_hist, r_hist, 0, hist_h, NORM_MINMAX, -1, Mat()); //步驟三:繪製直方圖並顯示 for (int i = 1; i < bins[0]; i++) { line(histImage, Point(bin_w*(i - 1), hist_h - cvRound(b_hist.at<float>(i - 1))), Point(bin_w*(i), hist_h - cvRound(b_hist.at<float>(i))), Scalar(255, 0, 0), 2, 8, 0); line(histImage, Point(bin_w*(i - 1), hist_h - cvRound(g_hist.at<float>(i - 1))), Point(bin_w*(i), hist_h - cvRound(g_hist.at<float>(i))), Scalar(0, 255, 0), 2, 8, 0); line(histImage, Point(bin_w*(i - 1), hist_h - cvRound(r_hist.at<float>(i - 1))), Point(bin_w*(i), hist_h - cvRound(r_hist.at<float>(i))), Scalar(0, 0, 255), 2, 8, 0); } // 顯示直方圖 namedWindow("Histogram Demo", WINDOW_AUTOSIZE); imshow("Histogram Demo", histImage);
自定義函式實現灰度影像直方圖繪製
?在明白直方圖的原理以後,那我們可不可以自己建構函式去實現對灰度影像的一維直方圖繪製呢?
函式實現思想:(思想很重要!!)
- 遍歷整幅影像的畫素點,統計灰度值0-256的畫素點個數並存到陣列img_num[]中
- 遍歷這個img_num[]陣列,對灰度值進行歸一化,計算出的高度為各灰度值所佔的比值
- 用畫直線函式進行繪製
opencv實現:
?主函式:
int main(int argc, char** argv) { Mat histogram_draw(Mat img, int *img_num); Mat src = imread("D:/opencv練習圖片/直方圖.png"); imshow("原圖片", src); cvtColor(src, src, COLOR_RGB2GRAY); int img_num[256] = { 0 }; //定義一個存放統計資料的一維陣列(256位,每位初始化為0) Mat histogram; //定義直方圖 histogram = histogram_draw(src, img_num); imshow("直方圖", histogram); waitKey(0); return 0; }
?自定義直方圖函式:
//自定義直方圖函式 //img:需要計算的影像 //img_num[]:計算直方圖的特徵空間子區段的數目 Mat histogram_draw(Mat img, int *img_num) { int r = 200; //定義高 int w = 1000; //定義寬 Mat histogram = Mat(r, w, CV_8UC3); //直方圖畫布 int row = img.rows; //圖片的高度 int col = img.cols; //圖片的寬度 for (int i = 0; i < row; i++) { for (int j = 0; j < col; j++) { int num = img.at<uchar>(i, j); //讀取圖片畫素位置(i,j)處的灰度值 img_num[num]++; //將對應灰度值的個數加一(統計0-255的畫素值的出現個數) } } int all = row * col; for (int i = 0; i < 256; i++) //對灰度值0-255迴圈處理 { int hight = int(double(img_num[i]) / double(all)*r); //(出現個數與總個數的比值)*高[即各灰度值所佔的比值的高度] //opencv影像的畫素座標系原點在左上角 Point ps(i * 4, r); Point pe(i * 4, r - hight*10); line(histogram, pe, ps, Scalar(0, 0, 255)); } return histogram; }
直方圖均衡化
直方圖均衡化是灰度變換的一個重要應用,廣泛應用於影像增強處理中。它是通過拉伸畫素強度分佈範圍來增強影像對比度的一種方法。
說得更清楚一些, 以直方圖為例, 你可以看到畫素主要集中在中間的一些強度值上. 直方圖均衡化要做的就是 拉伸 這個範圍. 見下面左圖: 綠圈圈出了 少有畫素分佈其上的 強度值. 對其應用均衡化後, 得到了中間圖所示的直方圖。
直方圖均衡化的API非常簡單:(輸入影像必須是單通道)
void equalizeHist(InputArray src, OutputArray dst) //第一個引數,源影像,需為8位單通道影像 //第二個引數,輸出影像,尺寸、型別和源影像一致
?opencv對於彩色(三通道)的影像如何均衡化呢?
灰常簡單:先將三通道拆分為3個單通道split(),再分別對其均衡化,最後合併為三通道merge()。
Mat dst; Mat src = imread("D:/opencv練習圖片/直方圖.png"); imshow("原圖片", src); //分割通道 vector<Mat>channels; split(src, channels); Mat blue, green, red; blue = channels.at(0); green = channels.at(1); red = channels.at(2); //分別對BGR通道做直方圖均衡化 equalizeHist(blue, blue); equalizeHist(green, green); equalizeHist(red, red); //合併通道 merge(channels, dst); imshow("output", dst);
直方圖的反向投影
直方圖反向投影用於影像分割或在影像中用直方圖查詢感興趣的物件。
直方圖在一定程度上可以反應影像的特徵,我們擷取一個有固定特徵的樣本圖,比如草地,然後計算該塊草地的直方圖,然後用這個直方圖去和整幅影像的直方圖做對比,根據一定的判斷條件,就能得出相似的即為草地。
是不是看起來像是語義分割?
其實一定意義上這就是語義分割,這不過直方圖反向分割的依據是人為計算的(直方圖),後者分割的依據是靠在神經網路中學習得來的。
?先來一看下opencv中直方圖反向投影的API:
void calcBackProject( const Mat * images, //要進行投影的輸入影像的地址,注意該API要求輸入的是地址 int nimages,//輸入影像的數目 const int * channels,//要進行投影的通道數 InputArray hist,//樣本的直方圖 OutputArray backProject,//輸出得反向投影,為Mat型別 const float ** ranges, //輸入直方圖得特徵空間的取值範圍 double scale = 1, bool uniform = true )
?該API實現的原理:
假設我們現在有一個四行四列得灰度圖,它得灰度值如下圖:
說這幅圖有什麼特徵呢?直觀上看類似於一個邊角,但這是直觀上,怎麼表示出來呢?深度學習是靠神經網路黑箱計算出來得,我們可以用直方圖。
那我們就計算這幅灰度圖得直方圖,如果以組距為1計算直方圖並反向投影到原圖,得到得為下圖:
大概表述一下邊角的特徵:左下角有6個畫素值相同得三角形區域,中間斜向下有四個畫素值相同得邊界線,以此類推。這就是用直方圖得到邊角的特徵。
那如果以組距為2計算直方圖呢?反向投影后為:
可以看到特徵描述得更為廣泛了,就像深度學習裡,提取更高層次的特徵,雖然更為普適,但也會忽略掉一些細節特徵。
我們就是拿這個反向投影所表達的特徵資訊,去和整幅圖做對比,來得到特徵相似的部分,達到分割的效果。
? 利用反向投影進行語義分割(opencv實現)
先看一下我們今天要處理得圖片:
目的:將公路提取出來
樣本圖片:
(一)先讀取原圖以及樣本圖,並轉換為HSV格式。
Mat src1 = imread("D:/opencv練習圖片/直方圖反向投影.jpg");//原圖 Mat src2 = imread("D:/opencv練習圖片/樣本圖.jpg");//樣本圖 imshow("原圖", src1); //轉換為HSV影像 Mat HsvImage, RoiImage_HSV; cvtColor(src1, HsvImage, COLOR_BGR2HSV); cvtColor(src2, RoiImage_HSV, COLOR_BGR2HSV);
為什麼轉HSV呢?
因為HSV表達顏色更為方便區分,我們今天只對前兩個通道直方圖統計:H(色調)和S(飽和度),不用V(亮度)。
(二)計算樣本圖的直方圖並進行歸一化
Mat roiHist; //直方圖物件 int dims = 2; //特徵數目(直方圖維度) float hranges[] = { 0,180 }; //特徵空間的取值範圍 float Sranges[] = { 0,256 }; const float *ranges[] = { hranges,Sranges }; int size[] = { 20,32 }; //存放每個維度的直方圖的尺寸的陣列 int channels[] = { 0,1 }; //通道數 calcHist(&RoiImage_HSV, 1, channels, Mat(), roiHist, dims, size, ranges); //直方圖歸一化 normalize(roiHist, roiHist, 0, 255, NORM_MINMAX);
為什麼要歸一化呢,直方圖反向投影到原圖後,原圖各位置表示的是整幅圖中等於該點畫素值的數量,歸一化後就變成概率了。
(三)將計算的歸一化後的直方圖進行反向投影
//反向投影 Mat proImage; //投影輸出影像 calcBackProject(&HsvImage, 1, channels, roiHist, proImage, ranges); imshow("反向投影", proImage);
可以看到,公路的輪廓以及提取出來了(雖然效果不是太好,肯定不如深度學習。。。)
(四)用掩碼將公路給摳出來顯示
//影像掩碼Mask操作 threshold(proImage, proImage, 50, 255, THRESH_BINARY);//對mask進行二值化,將mask進一步處理 Mat dstImage = Mat::zeros(src1.size(), CV_8UC3); src1.copyTo(dstImage, proImage); imshow("結果", dstImage);
後期再加上些邊緣檢測和霍夫直線變換,和一些其他騷操作,就可以提取公路啊,車道線啥的了。