深度学习FPGA实现基础知识16(图像处理中任意核卷积(matlab中conv2函数)的快速实现)

需求说明:深度学习FPGA实现知识储备

来自:http://www.2cto.com/kf/201411/355943.html

整理来自:时间的诗
卷积其实是图像处理中最基本的操作,我们常见的一些算法比如:均值模糊、高斯模糊、锐化、Sobel、拉普拉斯、prewitt边缘检测等等一些和领域相关的算法,都可以通过卷积算法实现。只不过由于这些算法的卷积矩阵的特殊性,一般不会直接实现它,而是通过一些优化的手段让计算量变小。但是有些情况下卷积矩阵的元素值无甚规律或者有特殊要求,无法通过常规手段优化,这个时候只能通过原始的方式实现。因此,如何快速的实现图像的任意卷积矩阵操作也有必要做适当的研究。
      目前,通过友人共享或自己搜索找到的一片关于任意核算法优化的文章有: Reshuf?ing: A Fast Algorithm for Filtering with Arbitrary Kernels,改文章称能够提高原始程序速度的40%左右,但是原始的程序是如何写的也还不明白。
      在matlab中有几个函数都与图像卷积有关,比如imfilter就可以实现卷积,或者 conv2也行,他们的速度都是相当快的,比如3000*3000的灰度图,卷积矩阵大小为15*15,在I5的CPU上运行时间只要170ms左右,相当的给力。
      在Celery的博客中,也提到了他的优化后的conv2和matlab相当甚至快于matlab,详见http://blog.csdn.net/celerychen2009/article/details/38852105
      由于matlab的代码中使用到了IPL库进行加速,目前我写的Conv2函数还无法做到和其相当,对于任何核速度约为matlab的一半。
      简单的记录下我在做卷积过程中用到的优化吧。
      原始的卷积的实现需要四重循环,简单的表达如下:


   
   
  1. for (Y = 0; Y < Height; Y++)
  2. {
  3. for ( X = 0; X < Width ; X ++)
  4. {
  5. Index = …..;
  6. Sum = 0;
  7. for ( XX = 0; XX < ConvW ; XX ++)
  8. {
  9. for ( YY = 0; YY < ConvH ; YY ++)
  10. {
  11. Index1 = ….. ;
  12. Index2 = ….. ;
  13. Sum += Conv[Index1] * Pixel [ Index2 ];
  14. }
  15. }
  16. Dest [ Index ] = Sum / Weight ;
  17. }
  18. }


 当卷积矩阵较大时,计算量将会很大,而且由于程序中的内存访问很频繁,cache miss现象比较严重,因此效率极为低下。
     我的优化方法主要包括以下几个方面: 
     一:使用SSE进行乘法计算,由于SSE可以一次性进行4个单精度浮点数的计算,因此可以有明显的速度提升。
     二:通过适当的处理方式,对每个取样点周边的卷积矩阵内的元素进行集中,使得每移动一个像素点不会需要从内存中进行大量的搜索工作。
     具体来说实现过程如下:
     1、为了使用SSE的优势,首先将卷积矩阵进行调整,调整卷积矩阵一行的元素个数,使其为不小于原始值的4的整数倍,并且让新的卷积矩阵的内存布局符合SSE相关函数的16字节对齐的要求。
 
实现代码如下:


   
   
  1. float *Conv16 = ( float *)_mm_malloc(PadConvLine * ConvH * sizeof( float), 16); // 保存16字节对齐的卷积矩阵,以方便使用SSE
  2. for(Y = 0; Y < ConvH; Y++)
  3. {
  4. memcpy (Conv16 + Y * PadConvLine, Conv->Data.F + Y * ConvW , ConvW * sizeof( float)); // 复制卷积矩阵的数据
  5. memset(Conv16 + Y * PadConvLine + ConvW, 0, (PadConvLine - ConvW) * sizeof( float)); // 把冗余部分的卷积数据设置为0
  6. }


    其中PadConvLine = Pad4(ConvW) 以及Pad4的原型为: #define Pad4(bits) (((bits) + 3) / 4 * 4);
    注意_mm_malloc函数分配的内存中的值是随机值,对于扩展的部分一定要填充0,否则就会破坏卷积的结果。
    那么如果我们也同时获得了需要被卷积的部分数据的话(卷积核肯定和卷积矩阵一样大小,且也应该是16字节对齐的),可以用如下的SSE的代码进行乘法计算:
 

   
   
  1. float MultiplySSE(float *Kernel, float *Conv, int Length)
  2. {
  3. int Block;
  4. const float *Data; // 将SSE变量上的多个数值合并时所用指针.
  5. float Sum = 0;
  6. if (Length > 16) // 可以进行四次SSE计算,测试表明,这个还是快些的
  7. {
  8. const int BlockWidth = 4 * 4; // 块宽. SSE寄存器能一次处理4个float,然后循环展开4次.
  9. Block = Length / BlockWidth; // 块数.
  10. float *KernelP = Kernel, *ConvP = Conv; // SSE批量处理时所用的指针.
  11. __m128 Sum0 = _mm_setzero_ps(); // 求和变量。SSE赋初值0
  12. __m128 Sum1 = _mm_setzero_ps();
  13. __m128 Sum2 = _mm_setzero_ps();
  14. __m128 Sum3 = _mm_setzero_ps();
  15. for( int I = 0; I < Block; I++)
  16. {
  17. Sum0 = _mm_add_ps(Sum0, _mm_mul_ps(_mm_load_ps(KernelP), _mm_load_ps(ConvP))); // SSE单精浮点紧缩加法
  18. Sum1 = _mm_add_ps(Sum1, _mm_mul_ps(_mm_load_ps(KernelP + 4), _mm_load_ps(ConvP + 4)));
  19. Sum2 = _mm_add_ps(Sum2, _mm_mul_ps(_mm_load_ps(KernelP + 8), _mm_load_ps(ConvP + 8)));
  20. Sum3 = _mm_add_ps(Sum3, _mm_mul_ps(_mm_load_ps(KernelP + 12), _mm_load_ps(ConvP + 12)));
  21. KernelP += BlockWidth;
  22. ConvP += BlockWidth;
  23. }
  24. Sum0 = _mm_add_ps(Sum0, Sum1); // 两两合并(0~1).
  25. Sum2 = _mm_add_ps(Sum2, Sum3); // 两两合并(2~3).
  26. Sum0 = _mm_add_ps(Sum0, Sum2); // 两两合并(0~2).
  27. Data = ( const float *)&Sum0;
  28. Sum = Data[ 0] + Data[ 1] + Data[ 2] + Data[ 3];
  29. Length = Length - Block * BlockWidth; // 剩余数量.
  30. }
  31. if (Length != 0)
  32. {
  33. const int BlockWidth = 4; // 程序已经保证了数量必然是4的倍数
  34. Block = Length / BlockWidth;
  35. float *KernelP = Kernel, *ConvP = Conv;
  36. __m128 Sum0 = _mm_setzero_ps();
  37. for( int I = 0; I < Block; I++)
  38. {
  39. Sum0 = _mm_add_ps(Sum0, _mm_mul_ps(_mm_load_ps(KernelP), _mm_load_ps(ConvP)));
  40. KernelP += BlockWidth;
  41. ConvP += BlockWidth;
  42. }
  43. Data = ( const float *)&Sum0;
  44. Sum += Data[ 0] + Data[ 1] + Data[ 2] + Data[ 3];
  45. }
  46. return Sum;
  47. }


当卷积矩阵(扩充后)的元素数量大于16时,我们采用了4路并行的SSE乘法实现,我在I3的CPU上测试时,2路SSE和4路SSE已经没有啥大的区别了,而在I5的CPU上则4路还是有较为明显的提高,因此采用4路SSE同时运行。当然1路SSE肯定还是比2路慢。另外,如果元素的数量少于16或者大于16但不能被16整除,那么余下的部分由于先前的扩充,剩余元素数量也肯定是4的倍数,因此可以用单路的SSE实现。 这也是编码上的技巧。
 
2、前面提到了需要被卷积的部分数据,这部分如何快速的获取呢。观察最原始的4重循环,其内部的2重即为获取需要被卷积的部分,但是这里其实有很多问题。第一:由于卷积取样时必然有部分取样点的坐标在原始图像的有效范围外,因此必须进行判断,耗时。第二:同样为了使用SSE,也必须把取样的数据放在和扩充的卷积矩阵一样大小的内存中。这里我先贴出我的代码在进行解释具体的实现:
 

   
   
  1. IS_RET __ stdcall Conv2(TImage *Src, TMatrix *Conv, TImage *Dest, EdgeMode Edge)
  2. {
  3. if (Src == NULL || Dest == NULL || Conv == NULL) return IS_RET_ERR_PARA;
  4. if (Src->Width != Dest->Width || Src->Height != Dest->Height || Src->BitCount != Dest->BitCount || Src->Stride != Dest->Stride) return IS_RET_ERR_PARA;
  5. if (Src->Scan0 == NULL || Dest->Scan0 == NULL || Conv->Data.F == NULL) return IS_RET_ERR_MEM;
  6. if (Conv->Width < 1 || Conv->Height < 1) return IS_RET_ERR_PARA;
  7. int Width = Src->Width, Height = Src->Height, Stride = Src->Stride;
  8. int ConvW = Conv->Width, ConvH = Conv->Height;
  9. unsigned char *PtSrc = Src->Scan0, *PtDest = Dest->Scan0;
  10. if (Src->BitCount == 24)
  11. {
  12. }
  13. else
  14. {
  15. int Left = ConvW / 2, Top = ConvH / 2, Right = ConvW - Left - 1, Bottom = ConvH - Top - 1, ExpHeight = Height + ConvH - 1;
  16. // 注意核中心那个元素不用扩展,比如核的宽度为3,则只要左右各扩展一个像素就可以了
  17. int PadConvLine = Pad4(ConvW), Length = PadConvLine * ConvH;
  18. int X, Y, IndexD, IndexE, IndexK, ExpStride;
  19. float *CurKer, Inv, Sum = 0;
  20. unsigned char *PtExp, *PtDest;
  21. TImage *Expand;
  22. IS_RET Ret = GetPadImage(Src, &Expand, Left, Right, Top, Bottom, Edge);
  23. // 得到扩展后的数据,可以提速和方便编程,但是多占用一份内存
  24. if (Ret != IS_RET_OK) return Ret;
  25. PtExp = Expand->Scan0; PtDest = Dest->Scan0; ExpStride = Expand->Stride;
  26. for (X = 0; X < ConvH * ConvW; X ++) Sum += Conv->Data.F[X];
  27. Inv = (Sum == 0 ? 1: 1 / Sum); // 如果卷积举证的和为0,则设置为1
  28. float *Conv16 = ( float *)_mm_malloc(PadConvLine * ConvH * sizeof( float), 16);
  29. // 保存16字节对齐的卷积矩阵,以方便使用SSE
  30. float *Kernel = ( float *)_mm_malloc(PadConvLine * ExpHeight * sizeof( float), 16);
  31. // 保存16字节对齐的卷积核矩阵,以方便使用SSE
  32. for(Y = 0; Y < ConvH; Y++)
  33. {
  34. memcpy (Conv16 + Y * PadConvLine, Conv->Data.F + Y * ConvW , ConvW * sizeof( float)); // 复制卷积矩阵的数据
  35. memset(Conv16 + Y * PadConvLine + ConvW, 0, (PadConvLine - ConvW) * sizeof( float)); // 把冗余部分的卷积数据设置为0
  36. }
  37. for (Y = 0; Y < ExpHeight; Y++)
  38. {
  39. IndexE = Y * ExpStride;
  40. CurKer = Kernel + Y * PadConvLine; // 计算第一列所有像素将要取样的卷积核数据
  41. for (X = 0; X < ConvW; X++)
  42. {
  43. CurKer[X] = PtExp[IndexE++];
  44. }
  45. }
  46. for (X = 0 ; X < Width ; X ++)
  47. {
  48. if (X != 0) // 如果不是第一列,需要更新卷积核的数据
  49. {
  50. memcpy(Kernel, Kernel + 1, (PadConvLine * ExpHeight - 1) * sizeof( float)); // 往前移动一个数据
  51. IndexK = ConvW - 1 ;
  52. IndexE = IndexK + X;
  53. for (Y = 0; Y < ExpHeight; Y++)
  54. {
  55. Kernel[IndexK] = PtExp[IndexE]; // 只要刷新下一个元素
  56. IndexK += PadConvLine;
  57. IndexE += ExpStride;
  58. }
  59. }
  60. CurKer = Kernel; IndexD = X;
  61. for (Y = 0; Y < Height; Y ++) // 沿列的方向进行更新
  62. {
  63. PtDest[IndexD] = Clamp(( int)( MultiplySSE(Conv16, CurKer, Length) * Inv + 0.5));
  64. // 直接把函数放在这里也没有啥提速的,注意改函数不会被内联的
  65. CurKer += PadConvLine;
  66. IndexD += Stride;
  67. }
  68. }
  69. _mm_free(Conv16);
  70. _mm_free(Kernel);
  71. FreeImage(Expand);
  72. return IS_RET_OK;
  73. }
  74. }

     对于第一个问题,解决的方式很简答,即用空间换时间,新建一副(Width + ConvW - 1, Height + ConvH -1)大小的图像,然后四周的ConvW及ConvH的像素用边缘的值或者边缘镜像的值填充,正中间的则用原来的图复制过来,这样操作后进行取样时不再原图取样,而在这福扩展的图中取样,就避免了坐标判断等if语句的跳转耗时了,上GetPadImage即实现了改功能。
     第二个问题则需要有一定的实现技巧,我们分配一块PadConvLine * (Height + ConvH - 1) 大小的内存,然后计算原图第一列像素串联起来的需要卷积的部分的数据,这一部分代码如上述44-52行所示。有了这样的数据,如果需要计算第一列的卷积结果,则很简单了,每跳过一列则把被卷积的数据起点增加PadConvLine个元素,在调用上述MultiplySSE函数获得卷积结果。接着则计算第二列像素的卷积值,此时需要整体更新这一列像素串联起来的需要被卷积的数据,更新也很简单,就是把原来的数据整体向左移动一个像素,这个可以用memcpy快速实现,然后在填充入新进来的那个元素,就ok了,接着就是再次调用MultiplySSE函数,如此重复下去。
     经过编码测试,对于3000*3000的灰度图,15*15的核在I5的CPU上的测试平均结果为360ms,比matlab的慢了一半。
     最后说明一点,很多人都说用FFT可以快速的实现卷积,并且是O(1)的,我比较同意后半句,但是前面半句是绝对的有问题的,至少在核小于50*50时,FFT实现的卷积不会比直接实现块。要知道FFT的计算量其实是很大的。

            </div>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值