本文主要介紹了灰度直方圖相關的處理,包括以下幾個方面的內容:
- 利用OpenCV計算圖像的灰度直方圖,並繪制直方圖曲線
- 直方圖均衡化的原理及實現
- 直方圖規定化(匹配)的原理及實現
圖像的灰度直方圖
一幅圖像由不同灰度值的像素組成,圖像中灰度的分布情況是該圖像的一個重要特征。圖像的灰度直方圖就描述了圖像中灰度分布情況,能夠很直觀的展示出圖像中各個灰度級所占的多少。
圖像的灰度直方圖是灰度級的函數,描述的是圖像中具有該灰度級的像素的個數:其中,橫坐標是灰度級,縱坐標是該灰度級出現的頻率。
不過通常會將縱坐標歸一化到[0,1]區間內,也就是將灰度級出現的頻率(像素個數)除以圖像中像素的總數。灰度直方圖的計算公式如下:
p(r_k)=\frac{n_k}{MN}
其中,rk是像素的灰度級,nk是具有灰度rk的像素的個數,MN是圖像中總的像素個數。
OpenCV灰度直方圖的計算
直方圖的計算是很簡單的,無非是遍歷圖像的像素,統計每個灰度級的個數。在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 );
該函數能夠同時計算多個圖像,多個通道,不同灰度范圍的灰度直方圖.
其參數如下
- images,輸入圖像的數組,這些圖像要有相同大大小,相同的深度(CV_8U CV_16U CV_32F).
- nimages ,輸入圖像的個數
- mask,可選的掩碼,不使用時可設為空。要和輸入圖像具有相同的大小,在進行直方圖計算的時候,只會統計該掩碼不為0的對應像素
- hist,輸出的直方圖
- dims,直方圖的維度
- histSize,直方圖每個維度的大小
- ranges,直方圖每個維度要統計的灰度級的范圍
- uniform,是否進行歸一化,默認為true
- accumulate,累積標志,默認值為false。
為了計算的靈活性和通用性,OpenCV的灰度直方圖提供了較多的參數,但對於只是簡單的計算一幅灰度圖的直方圖的話,又顯得較為累贅。這里對calcHist進行一次封裝,能夠方便的得到一幅灰度圖直方圖。
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將圖像的直方圖以線條的形式畫出來,並返回包含直方圖的圖像。測試代碼如下:
Histogram1D hist;
Mat histImg;
histImg = hist.getHistogramImage(image);
imshow("Image", image);
imshow("Histogram", histImg);
直方圖均衡化 Histogram Equalization
假如圖像的灰度分布不均勻,其灰度分布集中在較窄的范圍內,使圖像的細節不夠清晰,對比度較低。通常采用直方圖均衡化及直方圖規定化兩種變換,使圖像的灰度范圍拉開或使灰度均勻分布,從而增大反差,使圖像細節清晰,以達到增強的目的。
直方圖均衡化,對圖像進行非線性拉伸,重新分配圖像的灰度值,使一定范圍內圖像的灰度值大致相等。這樣,原來直方圖中間的峰值部分對比度得到增強,而兩側的谷底部分對比度降低,輸出圖像的直方圖是一個較為平坦的直方圖。
均衡化算法
直方圖的均衡化實際也是一種灰度的變換過程,將當前的灰度分布通過一個變換函數,變換為范圍更寬、灰度分布更均勻的圖像。也就是將原圖像的直方圖修改為在整個灰度區間內大致均勻分布,因此擴大了圖像的動態范圍,增強圖像的對比度。通常均衡化選擇的變換函數是灰度的累積概率,直方圖均衡化算法的步驟:
- 計算原圖像的灰度直方圖
P(S_k) = \frac{n_k}{n}
,其中n為像素總數,Nk為灰度級Sk的像素個數
- 計算原始圖像的累積直方圖
CDF(S_k) = \sum\limits^k_{i=0}\frac{n_i}{n}=\sum\limits^k_{i=0}P_s(S_i)
D_j = L\cdot CDF(S_i)
其中Dj為目的圖像的像素,
CDF(S_i)
是源圖像灰度為i的累積分布,L是圖像中最大灰度級(灰度圖為255)
其代碼實現如下:
- 在上面中封裝了求灰度直方圖的類,這里直接應用該方法得到圖像的灰度直方圖;
- 將灰度直方圖進行歸一化,計算灰度的累積概率;
- 創建灰度變化的查找表
- 應用查找表,將原圖像變換為灰度均衡的圖像
具體代碼如下:
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封裝函數的結果,右邊為上面代碼得到的結果。
直方圖規定化
從上面可以看出,直方圖的均衡化自動的確定了變換函數,可以很方便的得到變換后的圖像,但是在有些應用中這種自動的增強並不是最好的方法。有時候,需要圖像具有某一特定的直方圖形狀(也就是灰度分布),而不是均勻分布的直方圖,這時候可以使用直方圖規定化。
直方圖規定化,也叫做直方圖匹配,用於將圖像變換為某一特定的灰度分布,也就是其目的的灰度直方圖是已知的。這其實和均衡化很類似,均衡化后的灰度直方圖也是已知的,是一個均勻分布的直方圖;而規定化后的直方圖可以隨意的指定,也就是在執行規定化操作時,首先要知道變換后的灰度直方圖,這樣才能確定變換函數。規定化操作能夠有目的的增強某個灰度區間,相比於,均衡化操作,規定化多了一個輸入,但是其變換后的結果也更靈活。
- 將原始圖像的灰度直方圖進行均衡化,得到一個變換函數
s = T(r)
其中s是均衡化后的像素,r是原始像素
- 對規定的直方圖進行均衡化,得到一個變換函數
v = G(z)
其中v是均衡化后的像素,z是規定化的像素
- 上面都是對同一圖像的均衡化,其結果應該是相等的,
s = v,且 z = G^{-1}(v) = G^{-1}(T(r))
詳解規定化過程
對圖像進行直方圖規定化操作,原始圖像的直方圖和以及規定化后的直方圖是已知的。假設
P_r(r)
表示原始圖像的灰度概率密度,Pz(z)表示規定化圖像的灰度概率密度,(r和z分別是原始圖像的灰度級,規定化后圖像的灰度級)。
- 對原始圖像進行均衡化操作,則有
s_k = T(r_k) = L \cdot \sum\limits_{i=0}^{i=k}P_r(r_k)
- 對規定化的直方圖進行均衡化操作,則
v_k = G(z_m) = L \cdot \sum\limits_{j=0}^{j=m}P_z(z_m)
- 由於是對同一圖像的均衡化操作,所以有
s_k = v_m
-
規定化操作的目的就是找到原始圖像的像素sk
sk 到規定化后圖像像素的zk之間的一個映射。有了上一步的等式后,可以得到sk=G(zk),因此要想找到sk想對應的zk只需要在z進行迭代,找到使式子G(zm)−sk的絕對值最小即可。 -
上述描述只是理論的推導過程,在實際的計算過程中,不需要做兩次的均衡化操作,具體的推導過程如下:
\begin{array}{c}
s_k = v_k \ L \cdot \sum\limits_{i=0}^{i=k}P_r(r_k) = L \cdot \sum\limits_{j=0}^{j=m}P_z(z_m) \ \sum\limits_{i=0}^{i=k}P_r(r_k) = \sum\limits_{j=0}^{j=m}P_z(z_m)
\end{array}
上面公式表示,假如\(s_k\) 規定化后的對應灰度是\(z_m\)的話,需要滿足的條件是\(s_k\)的累積概率和\(z_m\)的累積概率是最接近的。
下面是一個具體計算的例子:
首先得到原直方圖的各個灰度級的累積概率\(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);
}
上面函數的第二個參數的直方圖就是規定化的直方圖。代碼比較簡單,這里就不一一解釋了。其結果如下:
左邊是原圖像,右邊是規定化的圖像,也就是上面函數的第一個和第二個輸入參數。原圖像規定化的結果如下:
原圖像規定化后的直方圖和規定化的圖像的直方圖的形狀比較類似, 並且原圖像規定化后整幅圖像的特征和規定化的圖像也比較類似,例如:原圖像床上的被子,明顯帶有規定化圖像中水的波紋特征。
直方圖規定化過程中,在做灰度映射的時候,有兩種常用的方法:
- 單映射 Single Mapping Law,SML,這種方法也是上面使用的方法,根據累積直方圖的差值,從原圖像中找到其在規定化圖像中的映射。
- 組映射 Group Mapping Law,GML 這種方法較上述方法復雜不少,但是處理效果較好。
對於GML的映射方法,一直沒有很好的理解,但是根據其算法描述實現了該方法,代碼這里先不放出,其處理結果如下:
其結果較SML來說更為亮一些,床上的波浪特征也更為明顯,但是其直方圖形狀,和規定化的直方圖對比,第一個峰不是很明顯。
總結
- 圖像的灰度直方圖能夠很直觀的展示圖像中灰度級的整體分布情況,對圖像的后續處理有很好的指導作用。
- 直方圖的均衡化的是將一幅圖像的直方圖變平,使各個灰度級的趨於均勻分布,這樣能夠很好的增強圖像對比度。直方圖均衡化是一種自動化的變換,僅需要輸入圖像,就能夠確定圖像的變換函數。但是直方圖的均衡化操作也有一定的確定,在均衡化的過程中對圖像中的數據不加選擇,這樣有可能會增強圖像的背景;變換后圖像的灰度級減少,有可能造成某些細節的消失;會壓縮圖像直方圖中的高峰,造成處理后圖像對比度的不自然等。
- 直方圖規定化,也稱為直方圖匹配,經過規定化處理將原圖像的直方圖變換為特定形狀的直方圖(上面中的示例,就是將圖像的直方圖變換為另一幅圖像的直方圖)。它可以按照預先設定的它可以按照預先設定的某個形狀來調整圖像的直方圖,運用均衡化原理的基礎上,通過建立原始圖像和期望圖像