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,有兴趣的朋友可以看看。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏ImportSource

一个程序员才能看懂的母亲节礼物

母亲,节日快乐,今天周日,这会我正在屏幕前写代码,借用这个公众号,向您说声:我爱你。祝天下所有的母亲节日快乐! Make Your Mama Proud ? ?...

3406
来自专栏一心无二用,本人只专注于基础图像算法的实现与优化。

24位真彩色图像转换为16位高彩色图像的实现方法及效果改进

 本文是对多年前作者的一篇博文的重新整理和书写。 一、前言        高彩色位图像即我们常说16位图像,每个像素占用两个字节,相比于24位真彩色来说,...

3075
来自专栏一心无二用,本人只专注于基础图像算法的实现与优化。

13行代码实现最快速最高效的积分图像算法。

  研究图像到一定程度的人,应该都对积分图像有所了解,大家在百度或者google中都可以搜索到大量的相关博客,我这里不做多介绍。用积分图也确实能解决很多实际的问...

3238
来自专栏移动端开发

AVFoundation 框架初探究(四)

叨叨两句 ----       动手写这篇总结时候也是二月底过完年回来上班了,又开始新的一年了,今年会是什么样子?这问题可能得年底再回答自己了。在家窝了那么久,...

3085
来自专栏小樱的经验随笔

CDOJ 1330 柱爷与远古法阵【高斯消元,卡精度】

柱爷与远古法阵 Time Limit: 125/125MS (Java/Others)     Memory Limit: 240000/240000KB (J...

2577
来自专栏我爱编程

Matplotlib入门

qiangbo.space/2018-04-06/matplotlib_l1/ 入门代码示例 import matplotlib.pyplot as plt ...

3679
来自专栏一“技”之长

Cocos2d-x-v3动作体系 原

        cocos2d-x-v3版本v2的版本有的很大的改动,最直观的是在一些函数的改动和类名的改动上,首先以CC开头的类,都不再使用CC。在我个人的理...

561
来自专栏行者常至

pgsql GIS 将线段等分,均分,为点 st_lineinterpolatepoint

geometry ST_LineInterpolatePoint(geometry a_linestring, float8 a_fraction);

712
来自专栏一个会写诗的程序员的博客

来看看几种 Monad来看看几种 Monad

https://learnyoua.haskell.sg/content/zh-cn/ch12/a-fistful-of-monads.html

532
来自专栏GopherCoder

『Golang 内置模块库 template 』

3704

扫码关注云+社区