本文的分析基於《Adaptive and integrated neighborhood-dependent approach for nonlinear enhancement of color images》一文相關內容,但對其進行了深度的改良。算法
咱們首先解讀或者說翻譯下這篇論文。app
論文公佈的時間是2005年了,已是比較久遠的了,我第一次看到該論文大概是在2014年,後面在2016年左右實現了該算法,這裏還有當時開發留下的記錄,竟然是除夕左右作的。佩服本身。框架
之前沒有特別注意到該算法的效果,以爲也就那樣吧,因此沒有怎麼去發揮它,可是,最近再次審視,發現他除了實現實現簡單、速度快,並且還具備效果佳、適應性廣、不破壞自己就光照好的位置等等衆多優勢,彷佛比目前我看到的低照度加強算法都好。ide
算法自己的步驟分爲三步,第一步是根據的圖像亮度分佈創建一個自適應的全局映射函數,這一步大大的提升了圖像中暗部像素的像素值,同時也壓縮了圖像的動態範圍。第二步是所謂的自適應對比度加強,根據像素領域內的平均值和像素值自己的比例,作一個映射,提升總體的對比度。後續還有一個步驟是顏色恢復的過程。函數
第一步的全局曲線調整以下所示:測試
首先計算出彩色圖像的亮度值,這個其實能夠有不少方式,包括經常使用的YUV空間的Y通道,HSL空間的L份量,甚至可使用我提到的對比度保留去色那種方式獲取,論文裏使用的是最普通的計算公式:優化
咱們把亮度歸一化獲得:spa
而後咱們用下面這個傳輸函數(映射函數)來將亮度圖像進行一個線性的加強:翻譯
其中的參數Z值由圖像自己的內容決定,以下所示:code
式中的L表示亮度圖像的累計直方圖(CDF)達到0.1時的色階值,也就是說若是亮度圖像中90%的像素值都大於150,則Z=1,若是10%或者更多的像素值都小於50,則Z取值爲0,不然其餘狀況Z則根據L的值線性插值。
稍做分析下,若是Z=0,說明圖像中存在大量的偏暗像素,圖像有必要變亮一些,若是Z=1,則說明圖像已經很亮了,則此時圖像無需繼續加亮處理。介於二者之間時,咱們也就作中和處理。
那麼咱們採用的傳輸函數是否能達到這種需求呢,咱們來看下公式(3)的組成和曲線。
以Z=0爲例,如上圖2所示,曲線6爲公式(3)最後對應的結果。咱們看看其組成。公式(3)的第一和第二部分對應圖2中的曲線2和3,他們的和獲得曲線4。曲線2中在低亮度區域的加強很顯著,在高亮度區域逐漸變緩,曲線3是一個線性函數,隨亮度增長線性的減小。公式(3)的第三部分對應曲線5,曲線4和曲線5累加後獲得曲線6。可見最終的曲線在低亮度區域顯著加強亮度,在高亮度區域緩慢增長亮度。固然這裏也是能夠採用其餘具備相似做用的曲線的。
對於不一樣的Z值,圖像給出了最終的相應曲線,可見,隨着Z值的增長,曲線逐漸變爲一條直線(不變化),而咱們前面的自適應Z值計算的過程也隨着圖像亮度的增長而增長,也就是說若是圖形本來就很亮,則Z值就越大,基本上圖像就沒什麼調整了,這基本上就是自適應了。
第二步:自適應對比度加強。
通過第一步處理後,圖像的亮度自適應的獲得了加強,可是圖像的對比度明顯減小了。普通的全局對比度加強算法的過程是使亮的像素更亮,暗的像素更暗(彷佛和第一步有點相反的感受),這樣圖像的動態方範圍就更廣了,同時因爲這種方法在處理不考慮領域的信息,對於那些領域和他只有細微的差別的像素,其細節很可貴到有效提高。所以,咱們須要考慮局部的對比度加強,這種加強下,不一樣位置相同像素值得像素在加強後能夠獲得不一樣的結果,由於他們通常會有不一樣的領域像素值。噹噹前像素值比周邊像素的平均值大時,咱們增大當前像素值,而當前像素值比周邊像素平均值小時,咱們減小它的值。這樣的處理過程結果值和當前像素值的絕對值沒有關係,至於領域信息有關了。這樣,圖像的的對比度和細節都能獲得有效的提高,同時圖像的動態範圍也有獲得有效的壓縮。
一般,一個比較好的領域計算方式是高斯模糊,咱們採用的計算公式以下所示:
式中,S(x,y)爲對比度加強的結果。指數E(x,y)以下所示:
下標Conv表示卷積,注意這裏是對原始的亮度數據進行卷積。P是一個和圖像有關的參數,若是原始圖像的對比度比較差,P應該是一個較大的值,來提升圖像的總體對比度,咱們經過求原始亮度圖的全局均方差來決定P值的大小。
當全局均方差小於3時(說明圖像大部分地方基本是同一個顏色了,對比度不好),此時P值取大值,當均方差大於10時,說明原圖的對比度仍是能夠的,減小加強的程度,均方差介於3和10之間則適當線性加強。
這個對比度加強的過程分析以下,
當卷積值小於原始值時,也就是說中心點的亮度大於周邊的亮度,此時E(x,y)必然小於1,因爲I’(x,y)在前面已經歸一化,他是小於或等於1的,此時式(7)
的值必然大於原始亮度值,也就是亮的更亮(這裏的亮不是說全局的亮,而是局部的亮)。若是卷積值大於原始值,說明中心點的亮度比周邊的暗,此時E(x,y)大於1,致使處式(7)處理後的結果值更暗。
爲了獲得更好的對比度加強效果,咱們通常都使用多尺度的卷積和加強,由於各個不一樣的尺度能帶來不一樣的全局信息。通常來講,尺度較小時,能提升局部的對比度,可是可能總體看起來不是很協調,尺度較大時,能得到總體圖像的更多信息,可是細節加強的力度稍差。中等程度的尺度在細節和協調方面作了協調。因此,通常相似於MSRCR,咱們用不一樣尺度的數據來混合獲得更爲合理的結果。
尺度的大小,咱們能夠設置爲固定值,好比5,20,120等,也能夠根據圖像的大小進行一個自適應的調整。
第三步:顏色恢復。
此步比較簡單,通常就是使用下式:
Lamda一般取值就爲1,這樣能夠保證圖像總體沒有色彩偏移。
以上就是論文的主要步驟,按照這個步驟去寫代碼也不是一件很是困難的事情:
int IM_BacklightRepair(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride) { int Channel = Stride / Width; if (Channel != 3) return IM_STATUS_INVALIDPARAMETER; if ((Src == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE; if ((Width <= 0) || (Height <= 0)) return IM_STATUS_INVALIDPARAMETER; int Status = IM_STATUS_OK; int RadiusS = 5, RadiusM = 20, RadiusL = 120; const int LowLevel = 50, HighLevel = 150; const float MinCDF = 0.1f; int *Histgram = (int *)calloc(256, sizeof(int)); unsigned char *Table = (unsigned char *)malloc(256 * 256 * sizeof(unsigned char)); // 各尺度的模糊
unsigned char *BlurS = (unsigned char *)malloc(Width * Height * sizeof(unsigned char)); // 各尺度的模糊
unsigned char *BlurM = (unsigned char *)malloc(Width * Height * sizeof(unsigned char)); unsigned char *BlurL = (unsigned char *)malloc(Width * Height * sizeof(unsigned char)); unsigned char *Luminance = (unsigned char *)malloc(Width * Height * sizeof(unsigned char)); if ((Histgram == NULL) || (Table == NULL) || (BlurS == NULL) || (BlurM == NULL) || (BlurL == NULL) || (Luminance == NULL)) { Status = IM_STATUS_OUTOFMEMORY; goto FreeMemory; } float Z = 0, P = 0; Status = IM_GetLuminance(Src, Luminance, Width, Height, Stride, false); // 獲得亮度份量
if (Status != IM_STATUS_OK) goto FreeMemory; for (int Y = 0; Y < Height * Width; Y++) Histgram[Luminance[Y]]++; // 統計亮度份量的直方圖
float Sum = 0, Mean = 0, StdDev = 0; for (int Y = 0; Y < 256; Y++) Sum += Histgram[Y] * Y; // 像素的總和,注意用float類型保存
Mean = Sum / (Width * Height); // 平均值
for (int Y = 0; Y < 256; Y++) StdDev += Histgram[Y] * (Y - Mean) * (Y - Mean); StdDev = sqrtf(StdDev / (Width * Height)); // 全局圖像的均方差
int CDF = 0, L = 0; for (L = 0; L < 256; L++) { CDF += Histgram[L]; if (CDF >= Width * Height * MinCDF) break; // where L is the intensity level corresponding to a cumulative distribution function CDF of 0.1.
} if (L <= LowLevel) Z = 0; else if (L <= HighLevel) Z = (L - LowLevel) * 1.0f / (HighLevel - LowLevel); // 計算Z值
else Z = 1; if (StdDev <= 3) // 計算P值,Also, P is determined by the globaln standard deviation of the input intensity image Ix, y as
P = 3; else if (StdDev <= 10) P = (27 - 2 * StdDev) / 7.0f; else P = 1; for (int Y = 0; Y < 256; Y++) // Y表示的是I的卷積值
{ for (int X = 0; X < 256; X++) // X表示的I(原始亮度值)
{ float I = X * IM_INV255; // 公式2
I = (powf(I, 0.75f * Z + 0.25f) + (1 - I) * 0.4f * (1 - Z) + powf(I, 2 - Z)) * 0.5f; // 公式3
Table[Y * 256 + X] = IM_ClampToByte(255 * powf(I, powf((Y + 1.0f) / (X + 1.0f), P)) + 0.5f); // 公式7及8
} } Status = IM_GaussBlur(Luminance, BlurS, Width, Height, Width, RadiusS); if (Status != IM_STATUS_OK) goto FreeMemory; Status = IM_GaussBlur(Luminance, BlurM, Width, Height, Width, RadiusM); if (Status != IM_STATUS_OK) goto FreeMemory; Status = IM_GaussBlur(Luminance, BlurL, Width, Height, Width, RadiusL); if (Status != IM_STATUS_OK) goto FreeMemory; for (int Y = 0; Y < Height; Y++) { unsigned char *LinePS = Src + Y * Stride; unsigned char *LinePD = Dest + Y * Stride; int Index = Y * Width; for (int X = 0; X < Width; X++, Index++, LinePS += 3, LinePD += 3) { int L = Luminance[Index]; if (L == 0) { LinePD[0] = 0; LinePD[1] = 0; LinePD[2] = 0; } else { int Value = ((Table[L + (BlurS[Index] << 8)] + Table[L + (BlurM[Index] << 8)] + Table[L + (BlurL[Index] << 8)]) / 3); // 公式13
LinePD[0] = IM_ClampToByte(LinePS[0] * Value / L); LinePD[1] = IM_ClampToByte(LinePS[1] * Value / L); LinePD[2] = IM_ClampToByte(LinePS[2] * Value / L); } } } FreeMemory: if (Histgram != NULL) free(Histgram); if (Table != NULL) free(Table); if (BlurS != NULL) free(BlurS); if (BlurM != NULL) free(BlurM); if (BlurL != NULL) free(BlurL); if (Luminance != NULL) free(Luminance); }
爲了提升速度,咱們構建了一個2維(256*256)大小的查找表,這也是一種很常規的加速方法。
算法的代碼部分彷佛須要的解釋的部分很少,都是一些常規的處理,也基本就是一步一步按照流程來書寫的。咱們來看算法的效果。
由這兩幅圖的結果咱們初步獲得這樣的結論:一是該算法很好的保護了本來對比度和亮度就很是不錯的部分(好比兩幅圖的天空部分基本上沒有什麼變化),這比一些其餘的基於Log空間的算法,好比本人博客裏的MSRCR,全局Gamma校訂等算法要好不少,那些算法處理後本來細節很不錯的地方會發生較大的變化。這不利於圖像的總體和諧性。第二,就是暗部的加強效果確實不錯,不少細節和紋理都能更爲清晰的表達出來。
爲了更爲清晰的表達出步驟1和步驟2的處理能力,咱們把亮度圖、單獨的步驟1的結果圖和單獨的步驟的結果圖,以及步驟1和步驟2在一塊兒的結果圖比較以下:
亮度圖 全局曲線調整(步驟1)
局部對比度加強(步驟2) 步驟1和步驟2綜合
至於如何獲得這些中間結果,我想看看代碼稍做修改就應該沒有什麼大問題吧。
能夠看到,步驟1的結果圖中有一部分不是很和諧,有塊狀出現,這個在後續的步驟咱們會說起如何處理。
其實我把這四個圖放在一塊兒,我想說的就是通過這麼久的閱讀論文,我以爲全部的這類算法都是應該是這種框架,全局亮度調整+局部對比度調節。不一樣的算法只是體如今這兩個步驟使用不一樣的過程,而想MSRCR這類算法則把他們在一個過程內同時實現,這樣可能仍是沒有本算法這樣靈活。
通過嘗試,這裏的第一步全局亮度調整若是使用自動色階或者直方圖均衡化後獲得的結果並非很理想,至於爲何,暫時沒有仔細的去想。
接着咱們討論下這個算法的一些問題及其改進方法。
問題一:問題咱們注意到在上面的圖中全局亮度調整後的圖中的一些明顯的視覺瑕疵在通過和局部對比度加強混合後在最終的合成圖中彷佛表現得並非那麼誇張,可是這並不代表這個問題能夠忽視,咱們看一下下面這張圖的結果:
雖然原圖的亮度比較低,可是在視覺上原圖的可接受程度要比處理後的圖更爲好,這主要是由於處理後的圖在暗處顯示出了不少色塊和色斑,而這些色斑在原圖中是沒法直接看到的,通過加強後他們變得很是的突兀,也就是說他們加強的程度過於強烈,這個緣由是核心在於步驟的1的全局調整,在圖2中咱們看到低亮度處的調整曲線十分的陡峭,這也就意味着加強的程度特別高,會出現的一個現象就是哪怕原始的像素只差1個值,在處理後的結果中會相差幾十以上,視覺中表現爲色塊的現象。咱們從上述圖的(39,335)座標處取以20*20的放塊來觀察:
一種簡單的處理方式就是對放大的幅度進行限制,好比通常咱們認爲,先後處理的結果不該該超過4倍或者其餘值,也能夠根據圖像的內容去自適應設置這個值,這樣就能有效的避免原文出現這樣的問題,修正的代碼以下所示:
int Value = ((Table[L + (BlurS[Index] << 8)] + Table[L + (BlurM[Index] << 8)] + Table[L + (BlurL[Index] << 8)]) / 3); // 公式13
float Cof = IM_Min(Value * 1.0f / L, 4); LinePD[0] = IM_ClampToByte(LinePS[0] * Cof); LinePD[1] = IM_ClampToByte(LinePS[1] * Cof); LinePD[2] = IM_ClampToByte(LinePS[2] * Cof);
當Cof上限位不一樣值時,效果也有所區別,以下面兩圖所示:
Cof上限爲4 Cof上限爲8
根據我的的經驗,Cof設置爲4基本上能在加強效果和瑕疵之間達到一個平衡。
問題二:邊緣問題,咱們來看下面兩幅測試圖及其效果:
原圖 算法一次處理 算法二次處理後
原圖 算法二次處理後
咱們注意到,對於這兩幅圖,大部分的加強效果都是很是不錯的,特別是通過二次加強後,算法的細節和飽和度等都比較不錯,可是注意到在邊緣處,好比小孩的帽子處、聖誕樹和天空的邊緣處等等明顯發黑,也就是說他沒有獲得加強。這是怎麼回事呢,咱們以第一幅圖爲例,咱們查看下他的亮度圖單獨通過步驟1和步驟2後處理結果:
亮度圖 全局加強圖 局部對比度加強圖
能夠看出,全局加強圖在邊緣處未發現有任何問題,而局部對比度圖在邊緣處變得特別黑,咱們將亮度圖減去局部對比度加強後的圖獲得下圖:
在頭髮邊緣咱們看到了明顯的白邊。咱們分析下,在頭髮邊緣處,像素比較暗,進行卷積時,周邊是天空或者窗戶等亮區域,這樣進行卷積時,不管卷積的半徑大小是多少,獲得的結果必然是平均值大於中心像素值(並且偏離的比較遠),根據前述的對比度加強的原則,這個時候頭髮處就應該變得更黑了,而在遠離邊緣區,卷積的值不會和中心像素值有如此大的差別,應該對比度加強的程度也不會如此誇張,就出現上述最終的結果,咱們在本文第一個貼圖的地面影子加強處也用紅色線框標註出了連這種現象。
此現象在不少具備強邊緣的圖像中出現的比較明顯,而對於普通的天然照片通常難以發現,在論文做者提供的素材中彷佛未有該現象發生。
解決這個問題的方案很明顯,須要使用那些可以保證邊緣不受濾波器影響的或影響的比較少的卷積算法,這固然就是邊緣保留濾波器的範疇了,雖然如今邊緣濾波器有不少種類型,可是最爲普遍的仍是雙邊濾波器、導向濾波等等。咱們這裏使用導向濾波來試驗下是否能對結果有所改進。
在導向濾波中,導向的半徑和Eps是影響濾波器最爲核心的兩個參數,當Eps固定時,半徑很小時,圖像有一種毛絨絨的感受,稍大一點半徑,則圖像能顯示出較好的保邊效果,在非邊緣區則出現模糊效果,而當半徑進一步增大時,整個圖像的變換比較小,總體有一種淡淡的朦膿感。當半徑固定時,Eps較小時,圖像較爲清晰,隨着Eps增長,整個圖像就越模糊,到必定程度就和同半徑模糊沒有什麼區別了。通過摸索和屢次測試,我的認爲半徑參數配置爲IM_Min(Width, Height) / 50,Eps參數爲20時,能取得很是不錯的保邊效果。此時,咱們將前面的三次模糊直接用一個保邊代替,能獲得下面的處理效果:
能夠明顯的看出邊緣部分獲得了完美的解決,無任何瑕疵出現了。
使用單個保邊濾波代替多尺度的高斯模糊,我偶然在測試一幅圖中又發現了另一個問題,以下所示(只是從原圖中截取了部分顯示)。
原圖局部 使用單個導向濾波後處理的結果
看處處理後的圖,感受到很是的失望,這個是怎麼回事呢,後面我單獨測試這個圖後面亮度圖對應的導向濾波的結果,發現也是帶有明顯的紋路感受的結果。並且嘗試了雙邊、表面模糊等其餘EPF濾波器也獲得了一樣的結果,可是缺意外的發現做者原始的使用多尺度的高斯模糊的結果卻至關的不錯:
原始多尺度的處理結果 高斯單尺度處理結果
因而咱們又從新作了個試驗,把原始的多尺度也改爲單尺度的,而且尺度大小和導向濾波用的相同,獲得的結果如上圖所示。
這種分析代表對於這個圖像並非由於我用了導向濾波才致使這個紋路出現的,使用高斯濾波,在單尺度時也同樣有問題。但爲何多尺度時這個問題就消失了呢。
針對這一現象,我作了幾個方面的分析,第一,確實存在這一類圖像,在正常時咱們看不出這種塊狀,可是當進行模糊或卷積時,這種塊狀就至關的明顯了,哪怕最爲平滑的高斯模糊也會出現明顯的分塊現象,下面是對上述局部原圖分別進行了半徑10和20的高斯並適當加亮(以便顯示):
因此單尺度的高斯模糊處理後的結果也必然會帶有塊狀,可是當咱們使用多尺度時,咱們注意到尺度不一樣時這個色塊的邊界是不一樣的,而咱們多個尺度之間時相互求平均值,此時本來在一個尺度上分界很明顯的邊界線就變得較爲模糊了,下圖是上面兩個圖求平均後的結果:
相對來講色塊邊界要比前面兩個圖減弱了很多。
那麼對於使用EPF濾波器的過程,咱們若是也使用多尺度的方法,可否對結果進行改善呢,咱們這樣處理,也採用三個尺度的保邊濾波,一個比一個大,可是保持Eps的值不變,通過測試,獲得的結果如上所示,雖然仔細看仍是能看出色塊的存在,可是比原來的要好了不少。
最後,咱們仍是來簡單的談下算法的優化問題。
第一個可優化的地方是2維查找表的創建過程,開始覺得只有65536個元素的計算,因此查找表順序是沒有怎麼仔細考慮的,可是實測,這一塊佔用的時間仍是蠻可觀的,有好幾毫秒,主要是由於這裏的powf是個很耗時的過程,因此咱們主要稍微把循環的位置調換一下,就能夠減小大量的計算了,以下:
for (int Y = 0; Y < 256; Y++) // Y表示的I(原始亮度值) { float InvY = 1.0f / (Y + 0.01f); float I = Y * IM_INV255; // 公式2 I = (powf(I, 0.75f * Z + 0.25f) + (1 - I) * 0.5f * (1 - Z) + powf(I, 2 - Z)) * 0.5f; // 公式3 for (int X = 0; X < 256; X++) // X表示的是I的卷積值 { Table[Y * 256 + X] = IM_ClampToByte(255 * powf(I, powf((X + 0.01f) * InvY, P)) + 0.5f); // 公式7及8 } }
而後耗時的部分就是LinePD[0] = IM_ClampToByte(LinePS[0] * Cof);這樣的代碼了,這個也能夠經過定點化處理,並配合SSE優化作處理。
優化完成後的程序處理1080P的24位圖像大概須要50ms。
最後咱們分享幾組利用該算法處理圖像的結果。
固然,有些圖在本算法處理後還能夠加上自動色階或直方圖均衡等操做進一步提高圖像的質量。
至此,關於低照度圖像的加強算法,我想我應該不會再怎麼去碰他了,也該休息了。
本文算法的測試例程見 : http://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar,位於菜單Enhace->BackLightRepair菜單中。