SSE圖像算法優化系列八:自然飽和度(Vibrance)算法的模擬實現及其SSE優化(附源碼,可作為SSE圖像入門,Vibrance算法也可用於簡單的膚色調整)。


  Vibrance這個單詞搜索翻譯一般振動,抖動或者是響亮、活力,但是官方的詞匯里還從來未出現過自然飽和度這個詞,也不知道當時的Adobe中文翻譯人員怎么會這樣處理。但是我們看看PS對這個功能的解釋:

       Vibrance: Adjusts the saturation so that clipping is minimized as colors approach full saturation. This setting changes the saturation of all lower-saturated colors with less effect on the higher-saturated colors. Vibrance also prevents skin tones from becoming oversaturated.

       確實是和飽和度有關的,這樣理解中文的翻譯反而倒是合理,那么只能怪Adobe的開發者為什么給這個功能起個名字叫Vibrance了。

       閑話不多說了,其實自然飽和度也是最近幾個版本的PS才出現的功能,在調節有些圖片的時候會有不錯的效果,也可以作為簡單的膚色調整的一個算法,比如下面這位小姑娘,用自然飽和度即可以讓她失血過多,也可以讓他膚色紅暈。

                 

                                             原圖                                                                                                  面色蒼白                                                                                   膚色紅暈一點

       那么這個算法的內在是如何實現的呢,我沒有仔細的去研究他,但是在開源軟件PhotoDemon-master(開源地址:https://github.com/tannerhelland/PhotoDemon,visual basic 6.0的作品,我的最愛)提供了一個有點相似的功能,我們貼出他對改效果的部分注釋:

'***************************************************************************
'Vibrance Adjustment Tool
'Copyright 2013-2017 by Audioglider
'Created: 26/June/13
'Last updated: 24/August/13
'Last update: added command bar
'
'Many thanks to talented contributer Audioglider for creating this tool.
'
'Vibrance is similar to saturation, but slightly smarter, more subtle. The algorithm attempts to provide a greater boost
' to colors that are less saturated, while performing a smaller adjustment to already saturated colors.
'
'Positive values indicate "more vibrance", while negative values indicate "less vibrance"
'
'All source code in this file is licensed under a modified BSD license. This means you may use the code in your own
' projects IF you provide attribution. For more information, please visit http://photodemon.org/about/license/
'
'***************************************************************************

 其中的描述和PS官方文檔的描述有類似之處。

   我們在貼出他的核心代碼:

 For x = initX To finalX quickVal = x * qvDepth For y = initY To finalY 'Get the source pixel color values
            r = ImageData(quickVal + 2, y) g = ImageData(quickVal + 1, y) b = ImageData(quickVal, y) 'Calculate the gray value using the look-up table
            avgVal = grayLookUp(r + g + b) maxVal = Max3Int(r, g, b) 'Get adjusted average
            amtVal = ((Abs(maxVal - avgVal) / 127) * vibranceAdjustment) If r <> maxVal Then r = r + (maxVal - r) * amtVal End If
            If g <> maxVal Then g = g + (maxVal - g) * amtVal End If
            If b <> maxVal Then b = b + (maxVal - b) * amtVal End If
            
            'Clamp values to [0,255] range
            If r < 0 Then r = 0
            If r > 255 Then r = 255
            If g < 0 Then g = 0
            If g > 255 Then g = 255
            If b < 0 Then b = 0
            If b > 255 Then b = 255 ImageData(quickVal + 2, y) = r ImageData(quickVal + 1, y) = g ImageData(quickVal, y) = b Next
    Next

  很簡單的算法,先求出每個像素RGB分量的最大值和平均值,然后求兩者之差,之后根據輸入調節量求出調整量。

       VB的語法有些人可能不熟悉,我稍微做點更改翻譯成C的代碼如下:

 float VibranceAdjustment = -0.01 * Adjustment;        // Reverse the vibrance input; this way, positive values make the image more vibrant. Negative values make it less vibrant.
    for (int Y = 0; Y < Height; Y++) { unsigned char * LinePS = Src + Y * Stride; unsigned char * LinePD = Dest + Y * Stride; for (int X = 0; X < Width; X++) { int Blue = LinePS[0],    Green = LinePS[1],    Red = LinePS[2]; int Avg = (Blue + Green + Green + Red) >> 2; int Max = IM_Max(Blue, IM_Max(Green, Red)); float AmtVal = (abs(Max - Avg) / 127.0f) * VibranceAdjustment;                        // Get adjusted average
            if (Blue != Max)    Blue += (Max - Blue) * AmtVal; if (Green != Max)    Green += (Max - Green) * AmtVal; if (Red != Max)    Red += (Max - Red) * AmtVal; LinePD[0] = IM_ClampToByte(Blue); LinePD[1] = IM_ClampToByte(Green); LinePD[2] = IM_ClampToByte(Red); LinePS += 3; LinePD += 3; } }

  這個的結果和PS的是比較接近的,最起碼趨勢是非常接近的,但是細節還是不一樣,不過可以斷定的是方向是對的,如果你一定要復制PS的結果,我建議你花點時間改變其中的一些常數或者計算方式看看。應該能有收獲,國內已經有人摸索出來了。

      我們重點講下這個算法的優化及其SSE實現,特別是SSE版本代碼是本文的重中之重。

      第一步優化,去除掉不必要計算和除法,很明顯,這一句是本段代碼中耗時較為顯著的部分

        float AmtVal = (abs(Max - Avg) / 127.0f) * VibranceAdjustment;     

  /127.0f可以優化為乘法,同時注意VibranceAdjustment在內部不變,可以把他們整合到循環的最外層,即改為:

      float VibranceAdjustment = -0.01 * Adjustment / 127.0f;

  再注意abs里的參數, Max - Avg,這有必要取絕對值嗎,最大值難道會比平均值小,浪費時間,最后改為:

      float AmtVal = (Max - Avg) * VibranceAdjustment;

    這是浮點版本的簡單優化,如果不勾選編譯器的SSE優化,直接使用FPU,對於一副3000*2000的24位圖像耗時在I5的一台機器上運行用時大概70毫秒,但這不是重點。

  我們來考慮某些近似和定點優化。

       第一我們把/127改為/128,這基本不影響效果,同時Adjustment默認的范圍為[-100,100],把它也線性擴大一點,比如擴大1.28倍,擴大到[-128,128],這樣在最后我們一次性移位,減少中間的損失,大概的代碼如下:

int IM_VibranceI(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, int Adjustment)
{
    int Channel = Stride / Width;
    if ((Src == NULL) || (Dest == NULL))                return IM_STATUS_NULLREFRENCE;
    if ((Width <= 0) || (Height <= 0))                    return IM_STATUS_INVALIDPARAMETER;
    if (Channel != 3)                                    return IM_STATUS_INVALIDPARAMETER;
    
    Adjustment = -IM_ClampI(Adjustment, -100, 100) * 1.28;        //       Reverse the vibrance input; this way, positive values make the image more vibrant.  Negative values make it less vibrant.
    for (int Y = 0; Y < Height; Y++)
    {
        unsigned char *LinePS = Src + Y * Stride;
        unsigned char *LinePD = Dest + Y * Stride;
        for (int X = 0; X < Width; X++)
        {
            int Blue, Green, Red, Max;
            Blue = LinePS[0];    Green = LinePS[1];    Red = LinePS[2];
            int Avg = (Blue + Green + Green + Red) >> 2;
            if (Blue > Green)
                Max = Blue;
            else
                Max = Green;
            if (Red > Max)
                Max = Red;
            int AmtVal = (Max - Avg) * Adjustment;                                //    Get adjusted average
            if (Blue != Max)    Blue += (((Max - Blue) * AmtVal) >> 14);
            if (Green != Max)    Green += (((Max - Green) * AmtVal) >> 14);
            if (Red != Max)        Red += (((Max - Red) * AmtVal) >> 14);
            LinePD[0] = IM_ClampToByte(Blue);
            LinePD[1] = IM_ClampToByte(Green);
            LinePD[2] = IM_ClampToByte(Red);
            LinePS += 3;
            LinePD += 3;
        }
    }
    return IM_STATUS_OK;
}

  這樣優化后,同樣大小的圖像算法用時35毫秒,效果和浮點版本的基本沒啥區別。

       最后我們重點來講講SSE版本的優化。

   對於這種單像素點、和領域無關的圖像算法,為了能利用SSE提高程序速度,一個核心的步驟就是把各顏色分量分離為單獨的連續的變量,對於24位圖像,我們知道圖像在內存中的布局為:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
B1 G1 R1 B2 G2 R2 B3 G3 R3 B4 G4 R4 B5 G5 R5 B6 G6 R6 B7 G7 R7 B8 G8 R8 B9 G9 R9 B10 G10 R10 B11 G11 R11 B12 G12 R12 B13 G13 R13 B14 G14 R14 B15 G15 R15 B16 G16 R16

 

       

      我們需要把它們變為:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
B1 B2 B3 B4 B4 B5 B7 B8 B9 B10 B11 B12 B13 B14 B15 B16 G1 G2 G3 G4 G5 G6 G7 G8 G9 G10 G11 G12 G13 G14 G15 G16 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 R11 R12 R13 R14 R15 R16

 

     

     處理完后我們又要把他們恢復到原始的BGR布局。

     為了實現這個功能,我參考了采石工大俠的有關代碼,分享如下:

     我們先貼下代碼:

    Src1 = _mm_loadu_si128((__m128i *)(LinePS + 0));
    Src2 = _mm_loadu_si128((__m128i *)(LinePS + 16));
    Src3 = _mm_loadu_si128((__m128i *)(LinePS + 32));

    Blue8 = _mm_shuffle_epi8(Src1, _mm_setr_epi8(0, 3, 6, 9, 12, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1));
    Blue8 = _mm_or_si128(Blue8, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, 2, 5, 8, 11, 14, -1, -1, -1, -1, -1)));
    Blue8 = _mm_or_si128(Blue8, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 4, 7, 10, 13)));

    Green8 = _mm_shuffle_epi8(Src1, _mm_setr_epi8(1, 4, 7, 10, 13, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1));
    Green8 = _mm_or_si128(Green8, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-1, -1, -1, -1, -1, 0, 3, 6, 9, 12, 15, -1, -1, -1, -1, -1)));
    Green8 = _mm_or_si128(Green8, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, 5, 8, 11, 14)));

    Red8 = _mm_shuffle_epi8(Src1, _mm_setr_epi8(2, 5, 8, 11, 14, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1));
    Red8 = _mm_or_si128(Red8, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-1, -1, -1, -1, -1, 1, 4, 7, 10, 13, -1, -1, -1, -1, -1, -1)));
    Red8 = _mm_or_si128(Red8, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 3, 6, 9, 12, 15)));

      首先,一次性加載48個圖像數據到內存,正好放置在三個__m128i變量中,同時另外一個很好的事情就是48正好能被3整除,也就是說我們完整的加載了16個24位像素,這樣就不會出現斷層,只意味着下面48個像素可以和現在的48個像素使用同樣的方法進行處理。

      如上代碼,則Src1中保存着:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
B1 G1 R1 B2 G2 R2 B3 G3 R3 B4 G4 R4 B5 G5 R5 B6

 

 

      Src2中保存着:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
G6 R6 B7 G7 R7 B8 G8 R8 B9 G9 R9 B10 G10 R10 B11 G11

 

 

  Src3中的數據則為:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
R11 B12 G12 R12 B13 G13 R13 B14 G14 R14 B15 G15 R15 B16 G16 R16

 

 

    為了達到我們的目的,我們就要利用SSE中強大的shuffle指令了,如果能夠把shuffle指令運用的出神入化,可以獲取很多很有意思的效果,有如鳩摩智的小無相功一樣,可以催動拈花指發、袈裟服魔攻等等,成就世間能和我鳩摩智打成平成的沒有幾個人一樣的豐功偉績。哈哈,說遠了。

     簡單的理解shuffle指令,就是將__m128i變量內的各個數據按照指定的順序進行重新布置,當然這個布置不一定要完全利用原有的數據,也可以重復某些數據,或者某些位置無數據,比如在執行下面這條指令

    Blue8 = _mm_shuffle_epi8(Src1, _mm_setr_epi8(0, 3, 6, 9, 12, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1));

   Blue8中的數據為:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
B1 B2 B3 B4 B5 B6 0 0 0 0 0  0  0  0  0


_mm_setr_epi8指令的參數順序可能更適合於我們常用的從左到右的理解習慣,其中的某個參數如果不在0和15之間時,則對應位置的數據就是被設置為0。

   可以看到進行上述操作后Blue8的簽6個字節已經符合我們的需求了。

   在看代碼的下一句:

        Blue8 = _mm_or_si128(Blue8, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, 2, 5, 8, 11, 14, -1, -1, -1, -1, -1)));

  這句的后半部分和前面的類似,只是里面的常數不同,由_mm_shuffle_epi8(Src2, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, 2, 5, 8, 11, 14, -1, -1, -1, -1, -1))得到的臨時數據為:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
0 0 0 0 0 0 B7 B8 B9 B10 B11 0 0 0 0 0

 

 

     如果把這個臨時結果和之前的Blue8進行或操作甚至直接進行加操作,新的Blue8變量則為:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
B1 B2 B3 B4 B5 B6 B7 B8 B9 B10 B11 0 0 0 0 0

 

 

      最后這一句和Blue8相關的代碼為:

Blue8 = _mm_or_si128(Blue8, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 4, 7, 10, 13)));

  后面的shuffle臨時的得到的變量為:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
0 0 0 0 0 0 0 0 0 0 0 B12 B13 B14 B15 B16

 

 

     再次和之前的Blue8結果進行或操作得到最終的結果:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
B1 B2 B3 B4 B5 B6 B7 B8 B9 B10 B11 B12 B13 B14 B15 B16

 

 

     對於Green和Red分量,處理的方法和步驟是一樣的,只是由於位置不同,每次進行shuffle操作的常數有所不同,但原理完全一致。

  如果理解了由BGRBGRBGR ---》變為了BBBGGGRRR這樣的模式的原理后,那么由BBBGGGRRR-->變為BGRBGRBGR的道理就非常淺顯了,這里不贅述,直接貼出代碼:

    Dest1 = _mm_shuffle_epi8(Blue8, _mm_setr_epi8(0, -1, -1, 1, -1, -1, 2, -1, -1, 3, -1, -1, 4, -1, -1, 5));
    Dest1 = _mm_or_si128(Dest1, _mm_shuffle_epi8(Green8, _mm_setr_epi8(-1, 0, -1, -1, 1, -1, -1, 2, -1, -1, 3, -1, -1, 4, -1, -1)));
    Dest1 = _mm_or_si128(Dest1, _mm_shuffle_epi8(Red8, _mm_setr_epi8(-1, -1, 0, -1, -1, 1, -1, -1, 2, -1, -1, 3, -1, -1, 4, -1)));
            
    Dest2 = _mm_shuffle_epi8(Blue8, _mm_setr_epi8(-1, -1, 6, -1, -1, 7, -1, -1, 8, -1, -1, 9, -1, -1, 10, -1));
    Dest2 = _mm_or_si128(Dest2, _mm_shuffle_epi8(Green8, _mm_setr_epi8(5, -1, -1, 6, -1, -1, 7, -1, -1, 8, -1, -1, 9, -1, -1, 10)));
    Dest2 = _mm_or_si128(Dest2, _mm_shuffle_epi8(Red8, _mm_setr_epi8(-1, 5, -1, -1, 6, -1, -1, 7, -1, -1, 8, -1, -1, 9, -1, -1)));

    Dest3 = _mm_shuffle_epi8(Blue8, _mm_setr_epi8(-1, 11, -1, -1, 12, -1, -1, 13, -1, -1, 14, -1, -1, 15, -1, -1));
    Dest3 = _mm_or_si128(Dest3, _mm_shuffle_epi8(Green8, _mm_setr_epi8(-1, -1, 11, -1, -1, 12, -1, -1, 13, -1, -1, 14, -1, -1, 15, -1)));
    Dest3 = _mm_or_si128(Dest3, _mm_shuffle_epi8(Red8, _mm_setr_epi8(10, -1, -1, 11, -1, -1, 12, -1, -1, 13, -1, -1, 14, -1, -1, 15)));

  核心還是這些常數的選取。

      以上是處理的第一步,看上去這個代碼很多,實際上他們的執行時非常快的,3000*2000的圖這個拆分和合並過程也就大概2ms。

      當然由於字節數據類型的表達范圍非常有限,除了少有的幾個有限的操作能針對字節類型直接處理外,比如本例的丘RGB的Max值,就可以直接用下面的SIMD指令實現:

Max8 = _mm_max_epu8(_mm_max_epu8(Blue8, Green8), Red8);

      很其他多計算都是無法直接在這樣的范圍內進行了,因此就有必要將數據類型擴展,比如擴展到short類型或者int/float類型。

      在SSE里進行這樣的操作也是非常簡單的,SSE提供了大量的數據類型轉換的函數和指令,比如有byte擴展到short,則可以用_mm_unpacklo_epi8和_mm_unpackhi_epi8配合zero來實現:

BL16 = _mm_unpacklo_epi8(Blue8, Zero);
BH16 = _mm_unpackhi_epi8(Blue8, Zero);

  其中

Zero = _mm_setzero_si128();

   很有意思的操作,比如_mm_unpacklo_epi8是將兩個__m128i的低8位交錯布置形成一個新的128位數據,如果其中一個參數為0,則就是把另外一個參數的低8個字節無損的擴展為16位了,以上述BL16為例,其內部布局為:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
B1 0 B2 0 B3 0 B3 0 B4 0 B5 0 B6 0 B7 0

 

 

  如果我們需要進行在int范圍內進行計算,則還需進一步擴展,此時可以使用_mm_unpackhi_epi16/_mm_unpacklo_epi16配合zero繼續進行擴展,這樣一個Blue8變量需要4個__m128i int范圍的數據來表達。

      好,說道這里,我們繼續看我們C語言里的這句:

  int Avg = (Blue + Green + Green + Red) >> 2;

  可以看到,這里的計算是無法再byte范圍內完成的,中間的Blue + Green + Green + Red在大部分情況下都會超出255而絕對小於255*4,,因此我們需要擴展數據到16位,按上述辦法,對Blue8\Green8\Red8\Max8進行擴展,如下所示:

    BL16 = _mm_unpacklo_epi8(Blue8, Zero);
    BH16 = _mm_unpackhi_epi8(Blue8, Zero);
    GL16 = _mm_unpacklo_epi8(Green8, Zero);
    GH16 = _mm_unpackhi_epi8(Green8, Zero);
    RL16 = _mm_unpacklo_epi8(Red8, Zero);
    RH16 = _mm_unpackhi_epi8(Red8, Zero);
    MaxL16 = _mm_unpacklo_epi8(Max8, Zero);
    MaxH16 = _mm_unpackhi_epi8(Max8, Zero);

  此時計算Avg就水到渠成了:

     AvgL16 = _mm_srli_epi16(_mm_add_epi16(_mm_add_epi16(BL16, RL16), _mm_slli_epi16(GL16, 1)), 2);
     AvgH16 = _mm_srli_epi16(_mm_add_epi16(_mm_add_epi16(BH16, RH16), _mm_slli_epi16(GH16, 1)), 2);

  中間兩個Green相加是用移位還是直接相加對速度沒啥影響的。

       接下來的優化則是本例的一個特色部分了。我們來詳細分析。

       我們知道,SSE對於跳轉是很不友好的,他非常擅長序列化處理一個事情,雖然他提供了很多比較指令,但是很多情況下復雜的跳轉SSE還是無論為力,對於本例,情況比較特殊,如果要使用SSE的比較指令也是可以直接實現的,實現的方式時,使用比較指令得到一個Mask,Mask中符合比較結果的值會為FFFFFFFF,不符合的為0,然后把這個Mask和后面需要計算的某個值進行And操作,由於和FFFFFFFF進行And操作不會改變操作數本身,和0進行And操作則變為0,在很多情況下,就是無論你符合條件與否,都進行后面的計算,只是不符合條件的計算不會影響結果,這種計算可能會低效SSE優化的部分提速效果,這個就要具體情況具體分析了。

      注意觀察本例的代碼,他的本意是如果最大值和某個分量的值不相同,則進行后面的調整操作,否則不進行調節。可后面的調整操作中有最大值減去該分量的操作,也就意味着如果最大值和該分量相同,兩者相減則為0,調整量此時也為0,並不影響結果,也就相當於沒有調節,因此,把這個條件判斷去掉,並不會影響結果。同時考慮到實際情況,最大值在很多情況也只會和某一個分量相同,也就是說只有1/3的概率不執行跳轉后的語句,在本例中,跳轉后的代碼執行復雜度並不高,去掉這些條件判斷從而增加一路代碼所消耗的性能和減少3個判斷的時間已經在一個檔次上了,因此,完全可以刪除這些判斷語句,這樣就非常適合於SSE實現了。

  接着分析,由於代碼中有((Max - Blue) * AmtVal) >> 14,其中AmtVal = (Max - Avg) * Adjustment,展開即為:  ((Max - Blue) * (Max - Avg) * Adjustment)>>14;這三個數據相乘很大程度上會超出short所能表達的范圍,因此,我們還需要對上面的16位數據進行擴展,擴展到32位,這樣就多了很多指令,那么有沒有不需要擴展的方式呢。經過一番思索,我提出了下述解決方案:

    在超高速指數模糊算法的實現和優化(10000*10000在100ms左右實現 一文中,我在文章最后提到了終極的一個指令:_mm_mulhi_epi16(a,b),他能一次性處理8個16位數據,其計算結果相當於對於(a*b)>>16,但這里很明a和b必須是short類型所能表達的范圍。

       注意我們的這個表達式:

              ((Max - Blue) * (Max - Avg) * Adjustment)>>14

       首先,我們將他擴展為移位16位的結果,變為如下:

         ((Max - Blue) * 4 * (Max - Avg) * Adjustment)>>16

      Adjustment我們已經將他限定在了[-128,128]之間,而(Max - Avg)理論上的最大值為255 - 85=170,(即RGB分量有一個是255,其他的都為0),最小值為0,因此,兩者在各自范圍內的成績不會超出short所能表達的范圍,而(Max-Blue)的最大值為255,最小值為0,在乘以4也在short類型所能表達的范圍內。所以,下一步你們懂了嗎?

       經過上述分析,下面這四行C代碼可由下述SSE函數實現:

    int AmtVal = (Max - Avg) * Adjustment;                                //    Get adjusted average
    if (Blue != Max)    Blue += (((Max - Blue) * AmtVal) >> 14);
    if (Green != Max)    Green += (((Max - Green) * AmtVal) >> 14);
    if (Red != Max)        Red += (((Max - Red) * AmtVal) >> 14);

  對應的SSE代碼為:

    AmtVal = _mm_mullo_epi16(_mm_sub_epi16(MaxL16, AvgL16), Adjustment128);
    BL16 = _mm_adds_epi16(BL16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxL16, BL16), 2), AmtVal));
    GL16 = _mm_adds_epi16(GL16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxL16, GL16), 2), AmtVal));
    RL16 = _mm_adds_epi16(RL16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxL16, RL16), 2), AmtVal));
            
    AmtVal = _mm_mullo_epi16(_mm_sub_epi16(MaxH16, AvgH16), Adjustment128);
    BH16 = _mm_adds_epi16(BH16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxH16, BH16), 2), AmtVal));
    GH16 = _mm_adds_epi16(GH16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxH16, GH16), 2), AmtVal));
    RH16 = _mm_adds_epi16(RH16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxH16, RH16), 2), AmtVal));

  最后一步就是將這些16位的數據再次轉換為8位的,注意原始代碼中有Clamp操作,這個操作其實是個耗時的過程,而SSE天然的具有抗飽和的函數。

  Blue8 = _mm_packus_epi16(BL16, BH16);
  Green8 = _mm_packus_epi16(GL16, GH16);
  Red8 = _mm_packus_epi16(RL16, RH16);
 _mm_packus_epi16這個的用法和含義自己去MSDN搜索一下吧,實在是懶得解釋了。

   最終優化速度:5ms。

   來個速度比較:

版本 VB6.0 C++,float優化版本 C++定點版 C++/SSE版
速度 400ms 70ms 35ms 5ms

 

     

  上面的VB6.0的耗時是原作者的代碼編譯后的執行速度,如果我自己去用VB6.0去優化他的話,有信心能做到70ms以內的。

  但無論如何,SSE優化的速度提升是巨大的。

結論:

       簡單的分析了自然飽和度算法的實現,分享了其SSE實現的過程,對於那些剛剛接觸SSE,想做圖像處理的朋友有一定的幫助。

        源代碼下載地址:http://files.cnblogs.com/files/Imageshop/Vibrance.rar

        寫的真的好累,休息去了,覺得對你有用的請給我買杯啤酒或者咖啡吧。

 


免責聲明!

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



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