圖像自動去暗角算法

 

暗角圖像是一種在現實中較爲常見的圖像,其主要特徵就是在圖像四個角有較爲顯著的亮度下降,比如下面兩幅圖。根據其形成的成因,主要有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,否則結果就會完全不對了。

    經過測試,對於沒有暗角的圖像,一般來說該算法不會對圖片產生很大的影響,很多圖片基本是無變換的,有些可能會有點區別,也就是整體變亮一點而已,因此,還是有比較好的普適性的。

     由於論文中的暗角強度減弱公式是根據一些光學原理獲得的,其可能並不符合一些軟件自己添加的暗角的規律,所以如果你用這中測試圖去測試算法,可能不會獲得非常滿意的結果,

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章