本文主要介紹了灰度直方圖相關的處理,包括如下幾個方面的內容:算法
一幅圖像由不一樣灰度值的像素組成,圖像中灰度的分佈狀況是該圖像的一個重要特徵。圖像的灰度直方圖就描述了圖像中灰度分佈狀況,可以很直觀的展現出圖像中各個灰度級所佔的多少。
圖像的灰度直方圖是灰度級的函數,描述的是圖像中具備該灰度級的像素的個數:其中,橫座標是灰度級,縱座標是該灰度級出現的頻率。
數組
不過一般會將縱座標歸一化到\([0,1]\)區間內,也就是將灰度級出現的頻率(像素個數)除以圖像中像素的總數。灰度直方圖的計算公式以下:
\[ p(r_k) = \frac{n_k}{MN} \]
其中,\(r_k\)是像素的灰度級,\(n_k\)是具備灰度\(r_k\)的像素的個數,\(MN\)是圖像中總的像素個數。app
直方圖的計算是很簡單的,無非是遍歷圖像的像素,統計每一個灰度級的個數。在OpenCV中封裝了直方圖的計算函數calcHist
,爲了更爲通用該函數的參數有些複雜,其聲明以下:函數
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 );
該函數可以同時計算多個圖像,多個通道,不一樣灰度範圍的灰度直方圖.
其參數以下:測試
CV_8U CV_16U CV_32F
).爲了計算的靈活性和通用性,OpenCV的灰度直方圖提供了較多的參數,但對於只是簡單的計算一幅灰度圖的直方圖的話,又顯得較爲累贅。這裏對calcHist
進行一次封裝,可以方便的獲得一幅灰度圖直方圖。spa
class Histogram1D { private: int histSize[1]; // 項的數量 float hranges[2]; // 統計像素的最大值和最小值 const float* ranges[1]; int channels[1]; // 僅計算一個通道 public: Histogram1D() { // 準備1D直方圖的參數 histSize[0] = 256; hranges[0] = 0.0f; hranges[1] = 255.0f; ranges[0] = hranges; channels[0] = 0; } MatND getHistogram(const Mat &image) { MatND hist; // 計算直方圖 calcHist(&image ,// 要計算圖像的 1, // 只計算一幅圖像的直方圖 channels, // 通道數量 Mat(), // 不使用掩碼 hist, // 存放直方圖 1, // 1D直方圖 histSize, // 統計的灰度的個數 ranges); // 灰度值的範圍 return hist; } Mat getHistogramImage(const Mat &image) { MatND hist = getHistogram(image); // 最大值,最小值 double maxVal = 0.0f; double minVal = 0.0f; minMaxLoc(hist, &minVal, &maxVal); //顯示直方圖的圖像 Mat histImg(histSize[0], histSize[0], CV_8U, Scalar(255)); // 設置最高點爲nbins的90% int hpt = static_cast<int>(0.9 * histSize[0]); //每一個條目繪製一條垂直線 for (int h = 0; h < histSize[0]; h++) { float binVal = hist.at<float>(h); int intensity = static_cast<int>(binVal * hpt / maxVal); // 兩點之間繪製一條直線 line(histImg, Point(h, histSize[0]), Point(h, histSize[0] - intensity), Scalar::all(0)); } return histImg; } };
Histogram1D
提供了兩個方法:getHistogram
返回統計直方圖的數組,默認計算的灰度範圍是[0,255];getHistogramImage
將圖像的直方圖以線條的形式畫出來,並返回包含直方圖的圖像。測試代碼以下:code
Histogram1D hist; Mat histImg; histImg = hist.getHistogramImage(image); imshow("Image", image); imshow("Histogram", histImg);
其結果以下:
orm
假如圖像的灰度分佈不均勻,其灰度分佈集中在較窄的範圍內,使圖像的細節不夠清晰,對比度較低。一般採用直方圖均衡化及直方圖規定化兩種變換,使圖像的灰度範圍拉開或使灰度均勻分佈,從而增大反差,使圖像細節清晰,以達到加強的目的。
直方圖均衡化,對圖像進行非線性拉伸,從新分配圖像的灰度值,使必定範圍內圖像的灰度值大體相等。這樣,原來直方圖中間的峯值部分對比度獲得加強,而兩側的谷底部分對比度下降,輸出圖像的直方圖是一個較爲平坦的直方圖。blog
直方圖的均衡化實際也是一種灰度的變換過程,將當前的灰度分佈經過一個變換函數,變換爲範圍更寬、灰度分佈更均勻的圖像。也就是將原圖像的直方圖修改成在整個灰度區間內大體均勻分佈,所以擴大了圖像的動態範圍,加強圖像的對比度。一般均衡化選擇的變換函數是灰度的累積機率,直方圖均衡化算法的步驟:ci
其代碼實現以下:
具體代碼以下:
void equalization_self(const Mat &src, Mat &dst) { Histogram1D hist1D; MatND hist = hist1D.getHistogram(src); hist /= (src.rows * src.cols); // 對獲得的灰度直方圖進行歸一化 float cdf[256] = { 0 }; // 灰度的累積機率 Mat lut(1, 256, CV_8U); // 灰度變換的查找表 for (int i = 0; i < 256; i++) { // 計算灰度級的累積機率 if (i == 0) cdf[i] = hist.at<float>(i); else cdf[i] = cdf[i - 1] + hist.at<float>(i); lut.at<uchar>(i) = static_cast<uchar>(255 * cdf[i]); // 建立灰度的查找表 } LUT(src, lut, dst); // 應用查找表,進行灰度變化,獲得均衡化後的圖像 }
上面代碼只是加深下對均衡化算法流程的理解,實際在OpenCV中也提供了灰度均衡化的函數equalizeHist
,該函數的使用很簡單,只有兩個參數:輸入圖像,輸出圖像。下圖爲,上述代碼計算獲得的均衡化結果和調用equalizeHist
的結果對比
最左邊爲原圖像,中間爲OpenCV封裝函數的結果,右邊爲上面代碼獲得的結果。
從上面能夠看出,直方圖的均衡化自動的肯定了變換函數,能夠很方便的獲得變換後的圖像,可是在有些應用中這種自動的加強並非最好的方法。有時候,須要圖像具備某一特定的直方圖形狀(也就是灰度分佈),而不是均勻分佈的直方圖,這時候可使用直方圖規定化。
直方圖規定化,也叫作直方圖匹配,用於將圖像變換爲某一特定的灰度分佈,也就是其目的的灰度直方圖是已知的。這其實和均衡化很相似,均衡化後的灰度直方圖也是已知的,是一個均勻分佈的直方圖;而規定化後的直方圖能夠隨意的指定,也就是在執行規定化操做時,首先要知道變換後的灰度直方圖,這樣才能肯定變換函數。規定化操做可以有目的的加強某個灰度區間,相比於,均衡化操做,規定化多了一個輸入,可是其變換後的結果也更靈活。
在理解了上述的均衡化過程後,直方圖的規定化也較爲簡單。能夠利用均衡化後的直方圖做爲一箇中間過程,而後求取規定化的變換函數。具體步驟以下:
經過,均衡化做爲中間結果,將獲得原始像素\(r\)和\(z\)規定化後像素之間的映射關係。
對圖像進行直方圖規定化操做,原始圖像的直方圖和以及規定化後的直方圖是已知的。假設\(P_r(r)\)表示原始圖像的灰度機率密度,\(P_z(z)\)表示規定化圖像的灰度機率密度(r和z分別是原始圖像的灰度級,規定化後圖像的灰度級)。
首先獲得原直方圖的各個灰度級的累積機率\(V_s\)以及規定化後直方圖的各個灰度級的累積機率\(V_z\),那麼肯定\(s_k\)到\(z_m\)之間映射關係的條件就是:\[\mid V_s - V_z \mid\]的值最小。
以\(k = 2\)爲例,其原始直方圖的累積機率是:0.65,在規定化後的直方圖的累積機率中和0.65最接近(相等)的是灰度值爲5的累積機率密度,則能夠獲得原始圖像中的灰度級2,在規定化後的圖像中的灰度級是5。
直方圖規定化的實現能夠分爲一下三步:
具體代碼實現以下:
void hist_specify(const Mat &src, const Mat &dst,Mat &result) { Histogram1D hist1D; MatND src_hist = hist1D.getHistogram(src); MatND dst_hist = hist1D.getHistogram(dst); float src_cdf[256] = { 0 }; float dst_cdf[256] = { 0 }; // 源圖像和目標圖像的大小不同,要將獲得的直方圖進行歸一化處理 src_hist /= (src.rows * src.cols); dst_hist /= (dst.rows * dst.cols); // 計算原始直方圖和規定直方圖的累積機率 for (int i = 0; i < 256; i++) { if (i == 0) { src_cdf[i] = src_hist.at<float>(i); dst_cdf[i] = dst_hist.at<float>(i); } else { src_cdf[i] = src_cdf[i - 1] + src_hist.at<float>(i); dst_cdf[i] = dst_cdf[i - 1] + dst_hist.at<float>(i); } } // 累積機率的差值 float diff_cdf[256][256]; for (int i = 0; i < 256; i++) for (int j = 0; j < 256; j++) diff_cdf[i][j] = fabs(src_cdf[i] - dst_cdf[j]); // 構建灰度級映射表 Mat lut(1, 256, CV_8U); for (int i = 0; i < 256; i++) { // 查找源灰度級爲i的映射灰度 // 和i的累積機率差值最小的規定化灰度 float min = diff_cdf[i][0]; int index = 0; for (int j = 1; j < 256; j++) { if (min > diff_cdf[i][j]) { min = diff_cdf[i][j]; index = j; } } lut.at<uchar>(i) = static_cast<uchar>(index); } // 應用查找表,作直方圖規定化 LUT(src, lut, result); }
上面函數的第二個參數的直方圖就是規定化的直方圖。代碼比較簡單,這裏就不一一解釋了。其結果以下:
左邊是原圖像,右邊是規定化的圖像,也就是上面函數的第一個和第二個輸入參數。原圖像規定化的結果以下:
原圖像規定化後的直方圖和規定化的圖像的直方圖的形狀比較相似, 而且原圖像規定化後整幅圖像的特徵和規定化的圖像也比較相似,例如:原圖像牀上的被子,明顯帶有規定化圖像中水的波紋特徵。
直方圖規定化過程當中,在作灰度映射的時候,有兩種經常使用的方法:
對於GML的映射方法,一直沒有很好的理解,可是根據其算法描述實現了該方法,代碼這裏先不放出,其處理結果以下:
其結果較SML來講更爲亮一些,牀上的波浪特徵也更爲明顯,可是其直方圖形狀,和規定化的直方圖對比,第一個峯不是很明顯。