SSE图像算法优化系列七:基于SSE实现的极速的矩形核腐蚀和膨胀(最大值和最小值)算法。

  因未测试其他作者的算法时间和效率,本文不敢自称是最快的,但是速度也可以肯定说是相当快的,在一台I5机器上占用单核的资源处理 3000 * 2000的灰度数据用时约 20ms,并且算法和核心的大小是无关的,即所谓的o(1)算法。

      在实现本算法之前,也曾经参考何凯明在暗通道去雾时提出的一篇参考论文中的算法: STREAMING MAXIMUM-MINIMUM FILTER USING NO MORE THAN THREE COMPARISONS PER ELEMENT ,这篇文章所描述的过程也是o(1)的,速度也相当快,不过还是不够快。

      我曾经自己构思了一个想法,也是基于行列分离的,在速度上比上文的代码又要快,并且也是o(1)算法,但是算法速度和图片的内容有关,比如对一个图进行了一次算法后,再次对结果执行相同的算法,可能后一次就要慢很多,这和我改进的算法本身有关系,但是这种情况是很少见的。

     本文所要介绍的算法也是在很久以前就看到过的,但是一直没有引起我的重视,其对应的参考论文是 A fast algorithm for local minimum and maximum filters on rectangular and octagonal kernels  ,当时觉得论文的实现起来可能没有我自己构思的快,也就没有去深究。

     论文的实现步骤也是基于行列分离的,即先进行行方向的一维运算,然后再对结果进行列方向的一维计算,具体的理论描述大家去研究论文吧。

     那其实论文的核心就是下面这个图。

    In表示一维行方向的输入数据,g/h分别运算过程中的两个中间数据,和输入数据大小一样。

      如上图所示,我们假定需要进行计算的核大小为R,那么将一行分为多个大小为 D =(2R+1) 的分段,例如图中R=2, D=5 ,对每一个分段进行预处理,其中 x 号位置存放的是箭头所在直线段上的点中的最大值(最小值),如此处理得到 g 和 h 两个数组,那么对于某个点(索引为I),其半径R内的最大(小)值为:Max/ Min(g(I+R),h(I-R))。

      过程好简单。

      我们拿一组数据来说明上述过程,假如一行数据如下,我们要进行膨胀操作(最大值),核半径为2:

          In:  20  12  35  9  10  7  32  15  20  45  28

  对应的g/h为:

          g:  20  20  35  35  35  7  32  32  32  45  45

       h:   35  35  35  10   9  45 45  45  45  45  28 

      如果我们要计算第4个点的半径为2的最大值,则对应   g(I+R) = g(4+2) = 7, h(I-R)=h(4-2)=35, 得到结果为max(7,35) = 35;

      如果我们要计算第6个点的半径为2的最大值,则对应   g(I+R) = g(6+2) = 32, h(I-R)=h(6-2)=10, 得到结果为max(32,10) = 32;

      注意上面索引是以1位小标计数起点的。

边缘处理

      注意到在边缘处,比如左侧边缘,当要计算的点的索引小于R时,这时h值是无效的,而在右侧边缘,g值是无法取到的,但是仔细观察,问题很简单,还拿上述数据为例,如果要计算索引为2处半径为2的最值,由于h(2-2)是超出索引的(前面说了本例以1为下标起点),因此不能用前述的方式,那我们回归到问题的本质,计算2处半径为2的最值,就是计算max(In(1), In(2), In(3), In(4)), 而这个值不就是g数据中索引为2+2处的数据吗,在这之前就已经帮我们算法,直接取值就可以了。

     在数据尾部(右侧),情况有所不同,我们应该从H中取值。

     从上述的分析可知,这个算法有个特性,就是半径越大,算法的耗时会越小,因为对边缘的处理只需要拷贝数据,而没有了更多的计算,可能很多人不信吧。

算法实现:

 有了上面的描述,要实现一个快速的腐蚀或膨胀算法相信对部分来说应该是一件非常容易的事情,先行方向处理,在列方向,好简单。

   最近我是迷上了SSE算法优化,于是就思考了这个算法的SSE优化,以前在看SSE的函数时,就一直在想_mm_max_epi8/_mm_min_epi8这种一次性能获取16个字节数据的最值的函数是否能用在腐蚀和膨胀上,不过由于他们是对两个连续数据的比较,在行方向上是难以运用的,但是如果数据比较是列方向上,那不就可以使用了吗。

      我们上述算法就有列方向的比较,不就有了用武之地了。

      首先,我们给出在列方向更新g值/h值在每个分段范围内的C语言实现代码,比如获取g值大概的代码如下:

memcpy(G + StartY * ValidDataLength, Src + StartY * Stride, ValidDataLength);
for (int Y = StartY + 1; Y < EndY; Y++)
{
    unsigned char *LinePS = Src + Y * Stride;
    unsigned char *LinePD = G + Y * ValidDataLength;
    unsigned char *LinePD = G + (Y - 1) * ValidDataLength;
    for (int X = 0; X < ValidDataLength; X++)    
    {
        LinePD[X] = IM_Max(LinePS[X], LinePL[X]);
    }
}

  StartY为计算好的分段范围的起点,EndY为分段范围的终点,我们观察g数据的规律,知道在分段范围内第一行的最大值就是数据本身,而后面的则是和前一行比较得到结果。

  很明显:

 for (int X = 0; X < Width * Channel; X++)    
 {
    LinePD[X] = IM_Max(LinePS[X], LinePL[X]);
 }

  这个代码很容易向量化,如果这里是浮点运算,编译器会直接帮我们向量处理的,但是对于字节,似乎编译器还没有那么智能,我们自己手动来向量化,代码如下:

memcpy(G + StartY * ValidDataLength, Dest + StartY * ValidDataLength, ValidDataLength);                    //    每段G数据第一行就是原始的数据
for (int Y = StartY + 1; Y < EndY; Y++)
{
    unsigned char *LinePS = Dest + Y * ValidDataLength;
    unsigned char *LinePD = G + Y * ValidDataLength;
    unsigned char *LinePL = G + (Y - 1) * ValidDataLength;
    for (int X = 0; X < Block * BlockSize; X += BlockSize)
    {
        _mm_storeu_si128((__m128i *)(LinePD + X), _mm_max_epu8(_mm_loadu_si128((__m128i *)(LinePS + X)), _mm_loadu_si128((__m128i *)(LinePL + X))));
    }
    for (int X = Block * BlockSize; X < ValidDataLength; X++)
    {
        LinePD[X] = IM_Max(LinePS[X], LinePL[X]);
    }
}

 其中BlockSize = 16, Block = ValidDataLength / BlockSize;

     这段代码很简单,对于h的处理也是类似的道理。

     当我们获得了g,h的数据后,后面的处理过程的C代码也很简单:

    for (int Y = Radius; Y < IM_Min(BlockV * Size - Radius, Width); Y++)                //    这些位于中间的数据是符合 G+Radius 和 R - Radius 取大的要求的
    {
        unsigned char *LinePG = G + IM_Min(Y + Radius, Width - 1) * Width;
        unsigned char *LinePH = H + (Y - Radius) * ValidDataLength;
        unsigned char *LinePD = T + Y * ValidDataLength;for (int X = 0; X < ValidDataLength; X++)
        {
            LinePD[X] = IM_Max(LinePG[X], LinePH[X]);
        }
    }

又是同样的道理,内部的for循环可直接用SSE代替。

     但是这里只是对列方向进行处理,行方向有没有可能用SSE做处理呢,可以肯定的说,绝对可以,但是除非你非常有耐心,以为其中各种pack和unpack或者shuffle会让你疯掉,而且最后的效率也许还不如直接使用普通的C语言。

     那么如何处理呢,我想大家肯定能想到转置,确实,对数据进行转置后再进行列方向的处理,然后再转置回来就相当于对原数据的行方向处理。

     关于转置,一直也是个耗时的过程,但是我在图像转置的SSE优化(支持8位、24位、32位),提速4-6倍 一文中提到了利用SSE实现高速的转置操作,利用它去实现本文的流程则非常靠谱。

  那么我们贴出整体的大部分处理代码:

  垂直方向的核心:

int IM_Vert_MaxFilter(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, int Radius)
{
    int Channel = Stride / Width;
    if ((Src == NULL) || (Dest == NULL))                                return IM_STATUS_NULLREFRENCE;
    if ((Width <= 0) || (Height <= 0))                                    return IM_STATUS_INVALIDPARAMETER;
    if ((Channel != 1) && (Channel != 3) && (Channel != 4))                return IM_STATUS_INVALIDPARAMETER;

    //    从节省内存的角度考虑,可以只需要两倍额外的内存    
    unsigned char *G = (unsigned char *)malloc(Height * Width * Channel * sizeof(unsigned char));
    unsigned char *H = (unsigned char *)malloc(Height * Width * Channel * sizeof(unsigned char));

    if ((G == NULL) || (H == NULL))
    {
        if (G != NULL) free(G);
        if (H != NULL) free(H);
        return IM_STATUS_OUTOFMEMORY;
    }

    //    垂直方向处理
    int Size = Radius * 2 + 1, ValidDataLength = Width * Channel;
    int BlockSize = 16, Block = ValidDataLength / BlockSize;
    int BlockV = ((Height % Size) == 0 ? Height / Size : (Height / Size) + 1);

    for (int Index = 0; Index < BlockV; Index++)
    {
        int StartY = Index * Size, EndY = IM_Min(Index * Size + Size, Height);
        memcpy(G + StartY * ValidDataLength, Src + StartY * Stride, ValidDataLength);                    //    每段G数据第一行就是原始的数据

        for (int Y = StartY + 1; Y < EndY; Y++)
        {
            unsigned char *LinePS = Src + Y * Stride;
            unsigned char *LinePD = G + Y * ValidDataLength;
            unsigned char *LinePL = G + (Y - 1) * ValidDataLength;
            for (int X = 0; X < Block * BlockSize; X += BlockSize)    
            {
                _mm_storeu_si128((__m128i *)(LinePD + X), _mm_max_epu8(_mm_loadu_si128((__m128i *)(LinePS + X)), _mm_loadu_si128((__m128i *)(LinePL + X))));
            }
            for (int X = Block * BlockSize; X < ValidDataLength; X++)
            {
                LinePD[X] = IM_Max(LinePS[X], LinePL[X]);
            }
        }

        memcpy(H + StartY * ValidDataLength, G + (EndY - 1) * ValidDataLength, ValidDataLength);        //    每段H数据的第一行就是对应G数据的最后一行
        memcpy(H + (EndY - 1) * ValidDataLength, Src + (EndY - 1) * Stride, ValidDataLength);            //    每段H数据的最后一行就是原始的数据

        for (int Y = EndY - 2; Y > StartY; Y--)                                                            //    注意循环次数的改变
        {
            unsigned char *LinePS = Src + Y * Stride;
            unsigned char *LinePD = H + Y * ValidDataLength;
            unsigned char *LinePL = H + (Y + 1) * ValidDataLength;
            for (int X = 0; X < Block * BlockSize; X += BlockSize)
            {
                _mm_storeu_si128((__m128i *)(LinePD + X), _mm_max_epu8(_mm_loadu_si128((__m128i *)(LinePS + X)), _mm_loadu_si128((__m128i *)(LinePL + X))));
            }
            for (int X = Block * BlockSize; X < ValidDataLength; X++)
            {
                LinePD[X] = IM_Max(LinePS[X], LinePL[X]);
            }
        }
        //    针对最值算法,在列方向最后一块不是Size大小时,后面的数据只能是重复边缘像素,这样后面跟的G/H值和Height - 1大小是相同的
    }

    //    整个的数据分为三个部分,[0, Radius]为第一组,[Radius, BlockV * Size - Radius]为第二组,[BlockV * Size - Radius, BlockV * Size]为第三组,
    //    第一组数据的结果取G中[Radius, 2 * Radius]的值,第二组数据取G + Radius和H - Radius中的小值,第三组取H - Radius的值。


    //    最顶部的一半数据,此时的H数据无效
    //
    //    此处删除若干代码
    //

    for (int Y = Radius; Y < IM_Min(BlockV * Size - Radius, Height); Y++)    //    这些位于中间的数据是符合 G + Radius 和 R - Radius 取大的要求的
    {
        unsigned char *LinePG = G + IM_Min(Y + Radius, Height - 1) * ValidDataLength;        //    有可能超出范围的
        unsigned char *LinePH = H + (Y - Radius) * ValidDataLength;
        unsigned char *LinePD = Dest + Y * Stride;
        for (int X = 0; X < Block * BlockSize; X += BlockSize)
        {
            _mm_storeu_si128((__m128i *)(LinePD + X), _mm_max_epu8(_mm_loadu_si128((__m128i *)(LinePG + X)), _mm_loadu_si128((__m128i *)(LinePH + X))));
        }
        for (int X = Block * BlockSize; X < ValidDataLength; X++)
        {
            LinePD[X] = IM_Max(LinePG[X], LinePH[X]);
        }
    }
    //    最底部的一半数据,此时的G数据无用
    //
    //    此处删除若干代码
    //
    free(G);
    free(H);
    return IM_STATUS_OK;
}

  综合的调用:

int IM_MaxFilter(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, int Radius)
{
    int Channel = Stride / Width;
    if ((Src == NULL) || (Dest == NULL))                                return IM_STATUS_NULLREFRENCE;
    if ((Width <= 0) || (Height <= 0))                                    return IM_STATUS_INVALIDPARAMETER;
    if ((Channel != 1) && (Channel != 3) && (Channel != 4))                return IM_STATUS_INVALIDPARAMETER;
    
    int Status = IM_STATUS_OK;
    unsigned char *T = (unsigned char *)malloc(Height * Stride * sizeof(unsigned char));
    if (T == NULL)    return IM_STATUS_OUTOFMEMORY;
    Status = IM_Vert_MaxFilter(Src, T, Width, Height, Stride, Radius);
    if (Status != IM_STATUS_OK)    goto FreeMemory;
    Status = IM_Transpose(T, Dest, Width, Height, Stride, Height, Width, Height * Channel);        //    转置,注意Dest我只用了Height * Channel的数据
    if (Status != IM_STATUS_OK)    goto FreeMemory;
    Status = IM_Vert_MaxFilter(Dest, T, Height, Width, Height * Channel, Radius);
    if (Status != IM_STATUS_OK)    goto FreeMemory;
    Status = IM_Transpose(T, Dest, Height, Width, Height * Channel, Width, Height, Stride);
FreeMemory:
    free(T);
    return Status;
}

 上面代码中还有很多细节,包括分块尾部不完整数据的处理,大家可以自己理解。

     有两处删除了部分代码,删除的代码是很容易补上去的,因为我不喜欢我的代码被别人直接复制黏贴。

进一步的分析:

由上面的代码可以看到,要实现整个的过程,我们需要原图3倍大小的额外内存,那么是否有降低这个的可能性呢,是有的,在处理列方向时,我们可以一次性只处理16列或32列,这样g/h数据只各需要Height * 16(32) * sizeof(unsigned char)大小的内存,而且这样做还有一个优势就是在每个分段内部比较时,由于更新的内容较少,可以用一个xmm寄存器保存最值得临时结果,这样就不用去加载上一行的内存数据,少了很多次内存读写的过程,一个简单的示例代码如下所示:

    unsigned char *LinePS = Src + StartY * Stride + X;
    unsigned char *LinePD = G + StartY * 32;
    __m128i Max1 = _mm_setzero_si128(), Max2 = _mm_setzero_si128();             //    这样写能减少一次内存加载
    for (int Y = StartY; Y < EndY; Y++)
    {
        Max1 = _mm_max_epu8(_mm_loadu_si128((__m128i *)(LinePS + 0)), Max1);    //    或者使用一条AVX指令
        Max2 = _mm_max_epu8(_mm_loadu_si128((__m128i *)(LinePS + 16)), Max2);
        _mm_store_si128((__m128i *)(LinePD + 0), Max1);
        _mm_store_si128((__m128i *)(LinePD + 16), Max2);
        LinePS += Stride;
        LinePD += 32;
    }

  在我的笔记本中测试,这个的速度要比上面的版本还快一点,并且有占用了更少的内存,一举两得啊。

     欢迎大家能提供更快速的算法的实现思路。

     本文Demo下载地址:  http://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar,里面的所有算法都是基于SSE实现的。

  如果觉得本文对你有帮助,请为本文点个赞。

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏数据小魔方

R预设配色系统及自定义色板

关于配色的话题,已经聊过很多次了,但是就像是之前说过的,对于图形可视化而言,配色决定着作品的“颜值”,谈再多次都不嫌多。 今天是R语言配色系统综合篇的上篇(当然...

4249
来自专栏落影的专栏

GPUImage详细解析(四)模糊图片处理

回顾 解析(一) 解析(二) 解析(三) 这次介绍的GPUImageContext、GPUImageFramebufferCache和GPUImagePi...

3425
来自专栏人工智能LeadAI

Python实现动态图的解析、合成与倒放

动态图现在已经融入了我们的日常网络生活,大大丰富了我们的表达方式和交流趣味性。常常是一言不合就放动图,我这里就不举例子了。咱们直接开始本文的内容。

852
来自专栏腾讯Bugly的专栏

Android自绘动画实现与优化实战——以Tencent OS录音机波形动画为实例

前言 我们所熟知的,Android 的图形绘制主要是基于 View 这个类实现。 每个 View 的绘制都需要经过 onMeasure、onLayout、onD...

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

二值图像中封闭孔洞的高效填充算法(附源码)。

     鉴于心情不好,这篇文章只是简单的说说这个算法的过程。      在对图像二值化后,不管用的是什么二值算法,总会存在一些瑕疵,这个时候我们就需要进行一些...

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

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

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

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

SSE图像算法优化系列十一:使用FFT变换实现图像卷积。

  本文重点主要不在于FFT的SSE优化,而在于使用FFT实现快速卷积的相关技巧和过程。   关于FFT变换,有很多参考的代码,特别是对于长...

3159
来自专栏点滴积累

PhiloGL学习(4)——三维对象、加载皮肤

前言 上一篇文章中介绍了如何响应鼠标和键盘事件,本文介绍如何加载三维对象并实现给三维对象添加一个漂亮的皮肤。 一、 原理分析 我对三维的理解为:所谓三维对象无非...

3286
来自专栏数据小魔方

R语言可视化——ggplot图表配色技巧

今天跟大家分享ggplot图表的配色原理与基本技巧。 图表配色是一个很深奥的话题,多亏了R语言平台的众多开发者贡献的配色包,让图表的配色不再深不可测。 这里我暂...

3574
来自专栏NewbieWeb

ThreeJS简易魔方自动还原实现(一)层先法

在ThreeJS四步制作一个简易魔方中介绍了怎么实现一个可以转动的简易魔方,接来下准备介绍下怎么让这个简易魔方具备自动还原的功能。

743

扫码关注云+社区