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

  Vibrance這個單詞搜索翻譯通常振動,抖動或者是響亮、活力,可是官方的詞彙裏還歷來未出現過天然飽和度這個詞,也不知道當時的Adobe中文翻譯人員怎麼會這樣處理。可是咱們看看PS對這個功能的解釋:html

       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.git

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

       閒話很少說了,其實天然飽和度也是最近幾個版本的PS纔出現的功能,在調節有些圖片的時候會有不錯的效果,也能夠做爲簡單的膚色調整的一個算法,好比下面這位小姑娘,用天然飽和度便可以讓她失血過多,也可讓他膚色紅暈。算法

                 

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

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

'***************************************************************************
'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官方文檔的描述有相似之處。ide

   咱們在貼出他的核心代碼:函數

 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的代碼以下:post

 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

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

相關文章
相關標籤/搜索