在不少场合须要高效率的肤色检测代码,本人经常使用的一个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,有兴趣的朋友能够看看。