對比度受限的自適應直方圖均衡化(CLAHE)
直方圖均衡化(HE)是一種很常用的直方圖類方法,基本思想是通過影象的灰度分佈直方圖確定一條對映曲線,用來對影象進行灰度變換,以達到提高影象對比度的目的。該對映曲線其實就是影象的累計分佈直方圖(CDF)(嚴格來說是呈正比例關係)。然而HE是對影象全域性進行調整的方法,不能有效地提高區域性對比度,而且某些場合效果會非常差。如:
上述原圖和HE結果圖的直方圖分別為:
因為從原圖的直方圖中求取的對映函式(CDF)形狀為:
將它作用於原影象會導致直方圖被整體右移,沒有充分利用整個灰度動態範圍。
為了提高影象的區域性對比度,有人提出將影象分成若干子塊,對子塊進行HE處理,這便是AHE(自適應直方圖均衡化),使用AHE處理上圖得到:
結果直方圖:
可以看出結果影象的灰度較好地分佈在了全部動態範圍上。從結果影象上也可以看出,區域性對比度的確得到了提高,視覺效果要優於HE。但是仍然有個問題:AHE對區域性對比度提高過大,導致影象失真。看看背景區,本來的黑色背景現在已經變成白色了,原因是因為背景區中的區域性子塊統計得到的直方圖在0灰度處幅值太高(實際上全黑子圖基本上就集中在0灰度處),這樣導致對映曲線斜率過高,將所有灰度值都對映到整個灰度軸的右側,所以結果圖中背景偏白(另外區域性對比度過高還會放大影象中的噪聲,不過上圖並沒有體現這一點)。
為了解決這個問題,我們必須對區域性對比度進行限制,這就是我們今天的主題:CLAHE。
從HE中我們知道,對映曲線T與CDF關係為:(M為最高灰度值,N為畫素個數)
限制對比度,其實就是限制CDF的斜率,又因累計分佈直方圖CDF是灰度直方圖Hist的積分:
反過來:
也就是說限制CDF的斜率就相當於限制Hist的幅度。
因此我們需要對子塊中統計得到的直方圖進行裁剪,使其幅值低於某個上限,當然裁剪掉的部分又不能扔掉,我們還需要將這部分裁剪值均勻地分佈在整個灰度區間上,以保證直方圖總面積不變,如下圖:
可以看到,這時直方圖又會整體上升了一個高度,貌似會超過我們設定的上限。其實在具體實現的時候有很多解決方法,你可以多重複幾次裁剪過程,使得上升的部分變得微不足道,或是用另一種常用的方法:
設裁剪值為ClipLimit,求直方圖中高於該值的部分的和totalExcess,此時假設將totalExcess均分給所有灰度級, 求出這樣導致的直方圖整體上升的高度L=totalExcess/N,以upper= ClipLimit-L為界限對直方圖進行如下處理:
(1)若幅值高於ClipLimit,直接置為ClipLimit;
(2)若幅值處於Upper和ClipLimit之間,將其填補至ClipLimit;
(3)若幅值低於Upper,直接填補L個畫素點;
經過上述操作,用來填補的畫素點個數通常會略小於totalExcess,也就是還有一些剩餘的畫素點沒分出去,這個剩餘來自於(1)(2)兩處。這時我們可以再把這些點均勻地分給那些目前幅值仍然小於ClipLimit的灰度值。
這裡給出一段程式碼:(摘自Matlab的adapthisteq.m),描述的就是上述過程:
% total number of pixels overflowing clip limit in each bin
totalExcess = sum(max(imgHist - clipLimit,0));
% clip the histogram and redistribute the excess pixels in each bin
avgBinIncr = floor(totalExcess/numBins);
upperLimit = clipLimit - avgBinIncr; % bins larger than this will be
% set to clipLimit
% this loop should speed up the operation by putting multiple pixels
% into the "obvious" places first
for k=1:numBins
if imgHist(k) > clipLimit
imgHist(k) = clipLimit;
else
if imgHist(k) > upperLimit % high bin count
totalExcess = totalExcess - (clipLimit - imgHist(k));
imgHist(k) = clipLimit;
else
totalExcess = totalExcess - avgBinIncr;
imgHist(k) = imgHist(k) + avgBinIncr;
end
end
end
% this loops redistributes the remaining pixels, one pixel at a time
k = 1;
while (totalExcess ~= 0)
%keep increasing the step as fewer and fewer pixels remain for
%the redistribution (spread them evenly)
stepSize = max(floor(numBins/totalExcess),1);
for m=k:stepSize:numBins
if imgHist(m) < clipLimit
imgHist(m) = imgHist(m)+1;
totalExcess = totalExcess - 1; %reduce excess
if totalExcess == 0
break;
end
end
end
k = k+1; %prevent from always placing the pixels in bin #1
if k > numBins % start over if numBins was reached
k = 1;
end
end
CLAHE和AHE中另一個重要的問題:插值。
將影象進行分塊處理,若每塊中的畫素點僅通過該塊中的對映函式進行變換,則會導致最終影象呈塊狀效應:
為了解決這個問題,我們需要利用插值運算,也就是每個畫素點出的值由它周圍4個子塊的對映函式值進行雙線性插值得到,如下圖:
上圖中,為了求藍色畫素點處的值,需要利用它周圍四個子塊的對映函式分別做變換得到四個對映值,再對這四個值做雙線性插值即可。
當然對於邊界處的畫素點則不是通過四個子塊進行插值,如上圖紅色畫素點直接以一個子塊的對映函式做變換,綠色畫素則以兩個子塊做對映函式做線性插值。這裡講的邊界處畫素是指落在影象左上角,左下角、右上角,右下角的四個子塊中心畫素點圍成的四邊形之外的畫素。如下圖,將影象分為8x8子塊,邊界畫素即落在灰色區域的畫素點。
利用插值對影象進行處理的整體架構如下:
for (Y = 0; Y <= TileY; Y++) //TileY為Y方向網格數
{
if (Y == 0)
{
SubY = TileYDim >> 1; YU = 0; YB = 0;
}
else if (Y == TileY)
{
SubY = TileYDim >> 1; YU = TileY-1; YB = YU;
}
else
{
SubY = TileYDim; YU = Y - 1; YB = Y;
}
for (X = 0; X <= TileX; X++) //TileX為X方向網格數
{
if (X == 0)
{
SubX = TileXDim >> 1; XL = 0; XR = 0;
}
else if (X == TileX)
{
SubX = TileXDim >> 1; XL = TileX - 1; XR = XL;
}
else
{
SubX = TileXDim; XL = X - 1; XR = X;
}
MapLU = &pMapArray[numBins * (YU * TileX + XL)];//左上角對映函式
MapRU = &pMapArray[numBins * (YU * TileX + XR)];//右上角對映函式
MapLB = &pMapArray[numBins * (YB * TileX + XL)];//左下角對映函式
MapRB = &pMapArray[numBins * (YB * TileX + XR)];//右下角對映函式
Interpolate(pImPointer,Stride,Channel,MapLU,MapRU,MapLB,MapRB,SubX,SubY,aLUT);//插值
pImPointer += SubX;
}
pImPointer+=(SubY-1)*Stride;
}
注意的是,上述迴圈需要(TileX+1)*(TileY+1)次,而不是TileX*TileY次。,原因很簡單,以X方向為例,兩側邊界處的半寬子塊(灰色區)也各需要處理一次,如下圖:
通過雙線性插值可以基本消除塊狀效應:
對於彩色影象,三通處理分開處理會導致嚴重的偏色,故我們可以將其進行顏色空間轉換(如RGB轉為HSV),然後僅對亮度分量處理,再反變換回RGB空間。不過網上有高手將R、G、B統一處理[2](也就相當於把一個畫素點拆成三個畫素點),這樣得到的效果也不錯,而且省去了顏色空間轉換的時間,我們這裡也仿照他來吧:
另外,CLAHE對霧天影象處理效果也不錯:
至於程式設計,我基本就是翻譯adaphisteq.m,另外還有一些參考資源:CLAHE程式碼(MATLAB)
參考: