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语言的跳转也还是耗时的。
完整工程的地址:https://files.cnblogs.com/files/Imageshop/GetSkinArea.rar
结合肤色检测以及以前研究的积分图、均方差去噪等算法,我用纯SSE写了一个综合的MakeUp算法,处理单帧的1080P的图像用时大概也就在25ms内实现(单核),比纯C语言的要快了3到4倍,如下图所示:
https://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar,这里是一个我全部用SSE优化的图像处理的Demo,有兴趣的朋友可以看看。