SSE图像算法优化系列九:灵活运用SIMD指令16倍提升Sobel边缘检测的速度(4000*3000的24位图像时间由480ms降低到30ms)。
这半年多时间,基本都在折腾一些基本的优化,有很多都是十几年前的技术了,从随大流的角度来考虑,研究这些东西在很多人看来是浪费时间了,即不能赚钱,也对工作能力提升无啥帮助。可我觉得人类所谓的幸福,可以分为物质档次的享受,还有更为复杂的精神上的富有,哪怕这种富有只是存在于短暂的自我满足中也是值得的。
闲话少说, 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)。的更多相关文章
- SSE再学习:灵活运用SIMD指令6倍提升Sobel边缘检测的速度(4000*3000的24位图像时间由180ms降低到30ms)。
这半年多时间,基本都在折腾一些基本的优化,有很多都是十几年前的技术了,从随大流的角度来考虑,研究这些东西在很多人看来是浪费时间了,即不能赚钱,也对工作能力提升无啥帮助.可我觉得人类所谓的幸福,可以分为 ...
- SSE图像算法优化系列二十五:二值图像的Euclidean distance map(EDM)特征图计算及其优化。
Euclidean distance map(EDM)这个概念可能听过的人也很少,其主要是用在二值图像中,作为一个很有效的中间处理手段存在.一般的处理都是将灰度图处理成二值图或者一个二值图处理成另外一 ...
- SSE图像算法优化系列二十二:优化龚元浩博士的曲率滤波算法,达到约1000 MPixels/Sec的单次迭代速度
2015年龚博士的曲率滤波算法刚出来的时候,在图像处理界也曾引起不小的轰动,特别是其所说的算法的简洁性,以及算法的效果.执行效率等方面较其他算法均有一定的优势,我在该算法刚出来时也曾经有关注,不过 ...
- SSE图像算法优化系列二十:一种快速简单而又有效的低照度图像恢复算法。
又有很久没有动笔了,主要是最近没研究什么东西,而且现在主流的趋势都是研究深度学习去了,但自己没这方面的需求,同时也就很少有动力再去看传统算法,今天一个人在家,还是抽空分享一个简单的算法吧. 前段日子在 ...
- SSE图像算法优化系列十:简单的一个肤色检测算法的SSE优化。
在很多场合需要高效率的肤色检测代码,本人常用的一个C++版本的代码如下所示: void IM_GetRoughSkinRegion(unsigned char *Src, unsigned char ...
- SSE图像算法优化系列十三:超高速BoxBlur算法的实现和优化(Opencv的速度的五倍)
在SSE图像算法优化系列五:超高速指数模糊算法的实现和优化(10000*10000在100ms左右实现) 一文中,我曾经说过优化后的ExpBlur比BoxBlur还要快,那个时候我比较的BoxBlur ...
- SSE图像算法优化系列十五:YUV/XYZ和RGB空间相互转化的极速实现(此后老板不用再担心算法转到其他空间通道的耗时了)。
在颜色空间系列1: RGB和CIEXYZ颜色空间的转换及相关优化和颜色空间系列3: RGB和YUV颜色空间的转换及优化算法两篇文章中我们给出了两种不同的颜色空间的相互转换之间的快速算法的实现代码,但是 ...
- SSE图像算法优化系列二十三: 基于value-and-criterion structure 系列滤波器(如Kuwahara,MLV,MCV滤波器)的优化。
基于value-and-criterion structure方式的实现的滤波器在原理上其实比较简单,感觉下面论文中得一段话已经描述的比较清晰了,直接贴英文吧,感觉翻译过来反而失去了原始的韵味了. T ...
- SSE图像算法优化系列十四:局部均方差及局部平方差算法的优化。
关于局部均方差有着较为广泛的应用,在我博客的基于局部均方差相关信息的图像去噪及其在实时磨皮美容算法中的应用及使用局部标准差实现图像的局部对比度增强算法中都有谈及,即可以用于去噪也可以用来增强图像,但是 ...
随机推荐
- JavaSE| 面向对象-类的五大成员
面向对象 面向对象只是其中一种编程思想,还有很多其他的编程思想:面向过程.面向切面.面向服务编程... 面向过程的思维方式:注重步骤.过程,面向过程强调的是功能行为: 面向对象的思维方式:关注的是“对 ...
- Machine Learning 算法可视化实现1 - 线性回归
一.原理和概念 1.回归 回归最简单的定义是,给出一个点集D,用一个函数去拟合这个点集.而且使得点集与拟合函数间的误差最小,假设这个函数曲线是一条直线,那就被称为线性回归:假设曲线是一条二次曲线,就被 ...
- Linux下C语言的socket网络编程
关于详细的服务器建立的步骤以及相关的socket套接字的知识我已经在python socket编程的文章中提到过了,大家可以参看那一篇博客来历接socket套接字编程的内容,由于要是用C相关的API所 ...
- Python isinstance 方法 判断 built-in types(内置类型)技巧
Python isinstance 方法 判断 built-in types(内置类型)技巧 d = {} isinstance(d, type({})) isinstance(d, dict) l ...
- 斐波那契数列-java编程:三种方法实现斐波那契数列
题目要求:编写程序在控制台输出斐波那契数列前20项,每输出5个数换行 斐波那契数列指的是这样一个数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, … 这个数列 ...
- linux的文件打包与压缩
简介 Linux 上常用的压缩/解压工具,介绍了zip.rar.tar的使用. 文件打包和压缩 Linux 上的压缩包文件格式,除了 Windows 最常见的*.zip.*.rar..7z 后缀的压缩 ...
- poj 1386 Play on Words门上的单词【欧拉回路&&并查集】
题目链接:http://poj.org/problem?id=1386 题目大意:给你若干个字符串,一个单词的尾部和一个单词的头部相同那么这两个单词就可以相连,判断给出的n个单词是否能够一个接着一个全 ...
- VMware5.5-虚拟机的迁移和资源分配
虚拟机的迁移 迁移:将虚拟机从一台主机(或数据存储)移到另一台主机(或数据存储). 迁移类型: 冷迁移 迁移处于关闭状态的虚拟机. 挂起 迁移处于挂起状态的虚拟机. vMotion 迁移处于开启状态的 ...
- 关于eclipse启动报错,an error has occurred.see the log file
网上搜索各种方法,得知为由于Eclipse卡死或强制关闭之后会出现的情况 提供解决方法一: 查看log文件,发现有这样的信息: !MESSAGE The workspace exited with u ...
- Codeforces.487C.Prefix Product Sequence(构造)
题目链接 \(Description\) 对于一个序列\(a_i\),定义其前缀积序列为\(a_1\ \mathbb{mod}\ n,\ (a_1a_2)\ \mathbb{mod}\ n,...,( ...