暗角圖像是一種在現實中較爲常見的圖像,其主要特徵就是在圖像四個角有較爲顯著的亮度下降,比如下面兩幅圖。根據其形成的成因,主要有3種:natural vignetting, pixel vignetting, 以及mechanic vignetting,當然,不管他的成因如何,如果能夠把暗角消除或者局部消除,則就有很好的工程意義。
這方面的資料和論文也不是很多,我最早是在2014年看到Y. Zheng等人的論文《Single image vignetting correction》以及同樣有他們撰寫的論文《Single image vignetting correction using radial gradient symmetry》有講這方面的算法,不過其實現的複雜度較高,即使能編程實現,速度估計也很慢,其實用性就不高了。
前不久,偶爾的機會看到一篇名爲《Single-Image Vignetting Correction by Constrained Minimization of log-Intensity Entropy》的論文,並且在github上找到了相關的一些參考代碼,雖然那個代碼寫的實在是噁心加無聊,但是對於我來說這並不重要,只要稍有參考,在結合論文那自己來實現就不是難事了。
論文裏的算法核心其實說起來也沒啥難的,我就我的理解來簡單的描述下:
第一:去暗角可以說是陰影校正的一種特例,而將整副圖像的熵最小化也被證明爲進行陰影校正的一種有效方法,但是普通的熵在優化過程中會優化到局部最優的。因此論文中提出了一種對數熵的概念(Log-Intensity Entropy),論文中用數據做了說明,假設一副普通正常的圖像其直方圖是單峯分佈,那麼如果這幅圖像有暗角,其直方圖必然會存在另外一個低明度的分佈,如下圖所示:
我們校正暗角的過程就是使低明度的分佈向原來的正常明度靠近,由上圖第一行的數據可以看到,普通的熵計算直到兩個直方圖有部分重疊的時候熵纔會下降,之前熵一直都是增加的,而對數熵則在沒有重疊前至少是保持不增的,因此能夠更好的獲取全局最優解。
那麼論文提出的對數熵的計算公式爲:
首先先將亮度進行對數映射,映射公式爲:
也就是將[0,255]內的像素值映射到[0, N-1]內,但不是線性映射,而是基於對數關係的映射,通常N就是取256,這樣映射後的像素範圍還是[0,255],但是注意這裏的i(L)已經是浮點數了。我們繪製出N等於256時上式的曲線:
可見,這種操作實際上把圖像整體調亮了。
由於映射後的色階已經是浮點數了,因此,直方圖信息的統計就必須換種方式了,論文給出的公式爲:
公式很複雜, 其實就是有點類似線性插值那種意思,不認識了那兩個數學符號了,就是向上取整和向下取整。
這樣的對數熵直方圖信息會由於巨大的色階調整,導致很多色階是沒有直方圖信息的,一般可以對這個直方圖信息進行下高斯平滑的,得到新的直方圖。
最後圖像的對數熵,計算方法如下:
其中:
第二:論文根據資料《Radiometric alignment and vignetting calibration》提出了一個暗角圖像亮度下降的關係式,而我去看《Radiometric alignment and vignetting calibration》這篇論文時,他的公式又是從《Vignette and exposure calibration and compensation》中獲取的,所以這個論文的作者寫得文章還不夠嚴謹。這個公式是一個擁有五個未知參數的算式,如下所示:
其中:
其中,x和y是圖像每一點的座標,而則表示暗角的中心位置,他們和a、b、c均爲未知量。
我們可以看到,當r=0時,校正係數爲1,即無需校正。當r=1時,校正係數爲1+a+b+c。
那麼經過暗角校正後的圖像就爲:
按照我們的常識,暗角圖像從暗角的中心點想四周應該是逐漸變暗的,根據上式函數g應該是隨着r單調遞增的(因爲我們是校正暗角圖像,所以越靠近邊緣上式的乘法中g值也就應該越大),因此函數g的一階導數應該大於0,即:
同時,我們注意到參數r的範圍很明顯應該在[0,1]之間,這樣上式則可以轉換爲:
如果令,則上式變爲:
根據二次不等式相關知識,令:
則論文總結了滿足下述關係式的a,b,c就能滿足上述要求了:
這個我也沒有去驗證了。
第三: 上面描述了校正暗角圖像的公式(帶參數)以及評價一副圖像是否有暗角的指標,那麼最後一步就是用這個指標來確定公式的參數。我們未知的參數有5個,即a、b、c以及暗角的中心點。解這種受限的最優問題是有專門的算法的,也是非常計算耗時的。因此,作者提出了一種快速的算法:Hill climbing with rejection of invalid solutions.
我稍微看了下這個算法,確實是個不錯的想法,不過我並沒有去實踐,我採用了另外一種粗略的優化方式。
首先,很明顯,爲了計算這些最優參數,我們沒有必要直接在原圖大小上直接計算,這點在原論文也有說明,我們即使把他們的寬高分別縮小到原圖的1/5甚至1/10計算出來的結果也不會有太大的差異,而這些參數的差異對最終的的結果影響也不大,但是計算量就能減少到原來的1/25和1/100。
接着,我們觀察到a、b以及c的最優結果範圍一般都在-2和2之間,並且從g的計算公式中知道,由於r是屬於0和1之間的正數,r^2, r^4, r^6在數值遞減的非常快,比如r=0.8,則三者對應的結果就分別爲0.64、0.4096、0.2621,因此,a和b及c在公式中的對最後結果的影響也依次越來越小。
那麼,我們可以參考以前的對比度保留之彩色圖像去色算法---基礎算法也可以上檔次一文中的優化方式,把a, b ,c 三個參數分別在[-2,2]之間離散化,考慮到參數稍微差異不會對結果有太大的影響,以及a、b、c的重要性,我們可以設置a、b、c三者的離散間隔分別爲0.2、0.3、0.4,然後綜合上述判斷a、b、c是否爲合理組合的函數,離散取樣的計算量組合大概有300種可能,對小圖計算着300種可能性的耗時是完全可以接受的,甚至考慮極端一點,把c的計算放到循環外側,即C取固定值0,然後計算出優選的a和b值後,在計算C值。
上述計算過程並未考慮暗角中心點的範圍,我們是固定把暗角的中心點放置在圖像的正中心位置的,即 (Width/2, Height /2),實際上,對於大部分拍攝的圖來說,暗角就是位於中心位置的,因此這種假設也無可厚非,因爲暗角中心計算的增加必然會嚴重增加計算量, 爲了求出暗角中心的合理位置,我們在計算出上述a、b、c後,在小圖中以一定步長按照公式計算出粗略的中心位置,再放大到原圖中去。
計算出上述a、b、c以及中心點後,就可以再次按照校正公式來進行校正了,注意暗角的影響對每個通道都是等同的,因此,每個通道都應該乘以相同的值。
下面貼出一些用論文中的算法處理的結果圖:
注意到上面最後一副圖的結果,那個女的婚紗以及衣服那些地方已經嚴重的過曝了,我不清楚理論上造成整個原因的是什麼,但是如果把計算i(L)的公式中的N修改爲小一點的值,比如64,則可以避免這個結果。
github上的那個代碼則對這個對數熵的過程做了一點改造,這個改造相當暴力,就是什麼呢,他把原來的[0,255]直接量化爲8個等級,量化的依據是整形LOG2函數,即0->0,[1, 2]->1,[3, 4]->3,[5,8]->4,[9,16]->5,[17,32]->6,[33,64]->7,[65,128]->8,[129,255]->9, 原來的一條曲線映射函數變成了階躍函數了。這樣直方圖實際上只有9個值了,那麼也不需要什麼直方圖插值和高斯模糊了,直方圖則可以用整形表示,相對來說速度也能有很大的提升,並且也能克服上述最後一張圖片出現的那個瑕疵,其結果如下:
最後我們貼一些代碼對上述過程予以解釋:
第一個是判斷a、b、c是否爲合理值的函數:
// 按論文中公式18得條件判斷是否是合理的參數 bool IsValidABC_Original(float A, float B, float C) { const int MAX_BRIGHTNESS_MULTIPLICATION = 3; if ((1 + A + B + C) > MAX_BRIGHTNESS_MULTIPLICATION) return false; // 當r==1時,出現最大的亮度調整 if (C == 0) { if (A <= 0 || (A + 2 * B <= 0)) // 如果C==0,則根據公式(15)知,當r==0時,A必須大於0,而當r==1時,A+2B必須大於0 return false; } else { float D = 4 * B * B - 12 * A * C; float qMins = (-2 * B - sqrtf(D)) / (6 * C); // 公式(17) float qPlus = (-2 * B + sqrtf(D)) / (6 * C); if (C < 0) { if (D >= 0) { if (qMins > 0 || qPlus < 1) return false; } else return false; } else { if (D >= 0) { if (!((qMins <= 0 && qPlus <= 0) || (qMins >= 1 && qPlus >= 1))) return false; } } } return true; }
可見除了文中公式的一些限制,我們還增加了幾個額外的小限制,比如最大亮度調節比例爲3等等。
第二個是計算指定參數下計算對數熵的過程:
// 計算不同參數修復後的圖像的整體對數熵 float CalculateEntropyFromImage(unsigned char *Src, int Width, int Height, float A, float B, float C, int CenterX, int CenterY) { float Histgram[256] = { 0 }; float Invert = 1.0f / (CenterX * CenterX + CenterY * CenterY + 1); float Mul_Factor = 256.f / log(256.0f); for (int Y = 0; Y < Height; Y++) { unsigned char *LinePS = Src + Y * Width * 4; int SquareY = (Y - CenterY) * (Y - CenterY); for (int X = 0; X < Width; X++) { int Intensity = (LinePS[0] + (LinePS[1] << 1) + LinePS[2]) >> 2; // 公式(2) int RadiusSqua2 = (X - CenterX) * (X - CenterX) + SquareY; float R = RadiusSqua2 * Invert; // 公式(12) float Gain = 1 + (R * (A + R * (B + R * C))); // gain = 1 + a * r^2 + b * r^4 + c * r^6 ,公式(11) float Correction = Gain * Intensity; // 直接校正後的結果值 if (Correction >= 255) { Correction = 255; // It is possible that, due to local intensity increases applied by devignetting, the corrected image intensity range exceeds 255. Histgram[255]++; // In this case the algorithm simply adds histogram bins at the upper end without rescaling the distribution, } else { float Pos = Mul_Factor * log(Correction + 1); // 公式(6) int Int = int(Pos); Histgram[Int] += 1 - (Pos - Int); // 公式(7) Histgram[Int + 1] += Pos - Int; } LinePS += 4; } } float TempHist[256 + 2 * 4]; // SmoothRadius = 4 TempHist[0] = Histgram[4]; TempHist[1] = Histgram[3]; TempHist[2] = Histgram[2]; TempHist[3] = Histgram[1]; TempHist[260] = Histgram[254]; TempHist[261] = Histgram[253]; TempHist[262] = Histgram[252]; TempHist[263] = Histgram[251]; memcpy(TempHist + 4, Histgram, 256 * sizeof(float)); for (int X = 0; X < 256; X++) // 公式(8),進行一個平滑操作 Histgram[X] = (TempHist[X] + 2 * TempHist[X + 1] + 3 * TempHist[X + 2] + 4 * TempHist[X + 3] + 5 * TempHist[X + 4] + 4 * TempHist[X + 5] + 3 * TempHist[X + 6] + 2 * TempHist[X + 7]) + TempHist[X + 8] / 25.0f; return CalculateEntropyFromHistgram_Original(Histgram, 256); }
其中計算熵的函數爲:
// 從直方圖中計算熵值,原論文中直方圖肯定是浮點數 float CalculateEntropyFromHistgram_Original(float Histgram[], int Length) { float Sum = 0; for (int X = 0; X < Length; X++) { Sum += Histgram[X]; } float Entropy = 0; for (int X = 0; X < Length; X++) { if (Histgram[X] == 0) continue; float p = (float)Histgram[X] / Sum; Entropy += p * logf(p); } return -Entropy; }
其中
int Int = int(Pos); Histgram[Int] += 1 - (Pos - Int); // 公式(7) Histgram[Int + 1] += Pos - Int;
就是公式7所描述的過程的實現。
論文中的高斯模糊,我這裏只是藉助了一個簡單的線性模糊來代替,這個不會對結果造成本質的區別。
最後圖像的校正代碼大概如下:
int Devignetting_Original(unsigned char *Src, unsigned char *Dest, int Width, int Height) { if ((Src == NULL) || (Dest == NULL)) return STATUS_NULLREFRENCE; if ((Width <= 0) || (Height <= 0)) return STATUS_INVALIDPARAMETER; const float Step = 0.2f; //` 粗選A\B\C三個變量的步長 float SmallestEntropy = 1000000000000.0f; float A = 0, B = 0, C = 0; int CenterX = Width / 2, CenterY = Height / 2; // 中心就默認爲圖片中心 for (int X = -10; X <= 10; X++) // 多次測試,表面最優的A\B\C的範圍均在[-2,2]之間 { for (int Y = -10; Y <= 10; Y++) { for (int Z = -10; Z <= 10; Z++) { if (IsValidABC_Original(X * Step, Y * Step, Z * Step) == true) // 判斷這個組合時候有效 { float Entropy = CalculateEntropyFromImage(Src, Width, Height, X * Step, Y * Step, Z * Step, CenterX, CenterY); if (Entropy < SmallestEntropy) // 取熵值最小的 { A = X * Step; B = Y * Step; C = Z * Step; SmallestEntropy = Entropy; } } } } } float Invert = 1.0 / (CenterX * CenterX + CenterY * CenterY + 1); for (int Y = 0; Y < Height; Y++) { byte *LinePS = Src + Y * Width * 4; byte *LinePD = Dest + Y * Width * 4; int SquareY = (Y - CenterY) * (Y - CenterY); for (int X = 0; X < Width; X++) { int RadiusSqua2 = (X - CenterX) * (X - CenterX) + SquareY; float R2 = RadiusSqua2 * Invert; // 公式12 float Gain = 1 + (R2 * (A + R2 * (B + R2 * C))); // 公式11 LinePD[0] = ClampToByte(LinePS[0] * Gain); // 增益 LinePD[1] = ClampToByte(LinePS[1] * Gain); LinePD[2] = ClampToByte(LinePS[2] * Gain); LinePD += 4; LinePS += 4; } } return STATUS_OK; }
上面的代碼是未經特別的優化的,只是表達了大概的意思,並且把暗角的中心點默認爲圖像的中心點,如果暗角的中心點不是圖像中心,要注意計算r時可能會出現r>1的情況,這個時候一定要注意重置r=1,否則結果就會完全不對了。
經過測試,對於沒有暗角的圖像,一般來說該算法不會對圖片產生很大的影響,很多圖片基本是無變換的,有些可能會有點區別,也就是整體變亮一點而已,因此,還是有比較好的普適性的。
由於論文中的暗角強度減弱公式是根據一些光學原理獲得的,其可能並不符合一些軟件自己添加的暗角的規律,所以如果你用這中測試圖去測試算法,可能不會獲得非常滿意的結果,