在不少場合須要高效率的膚色檢測代碼,本人經常使用的一個C++版本的代碼以下所示:html
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語言的方面的優化可能也就是循環展開了吧,實測速度也沒啥大的區別。ide
那咱們接着來嘗試下SIMD指令會有什麼結果。函數
在決定使用SIMD以前,我一直在猶豫,由於這個算法自己很簡單的,就是一些條件判斷組合,而SSE很是不適合於作判斷運算,同時普通C語言的&&運算具備短路功能,對於本例,當發現其中之一不符合條件後就直接跳出了循環,再也不進行後面的條件的計算和判斷了,而我代碼裏也已經把簡單的判斷條件放在前面,複雜一點的放在後面了。若是使用SSE去實現一樣的功能,因爲SSE的特性,咱們只能對全部的條件進行判斷,而後把每一個條件判斷的結果進行and操做,這個過程是沒法從中間中斷的(從代碼實現上說,是能夠的,可是那種方式必然更慢)。這種全面判斷的耗時和SSE處理器級別多路並行所帶來的加速孰重孰輕,在沒有實現以前內心確實有點不肯定。post
既然寫了本文,那必定是已經實現了該算法的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 一文裏有答案。能夠用以下代碼實現:url
#define _mm_cmpge_epu8(a, b) _mm_cmpeq_epi8(_mm_max_epu8(a, b), a)
第四個條件Red >= Blue 一樣能夠利用上面這個判斷來實現。spa
咱們再來看第五個條件(Red - Green) >= 10,若是直接計算Red - Green,則須要把他們轉換爲ushort類型才能知足可能存在的負數的狀況,可是若是使用_mm_subs_epu8這個飽和計算函數,當Red < Green時,Red - Green就被截斷爲0了,這個時候 (Red - Green) >= 10就會返回false了,而若是Red > Green, 則Red - Green的結果就不會發生截斷,就是理想的效果,所以,這個問題解決。code
最後一個條件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,有興趣的朋友能夠看看。