Wellner 自適應閾值二值化算法


      參考文檔: Adaptive Thresholding for the DigitalDesk.pdf

            Adaptive Thresholding Using the Integral Image.pdf 


一、問題的由來

      一個現實: 當用照像機拍攝一副黑紙白字的紙張時,照相機獲得的圖像並不是真正的黑白圖像。不管從什么角度拍攝,這幅圖像實際上是灰度或者彩色的。除非仔細的設置燈光,否則照相機所拍攝的放在桌子上的紙張圖像並不能代表原始效果。不像在掃描儀或打印機內部,想控制好桌子表面的光源是非常困難的。這個開放的空間可能會受到台燈、吊燈、窗戶、移動的影子等影響。人類的視覺系統能自動補償這些,但是機器沒有考慮到這些因素因此拍出的效果會很差。

     這個問題在處理那種高對比度的藝術線條或文字時尤為突出,因為這些東西都是真正的黑色或白色。而攝像頭會產生一副具有不同等級的灰度圖像。許多應用都必須清楚的知道圖像的那一部分是純黑或純白,以便將文字傳遞給OCR軟件去識別。這些系統無法使用灰度圖像(典型的是8位每像素),因此必須將他們轉換為黑白圖像。這有很多種方式去實現。在某些情況下,如果這些圖像最終是給人看的,這些圖像會使用一些抖動技術,以便使他們看起來更像灰度圖像。但是對於機器處理的過程,比如文字識別,選擇復制操作,或多個圖像合成,系統就不可以使用抖動的圖像。系統僅僅需要簡單的線條、文字或相對大塊的黑色和白色。從灰度圖像獲得這種黑白圖像的過程通常稱作為閾值化。

     有很多種方式來閾值化一副圖像,但是基本的處理過程都是檢查每一個灰度像素,然后決定他是白色和還是黑色。本文描述了已經開發的不同的算法來閾值一副圖像,然后提出了一種比較合適的算法。這個算法(這里我們稱之為快速自適應閾值法)可能不是最合適的。但是他對我們所描述的問題處理的相當好。

二、全局閾值法

       在某種程度上說,閾值法是對比度增強的極端形式,或者說他使得亮的像素更亮而暗的像素更暗。最簡單的(也是最常用的)方法就是將圖像中低於某個閾值的像素設置為黑色,而其他的設置為白色。那么接着問題就是如何設置這個閾值。一種可能性就是選擇所有可能取值的中間值,因此對於8位深的圖像(范圍從0到255),128將會被選中。這個方法在圖像黑色像素確實在128以下,而白色也在128以上時工作的很好。但是如果圖像過或欠曝光,圖像可能全白或全黑。所以找到圖像實際的取值范圍代替可能取值范圍會更好些。首先找到圖像中所有像素的最大值和最小值,然后取中點作為閾值。一個更好的選擇閾值的方法是不僅查看圖像實際的范圍,還要看其分布。比如說,你希望圖像類似於一副黑色線條畫,或者在白紙上的文字效果,那么你就期望大部分像素是背景顏色,而少部分是黑色。一副像素的直方圖可能如圖1所示。

 

                                      

                                圖1 

       上圖中,可以發現一個背景顏色的大峰值,以及一個黑色墨水的小的峰值。根據周圍的光線整個曲線可想向左或者向右偏移,但是在任何情況下,最理想的閾值就是在兩個峰值之間的波谷處。這在理論上很好,但是他在實際中到底表現如何呢。

        

                                   圖 2 

      圖2及其直方圖顯示整個技術工作的很好。平滑后的直方圖顯示出2個潛在的峰值,通過擬合直方圖曲線或簡單的取兩個峰值之間的平均值來計算出一個近似理想閾值並不是一件困難的事情。這不是一個典型的圖像,因為他有大量的黑色和白色的像素點。算法必須還要對類似圖3這樣的圖像進行閾值處理。這這幅圖像的直方圖中,較小的黑色峰值已經掩埋在噪音中,因此要可靠地在峰值之間確定哈一個最小值是不太可能的。

           

                                                                          圖   3

       在任何情況下,一個大的(背景)峰值總是存在的並且也容易找到,因此,一個有用的閾值策略可描述如下:

  1)  計算直方圖。

     2)  按照一定的半徑對直方圖數據進行平滑,並計算平滑后數據的最大值。平滑的目的減少噪音對最大值的影響,如圖2和圖3所示。

     3)  根據上述峰值和最小值(不包括在直方圖中為0的項)的距離按照一定的比例選擇閾值。

      試驗表明這個距離的一半能夠對很大范圍內的圖像產生相當好的效果,從非常亮到幾乎完全黑的圖像。比如,在圖3中,峰值在215處,而最小值為75,因此可以使用的閾值為145。圖4是四副在不同的光照條件下抓取的圖像以及根據上述基於直方圖技術閾值處理后的效果。盡管私服圖像有這較廣的光照范圍(可以從直方圖中看出),該算法都選擇了較為合適的閾值,而閾值處理后的圖像基本一樣。

                      

                          

                      

                     

                                圖         4

       這個基於直方圖的全局閾值技術對於如上面所舉的那些光線條件均勻或那些光線變化不多的部分圖像處理的很好。但是對於在正常辦公室光照條件下他無法獲得滿意的結果。因為對整個圖像使用一個相同的閾值,圖像的部分區域變得太白而其他地區又太黑。因此大部分文字變得不可讀,如圖5所示。

             

                                  圖         5

       從光照不均勻的紙張圖像中產生較好的二值化圖像需要一種自適應的閾值算法。這個技術根據每個像素的背景亮度來改變閾值。下面的討論都配以圖5先顯示新算法的效果。這是一個具有挑戰性的測試,因為圖像邊緣有光源,並且其在白色背景上有黑色文字(PaperWorks整個詞,以及黑色背景中的白色文字(“XEROX”),還有白色背景中的灰色文字(”The best way。。。”)還有不同的陰影和一個在單詞“PaperWorks”下很細小的水平黑色線。

三 自適應閾值

       一個理想的自適應閾值算法應該能夠對光照不均勻的圖像產生類似上述全局閾值算法對光照均勻圖像產生的效果一樣好。 為了補償或多或少的照明,每個像素的亮度需要正規化,之后才能決定某個像素時黑色還是白色。問題是如何決定每個點的背景亮度。一個簡單的方式就是在拍攝需要二值圖片之前先拍一張空白的頁面。這個空白的頁面可以當做參考圖像。對於每一個要處理的像素,在處理前對應的參考圖像像素都將從其中減去。

       只要在參考圖像和實際要處理的圖像拍攝時光照條件沒有發生任何變化,這個方法能產生非常好的效果,但是,光照條件會收到人、台燈或其他移動物體的影子的影響。如果房間有窗戶,那么光照條件還會隨着時間變化而改變。一個解決方案就是在同樣的位置,同樣的時刻拍攝一種空白的頁面作為參考,但是這如果使用掃描儀一樣不太方便

       另外一種方式就是通過一些關於圖像實際該是什么樣的假設來估計每個像素的背景亮度。例如,我們可以假設,圖像大部分是背景(也就是白色),黑色只占圖像的小部分。另外一個假設就是背景光改變相對較為緩慢。基於以上假設有很多算法都可行。由於沒有關於自適應閾值的數學理論,因此,也就沒有一個標准或者最優的方法來實現它。代替的是,有一些特別的方法要比另外一些更為使用的多。由於這些方法比較特別,因此測量他們的性能比較有用。為此,Haralick 和 Shapiro提出了以下建議:區域需和灰度調統一;區域內部應該簡單,沒有過多的小孔;相鄰的區域應該有顯著的不同值;每個部分的邊緣也應該簡單,不應凹凸不平,其空間上要准確。

       根據Pratt的理論,對於圖像二值化,還沒有任何量化性能指標提出過。似乎主要評價算法性能的方式就是簡單看看結果然后判斷其是否很好。對於文字圖像,有一個可行的量化辦法:不同光照條件下的圖片使用不同的二值化算法處理的后的結果被送往OCR系統,然后將OCR識別的結果和原文字比較。雖然該法可能有用,但是他不能用在以下描述的算法中,因為不可能給出一個看起來好的標准。對於一些交互式的應用,比如復制黏貼操作用戶必須等到二值的處理。因此另外一個重要的指標就是速度。以下部分提出了不同的自適應閾值算法已經他們產生的結果。

四、基於Wall算法的自適應閾值

       R. J. Wall開發的根據背景亮度動態計算閾值的算法描述可見《Castleman, K. Digital Image Processing. Prentice-Hall Signal Pro-cessing Series, 1979.》 。以下描述基本是按照其論文的。首先,將圖像分成較小的塊,然后分別計算每塊的直方圖。根據每個直方圖的峰值,然后為每個塊計算其閾值。然后,每個像素點的閾值根據相鄰的塊的閾值進行插值獲得。圖6是用該算法對圖5進行處理的結果。

                                                        

                                       圖     6

     這個圖像被分成9個塊(3*3),每個塊的閾值選擇為比峰值低20%。這個結果比全局閾值要好,但是他的計算量大,速度交慢。另外一個問題就是,對於有些圖像,局部的直方圖可能會被大量的黑色或白色點欺騙,導致閾值在整幅圖像中不是平滑的變化,結果可能非常糟糕,見圖7.

            

                                圖    7 

五、快速自適應閾值

       文獻中記載的大部分算法都比Wall算法更為復雜,因此需要更多的運行時間。開發一個簡單的更快的自適應閾值算法是可行的,因此這接我們介紹下相關的理論。

       算法基本的細想就是遍歷圖像,計算一個移動的平均值。如果某個像素明顯的低於這個平均值,則設置為黑色,否則設置為白色。僅需一個遍歷就夠了,用硬件去實現算法也很簡答。注意到下面的算法和IBM 1968年用硬件實現的算法的相似性是比較有趣的。

      假設Pn為圖像中位於點n處的像素。此刻我們假設圖像是由所有行按順序連接起來的一個單行。這這導致了在每行開始的時候會產生一些異常,但這個異常要比每行都從零開始要小。

        

       假設fs(n)是點n處最后 s個像素的總和:

                 

       最后的圖像T(n)是1(黑色)或0(白色)則依賴於其是否比其前s個像素的平均值的百分之t的暗。

        

       對於s使用圖像的1/8寬而t取值15似乎對不同的圖像都能產生較好的效果。圖8顯示了使用該算法從左到右掃描行的結果。

           

            圖   8                            圖   9  

       圖9是使用相同算法從右到左處理的結果,注意在這個圖像中,最左側的較小的文字是不完整的。在字符PaperWorks中也有更多的孔洞。同樣,最右側的黑色邊緣也窄很多。這主要是由於圖像的背景光源是從左到右逐漸變黑的。

       另外一個問題就是如何開始算法,或者說怎么計算g(0)。一個可能性是使用s*p0,但是由於邊緣的結果,P0不是一個典型的值。因此另外一個可行是127*s(基於8位圖像的中值)。不論如何,這兩種方案都只會影響到g的很少一部分值。在計算gs(n)時,g(0)的權重是:

             

      因此如果s=10,那么對於任何的n>6,g(0)的貢獻則少於g10(n)的10%,對於n>22,g(0)的貢獻值則少於1%。對於s=100,在8個像素之后g(0)的共享就小於10%,在68像素后則少於1%.

       如果計算均值時不是從某一個方向效果應該會更好,圖12顯示使用另外一種方法來計算平均值的效果。該方法通過計算點n對稱兩側的像素的平均值來代替某一個方向的平均值。此時f(n)的定義如下:

         

      另外一種替代的方法就是交替的從左到右及從右到左計算平均值,如下所示:

     

    

       這產生的效果和中心平均相比,沒有多大的區別。

        一個小小的修改可能會對大部分圖像產生更好的效果,那就是保留前一行的平均效果(和當前行反方向的),然后把當前行的平均值和上一上的平均值再取平均作為新的平均值,即使用:

      

       這使得閾值的計算考慮了垂直方向上的信息,產生的結果如圖:

                 

       請注意他對字符的分割的效果。這也是為數不多的保留了PaperWorks下那條水平線的算法之一。

   部分原文因現在看來已經不合理了,未做翻譯。

      從上面的東西來看,Wellner 自適應濾波閾值實際上就是對像素做指定半徑的一維平滑,然后原像素和平滑后的值做比較來決定黑還是白。文章中很大一部分篇幅都是在討論取樣的那些像素的方向問題,是完全在左側、完全在右側還是左右對稱,抑或是考慮到前一行的效果。 但是,總的來看,他只考慮到了行方向上的像素對平滑的影響。之后,Derek Bradley和Gerhard Roth 在他們的論文Adaptive Thresholding Using the Integral Image 中 提出了以W*W為模板的矩形區域的二維平滑值來代替一維加權值。從而拋開了一維平滑的方向性問題。

     當然,對於二維平滑他們提出了0(1)時間復雜度的算法,其實很簡單,就是先計算整幅圖像的累加表。然后再一次循環通過加單的加減獲得以某個像素為中心的累加值。

    下面給出一個簡答的從左到右的原始的Wellner算法的代碼(我認為那個所有行並成一行並不會好,並且如果那樣做還要考慮掃描行的無效數據):

        public static void WellneradaptiveThreshold1(FastBitmap bmp, int Radius = 5, int Threshold = 15)
        {
            if (bmp == null) throw new ArgumentNullException();
            if (bmp.Handle == IntPtr.Zero) throw new ArgumentNullException();
            if (bmp.IsGrayBitmap() == false) throw new ArgumentException("Binaryzation functions can only be applied to 8bpp graymode Image.");
            if (Radius < 0 || Radius > 255) throw new ArgumentOutOfRangeException();
            if (Threshold < 0 || Threshold > 100) throw new ArgumentOutOfRangeException();
            int Width, Height, Stride, X, Y;
            int Sum, InvertThreshold, XX, OldValue;
            byte* Pointer;
            Width = bmp.Width; Height = bmp.Height; Stride = bmp.Stride; Pointer = bmp.Pointer; InvertThreshold = 100 - Threshold;
            byte* Row = (byte*)Marshal.AllocHGlobal(Width);
            for (Y = 0; Y < Height; Y++)
            {
                Pointer = bmp.Pointer + Stride * Y;
                Sum = *Pointer * Radius;
                Win32Api.CopyMemory(Row, Pointer, Width);
                for (X = 0; X < Width; X++)
                {
                    XX = X - Radius;
                    if (XX < 0) XX = 0;
                    Sum += Row[X] - Row[XX];
                    if (Row[X] * 100 * Radius < Sum * InvertThreshold)
                        Pointer[X] = 0;
                    else
                        Pointer[X] = 255;
                }
            }
            Marshal.FreeHGlobal((IntPtr)Row);
        }

  

  這個是基於我自己的一個FastBitmap類的,要改成GDI+的 Bitmap 類也很簡單,注意二值處理一般只針對灰度圖像。

     操作中必須先對於行數據進行一個備份,因為在計算過程中會改變像素值的。

     同時也給出二維的Wellner算法的代碼供大家參考:

       public static void WellneradaptiveThreshold2(FastBitmap bmp, int Radius = 5, int Threshold = 50)
        {
            if (bmp == null) throw new ArgumentNullException();
            if (bmp.Handle == IntPtr.Zero) throw new ArgumentNullException();
            if (bmp.IsGrayBitmap() == false) throw new ArgumentException("Binaryzation functions can only be applied to 8bpp graymode Image.");
            if (Radius < 0 || Radius > 255) throw new ArgumentOutOfRangeException();
            if (Threshold < 0 || Threshold > 100) throw new ArgumentOutOfRangeException();

            int Width, Height, Stride, X, Y;
            int Sum, X1, X2, Y1, Y2, Y2Y1, InvertThreshold;
            byte* Pointer;
            Width = bmp.Width; Height = bmp.Height; Stride = bmp.Stride; Pointer = bmp.Pointer; InvertThreshold = 100 - Threshold;
            int* Integral = (int*)Marshal.AllocHGlobal(Width * Height * 4);
            int* IndexOne, IndexTwo;
            for (Y = 0; Y < Height; Y++)
            {
                Sum = 0;
                Pointer = bmp.Pointer + Stride * Y;
                IndexOne = Integral + Width * Y;
                for (X = 0; X < Width; X++)
                {
                    Sum += *Pointer;
                    if (Y == 0)
                        *IndexOne = Sum;
                    else
                        *IndexOne = *(IndexOne - Width) + Sum;
                    IndexOne++;
                    Pointer++;
                }
            }

            for (Y = 0; Y < Height; Y++)
            {
                Pointer = bmp.Pointer + Stride * Y;
                Y1 = Y - Radius; Y2 = Y + Radius;
                if (Y1 < 0) Y1 = 0;
                if (Y2 >= Height) Y2 = Height - 1;
                IndexOne = Integral + Y1 * Width;
                IndexTwo = Integral + Y2 * Width;
                Y2Y1 = (Y2 - Y1) * 100;
                for (X = 0; X < Width; X++)
                {
                    X1 = X - Radius; X2 = X + Radius;
                    if (X1 < 0) X1 = 0;
                    if (X2 >= Width) X2 = Width - 1;
                    Sum = *(IndexTwo + X2) - *(IndexOne + X2) - *(IndexTwo + X1) + *(IndexOne + X1);
                    if (*Pointer * (X2 - X1) * Y2Y1 < Sum * InvertThreshold)
                        *Pointer = 0;
                    else
                        *Pointer = 255;
                    Pointer++;
                }
            }
            Marshal.FreeHGlobal((IntPtr)Integral);
        }

  

  其中if (*Pointer * (X2 - X1) * Y2Y1 < Sum * InvertThreshold) 是為了避免耗時的除操作,提高程序速度。

      其實,上述計算平均值的方式並不是最快的,有能達到其2倍以上速度的代碼。 並且上述算法還存在一個問題,就是對於稍微大一點的圖像,累加的過程會超出int所能表達的范圍,從而使得結果不正確,當然,在C#中,我們可以使用long類型來保存結果,但是這造成2個后果:一是程序占用內存更大,二十對於現在大部分的32位操作系統,long所代表的64位數的計算速度比32位要慢不少。當然對於64位就不同了。

      關於論文中的貼的效果,似乎按照他的算法的參數是達不到的,不知道是不是因為我們的原始圖片是從其論文中截圖得到而有降質的原因。

      總的而言,這種基於局部特征的二值,在不少情況下比全局閾值的一刀切的效果要好些,對於Wellner來說,搜索的半徑的大小對於結果的影響還是很大的,不過也不一定。比如Lena這個美女圖:

           

                          原圖                                                           大律法                       S=50,T=15的效果。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM