SSE圖像算法優化系列十:簡單的一個膚色檢測算法的SSE優化。


  在很多場合需要高效率的膚色檢測代碼,本人常用的一個C++版本的代碼如下所示:

void IM_GetRoughSkinRegion(unsigned char *Src, unsigned char *Skin, int Width, int Height, int Stride)
{
    for (int Y = 0; Y < Height; Y++)
    {
        unsigned char *LinePS = Src + Y * Stride;                    //    源圖的第Y行像素的首地址
        unsigned char *LinePD = Skin + Y * Width;                    //    Skin區域的第Y行像素的首地址    for (int X = 0; X < Width; X++)
        for (int X = 0; X < Width; X++)
        {
            int Blue = LinePS[0], Green = LinePS[1], Red = LinePS[2];
            if (Red >= 60 && Green >= 40 && Blue >= 20 && Red >= Blue && (Red - Green) >= 10 && IM_Max(IM_Max(Red, Green), Blue) - IM_Min(IM_Min(Red, Green), Blue) >= 10)
                LinePD[X] = 255;                                    //    全為膚色部分                                                                            
            else
                LinePD[X] = 16;
            LinePS += 3;                                            //    移到下一個像素        
        }
    }
}

  這段代碼效率的效率已經很高了,對於1080P含有人臉的一般圖像大概也就4.0ms就能處理完,效果嘛對於正常光照和膚色的檢測也還湊合,如下所示。

                  

      4.0ms確實已經很快了,不過在很多實時的場合,每幀里能節省下來1MS對於整體的流暢性都是有好處的,這個算法還有沒有提升速度的空間呢。常規的C語言的方面的優化可能也就是循環展開了吧,實測速度也沒啥大的區別。

      那我們接着來嘗試下SIMD指令會有什么結果。

      在決定使用SIMD之前,我一直在猶豫,因為這個算法本身很簡單的,就是一些條件判斷組合,而SSE非常不適合於做判斷運算,同時普通C語言的&&運算具有短路功能,對於本例,當發現其中之一不符合條件后就直接跳出了循環,不再進行后面的條件的計算和判斷了,而我代碼里也已經把簡單的判斷條件放在前面,復雜一點的放在后面了。如果使用SSE去實現同樣的功能,由於SSE的特性,我們只能對所有的條件進行判斷,然后把每個條件判斷的結果進行and操作,這個過程是無法從中間中斷的(從代碼實現上說,是可以的,但是那種方式必然更慢)。這種全面判斷的耗時和SSE處理器級別多路並行所帶來的加速孰重孰輕,在沒有實現之前心里確實有點不確定。

    既然寫了本文,那一定是已經實現了該算法的SSE版本代碼,我們來說為分析下實現的方式和可能用到的函數。 

      首先,我們要把R/G/B分量分別提取到一個SSE變量中,這個我們在SSE圖像算法優化系列八:自然飽和度(Vibrance)算法的模擬實現及其SSE優化(附源碼,可作為SSE圖像入門,Vibrance算法也可用於簡單的膚色調整) 一文里已經有提到了實現。

      接着看前面的三個判斷條件   Red >= 60 && Green >= 40 && Blue >= 20 , 我們需要一個unsigned char類型的比較函數,而SSE只提供了singed char類型的SSE比較函數,這個問題在A few missing SSE intrinsics 一文里有答案。可以用如下代碼實現:

#define _mm_cmpge_epu8(a, b) _mm_cmpeq_epi8(_mm_max_epu8(a, b), a)
 
        

      第四個條件Red >= Blue 同樣可以利用上面這個判斷來實現。

      我們再來看第五個條件(Red - Green) >= 10,如果直接計算Red - Green,則需要把他們轉換為ushort類型才能滿足可能存在的負數的情況,但是如果使用_mm_subs_epu8這個飽和計算函數,當Red < Green時,Red - Green就被截斷為0了,這個時候 (Red - Green) >= 10就會返回false了,而如果Red > Green, 則Red - Green的結果就不會發生截斷,就是理想的效果,因此,這個問題解決。

      最后一個條件IM_Max(IM_Max(Red, Green), Blue) - IM_Min(IM_Min(Red, Green), Blue) >= 10,這個也很簡單,先用_mm_max_epu8和_mm_min_epu8獲得B/G/R三分量的最大值和最小值,這個時候很明顯max>min,因此有可以直接使用_mm_subs_epu8函數生產不會截斷的正確結果。

      我們注意到SSE的比較函數(字節類型的)的返回結果只有0和255這兩種,因此上述的6個判斷條件結果直接進行and操作就可以獲得最后的組合值了,滿足所有的條件的像素結果就為255,而其他的則為0。

      在我們C語言版本的代碼中,不滿足條件的像素被設置為了16或者其他非零的值,這又怎么辦呢,同樣的道理,255和其他數進行or操作還是255,而0和其他數進行or操作就會變為其他數,因此最后再把上述結果和16這個常數進行or操作就可以得到正確的結果了,整理下來,主要代碼如下所示:

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

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

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

Red = _mm_shuffle_epi8(Src1, _mm_setr_epi8(2, 5, 8, 11, 14, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1));
Red = _mm_or_si128(Red, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-1, -1, -1, -1, -1, 1, 4, 7, 10, 13, -1, -1, -1, -1, -1, -1)));
Red = _mm_or_si128(Red, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 3, 6, 9, 12, 15)));
            
Max = _mm_max_epu8(_mm_max_epu8(Blue, Green), Red);                                                //    IM_Max(IM_Max(Red, Green), Blue)
Min = _mm_min_epu8(_mm_min_epu8(Blue, Green), Red);                                                //    IM_Min(IM_Min(Red, Green), Blue)
Result = _mm_cmpge_epu8(Blue, _mm_set1_epi8(20));                                                //    Blue >= 20
Result = _mm_and_si128(Result, _mm_cmpge_epu8(Green, _mm_set1_epi8(40)));                        //    Green >= 40
Result = _mm_and_si128(Result, _mm_cmpge_epu8(Red, _mm_set1_epi8(60)));                            //    Red >= 60
Result = _mm_and_si128(Result, _mm_cmpge_epu8(Red, Blue));                                        //  Red >= Blue
Result = _mm_and_si128(Result, _mm_cmpge_epu8(_mm_subs_epu8(Red, Green), _mm_set1_epi8(10)));    //    (Red - Green) >= 10 
Result = _mm_and_si128(Result, _mm_cmpge_epu8(_mm_subs_epu8(Max, Min), _mm_set1_epi8(10)));        //    IM_Max(IM_Max(Red, Green), Blue) - IM_Min(IM_Min(Red, Green), Blue) >= 10
Result = _mm_or_si128(Result, _mm_set1_epi8(16));
_mm_storeu_si128((__m128i*)(LinePD + 0), Result);

  循環計算100次的速度測試:

環境

1920*1080 膚色約占一半圖

1920*1080 全圖膚色

1920*1080 全圖無膚色

標准C語言

 400ms

 550ms

360ms 

SSE優化

 70ms

 70ms

70ms 



 

 

  

     

     可以看到,雖然SSE優化后的計算量理論上比普通的C語言大很多,但是SSE優化的算法有兩個好處,第一是速度快很多,最大加速比約有8倍了,第二是SSE的計算時間和圖像內容是無關的。

     這個結果令我大為震驚,看樣子SSE一次性處理16個字節的能力不是蓋的,同時也說明普通的C語言的跳轉也還是耗時的。

     完整工程的地址:http://files.cnblogs.com/files/Imageshop/GetSkinArea.rar

     結合膚色檢測以及以前研究的積分圖、均方差去噪等算法,我用純SSE寫了一個綜合的MakeUp算法,處理單幀的1080P的圖像用時大概也就在25ms內實現(單核),比純C語言的要快了3到4倍,如下圖所示:

   http://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar,這里是一個我全部用SSE優化的圖像處理的Demo,有興趣的朋友可以看看。

 


免責聲明!

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



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