最近一直沉迷于SSE方面的优化,实在找不到想学习的参考资料了,就拿个笔记本放在腿上翻翻OpenCv的源代码,无意中看到了OpenCv中关于积分图的代码,仔细研习了一番,觉得OpenCv对SSE的灵活运用真的做的很好,这里记录下我对该段代码的品味并将其思路扩展到其他通道数的图像。

该核心代码位于:Opencv 3.0\opencv\sources\modules\imgproc\src\sumpixels.cpp文件中。

我们贴出最感兴趣的一部分代码以便分析:

    bool operator()(const uchar * src, size_t _srcstep,int * sum, size_t _sumstep,double * sqsum, size_t, int * tilted, size_t,Size size, int cn) const
{
if (sqsum || tilted || cn != || !haveSSE2) return false;
// the first iteration
memset(sum, , (size.width + ) * sizeof(int));
__m128i v_zero = _mm_setzero_si128(), prev = v_zero;
int j = ;
// the others
for (int i = ; i < size.height; ++i)
{
const uchar * src_row = src + _srcstep * i;
int * prev_sum_row = (int *)((uchar *)sum + _sumstep * i) + ;
int * sum_row = (int *)((uchar *)sum + _sumstep * (i + )) + ;
sum_row[-] = ;
prev = v_zero;
j = ;
for ( ; j + < size.width; j += )
{
__m128i vsuml = _mm_loadu_si128((const __m128i *)(prev_sum_row + j));
__m128i vsumh = _mm_loadu_si128((const __m128i *)(prev_sum_row + j + ));
__m128i el8shr0 = _mm_loadl_epi64((const __m128i *)(src_row + j));
__m128i el8shr1 = _mm_slli_si128(el8shr0, );
__m128i el8shr2 = _mm_slli_si128(el8shr0, );
__m128i el8shr3 = _mm_slli_si128(el8shr0, );
vsuml = _mm_add_epi32(vsuml, prev);
vsumh = _mm_add_epi32(vsumh, prev);
__m128i el8shr12 = _mm_add_epi16(_mm_unpacklo_epi8(el8shr1, v_zero),
_mm_unpacklo_epi8(el8shr2, v_zero));
__m128i el8shr03 = _mm_add_epi16(_mm_unpacklo_epi8(el8shr0, v_zero),
_mm_unpacklo_epi8(el8shr3, v_zero));
__m128i el8 = _mm_add_epi16(el8shr12, el8shr03);
__m128i el4h = _mm_add_epi16(_mm_unpackhi_epi16(el8, v_zero),
_mm_unpacklo_epi16(el8, v_zero));
vsuml = _mm_add_epi32(vsuml, _mm_unpacklo_epi16(el8, v_zero));
vsumh = _mm_add_epi32(vsumh, el4h);
_mm_storeu_si128((__m128i *)(sum_row + j), vsuml);
_mm_storeu_si128((__m128i *)(sum_row + j + ), vsumh);
prev = _mm_add_epi32(prev, _mm_shuffle_epi32(el4h, _MM_SHUFFLE(, , , )));
}
for (int v = sum_row[j - ] - prev_sum_row[j - ]; j < size.width; ++j)
sum_row[j] = (v += src_row[j]) + prev_sum_row[j];
}

为了说明更方便,这里贴出我做的普通C语言的代码和重新优化后的SSE代码。

普通C语言:

 void GetGrayIntegralImage(unsigned char *Src, int *Integral, int Width, int Height, int Stride)
{
memset(Integral, , (Width + ) * sizeof(int)); // 第一行都为0
for (int Y = ; Y < Height; Y++)
{
unsigned char *LinePS = Src + Y * Stride;
int *LinePL = Integral + Y * (Width + ) + ;  // 上一行位置
int *LinePD = Integral + (Y + ) * (Width + ) + ; // 当前位置,注意每行的第一列的值都为0
LinePD[-] = ; // 第一列的值为0
for (int X = , Sum = ; X < Width; X++)
{
Sum += LinePS[X]; // 行方向累加
LinePD[X] = LinePL[X] + Sum; // 更新积分图
}
}
}

优化后的SSE算法:

void GetGrayIntegralImage(unsigned char *Src, int *Integral, int Width, int Height, int Stride)
{
memset(Integral, , (Width + ) * sizeof(int)); // 第一行都为0
int BlockSize = , Block = Width / BlockSize;
for (int Y = ; Y < Height; Y++)
{
unsigned char *LinePS = Src + Y * Stride;
int *LinePL = Integral + Y * (Width + ) + ; // 上一行位置
int *LinePD = Integral + (Y + ) * (Width + ) + ; // 当前位置,注意每行的第一列的值都为0
LinePD[-] = ;
__m128i PreV = _mm_setzero_si128();
__m128i Zero = _mm_setzero_si128();
for (int X = ; X < Block * BlockSize; X += BlockSize)
{
__m128i Src_Shift0 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i *)(LinePS + X)), Zero); // A7 A6 A5 A4 A3 A2 A1 A0
__m128i Src_Shift1 = _mm_slli_si128(Src_Shift0, ); // A6 A5 A4 A3 A2 A1 A0 0
__m128i Src_Shift2 = _mm_slli_si128(Src_Shift1, ); // 移位改成基于Shift0,速度慢,Why? // A5 A4 A3 A2 A1 A0 0 0
__m128i Src_Shift3 = _mm_slli_si128(Src_Shift2, ); // A4 A3 A2 A1 A0 0 0 0
__m128i Shift_Add12 = _mm_add_epi16(Src_Shift1, Src_Shift2); // A6+A5 A5+A4 A4+A3 A3+A2 A2+A1 A1+A0 A0+0 0+0
__m128i Shift_Add03 = _mm_add_epi16(Src_Shift0, Src_Shift3); // A7+A4 A6+A3 A5+A2 A4+A1 A3+A0 A2+0 A1+0 A0+0
__m128i Low = _mm_add_epi16(Shift_Add12, Shift_Add03); // A7+A6+A5+A4 A6+A5+A4+A3 A5+A4+A3+A2 A4+A3+A2+A1 A3+A2+A1+A0 A2+A1+A0+0 A1+A0+0+0 A0+0+0+0
__m128i High = _mm_add_epi32(_mm_unpackhi_epi16(Low, Zero), _mm_unpacklo_epi16(Low, Zero)); // A7+A6+A5+A4+A3+A2+A1+A0 A6+A5+A4+A3+A2+A1+A0 A5+A4+A3+A2+A1+A0 A4+A3+A2+A1+A0
__m128i SumL = _mm_loadu_si128((__m128i *)(LinePL + X + ));
__m128i SumH = _mm_loadu_si128((__m128i *)(LinePL + X + ));
SumL = _mm_add_epi32(SumL, PreV);
SumL = _mm_add_epi32(SumL, _mm_unpacklo_epi16(Low, Zero));
SumH = _mm_add_epi32(SumH, PreV);
SumH = _mm_add_epi32(SumH, High);
PreV = _mm_add_epi32(PreV, _mm_shuffle_epi32(High, _MM_SHUFFLE(, , , )));
_mm_storeu_si128((__m128i *)(LinePD + X + ), SumL);
_mm_storeu_si128((__m128i *)(LinePD + X + ), SumH);
}
for (int X = Block * BlockSize, V = LinePD[X - ] - LinePL[X - ]; X < Width; X++)
{
V += LinePS[X];
LinePD[X] = V + LinePL[X];
}
}

  我们先来解释下这段代码的SSE优化过程吧。

首先,用_mm_loadl_epi64一次性加载8个字节数据到XMM寄存器中,其中寄存器的高8位位0,此时寄存器的数据为:

高位            0  0  0  0  0  0  0  0 A7 A6 A5 A4 A3 A2 A1 A0        低位   (8位)

因为涉及到加法,并且最大为8个字节数据的加法,因此转换到16位数据类型,使用_mm_unpacklo_epi8结合zero即可实现。

此时XMM寄存器内容变为:

           Src_Shift0    A7 A6 A5 A4 A3 A2 A1 A0    (16位)

此后有3次移位分别得到:

            Src_Shift1    A6 A5 A4 A3 A2 A1 A0 0       (16位)
Src_Shift2 A5 A4 A3 A2 A1 A0 0 0     (16位)
Src_Shift3 A4 A3 A2 A1 A0 0 0 0 (16位) 通过_mm_add_epi16分别对4组16位数据进行8次相加:
            Shift_Add12   A6+A5 A5+A4 A4+A3 A3+A2 A2+A1 A1+A0 A0+0  0+0   (16位)
Shift_Add03   A7+A4 A6+A3 A5+A2 A4+A1 A3+A0 A2+0 A1+0 A0+0 (16位)
  再对他们进行相加:
        Low            A7+A6+A5+A4 A6+A5+A4+A3 A5+A4+A3+A2 A4+A3+A2+A1 A3+A2+A1+A0 A2+A1+A0+0 A1+A0+0+0 A0+0+0+0

注意到低4位的16位数已经是连续相加的数据了,只要将他们转换为32位就可以直接使用。

而通过 __m128i High = _mm_add_epi32(_mm_unpackhi_epi16(Low, Zero), _mm_unpacklo_epi16(Low, Zero)); 这一句则可以把前面的高4位连续相加的值拼接起来得到:

       High                  A7+A6+A5+A4+A3+A2+A1+A0  A6+A5+A4+A3+A2+A1+A0  A5+A4+A3+A2+A1+A0  A4+A3+A2+A1+A0

  后面的操作则顺理成章了。

注意到我核心的改动在于原始代码中的el8shr12和el8shr03的计算中的_mm_unpacklo_epi8被消除了,而在el8shr0一句中增加了一个_mm_unpacklo_epi8,因此少了3次这个函数,很明显这样做是不会改变计算结果的。

另外源代码中的部分_mm_add_epi16被我用_mm_add_epi32代替了,这主要是因为用_mm_add_epi32意义更明显,而且由于高位数据为0,他们的执行结果不会有任何区别。

   还有一点在测试时发现,如果Src_Shift2,Src_Shift3的移位是基于Src_Shift0,即使用如下代码:

__m128i Src_Shift2 = _mm_slli_si128(Src_Shift0, );
__m128i Src_Shift3 = _mm_slli_si128(Src_Shift0, );

   速度会有较为明显的下降,难道说移动的位数多少和CPU的耗时有关?

以上是灰度模式的算法,在我的笔记本电脑上,SSE优化后的语句虽然增加了很多,但是执行效率约能提升30%,不过在一些PC上,普通的C和SSE优化后却没有啥速度区别了,这也不知道是为什么了。

如果是针对24位或者32位图像,基本的优化思想是一致的,不过有更多的细节需要自己注意。

24位或者32位图像在任何机器配置上,速度都能有30%的提升的。

还是感觉这种算法用文字很难表述清楚,用代码再加上自己的空间组合可能更能理解吧。

SSE图像算法优化系列六:OpenCv关于灰度积分图的SSE代码学习和改进。的更多相关文章

  1. OpenCv关于灰度积分图的SSE代码学习和改进。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

随机推荐

  1. BZOJ4025 二分图 分治 并查集 二分图 带权并查集按秩合并

    原文链接http://www.cnblogs.com/zhouzhendong/p/8683831.html 题目传送门 - BZOJ4025 题意 有$n$个点,有$m$条边.有$T$个时间段.其中 ...

  2. logging日志文件配置

    Django配置如下 简洁版: LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console' ...

  3. git合并

    git 里合并了两个分支以后,是不是两个分支的内容就完全一样了? 不是.看合并到哪个分支,这个分支有两个分支所有的内容.另外一个分支不变. 合并操作( merge )对当前所在分支产生影响. 合并分支 ...

  4. RFC2616-HTTP1.1-Status Code(状态码规定部分—译文)

    part of Hypertext Transfer Protocol -- HTTP/1.1 RFC 2616 Fielding, et al. 10 状态码规定(Status Code Defin ...

  5. window下的计划任务

    0x00前言: 这几天看了看信息安全就业的面试题,其中有一条是计划任务如何设置,好几个月前稍微接触了,但是很久没用差不多都忘了>_<,这里就稍微学习下windows的计划任务 写着写着就偏 ...

  6. python数据结构之quick_sort

    Quick sort , also known as partition-exchange sort, divides the data to be sorted into two separate ...

  7. Linux学习笔记6

    less 命令 空格或者 ctrl+f  前进一屏 ctrl+b  回退一屏 回车    前进一行 /String 查找含有string 字符串的页 ?String  同上 <反方向查找> ...

  8. C# 动态调用WebService 2

    using Microsoft.CSharp; using System; using System.CodeDom; using System.CodeDom.Compiler; using Sys ...

  9. XamarinEssentials教程获取首选项的值

    XamarinEssentials教程获取首选项的值 如果开发者想要获取首选项中某一项的值时,可以使用Preferences类的Get()方法实现,该方法可以对指定键的值进行获取.该方法有12种形式, ...

  10. [CC-BLREDSET]Black and Red vertices of Tree

    [CC-BLREDSET]Black and Red vertices of Tree 题目大意: 有一棵\(n(\sum n\le10^6)\)个结点的树,每个结点有一种颜色(红色.黑色.白色).删 ...