这半年多时间,基本都在折腾一些基本的优化,有很多都是十几年前的技术了,从随大流的角度来考虑,研究这些东西在很多人看来是浪费时间了,即不能赚钱,也对工作能力提升无啥帮助。可我觉得人类所谓的幸福,可以分为物质档次的享受,还有更为复杂的精神上的富有,哪怕这种富有只是存在于短暂的自我满足中也是值得的。

闲话少说, SIMD指令集,这个古老的东西,从第一代开始算起,也快有近20年的历史了,从最开始的MMX技术,到SSE,以及后来的SSE2、SSE3、SSE4、AVX以及11年以后的AVX2,逐渐的成熟和丰富,不过目前考虑通用性方面,AVX的辐射范围还是有限,大部分在优化时还是考虑使用128位的SSE指令集,我在之前的一系列文章中,也有不少文章涉及到了这个方面的优化了。

今天我们来学习下Sobel算法的优化,首先,我们给出传统的C++实现的算法代码:

int IM_Sobel(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride)
{
int Channel = Stride / Width;
if ((Src == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= ) || (Height <= )) return IM_STATUS_INVALIDPARAMETER;
if ((Channel != ) && (Channel != )) return IM_STATUS_INVALIDPARAMETER; unsigned char *RowCopy = (unsigned char *)malloc((Width + ) * * Channel);
if (RowCopy == NULL) return IM_STATUS_OUTOFMEMORY; unsigned char *First = RowCopy;
unsigned char *Second = RowCopy + (Width + ) * Channel;
unsigned char *Third = RowCopy + (Width + ) * * Channel; memcpy(Second, Src, Channel);
memcpy(Second + Channel, Src, Width * Channel); // 拷贝数据到中间位置
memcpy(Second + (Width + ) * Channel, Src + (Width - ) * Channel, Channel); memcpy(First, Second, (Width + ) * Channel); // 第一行和第二行一样 memcpy(Third, Src + Stride, Channel); // 拷贝第二行数据
memcpy(Third + Channel, Src + Stride, Width * Channel);
memcpy(Third + (Width + ) * Channel, Src + Stride + (Width - ) * Channel, Channel); for (int Y = ; Y < Height; Y++)
{
unsigned char *LinePS = Src + Y * Stride;
unsigned char *LinePD = Dest + Y * Stride;
if (Y != )
{
unsigned char *Temp = First; First = Second; Second = Third; Third = Temp;
}
if (Y == Height - )
{
memcpy(Third, Second, (Width + ) * Channel);
}
else
{
memcpy(Third, Src + (Y + ) * Stride, Channel);
memcpy(Third + Channel, Src + (Y + ) * Stride, Width * Channel); // 由于备份了前面一行的数据,这里即使Src和Dest相同也是没有问题的
memcpy(Third + (Width + ) * Channel, Src + (Y + ) * Stride + (Width - ) * Channel, Channel);
}
if (Channel == )
{
for (int X = ; X < Width; X++)
{
int GX = First[X] - First[X + ] + (Second[X] - Second[X + ]) * + Third[X] - Third[X + ];
int GY = First[X] + First[X + ] + (First[X + ] - Third[X + ]) * - Third[X] - Third[X + ];
LinePD[X] = IM_ClampToByte(sqrtf(GX * GX + GY * GY + 0.0F));
}
}
else
{
for (int X = ; X < Width * ; X++)
{
int GX = First[X] - First[X + ] + (Second[X] - Second[X + ]) * + Third[X] - Third[X + ];
int GY = First[X] + First[X + ] + (First[X + ] - Third[X + ]) * - Third[X] - Third[X + ];
LinePD[X] = IM_ClampToByte(sqrtf(GX * GX + GY * GY + 0.0F));
}
}
}
free(RowCopy);
return IM_STATUS_OK;
}

  代码很短,但是这段代码很经典,第一,这个代码支持In-Place操作,也就是Src和Dest可以是同一块内存,第二,这个代码本质就支持边缘。网络上很多参考代码都是只处理中间有效的区域。具体的实现细节我不愿意多述,自己看。

  那么Sobel的核心就是计算X方向的梯度GX和Y方向的梯度GY,最后有一个耗时的操作是求GX*GX+GY*GY的平方。

上面这段代码,在不打开编译器的SSE优化和快速浮点计算的情况,直接使用FPU,对4000*3000的彩色图约需要480ms,当开启SSE后,大概时间为220ms ,因此系统编译器的SSE优化也很厉害,反编译后可以看到汇编里这样的部分:

59AD12F8  movd        xmm0,ecx
59AD12FC cvtdq2ps xmm0,xmm0
59AD12FF sqrtss xmm0,xmm0
59AD1303 cvttss2si ecx,xmm0

  可见他是调用的单浮点数的sqrt优化。

    由于该Sobel的过程最后是把数据用图像的方式记录下来,因此,IM_ClampToByte(sqrtf(GX * GX + GY * GY + 0.0F))可以用查表的方式来实现。简单改成如下的版本, 避免了浮点计算。

int IM_SobelTable(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride)
{
int Channel = Stride / Width;
if ((Src == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= ) || (Height <= )) return IM_STATUS_INVALIDPARAMETER;
if ((Channel != ) && (Channel != )) return IM_STATUS_INVALIDPARAMETER; unsigned char *RowCopy = (unsigned char *)malloc((Width + ) * * Channel);
if (RowCopy == NULL) return IM_STATUS_OUTOFMEMORY; unsigned char *First = RowCopy;
unsigned char *Second = RowCopy + (Width + ) * Channel;
unsigned char *Third = RowCopy + (Width + ) * * Channel; memcpy(Second, Src, Channel);
memcpy(Second + Channel, Src, Width * Channel); // 拷贝数据到中间位置
memcpy(Second + (Width + ) * Channel, Src + (Width - ) * Channel, Channel); memcpy(First, Second, (Width + ) * Channel); // 第一行和第二行一样 memcpy(Third, Src + Stride, Channel); // 拷贝第二行数据
memcpy(Third + Channel, Src + Stride, Width * Channel);
memcpy(Third + (Width + ) * Channel, Src + Stride + (Width - ) * Channel, Channel); int BlockSize = , Block = (Width * Channel) / BlockSize; unsigned char Table[];
for (int Y = ; Y < ; Y++) Table[Y] = (sqrtf(Y + 0.0f) + 0.5f); for (int Y = ; Y < Height; Y++)
{
unsigned char *LinePS = Src + Y * Stride;
unsigned char *LinePD = Dest + Y * Stride;
if (Y != )
{
unsigned char *Temp = First; First = Second; Second = Third; Third = Temp;
}
if (Y == Height - )
{
memcpy(Third, Second, (Width + ) * Channel);
}
else
{
memcpy(Third, Src + (Y + ) * Stride, Channel);
memcpy(Third + Channel, Src + (Y + ) * Stride, Width * Channel); // 由于备份了前面一行的数据,这里即使Src和Dest相同也是没有问题的
memcpy(Third + (Width + ) * Channel, Src + (Y + ) * Stride + (Width - ) * Channel, Channel);
}
if (Channel == )
{
for (int X = ; X < Width; X++)
{
int GX = First[X] - First[X + ] + (Second[X] - Second[X + ]) * + Third[X] - Third[X + ];
int GY = First[X] + First[X + ] + (First[X + ] - Third[X + ]) * - Third[X] - Third[X + ];
LinePD[X] = Table[IM_Min(GX * GX + GY * GY, )];
}
}
else
{
for (int X = ; X < Width * ; X++)
{
int GX = First[X] - First[X + ] + (Second[X] - Second[X + ]) * + Third[X] - Third[X + ];
int GY = First[X] + First[X + ] + (First[X + ] - Third[X + ]) * - Third[X] - Third[X + ];
LinePD[X] = Table[IM_Min(GX * GX + GY * GY, )];
}
}
}
free(RowCopy);
return IM_STATUS_OK;
}

  对4000*3000的彩色图约需要180ms,比系统的SSE优化快了40ms,而这个过程完全无浮点计算,因此,可以知道计算GX和GY的耗时在本例中也占据了相当大的部分。

  这样的过程最适合于SSE处理了。

  我们分析之。

  第一来看一看这两句:

  int GX = First[X] - First[X + ] + (Second[X] - Second[X + ]) *  + Third[X] - Third[X + ];
int GY = First[X] + First[X + ] + (First[X + ] - Third[X + ]) * - Third[X] - Third[X + ];

里面涉及到了8个不同的像素,考虑计算的特性和数据的范围,在内部计算时这个int可以用short代替,也就是要把加载的字节图像数据转换为short类型先,这样SSE优化方式为用8个SSE变量分别记录8个连续的像素风量的颜色值,每个颜色值用16位数据表达。

  这可以使用_mm_unpacklo_epi8配合_mm_loadl_epi64实现:

    __m128i FirstP0 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(First + X)), Zero);
__m128i FirstP1 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(First + X + )), Zero);
__m128i FirstP2 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(First + X + )), Zero); __m128i SecondP0 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(Second + X)), Zero);
__m128i SecondP2 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(Second + X + )), Zero); __m128i ThirdP0 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(Third + X)), Zero);
__m128i ThirdP1 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(Third + X + )), Zero);
__m128i ThirdP2 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(Third + X + )), Zero);

  接着就是搬积木了,用SSE的指令代替普通的C的函数指令实现GX和GY的计算。

    __m128i GX16 = _mm_abs_epi16(_mm_add_epi16(_mm_add_epi16(_mm_sub_epi16(FirstP0, FirstP2), _mm_slli_epi16(_mm_sub_epi16(SecondP0, SecondP2), )), _mm_sub_epi16(ThirdP0, ThirdP2)));
__m128i GY16 = _mm_abs_epi16(_mm_sub_epi16(_mm_add_epi16(_mm_add_epi16(FirstP0, FirstP2), _mm_slli_epi16(_mm_sub_epi16(FirstP1, ThirdP1), )), _mm_add_epi16(ThirdP0, ThirdP2)));

  找个时候的GX16和GY16里保存的是8个16位的中间结果,由于SSE只提供了浮点数的sqrt操作,我们必须将它们转换为浮点数,那么这个转换的第一步就必须是先将它们转换为int的整形数,这样,就必须一个拆成2个,即:

    __m128i GX32L = _mm_unpacklo_epi16(GX16, Zero);
__m128i GX32H = _mm_unpackhi_epi16(GX16, Zero);
__m128i GY32L = _mm_unpacklo_epi16(GY16, Zero);
__m128i GY32H = _mm_unpackhi_epi16(GY16, Zero);

  接着又是搬积木了:

    __m128i ResultL = _mm_cvtps_epi32(_mm_sqrt_ps(_mm_cvtepi32_ps(_mm_add_epi32(_mm_mullo_epi32(GX32L, GX32L), _mm_mullo_epi32(GY32L, GY32L)))));
__m128i ResultH = _mm_cvtps_epi32(_mm_sqrt_ps(_mm_cvtepi32_ps(_mm_add_epi32(_mm_mullo_epi32(GX32H, GX32H), _mm_mullo_epi32(GY32H, GY32H)))));

  整形转换为浮点数,最后计算完之后又要将浮点数转换为整形数。

  最后一步,得到8个int型的结果,这个结果有要转换为字节类型的,并且这些数据有可能会超出字节所能表达的范围,所以就需要用到SSE自带的抗饱和向下打包函数了,如下所示:

_mm_storel_epi64((__m128i *)(LinePD + X), _mm_packus_epi16(_mm_packus_epi32(ResultL, ResultH), Zero));

  Ok, 一切搞定了,还有一些细节处理自己慢慢补充吧。

  运行,哇,只要37ms了,速度快了N倍,可结果似乎和其他方式实现的不一样啊,怎么回事。

  我也是找了半天都没有找到问题所在,后来一步一步的测试,最终问题定位在16位转换为32位整形那里去了。

  通常,我们都是对像素的字节数据进行向上扩展,他们都是正数,所以用unpack之类的配合zero把高8位或高16位的数据填充为0就可以了,但是在本例中,GX16或者GY16很有可能是负数,而负数的最高位是符号位,如果都填充为0,则变为正数了,明显改变原始的数据了,所以得到了错误的结果。

  那如何解决问题呢,对于本例,很简单,因为后面只有一个平方操作,因此,对GX先取绝对值是不会改变计算的结果的,这样就不会出现负的数据了,修改之后,果然结果正确。

  还可以继续优化,我们来看最后的计算GX*GX+GY*GY的过程,我们知道,SSE3提供了一个_mm_madd_epi16指令,其作用为:

r0 := (a0 * b0) + (a1 * b1)
r1 := (a2 * b2) + (a3 * b3)
r2 := (a4 * b4) + (a5 * b5)
r3 := (a6 * b6) + (a7 * b7)

如果我们能把GX和GY的数据拼接成另外两个数据:

    GXYL   =   GX0  GY0  GX1  GY1  GX2  GY2  GX3  GY3

    GXYH   =   GX4  GY4  GX5  GY5  GX6  GY6  GX7  GY7

  那么调用_mm_madd_epi16(GXYL ,GXYL )和_mm_madd_epi16(GXYH ,GXYH )不就是能得到和之前一样的结果了吗,而这个拼接SSE已经有现成的函数了即:

__m128i GXYL = _mm_unpacklo_epi16(GX16, GY16);
__m128i GXYH = _mm_unpackhi_epi16(GX16, GY16);

  这样就把原来需要的10个指令变为了4个指令,代码简洁了并且速度也更快了。

  尝试如此修改,整个的计算过程时间减少到了32ms左右。

  另外,还有一个可以优化的地方就是借用  _mm_maddubs_epi16  函数实现像素之间的加减乘除和扩展。

  这个函数的作用如下:

r0 := SATURATE_16((a0 * b0) + (a1 * b1))
r1 := SATURATE_16((a2 * b2) + (a3 * b3))
...
r7 := SATURATE_16((a14 * b14) + (a15 * b15))

  他的第一个参数是16个无符号的字节数据,第二个参数是16个有符号的char数据。

  配合unpack使用类似上面的技术就可以一次性处理16个字节的像素简加减了,这样做整个过程大概能再加速2ms,达到最终的30ms。

  源代码地址:http://files.cnblogs.com/files/Imageshop/Sobel.rar (其中的SSE代码请按照本文的思路自行整理。)

  http://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar,这里是一个我全部用SSE优化的图像处理的Demo,有兴趣的朋友可以看看。

  欢迎点赞和打赏。

SSE图像算法优化系列九:灵活运用SIMD指令16倍提升Sobel边缘检测的速度(4000*3000的24位图像时间由480ms降低到30ms)。的更多相关文章

  1. SSE再学习:灵活运用SIMD指令6倍提升Sobel边缘检测的速度(4000*3000的24位图像时间由180ms降低到30ms)。

    这半年多时间,基本都在折腾一些基本的优化,有很多都是十几年前的技术了,从随大流的角度来考虑,研究这些东西在很多人看来是浪费时间了,即不能赚钱,也对工作能力提升无啥帮助.可我觉得人类所谓的幸福,可以分为 ...

  2. SSE图像算法优化系列二十五:二值图像的Euclidean distance map(EDM)特征图计算及其优化。

    Euclidean distance map(EDM)这个概念可能听过的人也很少,其主要是用在二值图像中,作为一个很有效的中间处理手段存在.一般的处理都是将灰度图处理成二值图或者一个二值图处理成另外一 ...

  3. SSE图像算法优化系列二十二:优化龚元浩博士的曲率滤波算法,达到约1000 MPixels/Sec的单次迭代速度

      2015年龚博士的曲率滤波算法刚出来的时候,在图像处理界也曾引起不小的轰动,特别是其所说的算法的简洁性,以及算法的效果.执行效率等方面较其他算法均有一定的优势,我在该算法刚出来时也曾经有关注,不过 ...

  4. SSE图像算法优化系列二十:一种快速简单而又有效的低照度图像恢复算法。

    又有很久没有动笔了,主要是最近没研究什么东西,而且现在主流的趋势都是研究深度学习去了,但自己没这方面的需求,同时也就很少有动力再去看传统算法,今天一个人在家,还是抽空分享一个简单的算法吧. 前段日子在 ...

  5. SSE图像算法优化系列十:简单的一个肤色检测算法的SSE优化。

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

  6. SSE图像算法优化系列十三:超高速BoxBlur算法的实现和优化(Opencv的速度的五倍)

    在SSE图像算法优化系列五:超高速指数模糊算法的实现和优化(10000*10000在100ms左右实现) 一文中,我曾经说过优化后的ExpBlur比BoxBlur还要快,那个时候我比较的BoxBlur ...

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

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

  8. SSE图像算法优化系列二十三: 基于value-and-criterion structure 系列滤波器(如Kuwahara,MLV,MCV滤波器)的优化。

    基于value-and-criterion structure方式的实现的滤波器在原理上其实比较简单,感觉下面论文中得一段话已经描述的比较清晰了,直接贴英文吧,感觉翻译过来反而失去了原始的韵味了. T ...

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

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

随机推荐

  1. 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

    假设你正在爬楼梯.需要 n 阶你才能到达楼顶. 每次你可以爬 1 或 2 个台阶.你有多少种不同的方法可以爬到楼顶呢? 注意:给定 n 是一个正整数. 示例 1: 输入: 2 输出: 2 解释: 有两 ...

  2. 使用soupUI做接口测试

    第一步:点击“file”,选择测试项目采用的协议:(这里我们测试的是http协议的,所以选择第三项)   第二步:在弹窗中输入测试项目的接口URL,并点击“OK”: 第三步:设置和填写请求项的内容并点 ...

  3. react学习三

    三点运算符  (...)的用法 1:展开运算符 let a=[1,2,3]; let b=[0,...a,4];//[0,1,2,3,4] let obj ={a:1,b:2}; let obj2 = ...

  4. sql查询一个字段不同值并返回

    sql SELECT COUNT(字段),分组字段,SUM(字段),SUM(字段) FROM 表 GROUP BY 分组字段 java EntityWrapper<ProjectEntity&g ...

  5. TF之RNN:实现利用scope.reuse_variables()告诉TF想重复利用RNN的参数的案例—Jason niu

    import tensorflow as tf # 22 scope (name_scope/variable_scope) from __future__ import print_function ...

  6. Philosopher’s Walk(递归)

    In Programming Land, there are several pathways called Philosopher’s Walks for philosophers to have ...

  7. Intellij IDEA 解决 Maven 依赖下载慢的问题

    最近用 IDEA 导入 Hadoop 源码, 但下载依赖特别慢.导致经常需要重启 IDEA 并且下载的过程非常艰难, 网上找了一些方法,各种尝试,终于解决了这个问题.本篇文章总结最关键的两点,希望能帮 ...

  8. 向cmd中添加字体的方法

    首先下载字体到C:\Windows\Fonts中,然后参考 http://blog.csdn.net/bbirdsky/article/details/38495661 中所讲的方法进行添加.

  9. 解决VS打开文件出现No EditorOptionDefinition export found for the given option name问题

    转载自http://stackoverflow.com/questions/23893497/no-editoroptiondefinition-export-found-error的第一个回答 Af ...

  10. Xamarin SQLite教程数据库访问与生成

    Xamarin SQLite教程数据库访问与生成 在本教程中,我们将讲解如何开发SQLite相关的App.在编写程序前,首先需要做一些准备工作,如了解Xamarin数据库访问方式,添加引用,构建使用库 ...