PhotoShop算法原理解析系列 - 風格化---》查找邊緣。

      之因此不寫系列文章1、系列文章二這樣的標題,是由於我不知道我能堅持多久。我知道我對事情的表達能力和語言的豐富性方面的天賦不高。而一段代碼須要我去用心的把他從基本原理--》初步實現--》優化速度 等過程用文字的方式表述清楚,恐怕不是一件很容易的事情。 html

      我所掌握的一些Photoshop中的算法,不能說百分之一百就是正確的,可是從執行的效果中,大的方向確定是沒有問題的。算法

      目前,從別人的文章、開源的代碼以及本身的思考中我掌握的PS的算法可能有近100個吧。若是時間允許、自身的耐心允許,我會將這些東西慢慢的整理開來,雖然在不少人看來,這些算法並不具備什麼研究的價值了,畢竟人家都已經商業化了。說的也有道理,我姑且把他做爲自我欣賞和自我知足的一種方式吧。數組

      今天,咱們講講查找邊緣算法。可能我說了原理,不少人就不會看下去了,可有幾人層仔細的研究過呢。ide

  先貼個效果圖吧:函數

    

  原理:常見的Sobel邊緣算子的結果進行反色便可post

      爲了能吸引你繼續看下去,我先給出個人代碼的執行速度: 針對3000*4000*3的數碼圖片,處理時間300ms優化

      何爲Sobel,從百度抄幾張圖過來了並修改地址後: this

                            

  對上面兩個式子不作過多解釋,你只須要知道其中A爲輸入圖像,把G做爲A的輸出圖像就能夠了,最後還要作一步: G=255-G,就是查找邊緣算法。編碼

      查找邊緣類算法都有個問題,對圖像物理邊緣處的像素如何處理,在平日的處理代碼中,不少人就是忽略四個邊緣的像素,做爲專業的圖像處理軟件,這但是違反最基本的原則的。對邊緣進行的單獨的代碼處理,又會給編碼帶來冗餘和繁瑣的問題。解決問題的最簡單又高效的方式就是採用哨兵邊界。url

      寫多了特效類算法的都應該知道,除了那種對單個像素進行處理的算法不須要對原始圖像作個備份(不必定去全局備份),那些須要領域信息的算法因爲算法的前一步修改了一個像素,而算法的當前步須要未修改的像素值,所以,通常這種算法都會在開始前對原始圖像作個克隆,在計算時,須要的領域信息從克隆的數據中讀取。若是這個克隆的過程不是完徹底全的克隆,而是擴展適當邊界後再克隆,就有可能解決上述的邊界處理問題。

  好比對下面的一個圖,19×14像素大小,咱們的備份圖爲上下左右各擴展一個像素的大小,並用邊緣的值填充,變爲21*16大小:

           

  這樣,在計算原圖的3*3領域像素時,從擴展後的克隆圖對應點取樣,就不會出現不在圖像範圍內的問題了,編碼中便可以少不少判斷,可讀性也增強了。

      在計算速度方面,注意到上面的計算式G中有個開方運算,這是個耗時的過程,因爲圖像數據的特殊性,都必須是整數,能夠採用查找表的方式優化速度,這就須要考慮表的創建。

       針對本文的具體問題,咱們分兩步討論,第一:針對根號下的全部可能狀況創建查找表。看看GX和GY的計算公式,考慮下二者的平方和的最大值是多少,可能要考慮一會吧。第二:就是隻創建0^2到255^2範圍內的查找表,而後確保根號下的數字不大於255^2。爲何能夠這樣作,就是由於圖像數據的最大值就是255,若是根號下的數字大於255^2,在求出開方值後,仍是須要規整爲255的。所以,本算法中應該取後者。

      貼出代碼:

private void CmdFindEdgesArray_Click(object sender, EventArgs e) { int X, Y; int Width, Height, Stride, StrideC, HeightC; int Speed, SpeedOne, SpeedTwo, SpeedThree; int BlueOne, BlueTwo, GreenOne, GreenTwo, RedOne, RedTwo; int PowerRed, PowerGreen, PowerBlue; Bitmap Bmp = (Bitmap)Pic.Image; if (Bmp.PixelFormat != PixelFormat.Format24bppRgb) throw new Exception("不支持的圖像格式."); byte[] SqrValue = new byte[65026]; for (Y = 0; Y < 65026; Y++) SqrValue[Y] = (byte)(255 - (int)Math.Sqrt(Y));      // 計算查找表,注意已經砸查找表裏進行了反色
 Width = Bmp.Width; Height = Bmp.Height; Stride = (int)((Bmp.Width * 3 + 3) & 0XFFFFFFFC); StrideC = (Width + 2) * 3; HeightC = Height + 2;                                 // 寬度和高度都擴展2個像素

    byte[] ImageData = new byte[Stride * Height];                                    // 用於保存圖像數據,(處理先後的都爲他)
    byte[] ImageDataC = new byte[StrideC * HeightC];                                // 用於保存擴展後的圖像數據

    fixed (byte* Scan0 = &ImageData[0]) { BitmapData BmpData = new BitmapData(); BmpData.Scan0 = (IntPtr)Scan0;                                              // 設置爲字節數組的的第一個元素在內存中的地址
        BmpData.Stride = Stride; Bmp.LockBits(new Rectangle(0, 0, Bmp.Width, Bmp.Height), ImageLockMode.ReadWrite | ImageLockMode.UserInputBuffer, PixelFormat.Format24bppRgb, BmpData); Stopwatch Sw = new Stopwatch();                                             // 只獲取計算用時
 Sw.Start(); for (Y = 0; Y < Height; Y++) { System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + 1), 3);        // 填充擴展圖的左側第一列像素(不包括第一個和最後一個點)
            System.Buffer.BlockCopy(ImageData, Stride * Y + (Width - 1) * 3, ImageDataC, StrideC * (Y + 1) + (Width + 1) * 3, 3);  // 填充最右側那一列的數據
            System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + 1) + 3, Width * 3); } System.Buffer.BlockCopy(ImageDataC, StrideC, ImageDataC, 0, StrideC);              // 第一行
        System.Buffer.BlockCopy(ImageDataC, (HeightC - 2) * StrideC, ImageDataC, (HeightC - 1) * StrideC, StrideC);    // 最後一行 

        for (Y = 0; Y < Height; Y++) { Speed = Y * Stride; SpeedOne = StrideC * Y; for (X = 0; X < Width; X++) { SpeedTwo = SpeedOne + StrideC;          // 儘可能減小計算
                SpeedThree = SpeedTwo + StrideC;        // 下面的就是嚴格的按照Sobel算字進行計算,代碼中的*2通常會優化爲移位或者兩個Add指令的,若是你不放心,固然能夠直接改爲移位
                BlueOne = ImageDataC[SpeedOne] + 2 * ImageDataC[SpeedTwo] + ImageDataC[SpeedThree] - ImageDataC[SpeedOne + 6] - 2 * ImageDataC[SpeedTwo + 6] - ImageDataC[SpeedThree + 6]; GreenOne = ImageDataC[SpeedOne + 1] + 2 * ImageDataC[SpeedTwo + 1] + ImageDataC[SpeedThree + 1] - ImageDataC[SpeedOne + 7] - 2 * ImageDataC[SpeedTwo + 7] - ImageDataC[SpeedThree + 7]; RedOne = ImageDataC[SpeedOne + 2] + 2 * ImageDataC[SpeedTwo + 2] + ImageDataC[SpeedThree + 2] - ImageDataC[SpeedOne + 8] - 2 * ImageDataC[SpeedTwo + 8] - ImageDataC[SpeedThree + 8]; BlueTwo = ImageDataC[SpeedOne] + 2 * ImageDataC[SpeedOne + 3] + ImageDataC[SpeedOne + 6] - ImageDataC[SpeedThree] - 2 * ImageDataC[SpeedThree + 3] - ImageDataC[SpeedThree + 6]; GreenTwo = ImageDataC[SpeedOne + 1] + 2 * ImageDataC[SpeedOne + 4] + ImageDataC[SpeedOne + 7] - ImageDataC[SpeedThree + 1] - 2 * ImageDataC[SpeedThree + 4] - ImageDataC[SpeedThree + 7]; RedTwo = ImageDataC[SpeedOne + 2] + 2 * ImageDataC[SpeedOne + 5] + ImageDataC[SpeedOne + 8] - ImageDataC[SpeedThree + 2] - 2 * ImageDataC[SpeedThree + 5] - ImageDataC[SpeedThree + 8]; PowerBlue = BlueOne * BlueOne + BlueTwo * BlueTwo; PowerGreen = GreenOne * GreenOne + GreenTwo * GreenTwo; PowerRed = RedOne * RedOne + RedTwo * RedTwo; if (PowerBlue > 65025) PowerBlue = 65025;           // 處理掉溢出值
                if (PowerGreen > 65025) PowerGreen = 65025; if (PowerRed > 65025) PowerRed = 65025; ImageData[Speed] = SqrValue[PowerBlue];             // 查表
                ImageData[Speed + 1] = SqrValue[PowerGreen]; ImageData[Speed + 2] = SqrValue[PowerRed]; Speed += 3;                                  // 跳往下一個像素
                SpeedOne += 3; } } Sw.Stop(); this.Text = "計算用時: " + Sw.ElapsedMilliseconds.ToString() + " ms"; Bmp.UnlockBits(BmpData); // 必須先解鎖,不然Invalidate失敗 
 } Pic.Invalidate(); }

  爲簡單的起見,這裏先是用的C#的一維數組實現的,而且計時部分未考慮圖像數據的獲取和更新, 由於真正的圖像處理過程當中圖像數據確定是已經得到的了。

     針對上述代碼,編譯爲Release模式後,執行編譯後的EXE,對於3000*4000*3的彩色圖像,耗時約480ms,若是你是在IDE的模式先運行,記得必定要在選項--》調試--》常規裏不勾選    在模塊加載時取消JIT優化(僅限託管)一欄。    

       

     上述代碼中的填充克隆圖數據時並無新建一副圖,而後再填充其中的圖像數據,而是直接填充一個數組,圖像其實不就是一片連續內存加一點頭信息嗎,頭信息已經有了,因此只要一片內存就夠了。

     克隆數據的填充採用了系統Buffer.BlockCopy函數,該函數相似於咱們之前經常使用CopyMemory,速度很是快。

     爲進一步調高執行速度,咱們首先來看看算法的關鍵耗時部位的代碼,即for (X = 0; X < Width; X++)內部的代碼,咱們取一行代碼的反編譯碼來看看:

 BlueOne = ImageDataC[SpeedOne] + 2 * ImageDataC[SpeedTwo] + ImageDataC[SpeedThree] - ImageDataC[SpeedOne + 6] - 2 * ImageDataC[SpeedTwo + 6] - ImageDataC[SpeedThree + 6]; 

00000302 cmp ebx,edi 00000304 jae 0000073C             // 數組是否越界? 0000030a movzx eax,byte ptr [esi+ebx+8]    //  將ImageDataC[SpeedOne]中的數據傳送的eax寄存器 0000030f mov dword ptr [ebp-80h],eax 00000312 mov edx,dword ptr [ebp-2Ch] 00000315 cmp edx,edi 00000317 jae 0000073C            // 數組是否越界?            0000031d movzx edx,byte ptr [esi+edx+8]   //   將ImageDataC[SpeedTwo]中的數據傳送到edx寄存器 00000322 add edx,edx             // 計算2*ImageDataC[SpeedTwo]     00000324 add eax,edx             // 計算ImageDataC[SpeedOne]+2*ImageDataC[SpeedTwo],並保存在eax寄存器中            00000326 cmp ecx,edi 00000328 jae 0000073C 0000032e movzx edx,byte ptr [esi+ecx+8]   // 將ImageDataC[SpeedThree]中的數據傳送到edx寄存器 00000333 mov dword ptr [ebp+FFFFFF78h],edx 00000339 add eax,edx 0000033b lea edx,[ebx+6] 0000033e cmp edx,edi 00000340 jae 0000073C 00000346 movzx edx,byte ptr [esi+edx+8] 0000034b mov dword ptr [ebp+FFFFFF7Ch],edx 00000351 sub eax,edx 00000353 mov edx,dword ptr [ebp-2Ch] 00000356 add edx,6 00000359 cmp edx,edi 0000035b jae 0000073C 00000361 movzx edx,byte ptr [esi+edx+8] 00000366 add edx,edx 00000368 sub eax,edx 0000036a lea edx,[ecx+6] 0000036d cmp edx,edi 0000036f jae 0000073C 00000375 movzx edx,byte ptr [esi+edx+8] 0000037a mov dword ptr [ebp+FFFFFF74h],edx 00000380 sub eax,edx 00000382 mov dword ptr [ebp-30h],eax

   上述彙編碼我只註釋一點點,其中最0000073c  標號,咱們跟蹤後返現是調用了另一個函數:

             0000073c  call        685172A4 

      咱們看到在獲取每個數組元素前,都必須執行一個cmp 和 jae指令,從分析我認爲這裏是作相似於判斷數組的下標是否越界之類的工做的。若是咱們能確保咱們的算法那不會產生越界,這部分代碼有很用呢,不是耽誤我作正事嗎。

      爲此,我認爲須要在C#中直接利用指針來實現算法,C#中有unsafe模式,也有指針,因此很方便,並且指針的表達便可以用*,也能夠用[],好比*(P+4) 和P[4]是一個意思。那麼只要作不多的修改就能夠將上述代碼修改成指針版。

    

private void CmdFindEdgesPointer_Click(object sender, EventArgs e) { int X, Y; int Width, Height, Stride, StrideC, HeightC; int Speed, SpeedOne, SpeedTwo, SpeedThree; int BlueOne, BlueTwo, GreenOne, GreenTwo, RedOne, RedTwo; int PowerRed, PowerGreen, PowerBlue; Bitmap Bmp = (Bitmap)Pic.Image; if (Bmp.PixelFormat != PixelFormat.Format24bppRgb) throw new Exception("不支持的圖像格式."); byte[] SqrValue = new byte[65026]; for (Y = 0; Y < 65026; Y++) SqrValue[Y] = (byte)(255 - (int)Math.Sqrt(Y));      // 計算查找表,注意已經砸查找表裏進行了反色
 Width = Bmp.Width; Height = Bmp.Height; Stride = (int)((Bmp.Width * 3 + 3) & 0XFFFFFFFC); StrideC = (Width + 2) * 3; HeightC = Height + 2;                                 // 寬度和高度都擴展2個像素

        byte[] ImageData = new byte[Stride * Height];                                    // 用於保存圖像數據,(處理先後的都爲他)
        byte[] ImageDataC = new byte[StrideC * HeightC];                                 // 用於保存擴展後的圖像數據

        fixed (byte* P = &ImageData[0], CP = &ImageDataC[0], LP = &SqrValue[0]) { byte* DataP = P, DataCP = CP, LutP = LP; BitmapData BmpData = new BitmapData(); BmpData.Scan0 = (IntPtr)DataP;                                              // 設置爲字節數組的的第一個元素在內存中的地址
            BmpData.Stride = Stride; Bmp.LockBits(new Rectangle(0, 0, Bmp.Width, Bmp.Height), ImageLockMode.ReadWrite | ImageLockMode.UserInputBuffer, PixelFormat.Format24bppRgb, BmpData); Stopwatch Sw = new Stopwatch();                                             // 只獲取計算用時
 Sw.Start(); for (Y = 0; Y < Height; Y++) { System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + 1), 3);        // 填充擴展圖的左側第一列像素(不包括第一個和最後一個點)
                System.Buffer.BlockCopy(ImageData, Stride * Y + (Width - 1) * 3, ImageDataC, StrideC * (Y + 1) + (Width + 1) * 3, 3);  // 填充最右側那一列的數據
                System.Buffer.BlockCopy(ImageData, Stride * Y, ImageDataC, StrideC * (Y + 1) + 3, Width * 3); } System.Buffer.BlockCopy(ImageDataC, StrideC, ImageDataC, 0, StrideC);              // 第一行
            System.Buffer.BlockCopy(ImageDataC, (HeightC - 2) * StrideC, ImageDataC, (HeightC - 1) * StrideC, StrideC);    // 最後一行 

            for (Y = 0; Y < Height; Y++) { Speed = Y * Stride; SpeedOne = StrideC * Y; for (X = 0; X < Width; X++) { SpeedTwo = SpeedOne + StrideC;          // 儘可能減小計算
                    SpeedThree = SpeedTwo + StrideC;        // 下面的就是嚴格的按照Sobel算字進行計算,代碼中的*2通常會優化爲移位或者兩個Add指令的,若是你不放心,固然能夠直接改爲移位
                    BlueOne = DataCP[SpeedOne] + 2 * DataCP[SpeedTwo] + DataCP[SpeedThree] - DataCP[SpeedOne + 6] - 2 * DataCP[SpeedTwo + 6] - DataCP[SpeedThree + 6]; GreenOne = DataCP[SpeedOne + 1] + 2 * DataCP[SpeedTwo + 1] + DataCP[SpeedThree + 1] - DataCP[SpeedOne + 7] - 2 * DataCP[SpeedTwo + 7] - DataCP[SpeedThree + 7]; RedOne = DataCP[SpeedOne + 2] + 2 * DataCP[SpeedTwo + 2] + DataCP[SpeedThree + 2] - DataCP[SpeedOne + 8] - 2 * DataCP[SpeedTwo + 8] - DataCP[SpeedThree + 8]; BlueTwo = DataCP[SpeedOne] + 2 * DataCP[SpeedOne + 3] + DataCP[SpeedOne + 6] - DataCP[SpeedThree] - 2 * DataCP[SpeedThree + 3] - DataCP[SpeedThree + 6]; GreenTwo = DataCP[SpeedOne + 1] + 2 * DataCP[SpeedOne + 4] + DataCP[SpeedOne + 7] - DataCP[SpeedThree + 1] - 2 * DataCP[SpeedThree + 4] - DataCP[SpeedThree + 7]; RedTwo = DataCP[SpeedOne + 2] + 2 * DataCP[SpeedOne + 5] + DataCP[SpeedOne + 8] - DataCP[SpeedThree + 2] - 2 * DataCP[SpeedThree + 5] - DataCP[SpeedThree + 8]; PowerBlue = BlueOne * BlueOne + BlueTwo * BlueTwo; PowerGreen = GreenOne * GreenOne + GreenTwo * GreenTwo; PowerRed = RedOne * RedOne + RedTwo * RedTwo; if (PowerBlue > 65025) PowerBlue = 65025;           // 處理掉溢出值
                    if (PowerGreen > 65025) PowerGreen = 65025; if (PowerRed > 65025) PowerRed = 65025; DataP[Speed] = LutP[PowerBlue];                     // 查表
                    DataP[Speed + 1] = LutP[PowerGreen]; DataP[Speed + 2] = LutP[PowerRed]; Speed += 3;                                         // 跳往下一個像素
                    SpeedOne += 3; } } Sw.Stop(); this.Text = "計算用時: " + Sw.ElapsedMilliseconds.ToString() + " ms"; Bmp.UnlockBits(BmpData); // 必須先解鎖,不然Invalidate失敗 
 } Pic.Invalidate(); }

     一樣的效果,一樣的圖像,計算用時330ms。

     咱們在來看看相同代碼的彙編碼:

BlueOne = DataCP[SpeedOne] + 2 * DataCP[SpeedTwo] + DataCP[SpeedThree] - DataCP[SpeedOne + 6] - 2 * DataCP[SpeedTwo + 6] - DataCP[SpeedThree + 6]; 

00000318 movzx eax,byte ptr [esi+edi] 0000031c mov dword ptr [ebp-74h],eax 0000031f movzx edx,byte ptr [esi+ebx] 00000323 add edx,edx 00000325 add eax,edx 00000327 movzx edx,byte ptr [esi+ecx] 0000032b mov dword ptr [ebp-7Ch],edx 0000032e add eax,edx 00000330 movzx edx,byte ptr [esi+edi+6] 00000335 mov dword ptr [ebp-78h],edx 00000338 sub eax,edx 0000033a movzx edx,byte ptr [esi+ebx+6] 0000033f add edx,edx 00000341 sub eax,edx 00000343 movzx edx,byte ptr [esi+ecx+6] 00000348 mov dword ptr [ebp-80h],edx 0000034b sub eax,edx 0000034d mov dword ptr [ebp-30h],eax

      生產的彙編碼簡潔,意義明確,對比下少了不少指令。固然速度會快不少。

      注意這一段代碼:

  fixed (byte* P = &ImageData[0], CP = &ImageDataC[0], LP = &SqrValue[0]) { byte* DataP = P, DataCP = CP, LutP = LP;

     若是你把更換爲:

  fixed (byte* DataP = &ImageData[0], DataCP = &ImageDataC[0], LutP = &SqrValue[0]) {

      代碼的速度反而比純數組版的還慢,至於爲何,實踐爲王吧,我也沒有去分析,反正我知道有這個結果。你能夠參考鐵哥的一篇文章:

                 閒談.Net類型之public的不public,fixed的不能fixed

     固然這個還能夠進一步作小動做的的優化,好比movzx eax,byte ptr [esi+edi] 這句中,esi其實就是數組的基地址,向這樣寫DataCP[SpeedOne] ,每次都會有這個基址+偏移的計算的,若是能實時直接動態控制一個指針變量,使他直接指向索要的位置,則少了一次加法,雖然優化不是很明顯,基本能夠達到問中以前所提到的300ms的時間了。具體的代碼可見附件。

      不少人可能對我這些東西不感冒,說這些東西丟給GPU比你如今的.......但願這些朋友也不要過度的打擊吧,每一個人都有本身的愛好,我只愛好CPU。

      完整工程下載地址:http://files.cnblogs.com/Imageshop/FindEdges.rar

      同一個圖片,本例和PS所得結果有10%左右的差別。

 

 

 ***************************做者: laviewpbt   時間: 2013.7.4    聯繫QQ:  33184777  轉載請保留本行信息*************************

相關文章
相關標籤/搜索