在很多场合需要高效率的肤色检测代码,本人常用的一个C++版本的代码如下所示:

void IM_GetRoughSkinRegion(unsigned char *Src, unsigned char *Skin, int Width, int Height, int Stride)
{
for (int Y = ; 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 = ; X < Width; X++)
{
int Blue = LinePS[], Green = LinePS[], Red = LinePS[];
if (Red >= && Green >= && Blue >= && Red >= Blue && (Red - Green) >= && IM_Max(IM_Max(Red, Green), Blue) - IM_Min(IM_Min(Red, Green), Blue) >= )
LinePD[X] = ; // 全为肤色部分
else
LinePD[X] = ;
LinePS += ; // 移到下一个像素
}
}
}

  这段代码效率的效率已经很高了,对于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 + ));
Src2 = _mm_loadu_si128((__m128i *)(LinePS + ));
Src3 = _mm_loadu_si128((__m128i *)(LinePS + )); Blue = _mm_shuffle_epi8(Src1, _mm_setr_epi8(, , , , , , -, -, -, -, -, -, -, -, -, -));
Blue = _mm_or_si128(Blue, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-, -, -, -, -, -, , , , , , -, -, -, -, -)));
Blue = _mm_or_si128(Blue, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-, -, -, -, -, -, -, -, -, -, -, , , , , ))); Green = _mm_shuffle_epi8(Src1, _mm_setr_epi8(, , , , , -, -, -, -, -, -, -, -, -, -, -));
Green = _mm_or_si128(Green, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-, -, -, -, -, , , , , , , -, -, -, -, -)));
Green = _mm_or_si128(Green, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-, -, -, -, -, -, -, -, -, -, -, , , , , ))); Red = _mm_shuffle_epi8(Src1, _mm_setr_epi8(, , , , , -, -, -, -, -, -, -, -, -, -, -));
Red = _mm_or_si128(Red, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-, -, -, -, -, , , , , , -, -, -, -, -, -)));
Red = _mm_or_si128(Red, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-, -, -, -, -, -, -, -, -, -, , , , , , ))); 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()); // Blue >= 20
Result = _mm_and_si128(Result, _mm_cmpge_epu8(Green, _mm_set1_epi8())); // Green >= 40
Result = _mm_and_si128(Result, _mm_cmpge_epu8(Red, _mm_set1_epi8())); // 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())); // (Red - Green) >= 10
Result = _mm_and_si128(Result, _mm_cmpge_epu8(_mm_subs_epu8(Max, Min), _mm_set1_epi8())); // IM_Max(IM_Max(Red, Green), Blue) - IM_Min(IM_Min(Red, Green), Blue) >= 10
Result = _mm_or_si128(Result, _mm_set1_epi8());
_mm_storeu_si128((__m128i*)(LinePD + ), 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,有兴趣的朋友可以看看。

SSE图像算法优化系列十:简单的一个肤色检测算法的SSE优化。的更多相关文章

  1. SSE图像算法优化系列十四:局部均方差及局部平方差算法的优化。

    关于局部均方差有着较为广泛的应用,在我博客的基于局部均方差相关信息的图像去噪及其在实时磨皮美容算法中的应用及使用局部标准差实现图像的局部对比度增强算法中都有谈及,即可以用于去噪也可以用来增强图像,但是 ...

  2. SSE图像算法优化系列十二:多尺度的图像细节提升。

    无意中浏览一篇文章,中间提到了基于多尺度的图像的细节提升算法,尝试了一下,还是有一定的效果的,结合最近一直研究的SSE优化,把算法的步骤和优化过程分享给大家. 论文的全名是DARK IMAGE ENH ...

  3. SSE图像算法优化系列十五:YUV/XYZ和RGB空间相互转化的极速实现(此后老板不用再担心算法转到其他空间通道的耗时了)。

    在颜色空间系列1: RGB和CIEXYZ颜色空间的转换及相关优化和颜色空间系列3: RGB和YUV颜色空间的转换及优化算法两篇文章中我们给出了两种不同的颜色空间的相互转换之间的快速算法的实现代码,但是 ...

  4. SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。

    这是一篇2010年比较古老的文章了,是在QQ群里一位群友提到的,无聊下载看了下,其实也没有啥高深的理论,抽空实现了下,虽然不高大上,还是花了点时间和心思优化了代码,既然这样,就顺便分享下优化的思路和经 ...

  5. SSE图像算法优化系列十八:三次卷积插值的进一步SSE优化。

    本文是在学习https://blog.csdn.net/housisong/article/details/1452249一文的基础上对算法的理解和重新整理,再次非常感谢原文作者的深入分析以及分享. ...

  6. 第二十九节,目标检测算法之R-CNN算法详解

    Girshick, Ross, et al. “Rich feature hierarchies for accurate object detection and semantic segmenta ...

  7. SSE图像算法优化系列十六:经典USM锐化中的分支判断语句SSE实现的几种方法尝试。

    分支判断的语句一般来说是不太适合进行SSE优化的,因为他会破坏代码的并行性,但是也不是所有的都是这样的,在合适的场景中运用SSE还是能对分支预测进行一定的优化的,我们这里以某一个算法的部分代码为例进行 ...

  8. SSE图像算法优化系列六:OpenCv关于灰度积分图的SSE代码学习和改进。

    最近一直沉迷于SSE方面的优化,实在找不到想学习的参考资料了,就拿个笔记本放在腿上翻翻OpenCv的源代码,无意中看到了OpenCv中关于积分图的代码,仔细研习了一番,觉得OpenCv对SSE的灵活运 ...

  9. Mysql优化系列(1)--Innodb引擎下mysql自身配置优化

    1.简单介绍InnoDB给MySQL提供了具有提交,回滚和崩溃恢复能力的事务安全(ACID兼容)存储引擎.InnoDB锁定在行级并且也在SELECT语句提供一个Oracle风格一致的非锁定读.这些特色 ...

随机推荐

  1. 解决VMware虚拟机不能上网的问题

    这是其中一种情况,因为使用360等加速的时候把VMware 的DHCP等服务关闭了,重新启动服务即可,方法是WIN+R 输入cmd,在doc输入services.ms打开服务,找到 把这些都启动即可

  2. NPOI 生成 excel基本设置

    //设置页眉页脚 tempSheet.Header.Center = "2017-04-27"; tempSheet.Footer.Center = "√" + ...

  3. PHP xmapp 下面安装 Composer-Setup.exe

    1.打开PHP配置文件E:\xampp\php\php.ini确认以下模块已开启(移除前面的分号). extension=php_openssl.dll, (php.ini文档里面开启一次就OK了) ...

  4. CSS3学习系列之动画

    Transitions功能使用方法 在css3中,transitions功能通过将元素的某个属性从一个属性值在指定的时间内平滑过渡到另一个属性值来实现动画功能,可通过transitions属性来使用t ...

  5. 阿里云服务器 Windows连接不成功 提示“你的凭证不工作” 解决方法

    HKEY_LOCAL_MACHINE -> SYSTEM -> CurrentControlSet -> Control ->Terminal Server -> Wds ...

  6. voa 2015 / 4 / 15

    illustrated - v. to explain or decorate a story, book, etc., with pictures pediatrician – n. a docto ...

  7. POJ 3279 枚举(思维)

    Fliptile Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 10931   Accepted: 4029 Descrip ...

  8. 线程池与Threadlocal

    线程池与Threadlocal 线程池: 线程池是为了使线程能够得到循环的利用,线程池里面养着一些线程,有任务需要使用线程的时候就往线程池里抓线程对象出来使用.线程池里的线程能够重复使用,所以在资源上 ...

  9. Mongoose基础入门

    前面的话 Mongoose是在node.js异步环境下对mongodb进行便捷操作的对象模型工具.本文将详细介绍如何使用Mongoose来操作MongoDB NodeJS驱动 在介绍Mongoose之 ...

  10. ES6——块级作用域

    前面的话 过去,javascript缺乏块级作用域,var声明时的声明提升.属性变量等行为让人困惑.ES6的新语法可以帮助我们更好地控制作用域.本文将详细介绍ES6新引入的块级作用域绑定机制.let和 ...