SSE圖像算法優化系列二十四: 基於形態學的圖像後期抗鋸齒算法--MLAA優化研究。

       偶爾看到這樣的一個算法,以爲仍是蠻有意思的,花了將近10天多的時間研究了下相關代碼。算法

       如下爲百度的結果:MLAA全稱Morphological Antialiasing,意爲形態抗鋸齒是AMD推出的徹底基於CPU處理的抗鋸齒解決方案。對於遊戲廠商使用的MSAA抗鋸齒技術不一樣,Intel最新推出的MLAA將跨越邊緣像素的前景和背景色進行混合,用第2種顏色來填充該像素,從而更有效地改進圖像邊緣的變現效果,這就是MLAA技術。網絡

  其實就是這個是由Intel的工程師先於2009年提出的技術,可是由AMD將其發發揚光大。ide

  整個算法的渲染工做所有是交給CPU來完成,在這裏GPU的做用只是將最終渲染出來的畫面傳給顯示器。因此這項技術最大的優點是可讓GPU再也不承擔抗鋸齒的工做,大大下降GPU在運行3D遊戲時的壓力。相對於之前的抗鋸齒技術,MLAA採用Post-filtering(後濾波)機制,好處就在於能夠按照顏色是否連續來驅動抗鋸齒,而之前只能在初始邊緣來抗鋸齒。函數

  也就是說這項技術能夠在後期來修補那些由鋸齒的圖,所以咱們能夠想到其另一些用處,後續會對這方面進行一個簡單的擴展。學習

     

   如上面兩圖,左側圖中樹葉的邊緣有明顯的鋸齒狀圖像,而右側爲通過MLAA算法處理後的圖,邊緣光滑了許多,並且其餘部位未受任何的畫質影響。測試

  關於這方面的論文和資料主要有Morphological Antialiasing.pdf ,Intel官方還對改算法進行了一些額外的說明,詳見:https://software.intel.com/sites/default/files/m/d/4/1/d/8/MLAA.pdf,而且附帶了相關源代碼,代碼可從https://software.intel.com/zh-cn/articles/morphological-antialiasing-mlaa-sample處下載。咱們重點來研讀和改進下這部分代碼。優化

  下載代碼後,找到\SAA-samples\SAA文件夾下的 MLAAPostProcess.h和MLAAPostProcess.cpp文件,這就是咱們最關心的CPU實現的代碼。ui

  根據論文的描述,MLAA算法共包含3個步驟:this

  一、尋找在特定的圖像像素之間的不連續性,在有些圖像中梯度幅值較大的並非邊緣點。spa

  二、肯定預約模式,肯定渲染的圖像。

  三、在預約模式中進行領裏邊緣色彩混合處理。肯定模式中的相應模板。

    下面的文章若是你沒有看過代碼或者沒有看過論文,你根本不知道我在說什麼。

  第一步的計算連續性,在實現上實際是計算一個像素點和其右側及下方一個像素的顏色差別絕對值的大小,若是某個點和其下方像素的 AbsDiff大於某個指定的閾值,則設置這個點爲水平方向邊緣的標記(EdgeFlagH),若是和其右側像素的顏色差別大於閾值,則設置這個點爲爲垂直方向邊緣的標記(EdgeFlagV)。一個點能夠只是水平邊緣或垂直邊緣,也能夠只是其中一個,或者二者都不是。

  在得到連續性的基礎上,第二步是沿着圖像的某一個方向,好比寬度方向分析邊緣的形狀,這裏能夠有Z型,U形和L型,針對不一樣的形狀咱們有不一樣的處理方式。

  最後,就是在得到形狀後,按照必定的規則對這些比較硬性的拐角處(L和Z型都是直角的彎),進行融合和柔化。

  因爲MLAA算法的初衷是處理GPU初處理的圖,所以其主要針對的圖像必然是32位圖像,並且圖像的寬度和高度通常來講都是4的倍數,所以,在Intel給出的代碼中咱們能夠看到都是處理BGRA格式的圖像的。

  具體到代碼實時上,由於是和顯示打交道的,所以 ,算法的實時性必須有可靠的保證,否則這個算法粗在的意義就會大打折扣,網絡上說人有提供了改算法的GPU實現,可是同時又提到那個GPU代碼還不如沒有。我想也是,Intel做爲改算法的提出者,所分享的代碼確實仍是至關又學習意義的,我這裏來稍微分析下。

  首先是不連續性的相關代碼,對於32位圖形,Intel只計算其BGR三通道和周邊像素的差別,當三通道其中一個通道的差別絕對值大於16時,咱們就認爲這個點是某個方位的邊緣處。另外,在Intel的代碼中,其將這些標誌信息直接隱藏到了BGRA的A通道中,這個可能於DDS格式有關,DDS格式有一些並無用到全部的Alpha的8位信息。這樣作的一個好處是不須要額外的內存用來保存邊緣標誌。

  爲了速度起見,同時考慮在CPU端運行,這部分可充分利用SSE進行優化。咱們先貼出Intel的相關代碼進行分析。

//-------------------------------------------------------------------------------------------------------------------
// This task analyzes the color buffer for discontinuities between pixels to set the edge flags in the work buffer.
////-------------------------------------------------------------------------------------------------------------------
void MLAAFindDiscontinuitiesTask(unsigned char *Src, int Width, int Height, int Stride)
{
    for (unsigned int Y = 0; Y < Height; Y += 4)
    {
        unsigned char *LinePS = Src + Y * Stride;
        for (unsigned int X = 0; X < Width; X += 4)
        {
            // Load pixel block from color buffer. vPixels0 contains the 4 pixels indexed by iRow and iCol, 
            // vPixels1, the 4 pixels just below the pixels in vPixels0, and so on and so forth.
            __m128i vPixels0 = _mm_loadu_si128((__m128i *)LinePS);
            __m128i vPixels1 = _mm_loadu_si128((__m128i *)(LinePS + Stride));
            __m128i vPixels2 = _mm_loadu_si128((__m128i *)(LinePS + 2 * Stride));
            __m128i vPixels3 = _mm_loadu_si128((__m128i *)(LinePS + 3 * Stride));
            __m128i vPixels4 = (Y == Height - 4) ?
                vPixels3 // For the last block (vertically), add a virtual row by duplicating the last real block row.
                : _mm_loadu_si128((__m128i *)(LinePS + 4 * Stride));
    
            // zero alpha, we are using it to store discontinuity flags.
            __m128i vZeroAlpha = _mm_set1_epi32(0x00FFFFFF);
            vPixels0 = _mm_and_si128(vPixels0, vZeroAlpha);
            vPixels1 = _mm_and_si128(vPixels1, vZeroAlpha);
            vPixels2 = _mm_and_si128(vPixels2, vZeroAlpha);
            vPixels3 = _mm_and_si128(vPixels3, vZeroAlpha);
    
            // Check for horizontal pixel discontinuities, one row of 4 pixels checked per call.
            // (we compare a row of 4 pixels with its bottom neighbor)
            ComparePixelsSSE(vPixels0, vPixels1, EdgeFlagH);
            ComparePixelsSSE(vPixels1, vPixels2, EdgeFlagH);
            ComparePixelsSSE(vPixels2, vPixels3, EdgeFlagH);
            ComparePixelsSSE(vPixels3, vPixels4, EdgeFlagH);
    
            // Transpose pixel block so we can use ComparePixelsSSE to check for vertical discontinuities.
            _MM_TRANSPOSE4_PS(
                reinterpret_cast<__m128&>(vPixels0),
                reinterpret_cast<__m128&>(vPixels1),
                reinterpret_cast<__m128&>(vPixels2),
                reinterpret_cast<__m128&>(vPixels3));
            //_MM_TRANSPOSE4_EPI32(vPixels0, vPixels1, vPixels2, vPixels3);
    
            vPixels4 = (X == Width - 4) ?
                vPixels3 // For the last block (horizontally), add a virtual column by duplicating the last real column.
                : _mm_setr_epi32(
                *reinterpret_cast<unsigned int*>(LinePS + 16),
                *reinterpret_cast<unsigned int*>(LinePS + 16 + Stride),
                *reinterpret_cast<unsigned int*>(LinePS + 16 + 2 * Stride),
                *reinterpret_cast<unsigned int*>(LinePS + 16 + 3 * Stride));
    
            // Now vPixels0..4 contains the block of pixel data in column order, e.g. 
            // vPixels0 now stores the leftmost column of 4 pixels, vPixels1 the 2nd leftmost one, etc.
            // Now check for vertical pixel discontinuities, one column of 4 pixels checked per call.
            // (we compare a column of 4 pixels with its neighbor on the right)
            ComparePixelsSSE(vPixels0, vPixels1, EdgeFlagV);
            ComparePixelsSSE(vPixels1, vPixels2, EdgeFlagV);
            ComparePixelsSSE(vPixels2, vPixels3, EdgeFlagV);
            ComparePixelsSSE(vPixels3, vPixels4, EdgeFlagV);
    
            // Transpose back and store in color buffer the pixel data with added discontinuities flags.
            _MM_TRANSPOSE4_PS(
                reinterpret_cast<__m128&>(vPixels0),
                reinterpret_cast<__m128&>(vPixels1),
                reinterpret_cast<__m128&>(vPixels2),
                reinterpret_cast<__m128&>(vPixels3));
            //_MM_TRANSPOSE4_EPI32(vPixels0, vPixels1, vPixels2, vPixels3);
            _mm_storeu_si128((__m128i *)(LinePS), vPixels0);
            _mm_storeu_si128((__m128i *)(LinePS + Stride), vPixels1);
            _mm_storeu_si128((__m128i *)(LinePS + 2 * Stride), vPixels2);
            _mm_storeu_si128((__m128i *)(LinePS + 3 * Stride), vPixels3);
            LinePS += 16;
        }
    }
}

  第一感受就是代碼的註釋很豐富,不愧是你們製做,在解釋這段代碼以前,咱們先來看看ComparePixelsSSE函數。

//--------------------------------------------------------------------------------------------------------------
// Given a row of 4 pixels, check for color discontinuities between a pixel and its neighbor.
// SSE implementation, so we process the 4 consecutive pixels at a time.
// If a discontinuity is detected, the flag passed as 3rd arg. is set in the alpha component of the pixel.
// vPixels0 is the vector of 4 pixels to be flagged if discontinuities are detected.
// vPixels1 is the vector of 4 neighbor pixels.
//--------------------------------------------------------------------------------------------------------------
inline void ComparePixelsSSE(__m128i& vPixels0, __m128i vPixels1, const INT EdgeFlag)
{
    // Each byte of vDiff is the absolute difference between a pixel's color channel value, and the one from its neighbor.
    __m128i vDiff = _mm_sub_epi8(_mm_max_epu8(vPixels0, vPixels1), _mm_min_epu8(vPixels0, vPixels1));

    // We are only interested if the difference is greater than 16, and not interested in alpha differences.
    const INT Threshold = 0x00F0F0F0;
    __m128i vThresholdMask = _mm_set1_epi32(Threshold);
    vDiff = _mm_and_si128(vDiff, vThresholdMask);

    // Each of the four lanes of the selector is 0 if no discontinuity detected RGB, 0xFFFFFFFF else.
    __m128i vSelector = _mm_cmpeq_epi32(vDiff, _mm_setzero_si128());

    __m128i vEdgeFlag = _mm_set1_epi32(EdgeFlag);
    vEdgeFlag = _mm_andnot_si128(vSelector, vEdgeFlag);
    // vEdgeFlag now contains EdgeFlag for the pixels where a discontinuity was detected, 0 otherwise.

    vPixels0 = _mm_or_si128(vPixels0, vEdgeFlag);
}

     ComparePixelsSSE的做用就是比較8個BGRA像素的差別,若是像素的BGR差別值有一個大於閾值,則設置A的某個位爲Flag。
     第一行vDiff的計算就是計算差別值的絕對值,他一次性能夠計算16個字節值的差別,由於SSE沒有直接提供這樣的函數,所以,這裏使用max和min函數結合實現,也是很是的巧妙,其實還有另一個方式能夠實現這個功能,以下所示:

//    返回8位字節數數據的差的絕對值
inline __m128i _mm_absdiff_epu8(__m128i a, __m128i b)
{
    return _mm_or_si128(_mm_subs_epu8(a, b), _mm_subs_epu8(b, a)); }

  利用了飽和計算,一樣也是十分的巧妙。後續測試表面上述方法速度彷佛還能有必定的優點。

      vDiff = _mm_and_si128(vDiff, vThresholdMask); 這一句結合__m128i vSelector = _mm_cmpeq_epi32(vDiff, _mm_setzero_si128());是這個函數的靈魂所在,咱們注意到vThresholdMask對應的字節範圍內數據的高4位都是1,低4位都爲0,所以若是vDiff中某個值大於16(高4位有值不爲1),則進行and運算後返回值必然不爲0,4個字節中只要有一個不爲0,組成的32位數也必然不爲0,這樣一個BGRA像素只要有一個通道有AbsDiff大於0的值,就能夠經過_mm_cmpeq_epi32的比較(和0比較)返回值得以體現。

  後面使用_mm_andnot_si128是由於咱們_mm_cmpeq_epi32的函數在等於0時返回0xFFFFFF,而咱們實際上在此時想讓他爲0x00000000,由於系統不提供_mm_cmpneq_epi32這樣的函數,因此要先取反下(not),而後在和Flag進行And運算,特別須要注意的是,不想大多數的SSE函數,_mm_andnot_si128這個SSE函數對參數的順序是銘感的,若是放錯了位置,則沒法獲得正確的結果。 另外,這裏const INT EdgeFlagH = (1 << 31)以及const INT EdgeFlagV = (1 << 30),Intel也是考慮的很好,第一,和他們進行and操做不會影響BGR的值,第二,後面還有一個技巧也和這個值有關。

    最後的_mm_or_si128主要是考慮水平和垂直方向的邊緣的綜合識別。
      雖然咱們知道對32位數,SSE有_mm_cmpge_epi32這個比較函數,可是在這裏確實沒法直接使用他。

    咱們再來回頭看MLAAFindDiscontinuitiesTask這個函數,他的核心是一次性讀取4行4列共16個BGRA像素,佔用4個XMM寄存器得大小,而後在函數內部一次性的計算出水平邊緣和垂直邊緣的標記,這樣左一個核心的好處是能減小讀取內存的次數。那麼這裏最靈活的運用就是_MM_TRANSPOSE4_PS這個宏的使用。咱們看他的名字,就知道他是針對浮點進行轉置使用,而咱們的32位圖像加載後是__m128i類型的,可是其實在計算機內部,無論你表面是浮點的仍是整形的,都是把數據保存在XMM寄存上,所以,咱們用reinterpret_cast或者_mm_castsi128_ps這樣的一些語法糖來強制轉換,讓編譯器能編譯經過,_MM_TRANSPOSE4_PS這個照樣能夠處理整形的。

  好比,_mm_castsi128_ps這個函數intel給出的解釋是Cast vector of type __m128i to type __m128. This intrinsic is only used for compilation and does not generate any instructions, thus it has zero latency. 也就是說他並無產生任何額外的指令,只是個語法糖。

  固然,咱們不用_MM_TRANSPOSE4_PS也能夠用整形的unpack系列函數實現32位整形的轉置,實測他們效率基本無區別。

  話題扯得遠了點,MLAAFindDiscontinuitiesTask中,首先一次性加載四行各4各像素後,一次進行水平方向的比較,水平比較完後,這樣對這些數據進行轉置,有能夠繼續進行垂直方向的比較,比較完成後,再轉置回來,就一次性完成了水平和垂直方向的全部比較。所以,速度就能獲得大幅的提升。

   這一步的優化完成,第二步中咱們要尋找不一樣的模式,其中一個關鍵的步驟就是尋找具備同一邊緣標記的連續線段。這部分再函數裏實現,以下所示:

//-----------------------------------------------------------------------------------------------------------------------------------------
// Given a range of pixels offsets [OffsetCurrentPixel, OffsetEndRow], walk this range to find a discontinuity line.
// We call a "separation line" or "discontinuity line" a sequence of consecutive pixels that all have the same edge flag set.
// The 3 return values are: the length of the separation line, and the offsets in the buffer of the first and last pixel of the line.
//-----------------------------------------------------------------------------------------------------------------------------------------
inline UINT FindSeparationLine(int& OutOffsetLineStart, int& OutOffsetLineEnd, UINT* WorkBuffer, UINT OffsetCurrentPixel, UINT OffsetEndRow, const INT EdgeFlag)
{
    if (OffsetCurrentPixel >= OffsetEndRow)
    {    // We are done scanning this row/column; no separation line left to find.
        return 0;
    }
    // Find first extremity of the line...            找尋線的端頭
    OutOffsetLineStart = -1;

    int Shift = (EdgeFlag == EdgeFlagV) ? 1 : 0;

    for (;;)
    {    
        __m128i PixelFlags = _mm_loadu_si128((__m128i*)(WorkBuffer + OffsetCurrentPixel));
        PixelFlags = _mm_slli_epi32(PixelFlags, Shift);
        // Creates a 4-bit mask from the edge flag of each of the 4 pixels
        int HFlags = _mm_movemask_ps(_mm_castsi128_ps(PixelFlags));
        if (HFlags)
        {
            unsigned long Index;
            _BitScanForward(&Index, HFlags);
            OffsetCurrentPixel += Index;
            OutOffsetLineStart = OffsetCurrentPixel;
            break;
        }
        OffsetCurrentPixel += 4;    // None of the pixels had its flag set so we can jump ahead 4 pixels...
        if (OffsetCurrentPixel >= OffsetEndRow)
        {    // Done scanning this row/column, without finding a separation line.
            OutOffsetLineStart = OutOffsetLineEnd = OffsetEndRow - 1;
            return 0;
        }
    }

FindEndOffset:
    // Now look for the second extremity of the line.
    // We could use a SSE-based optimization as the one above, but it remains to be seen if it would help significantly the performance.
    // (discontinuity lines tend to be short)
    UINT LineLength = 1;
    ++OffsetCurrentPixel;
    while ((OffsetCurrentPixel <= OffsetEndRow) && ((WorkBuffer[OffsetCurrentPixel] & EdgeFlag) != 0))
    {
        ++LineLength;
        ++OffsetCurrentPixel;
    }
    OutOffsetLineEnd = OffsetCurrentPixel - 1;
    return LineLength;
}

  好像這段代碼我稍微修改了下,源代碼有對OffsetCurrentPixel不是4的整數倍作判斷,其實那個判斷是基於默認使用(__m128i *)這種方式加載變量到SSE寄存器是採用的_mm_load_si128(即16字節對齊有關),若是顯式的使用_mm_loadu_si128則無需那樣寫。

  咱們看下這裏的技巧主要在於_mm_movemask_ps的使用,在第一步不連續性的標記過程當中,咱們把邊緣比較分別設置在低31位(水平邊緣)和低30(垂直邊緣)位中,所以,若是是尋找垂直邊緣是,咱們把總體向左移動移位,這個時候在他們強制轉換爲浮點數,此時_mm_movemask_ps就會根據XMM寄存中每一個32位的浮點數的sign返回一個值,若是返回值的都爲0,則表示4各數都爲正數,說明這4個位置都沒有咱們須要尋找的標記,若是結果不爲0,這個時候咱們應該從低位到高位尋找到第一個不爲0的位,他對應的索引就是第一個標記所在的位置,這種位尋找的函數在Intel里正好有提供,即本例的_BitScanForward函數。

  這個方式在不少的優化或計算中均可以借鑑,確實是個不錯的方法。

  那麼當找到第一個位置的標記後,咱們就須要找到最後一個標記,通常狀況下連續的統一標記不會太長,所以後續的尋找不須要藉助SSE處理。

    還有一個使用了SSE優化的地方就在於最後一步的融合地方,以下所示函數:

inline void MixColors(UINT* ColorBuffer, UINT OffsetDst, float Weight1, UINT Offset1, float Weight2, UINT Offset2)
{
    // Load pixels and convert the bytes to dwords
    __m128i Col1i = _mm_cvtsi32_si128(ColorBuffer[Offset1]); __m128i Col2i = _mm_cvtsi32_si128(ColorBuffer[Offset2]); __m128i Zero = _mm_setzero_si128(); Col1i = _mm_unpacklo_epi16(_mm_unpacklo_epi8(Col1i, Zero), Zero); Col2i = _mm_unpacklo_epi16(_mm_unpacklo_epi8(Col2i, Zero), Zero); // Convert to floats so the pixels can be multplied with the weights __m128 Col1f = _mm_cvtepi32_ps(Col1i); __m128 Col2f = _mm_cvtepi32_ps(Col2i); Col1f = _mm_mul_ps(Col1f, _mm_set1_ps(Weight1)); Col2f = _mm_mul_ps(Col2f, _mm_set1_ps(Weight2)); Col1f = _mm_add_ps(Col1f, Col2f); // Go back to byte from float __m128i Coli = _mm_cvttps_epi32(Col1f); Coli = _mm_packs_epi32(Coli, Coli); Coli = _mm_packus_epi16(Coli, Coli); // Store the weighted pixel ColorBuffer[OffsetDst] = (ColorBuffer[OffsetDst] & 0xFF000000) | (_mm_cvtsi128_si32(Coli) & 0x00FFFFFF); }

  這個其實也很簡單,就是4個字節數據乘以4個浮點數,而後累加,最後結果保存爲字節數,而且要保留部分字節不變的一個過程,有興趣的朋友能夠本身研讀下。

  那麼在Intel的代碼中,還利用了TBB進行優化,將計算分紅了多任務處理過程,另外,考慮垂直方向的cache命中率問題,將垂直計算過程使用轉置後在調用水平方向的算法進行處理。

  那麼後面我在談下這個代碼裏幾個細節的東西。

  (1)代碼裏有ComputeUpperBounds和ComputeLowerBounds函數,他們的主要做用是輔助確認模式,代碼自己不難,可是若是你只去讀函數自己,你會發現他的判斷邏輯彷佛說不通,相似下面這句這樣的代碼:

  if (((WorkBuffer[BelowOffset] & OrthoEdgeFlag) != 0) && ((WorkBuffer[BelowOffset] & EdgeFlag) != 0))

  你會感受到應該不會出現這種狀況啊,我在這裏也看了半天,後來才發現,調用他們的代碼在參數上動了手腳,好比下面這個:

    ComputeUpperBounds(ui0, ui1, uh0, uh1,
        ColorBuffer,
        OffsetLineStart - 1, OffsetLineEnd, SeparationLineLength, StepToPreviousRow, BufferPitch, BufferPitch * Height, EdgeFlag);

  咱們注意OffsetLineStart - 1這裏的減1,原來他並非從找到的線條的第一個點開始,而是向前一個點,這樣這個模式就容易理解了。

  (2)在MLAABlendHTask以及MLAABlendVTask中,咱們注意到都沒有處理最後一行或一列像素,這是由於最後一行或最後一列按照前面的邊緣計算模式,都只可能出現一種模式, 好比,最後一行,其水平邊緣確定是步成立的。所以,咱們在這一行找不到Z或U這種模式,也就沒有必要進行處理。

  (3)在Intel的代碼中,有個計算ComputeHc函數:

inline float ComputeHc(UINT* WorkBuffer, UINT PrimaryEdgeLength, UINT OffsetC1, UINT OffsetC2, UINT OffsetD2, UINT OffsetD3)
{
    int C2 = SumColor(WorkBuffer[OffsetC2]); int C1 = SumColor(WorkBuffer[OffsetC1]); int D3 = SumColor(WorkBuffer[OffsetD3]); int D2 = SumColor(WorkBuffer[OffsetD2]); int D3D2Diff = D3 - D2; int C1C2Diff = C1 - C2; int D2C2Diff = D2 - C2; return float(PrimaryEdgeLength * D2C2Diff + C1C2Diff + D3D2Diff) / (PrimaryEdgeLength * (C1C2Diff - D3D2Diff) + C1C2Diff + D3D2Diff); }

  我曾經在修改代碼過程當中,把UINT PrimaryEdgeLength改成INT PrimaryEdgeLength,結果對一個測試圖像獲得的結果就會發生不錯,以下所示:

   

  第一張爲原圖,第二張使用UINT的結果圖,第三張爲改成int後的結果圖,很明顯在紅色方框部分的圓弧處依舊有較爲明顯的毛刺出現,而總體的代碼只是個數據類型發生了改動。

  我曾經弄了好久才發現這個問題出如今那個函數裏,可是發現了後卻一直不知道問題出如今那裏,當我把參數改成int類型,輸出了全部的PrimaryEdgeLength值,確認這裏面確實沒有任何的0或者負數,那按照個人想法不該該會出現這個問題,後來仔細搜索了下,原來問題仍是處在數據類型上,由於編譯器在同時存在無符號和有符號數計算時,是會將有符號數強制轉換爲無符號數的,即所謂的升格處理,上述代碼裏的D3D2Diff 都有可能有負數存在,所以,這中升格處理會使得計算結果徹底偏離了咱們的預想。

  那在本身想想,咱們確認應該在這裏使用int類型纔對,但爲何咱們卻得到了不理想的效果呢,這裏我認爲是原文做者的一個觀點有問題,即HC的計算裏,原文有這句話:

To state the requirements for smooth transition between two shapes, we compute the same blended color twice, using parameters of each shape: 

  即要保持平滑,而對黑白圖像,HC就是固定值0.5f.

  在Intel的代碼裏,存在好幾處相似這樣的代碼:

if (BelowOffset + StepToNextRow < BufferSizeInPixels)
{
    OutH1 = ComputeHc(WorkBuffer, LineLength - NSteps, BelowOffset + StepToNextRow + 1, BelowOffset + 1, BelowOffset, CurrentOffset); } else { // We are too close to the end of the buffer to be able to do the ComputeHc calculation, so use default hc value of 0.5 OutH1 = 0.5; } if ((OutH1 > 0) && (OutH1 < 1)) { // If the computed value of hc is in the acceptable range, then we're done else keep walking OutS1 = CurrentOffset; break; }

  即若是經過ComputeHc計算的值不在0和1之間,就取值0.5,那麼前面的使用UINT的狀況基本計算出的值都離譜的很,很難在0和1之間,所以,程序最後都至關於直接取Hc等於0.5,而使用int時結果不理想,說明原做者的部分思路也不靠譜。 所以,我建議這部分直接使用0.5,去掉這個ComputeHc的過程,又能節省時間又能還有不錯的效果,不曉得Intel的工程師有沒有注意到這一點

         (4) 這個算法其實自己的計算量是不大的,由於一幅圖像中須要修改的像素其實很少,另外,因爲計算特性,基本上支持InPlace操做。

   注意到Intel共享的代碼確實還有不少侷限性,只能處理32位,只能處理4個倍數大小的圖像,還會破壞原始圖像的Alpha通道,好像還有其餘的很差的內存問題(符合前面的條件的圖片運行也會掛),根據算法思路我重構了下代碼,使得其能處理任意大小即灰度和24位圖像。

  第一,連續性檢測。咱們爲了避免破壞原始圖像,從新定義一個Width*Height大小的字節空間來保存邊緣信息。對於灰度圖,可用以下代碼搞定。

int BlockSize = 16, Block = (Width - 1) / BlockSize;        //    垂直和水平方向比較同步進行,考慮到垂直方向比較時最後一列不能參與
__m128i T = _mm_set1_epi8(Threshold);
for (int Y = 0; Y < Height - 1; Y++)                        //    水平比較時最後一行像素不能參與
{ unsigned char *LinePS = Src + Y * Stride; unsigned char *LinePN = LinePS + Stride; unsigned char *LinePM = Mask + Y * Width; for (int X = 0; X < Block * BlockSize; X += BlockSize) { __m128i SrcH1 = _mm_loadu_si128((__m128i *)(LinePS + X)); //__m128i SrcH2 = _mm_loadu_si128((__m128i *)(LinePS + X + 1)); __m128i SrcH2 = _mm_insert_epi8(_mm_srli_si128(SrcH1, 1), LinePS[X + 16], 15); // 彷佛速度有沒啥區別 __m128i SrcV1 = _mm_loadu_si128((__m128i *)(LinePN + X)); __m128i AbsDiffH = _mm_absdiff_epu8(SrcH1, SrcV1); // 水平方向比較,abs(Bottom - Current),彷佛和常人瞭解的水平方向不一致 __m128i AbsDiffV = _mm_absdiff_epu8(SrcH1, SrcH2);; // 垂直方向比較,abs(Right - Current) __m128i FlagH = _mm_and_si128(_mm_set1_epi8(EdgeFlagH), _mm_cmpge_epu8(AbsDiffH, T)); // 設置水平方向的標誌位 __m128i FlagV = _mm_and_si128(_mm_set1_epi8(EdgeFlagV), _mm_cmpge_epu8(AbsDiffV, T)); // 設置垂直方向的標誌位 _mm_storeu_si128((__m128i *)(LinePM + X), _mm_or_si128(FlagH, FlagV)); // 設置總的標誌位 }
  //  ...............
}

  這裏我另一種方式來作水平和垂直方向的優化,咱們一次性只處理一行代碼,可是在垂直方向比較時,單獨日後加載一個像素,注意SrcH1和SrcH2 其實只有一個像素不一樣,所以,我嘗試用_mm_insert_epi8來減小內存讀取量,但同時增長了一個移位操做,實際測試和直接使用_mm_loadu_si128讀取速度差別基本可忽略。因爲使用的時字節變量來保存邊緣信息,所以可以使用字節比較_mm_cmpge_epu8的結果來進行位操做。

  對於24位或者32位圖像,這時個人處理方式是:

int BlockSize = 4, Block = (Width - 1) / BlockSize;        //    垂直和水平方向比較同步進行,考慮到垂直方向比較時最後一列不能參與
__m128i T = _mm_set1_epi8(Threshold);
for (int Y = 0; Y < Height - 1; Y++)                        //    水平比較時最後一行像素不能參與
{ unsigned char *LinePS = Src + Y * Stride; unsigned char *LinePN = LinePS + Stride; unsigned char *LinePM = Mask + Y * Width; for (int X = 0; X < Block * BlockSize; X += BlockSize) { __m128i SrcH1 = _mm_loadu_si128((__m128i *)(LinePS + X * 4)); __m128i SrcH2 = _mm_loadu_si128((__m128i *)(LinePS + X * 4 + 4)); __m128i SrcV1 = _mm_loadu_si128((__m128i *)(LinePN + X * 4)); __m128i AbsDiffH = _mm_absdiff_epu8(SrcH1, SrcV1); // 水平方向比較,abs(Bottom - Current),彷佛和常人瞭解的水平方向不一致 __m128i AbsDiffV = _mm_absdiff_epu8(SrcH1, SrcH2); // 垂直方向比較,abs(Right - Current) __m128i FlagH = _mm_andnot_si128(_mm_cmpeq_epi32(_mm_cmpge_epu8(AbsDiffH, T), _mm_setzero_si128()), _mm_set1_epi32(EdgeFlagH)); // 設置水平方向的標誌位,注意_mm_andnot_si128是對參數的順序敏感的,(~a) & b __m128i FlagV = _mm_andnot_si128(_mm_cmpeq_epi32(_mm_cmpge_epu8(AbsDiffV, T), _mm_setzero_si128()), _mm_set1_epi32(EdgeFlagV)); // 設置垂直方向的標誌位  _mm_storesi128_4char(LinePM + X, _mm_or_si128(FlagH, FlagV));// 設置總的標誌位 }
//  ..........
}

  其實前面沒提, Intel代碼的Threshold默認設置爲16,其實這是個特殊的值,若是要支持任意的閾值,則不能像ComparePixelsSSE那樣作,只有像16,32,64等這樣幾個特殊的值才能夠用(相反數高位連續的二進制都爲1),要支持任意閾值,咱們就能夠像上面那樣藉助_mm_cmpge_epu8和_mm_cmpeq_epi32來共同實現。

  在搜索水平線的時候,也一樣須要更換一種技巧,以下所示:

__m128i PixelFlags = _mm_loadu_si128((__m128i*)(Mask + Y));                                 //    合理加載數據, 不能超出範圍
int HFlags = _mm_movemask_epi8(_mm_cmpeq_epi8(_mm_and_si128(PixelFlags, Flag), Flag));      //    _mm_movemask_epi8提取每一個字節的最高位來組成一個16位數    
if (HFlags != 0)                                                                            //    說明在這16個元素裏有部分標記是符合條件的
{
    unsigned long Index; _BitScanForward(&Index, HFlags); // 找到第一個位爲1值得索引(從低到高位尋找),intel的bsf指令 OutOffsetLineStart = Y + Index; goto FindEndOffset; // 尋找結束位置 }

  基本的原理和以前的相似,只是要改改相關函數。

        結合其餘的優化技巧(好比轉置使用SSE處理),目前我作到的速度是:720P的32位圖像大約是8ms,1024P的圖像大約是18ms。以上都是單核的處理結果,固然這個的時間其實還和圖像自己的內容和複雜度有關。

  從算法自己的角度來講,是支持並行執行的,若是採用雙線程,應該還有個50%左右的提速,用於遊戲後期的渲染應該是夠了。

  算法還有一些有意思的特徵,因爲識別模式的一些問題,對於同一個圖像內容,旋轉必定角度後,其處理的結果並不必定同樣,好比下圖:

  

   第三圖是對原圖旋轉180度後,進行本算法處理,而後在旋轉180度後獲得的結果,很明顯,第三圖的結果和第二圖不同,並且要好不少。

      對於閾值的選擇,這也是個問題,好比下圖:

             

  中間一幅圖的閾值選擇位16,明顯感受肩膀和衣服下邊緣還有部分鋸齒,而第三個圖的閾值選擇爲32,則鋸齒感又少了不少。

  那麼因爲算法的內在特性和算法研發的背景,對於兩個方向寬度都大於2個像素的鋸齒,算法是沒法解決的,好比下面右側處理後的圖基本無效果。

 

  對於自己就已經光滑的邊緣,這個算法通常也不會產生很差的效果,以下圖所示:

 

        左圖的右半部分自己就是很光滑的,而左半部分又鋸齒,處理後的右圖,左半部分鋸齒基本消失,而右半部分基本沒變。

   對於黑白圖像,這個的去鋸齒功能也比較顯著。

 

  

  那麼我找了幾個真正的遊戲裏的畫面,嘗試了下,後續的去鋸齒效果,確實也能得到很好的結果:

  

  因爲這些圖都是網絡上下載的,通常都爲JPG格式,自己通過了壓縮,這樣會引入噪音,會對邊緣的識別有必定的影響,而若是是直接處理顯卡輸出的數據,則不會有任何的損失,所以,理論上會取得更好的效果。

      測試見附件的SSE_Optimization_Demo的Other菜單。

      Demo下載地址:https://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar

  

相關文章
相關標籤/搜索