對於輸入像素點r和輸出像素點s都在灰度級 [0,L-1]之間,r = 0 代表黑色, r = L - 1代表白色。對於r和s的變換形式為:
r和s滿足一下條件:
利用反函數來從s推r時,有以下定義:
r和s滿足條件:
條件a是為了保證輸出的灰度級不少於輸入,這是為了防止二義性。條件b是為了保證輸出的灰度范圍和輸入的灰度范圍相同。條件a`也是為了保證s到r 也是一一對應的,防止二義性。實驗中采用8bit整性的像素分布,不一定滿足這個情況。
在實驗中會類似左邊的圖像,出現多個輸入的r,輸出同一個s值。而在理論的約束上應該和右圖相似,r與s一一對應。
對於一副灰度圖像,
和
分別代表輸入圖像和輸出圖像的像素點的概率分布,我們簡稱為PDF。對於已知的和滿足公式:
(1)
直方圖均衡化的采用的公式如下:
(2)
其中w是積分假變量,L-1是最大的灰度級。
為什么要這么做呢。由萊布尼茲准則,我們知道上限的定積分的導數是被積函數在該上限的值。
(3)
我們將的結果帶入(1)中,
(4)
2 結論
使用(2)公式后,輸出圖像的像素點s分布是均勻的,PDF為1/(L-1)。
3 離散直方圖均衡化采用公式
對於離散的直方圖均衡化采用的公式為:
(5)
其中MN是總的像素點個數
編碼實現
1 統計輸入的灰度圖像的像素信息
void calHistInfo(const Mat& src, vector<unsigned int>& calVec, vector<unsigned int >& calVecBefor,unsigned int LMax){
int width = src.cols;
int height = src.rows;
int piexl = 0;
for (int h = 0; h < height; h++)
{
for (int w = 0; w < width; w++) {
piexl = (int)src.at<uchar>(h, w);
calVec[round(piexl)] += 1;
}
}
calVecBefor[0] = calVec[0];
for (int i = 1; i < calVec.size(); i++)
{
calVecBefor[i] = calVecBefor[i - 1] + calVec[i];
}
cout << "統計完畢" << endl;
}
這里我用了兩個vector數組calVec 和calVecBefor ,calVec是用於記錄每個灰度級別的像素個數,
calVecBefor是記錄在當前像素r個數和r之前的像素之和。LMax是用於記錄有多少個灰度級別,這里LMax =256。
2 根據直方圖統計信息繪制折線圖
void showHistChart(Mat& canvas,unsigned int LMax,vector<unsigned int> calVec,Scalar sca, int thikness)
{
//橫軸是灰度級
//縱軸是像素的分布情況,我采取的方法是像素數目最多的為單位1
int canvas_height = canvas.rows;
int canvas_step = canvas.cols / LMax;
auto it = max_element(calVec.begin(), calVec.end());
unsigned int calMax = *it;
//開始畫折線圖
for (int i = 0; i < LMax-1; i++)
{
line(canvas, Point(i * canvas_step, canvas_height - (canvas_height*(1.0)*calVec[i]/calMax)),
Point((i + 1) * canvas_step, canvas_height - (canvas_height * (1.0) * calVec[i+1] / calMax)),
sca, thikness, 8);
}
}
其中canvas是畫布,首先要找到像素點最多的灰度級別和該級別像素的個數calMax,以及根據畫布大小和灰度級個數確定步長canvas_step,sca是畫折線使用的顏色, thikness是折線的粗細,參數8是渲染方式。
3 確定像素點r與s之間的映射
void createRSTable(const vector<unsigned int>& calVecBefor,
unordered_map<unsigned int,unsigned int>& table_rs,unsigned int LMax)
{
double total = (double)calVecBefor[LMax - 1];
for (int i =0;i<LMax;i++)
{
double s = LMax*(double)calVecBefor[i] * (1.0) / total;
if (round(s) <= 255) {
table_rs.insert(make_pair(i, round(s)));
}
else
{
table_rs.insert(make_pair(i, 255));
}
}
}
這里我采用的unordered_map用於記錄輸入的r與輸出的s之間的映射關系。當然這里也可以用數組之類的記錄,記錄方法就是公式(5)。
4 灰度圖像的直方圖均衡化
void iHistImp(Mat& src, Mat&dst, unordered_map<unsigned int,unsigned int>& table_rs) {
dst = Mat::zeros(src.size(), src.type());
int width = src.cols;
int height = src.rows;
int piexl = 0;
for (int h = 0; h < height; h++)
{
for (int w = 0; w < width; w++) {
piexl = (int)src.at<uchar>(h, w);
auto it = table_rs.find(piexl);
if (it != table_rs.end())
{
dst.at<uchar>(h, w) = it->second;
}
}
}
}
這里通過遍歷灰度輸入的輸入像素點r,來找到相應的輸出s。
5 灰度圖像直方圖均衡化的結果
這里我除了使用自己實現的直方圖均衡化,還調用opencv中直方圖均衡化的api,來對比實驗效果。
實驗前后的折線圖:
紅色是直方圖均衡化前的像素統計,藍色是均衡化后的像素統計。
6 彩色圖像的直方圖均衡化
彩色圖像的直方圖均衡化,我是將輸入的彩色圖像分成bgr三個通道,分別進行直方圖均衡化,然后再將結果合並再一起。
/*
* 彩色圖像的直方圖均衡化
*/
void BGRHist(Mat& src, Mat& dst)
{
//將彩色圖像的按照BGR三個通道切分
vector<Mat> splitMat;
split(src,splitMat);
//分別對bgr 三個通道進行直方圖均衡化
vector<Mat> mergeMat;
split(src, mergeMat);
for (int i = 0; i < 3; i++)
{
unsigned int LMax = 256;
vector<unsigned int> calVec(LMax, 0);
vector<unsigned int > calVecBefor(LMax, 0);//用於記錄灰度級l之前的所以的像素點個數
calHistInfo(splitMat[i], calVec, calVecBefor, LMax);
//構建輸入像素r 與輸出像素s 之間的 一一映射表
unordered_map<unsigned int, unsigned int > table_rs;
createRSTable(calVecBefor, table_rs, LMax);
iHistImp(splitMat[i],mergeMat[i], table_rs);
}
merge(mergeMat,dst);
}
彩色圖像直方圖均衡化的結果:
實驗中的問題以及需要改進的地方
1、在實驗的像素的統計過程和r與s轉換的過程,可以采用基於指針的方式遍歷。
2、彩色圖像的直方圖均衡化效果不是很好,可以嘗試新的算法。
3、r與s的映射表可以轉成用數組存放,可能要快點。
4、可以將r->s的值存為一個灰度級的映射表
5、可以對直方圖均衡化的結果再進行直方圖均衡化,可以設置迭代次數
直方圖匹配(直方圖規定化)
后面有空再實現
局部直方圖處理
后面再實現
后記
1、使用高頻強調濾波,然后再使用直方圖均衡,可以更好的處理圖片。《數字圖像處理·第三版》P181 有介紹類似的方法。