这里的高斯模糊采用的是论文《Recursive implementation of the Gaussian filter》里描述的递归算法。

仔细观察和理解上述公式,在forward过程中,n是递增的,因此,如果在进行forward之前,把in数据先完整的赋值给w,然后式子(9a)就可以变为:

      w[n] = B w[n] + (b1 w[n-1] + b2 w[n-2] + b3 w[n-3]) / b0;          --------->     (1a)

 在backward过程中,n是递减的,因此在backward前,把w的数据完整的拷贝到out中,则式子(9b)变为:

     out[n] = B out[n] + (b1 out[n+1] + b2 out[n+2] + b3 out[n+3]) / b0 ;     <---------     (1b)

从编程角度看来,backward中的拷贝是完全没有必要的,因此 (1b)可以直接写为:

           w[n] = B w[n] + (b1 w[n+1] + b2 w[n+2] + b3 w[n+3]) / b0 ;               <---------     (1c)

     从速度上考虑,浮点除法很慢,因此,我们重新定义b1 = b1 / b0, b2 = b2 / b0, b3 = b3 / b0, 最终得到我们使用的递归公式:

w[n] = B w[n] + b1 w[n-1] + b2 w[n-2] + b3 w[n-3];          --------->     (a)

        w[n] = B w[n] + b1 w[n+1] + b2 w[n+2] + b3 w[n+3] ;             <---------      (b)

   上述公式是一维的高斯模糊计算方法,针对二维的图像,正确的做法就是先对每个图像行进行模糊处理得到中间结果,再对中间结果的每列进行模糊操作,最终得到二维的模糊结果,当然也可以使用先列后行这样的做法。

另外注意点就是,边缘像素的处理,我们看到在公式(a)或者(b)中,w[n]的结果分别依赖于前三个或者后三个元素的值,而对于边缘位置的值,这些都是不在合理范围内的,通常的做法是镜像数据或者重复边缘数据,实践证明,由于是递归的算法,起始值的不同会将结果一直延续下去,因此,不同的方法对边缘部分的结果还是有一定的影响的,这里我们采用的简单的重复边缘像素值的方式。

由于上面公式中的系数均为浮点类型,因此,计算一般也是对浮点进行的,也就是说一般需要先把图像数据转换为浮点,然后进行高斯模糊,在将结果转换为字节类型的图像,因此,上述过程可以分别用一下几个函数完成:

CalcGaussCof           //  计算高斯模糊中使用到的系数
      ConvertBGR8U2BGRAF      //  将字节数据转换为浮点数据 
      GaussBlurFromLeftToRight    //  水平方向的前向传播
      GaussBlurFromRightToLeft    //  水平方向的反向传播
      GaussBlurFromTopToBottom    //   垂直方向的前向传播
      GaussBlurFromBottomToTop    //   垂直方向的反向传播
      ConvertBGRAF2BGR8U        //   将结果转换为字节数据

  我们依次分析之。

CalcGaussCof,这个很简单,就按照论文中提出的计算公式根据用户输入的参数来计算,当然结合下上面我们提到的除法变乘法的优化,注意,为了后续的一些标号的问题,我讲上述公式(a)和(b)中的系数B写为b0了。

void CalcGaussCof(float Radius, float &B0, float &B1, float &B2, float &B3)
{
float Q, B;
if (Radius >= 2.5)
Q = (double)(0.98711 * Radius - 0.96330); // 对应论文公式11b
else if ((Radius >= 0.5) && (Radius < 2.5))
Q = (double)(3.97156 - 4.14554 * sqrt( - 0.26891 * Radius));
else
Q = (double)0.1147705018520355224609375; B = 1.57825 + 2.44413 * Q + 1.4281 * Q * Q + 0.422205 * Q * Q * Q; // 对应论文公式8c
B1 = 2.44413 * Q + 2.85619 * Q * Q + 1.26661 * Q * Q * Q;
B2 = -1.4281 * Q * Q - 1.26661 * Q * Q * Q;
B3 = 0.422205 * Q * Q * Q; B0 = 1.0 - (B1 + B2 + B3) / B;
B1 = B1 / B;
B2 = B2 / B;
B3 = B3 / B;
}

  由上述代码可见,B0+B1+B2+B3=1,即是归一化的系数,这也是算法可以递归进行的关键之一。

接着为了方便中间过程,我们先将字节数据转换为浮点数据,这部分代码也很简单:

void ConvertBGR8U2BGRAF(unsigned char *Src, float *Dest, int Width, int Height, int Stride)
{
//#pragma omp parallel for
for (int Y = ; Y < Height; Y++)
{
unsigned char *LinePS = Src + Y * Stride;
float *LinePD = Dest + Y * Width * ;
for (int X = ; X < Width; X++, LinePS += , LinePD += )
{
LinePD[] = LinePS[]; LinePD[] = LinePS[]; LinePD[] = LinePS[];
}
}
}

  大家可以尝试自己把内部的X循环再展开看看效果。

水平方向的前向传播按照公式去写也是很简单的,但是直接使用公式里面有很多访问数组的过程(不一定就慢),我稍微改造下写成如下形式:

void GaussBlurFromLeftToRight(float *Data, int Width, int Height, float B0, float B1, float B2, float B3)
{
//#pragma omp parallel for
for (int Y = ; Y < Height; Y++)
{
float *LinePD = Data + Y * Width * ;
float BS1 = LinePD[], BS2 = LinePD[], BS3 = LinePD[];          //  边缘处使用重复像素的方案
float GS1 = LinePD[], GS2 = LinePD[], GS3 = LinePD[];
float RS1 = LinePD[], RS2 = LinePD[], RS3 = LinePD[];
for (int X = ; X < Width; X++, LinePD += )
{
LinePD[] = LinePD[] * B0 + BS1 * B1 + BS2 * B2 + BS3 * B3;
LinePD[] = LinePD[] * B0 + GS1 * B1 + GS2 * B2 + GS3 * B3; // 进行顺向迭代
LinePD[] = LinePD[] * B0 + RS1 * B1 + RS2 * B2 + RS3 * B3;
BS3 = BS2, BS2 = BS1, BS1 = LinePD[];
GS3 = GS2, GS2 = GS1, GS1 = LinePD[];
RS3 = RS2, RS2 = RS1, RS1 = LinePD[];
}
}
}

  不多描述,请大家把上述代码和公式(a)对应一下就知道了。

有了GaussBlurFromLeftToRight的参考代码,GaussBlurFromRightToLeft的代码就不会有什么大的困难了,为了避免一些懒人不看文章不思考,这里我不贴这段的代码。

那么垂直方向上简单的做只需要改变下循环的方向,以及每次指针增加量更改为Width * 3即可。

那么我们来考虑下垂直方向能不能不这样处理呢,指针每次增加Width * 3会造成严重的Cache miss,特别是对于宽度稍微大点的图像,我们仔细观察垂直方向,会发现依旧可以按照Y  -- X这样的循环方式也是可以的,代码如下:

void GaussBlurFromTopToBottom(float *Data, int Width, int Height, float B0, float B1, float B2, float B3)
{
for (int Y = ; Y < Height; Y++)
{
float *LinePD3 = Data + (Y + ) * Width * ;
float *LinePD2 = Data + (Y + ) * Width * ;
float *LinePD1 = Data + (Y + ) * Width * ;
float *LinePD0 = Data + (Y + ) * Width * ;
for (int X = ; X < Width; X++, LinePD0 += , LinePD1 += , LinePD2 += , LinePD3 += )
{
LinePD0[] = LinePD0[] * B0 + LinePD1[] * B1 + LinePD2[] * B2 + LinePD3[] * B3;
LinePD0[] = LinePD0[] * B0 + LinePD1[] * B1 + LinePD2[] * B2 + LinePD3[] * B3;
LinePD0[] = LinePD0[] * B0 + LinePD1[] * B1 + LinePD2[] * B2 + LinePD3[] * B3;
}
}
}

  就是说我们不是一下子就把列方向的前向传播进行完,而是每次只进行一个像素的传播,当一行所有像素都进行完了列方向的前向传播后,在切换到下一行,这样处理就避免了Cache miss出现的情况。

注意到列方向的边缘位置,为了方便代码的处理,我们在高度方向上上下分别扩展了3个行的像素,当进行完中间的有效行的行方向前向和反向传播后,按照前述的重复边缘像素的方式填充上下那空出的三行数据。

同样的道理,GaussBlurFromBottomToTop的代码可由读者自己补充进去。

最后的ConvertBGRAF2BGR8U也很简单,就是一个for循环:

void ConvertBGRAF2BGR8U(float *Src, unsigned char *Dest, int Width, int Height, int Stride)
{
//#pragma omp parallel for
for (int Y = ; Y < Height; Y++)
{
float *LinePS = Src + Y * Width * ;
unsigned char *LinePD = Dest + Y * Stride;
for (int X = ; X < Width; X++, LinePS += , LinePD += )
{
LinePD[] = LinePS[]; LinePD[] = LinePS[]; LinePD[] = LinePS[];
}
}
}

  在有效的范围内,上述浮点计算的结果不会超出byte所能表达的范围,因此也不需要特别的进行Clamp操作。

最后就是一些内存分配和释放的代码了,如下所示:

void GaussBlur(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, float Radius)
{
float B0, B1, B2, B3;
float *Buffer = (float *)malloc(Width * (Height + ) * sizeof(float) * ); CalcGaussCof(Radius, B0, B1, B2, B3);
ConvertBGR8U2BGRAF(Src, Buffer + * Width * , Width, Height, Stride); GaussBlurFromLeftToRight(Buffer + * Width * , Width, Height, B0, B1, B2, B3);
GaussBlurFromRightToLeft(Buffer + * Width * , Width, Height, B0, B1, B2, B3); // 如果启用多线程,建议把这个函数写到GaussBlurFromLeftToRight的for X循环里,因为这样就可以减少线程并发时的阻力 memcpy(Buffer + * Width * , Buffer + * Width * , Width * * sizeof(float));
memcpy(Buffer + * Width * , Buffer + * Width * , Width * * sizeof(float));
memcpy(Buffer + * Width * , Buffer + * Width * , Width * * sizeof(float)); GaussBlurFromTopToBottom(Buffer, Width, Height, B0, B1, B2, B3); memcpy(Buffer + (Height + ) * Width * , Buffer + (Height + ) * Width * , Width * * sizeof(float));
memcpy(Buffer + (Height + ) * Width * , Buffer + (Height + ) * Width * , Width * * sizeof(float));
memcpy(Buffer + (Height + ) * Width * , Buffer + (Height + ) * Width * , Width * * sizeof(float)); GaussBlurFromBottomToTop(Buffer, Width, Height, B0, B1, B2, B3); ConvertBGRAF2BGR8U(Buffer + * Width * , Dest, Width, Height, Stride); free(Buffer);
}

  正如上所述,分配了Height + 6行的内存区域,主要是为了方便垂直方向的处理,在执行GaussBlurFromTopToBottom之前按照重复边缘的原则复制3行,然后在GaussBlurFromBottomToTop之前在复制底部边缘的3行像素。

至此,一个简单而又高效的高斯模糊就基本完成了,代码数量也不多,也没有啥难度,不晓得为什么很多人搞不定。

按照我的测试,上述方式代码在一台I5-6300HQ 2.30GHZ的笔记本上针对一副3000*2000的24位图像的处理时间大约需要370ms,如果在C++的编译选项的代码生成选项里的启用增强指令集选择-->流式处理SIMD扩展2(/arch:sse2),则编译后的程序大概需要220ms的时间。

我们尝试利用系统的一些资源进一步提高速度,首先我们想到了SSE优化,关于这方面Intel有一篇官方的文章和代码:

IIR Gaussian Blur Filter Implementation using Intel® Advanced Vector Extensions [PDF 513KB]

     source code: gaussian_blur.cpp [36KB]

我只是简单的看了下这篇文章,感觉他里面用到的计算公式和Deriche滤波器的很像,和本文参考的Recursive implementation 不太一样,并且其SSE代码对能处理的图还有很多限制,对我这里的参考意义不大。

我们先看下核心的计算的SSE优化,注意到  GaussBlurFromLeftToRight 的代码中, 其核心的计算部分是几个乘法,但是他只有3个乘法计算,如果能够凑成四行,那么就可以充分利用SSE的批量计算功能了,也就是如果能增加一个通道,使得GaussBlurFromLeftToRight变为如下形式:

void GaussBlurFromLeftToRight(float *Data, int Width, int Height, float B0, float B1, float B2, float B3)
{
//#pragma omp parallel for
for (int Y = ; Y < Height; Y++)
{
float *LinePD = Data + Y * Width * ;
float BS1 = LinePD[], BS2 = LinePD[], BS3 = LinePD[]; //  边缘处使用重复像素的方案
float GS1 = LinePD[], GS2 = LinePD[], GS3 = LinePD[];
float RS1 = LinePD[], RS2 = LinePD[], RS3 = LinePD[];
float AS1 = LinePD[], AS2 = LinePD[], AS3 = LinePD[];
for (int X = ; X < Width; X++, LinePD += )
{
LinePD[] = LinePD[] * B0 + BS1 * B1 + BS2 * B2 + BS3 * B3;
LinePD[] = LinePD[] * B0 + GS1 * B1 + GS2 * B2 + GS3 * B3; // 进行顺向迭代
LinePD[] = LinePD[] * B0 + RS1 * B1 + RS2 * B2 + RS3 * B3;
LinePD[] = LinePD[] * B0 + AS1 * B1 + AS2 * B2 + AS3 * B3;
BS3 = BS2, BS2 = BS1, BS1 = LinePD[];
GS3 = GS2, GS2 = GS1, GS1 = LinePD[];
RS3 = RS2, RS2 = RS1, RS1 = LinePD[];
AS3 = AS2, AS2 = AS1, AS1 = LinePD[];
}
}
}

  则很容易就把上述代码转换成SSE可以规范处理的代码了。

  而对于Y方向的代码,你仔细观察会发现,无论是及通道的图,天然的就可以使用SSE进行处理,详见下面的代码。

  好,我们还是一个一个的来分析:

  第一个函数 CalcGaussCof 无需进行任何的优化。

第二个函数 ConvertBGR8U2BGRAF按照上述分析需要重新写,因为需要增加一个通道,新的通道的值填0或者其他值都可以,但建议填0,这对有些SSE函数很有用,我把这个函数的SSE实现共享一下:

void ConvertBGR8U2BGRAF_SSE(unsigned char *Src, float *Dest, int Width, int Height, int Stride)
{
const int BlockSize = ;
int Block = (Width - ) / BlockSize;
__m128i Mask = _mm_setr_epi8(, , , -, , , , -, , , , -, , , , -); // Mask为-1的地方会自动设置数据为0
__m128i Zero = _mm_setzero_si128();
//#pragma omp parallel for
for (int Y = ; Y < Height; Y++)
{
unsigned char *LinePS = Src + Y * Stride;
float *LinePD = Dest + Y * Width * ;
int X = ;
for (; X < Block * BlockSize; X += BlockSize, LinePS += BlockSize * , LinePD += BlockSize * )
{
__m128i SrcV = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i*)LinePS), Mask); // 提取了16个字节,但是因为24位的,我们要将其变为32位的,所以只能提取出其中的前12个字节
__m128i Src16L = _mm_unpacklo_epi8(SrcV, Zero);
__m128i Src16H = _mm_unpackhi_epi8(SrcV, Zero);
_mm_store_ps(LinePD + , _mm_cvtepi32_ps(_mm_unpacklo_epi16(Src16L, Zero))); // 分配内存时已经是16字节对齐了,然后每行又是4的倍数的浮点数,因此,每行都是16字节对齐的
_mm_store_ps(LinePD + , _mm_cvtepi32_ps(_mm_unpackhi_epi16(Src16L, Zero))); // _mm_stream_ps是否快点?
_mm_store_ps(LinePD + , _mm_cvtepi32_ps(_mm_unpacklo_epi16(Src16H, Zero)));
_mm_store_ps(LinePD + , _mm_cvtepi32_ps(_mm_unpackhi_epi16(Src16H, Zero)));
}
for (; X < Width; X++, LinePS += , LinePD += )
{
LinePD[] = LinePS[]; LinePD[] = LinePS[]; LinePD[] = LinePS[]; LinePD[] = ; // Alpha最好设置为0,虽然在下面的CofB0等SSE常量中通过设置ALPHA对应的系数为0,可以使得计算结果也为0,但是不是最合理的
}
}
}

  稍作解释,_mm_loadu_si128一次性加载16个字节的数据到SSE寄存器中,对于24位图像,16个字节里包含了5 + 1 / 3个像素的信息,而我们的目标是把这些数据转换为4通道的信息,因此,我们只能一次性的提取处16/4=4个像素的值,这借助于_mm_shuffle_epi8函数和合适的Mask来实现,_mm_unpacklo_epi8/_mm_unpackhi_epi8分别提取了SrcV的高8位和低8位的8个字节数据并将它们转换为8个16进制数保存到Src16L和Src16H中, 而_mm_unpacklo_epi16/_mm_unpackhi_epi16则进一步把16位数据扩展到32位整形数据,最后通过_mm_cvtepi32_ps函数把整形数据转换为浮点型。

  可能有人注意到了 int Block = (Width - 2) / BlockSize; 这一行代码,为什么要-2操作呢,这也是我在多次测试发现程序总是出现内存错误时才意识到的,因为_mm_loadu_si128一次性加载了5 + 1 / 3个像素的信息,当在处理最后一行像素时(其他行不会),如果Block 取Width/BlockSize, 则很有可能访问了超出像素范围内的内存,而-2不是-1就是因为那个额外的1/3像素起的作用。

  接着看水平方向的前向传播的SSE方案:

void GaussBlurFromLeftToRight_SSE(float *Data, int Width, int Height, float B0, float B1, float B2, float B3)
{
const __m128 CofB0 = _mm_set_ps(, B0, B0, B0);
const __m128 CofB1 = _mm_set_ps(, B1, B1, B1);
const __m128 CofB2 = _mm_set_ps(, B2, B2, B2);
const __m128 CofB3 = _mm_set_ps(, B3, B3, B3);
//#pragma omp parallel for
for (int Y = ; Y < Height; Y++)
{
float *LinePD = Data + Y * Width * ;
__m128 V1 = _mm_set_ps(LinePD[], LinePD[], LinePD[], LinePD[]);
__m128 V2 = V1, V3 = V1;
for (int X = ; X < Width; X++, LinePD += ) // 还有一种写法不需要这种V3/V2/V1递归赋值,一次性计算3个值,详见 D:\程序设计\正在研究\Core\ShockFilter\Convert里的高斯模糊,但速度上没啥区别
{
__m128 V0 = _mm_load_ps(LinePD);
__m128 V01 = _mm_add_ps(_mm_mul_ps(CofB0, V0), _mm_mul_ps(CofB1, V1));
__m128 V23 = _mm_add_ps(_mm_mul_ps(CofB2, V2), _mm_mul_ps(CofB3, V3));
__m128 V = _mm_add_ps(V01, V23);
V3 = V2; V2 = V1; V1 = V;
_mm_store_ps(LinePD, V);
}
}
}

  和上面的4通道的GaussBlurFromLeftToRight_SSE比较,你会发现基本上语法上没有任何的区别,实在是太简单了,注意我没有用_mm_storeu_ps,而是直接使用_mm_store_ps,这是因为,第一,分配Data内存时,我采用了_mm_malloc分配了16字节对齐的内存,而Data每行元素的个数又都是4的倍数,因此,每行起始位置处的内存也是16字节对齐的,所以直接_mm_store_ps完全是可以的。

同理,在垂直方向的前向传播的SSE优化代码就更直接了:

void GaussBlurFromTopToBottom_SSE(float *Data, int Width, int Height, float B0, float B1, float B2, float B3)
{
const __m128 CofB0 = _mm_set_ps(, B0, B0, B0);
const __m128 CofB1 = _mm_set_ps(, B1, B1, B1);
const __m128 CofB2 = _mm_set_ps(, B2, B2, B2);
const __m128 CofB3 = _mm_set_ps(, B3, B3, B3);
for (int Y = ; Y < Height; Y++)
{
float *LinePS3 = Data + (Y + ) * Width * ;
float *LinePS2 = Data + (Y + ) * Width * ;
float *LinePS1 = Data + (Y + ) * Width * ;
float *LinePS0 = Data + (Y + ) * Width * ;
for (int X = ; X < Width * ; X += )
{
__m128 V3 = _mm_load_ps(LinePS3 + X);
__m128 V2 = _mm_load_ps(LinePS2 + X);
__m128 V1 = _mm_load_ps(LinePS1 + X);
__m128 V0 = _mm_load_ps(LinePS0 + X);
__m128 V01 = _mm_add_ps(_mm_mul_ps(CofB0, V0), _mm_mul_ps(CofB1, V1));
__m128 V23 = _mm_add_ps(_mm_mul_ps(CofB2, V2), _mm_mul_ps(CofB3, V3));
_mm_store_ps(LinePS0 + X, _mm_add_ps(V01, V23));
}
}
}

  对上面的代码不想做任何解释了。

  最有难度的应该算是ConvertBGRAF2BGR8U的SSE版本了,由于某些原因,我不太愿意分享这个函数的代码,有兴趣的朋友可以参考opencv的有关实现。

最后的SSE版本高斯模糊的主要代码如下:

void GaussBlur_SSE(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, float Radius)
{
float B0, B1, B2, B3;
float *Buffer = (float *)_mm_malloc(Width * (Height + ) * sizeof(float) * , ); CalcGaussCof(Radius, B0, B1, B2, B3);
ConvertBGR8U2BGRAF_SSE(Src, Buffer + * Width * , Width, Height, Stride); GaussBlurFromLeftToRight_SSE(Buffer + * Width * , Width, Height, B0, B1, B2, B3); // 在SSE版本中,这两个函数占用的时间比下面两个要多,不过C语言版本也是一样的
GaussBlurFromRightToLeft_SSE(Buffer + * Width * , Width, Height, B0, B1, B2, B3); // 如果启用多线程,建议把这个函数写到GaussBlurFromLeftToRight的for X循环里,因为这样就可以减少线程并发时的阻力 memcpy(Buffer + * Width * , Buffer + * Width * , Width * * sizeof(float));
memcpy(Buffer + * Width * , Buffer + * Width * , Width * * sizeof(float));
memcpy(Buffer + * Width * , Buffer + * Width * , Width * * sizeof(float)); GaussBlurFromTopToBottom_SSE(Buffer, Width, Height, B0, B1, B2, B3); memcpy(Buffer + (Height + ) * Width * , Buffer + (Height + ) * Width * , Width * * sizeof(float));
memcpy(Buffer + (Height + ) * Width * , Buffer + (Height + ) * Width * , Width * * sizeof(float));
memcpy(Buffer + (Height + ) * Width * , Buffer + (Height + ) * Width * , Width * * sizeof(float)); GaussBlurFromBottomToTop_SSE(Buffer, Width, Height, B0, B1, B2, B3); ConvertBGRAF2BGR8U_SSE(Buffer + * Width * , Dest, Width, Height, Stride); _mm_free(Buffer);
}

  对于同样的3000*2000的彩色图像,SSE版本的代码耗时只有145ms的耗时,相对于普通的C代码有约2.5倍左右的提速,这也情有可原,因为我们在执行SSE的代码时时多处理了一个通道的计算量的,但是同编译器自身的SSE优化220ms,只有1.5倍的提速了,这说明编译器的SSE优化能力还是相当强的。

进一步的优化就是我上面的注释掉的opemp相关代码,在ConvertBGR8U2BGRAF / GaussBlurFromLeftToRight / GaussBlurFromRightToLeft / ConvertBGRAF2BGR8U  4个函数中,直接加上简单的#pragma omp parallel for就可以了,至于GaussBlurFromTopToBottom_SSE、 GaussBlurFromBottomToTop_SSE则由于上下行之间数据的相关性,是无法实现并行计算的,但是测试表示他们的耗时要比水平方向的少很多。

比如我们指定openmp使用2个线程,在上述机器上(四核的),纯C版本能优化到252ms,而纯SSE的只能提高到100ms左右,编译器自身的SSE优化速度大约是150ms,基本上还是保持同一个级别的比例。

  对于灰度图像,很可惜,上述的水平方向上的优化方式就无论为力了,一种方式就是把4行灰度像素混洗成一行,但是这个操作不太方便用SSE实现,另外一种就是把水平方向的数据先转置,然后利用垂直方向的SSE优化代码处理,完成在转置回去,最后对转置的数据再次进行垂直方向SSE优化,当然转置的过程是可以借助于SSE的代码实现的,但是需要额外的一份内存,速度上可能和普通的C相比就不会有那么多的提升了,这个待有时间了再去测试。

前前后后写这个大概也花了半个月的时间,写完了整个流程,在倒过来看,真的是非常的简单,有的时候就是这样。

本文并没有提供完整的可以直接执行的全部代码,需者自勤,提供一段C#的调用工程供有兴趣的朋友测试和比对(未使用OPENMP版本)。

http://files.cnblogs.com/files/Imageshop/GaussBLur_Sample.rar

后记:后来测试发现,当半径参数较大时,无论是C版本还是SSE版本都会出现一些不规则的瑕疵,感觉像是溢出了,后来发现主要是当半径变大后,B0参数变得很小,以至于用float类型的数据来处理递归已经无法保证足够的精度了,解决的办法是使用double类型,这对于C语言版本来说是秒秒的事情,而对于SSE版本则是较大的灾难,double时换成AVX可能改动量不大,但是AVX的普及度以及.....,不过,一般情况下,半径不大于75时结果都还是正确的,这对于大部分的应用来说是足够的,半径75时,整个图像已经差不多没有任何的细节了,再大,区别也不大了。

解决上述问题一个可行的方案就是使用Deriche滤波器,我用该滤波器的float版本对大半径是不会出现问题的,并且也有相关SSE参考代码。

  后续文章:高斯模糊算法的全面优化过程分享(二)。

SSE图像算法优化系列二:高斯模糊算法的全面优化过程分享(一)。的更多相关文章

  1. SSE图像算法优化系列二十八:深度优化局部拉普拉斯金字塔滤波器。

    基于局部拉普拉斯金字塔的Edge-aware滤波器是在2011年由Adobe 公司的研究员Sylvain Paris(大神级人物,写了很多文章)提出的,我在4年前曾经参考有关代码实现过这个算法,但是速 ...

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

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

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

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

  4. SSE图像算法优化系列二十四: 基于形态学的图像后期抗锯齿算法--MLAA优化研究。

    偶尔看到这样的一个算法,觉得还是蛮有意思的,花了将近10天多的时间研究了下相关代码. 以下为百度的结果:MLAA全称Morphological Antialiasing,意为形态抗锯齿是AMD推出的完 ...

  5. SSE图像算法优化系列二十一:基于DCT变换图像去噪算法的进一步优化(100W像素30ms)。

    在优化IPOL网站中基于DCT(离散余弦变换)的图像去噪算法(附源代码) 一文中,我们曾经优化过基于DCT变换的图像去噪算法,在那文所提供的Demo中,处理一副1000*1000左右的灰度噪音图像耗时 ...

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

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

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

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

  8. 性能优化系列二:JVM概念及配置

    一.虚拟机组成 虚拟机主要由三部分组成:编译器(执行引擎),堆与栈. 1. 编译器 编译器分为即时编译器与解释器. 即时编译器将代码编译成本地代码存于code区.因此它快,但它有内存限制! 解释器逐行 ...

  9. SSE图像算法优化系列二十九:基础的拉普拉斯金字塔融合用于改善图像增强中易出现的过增强问题(一)

    拉普拉斯金字塔融合是多图融合相关算法里最简单和最容易实现的一种,我们在看网络上大部分的文章都是在拿那个苹果和橙子融合在一起,变成一个果橙的效果作为例子说明.在这方面确实融合的比较好.但是本文我们主要讲 ...

随机推荐

  1. related_name和related_query_name举例区别

    例1: class UserInfo(models.Model): nickname = models.CharField(max_length=32) username = models.CharF ...

  2. 指定某个方法在站点的Application_Start之前执行

    指定某个函数方法在站点的Application_Start之前执行:PreApplicationStartMethodAttribute 先预备一个类,用于Start时调用 public static ...

  3. etcd-v2第二集

    参考文章:https://github.com/coreos/etcd/blob/master/Documentation/v2/api.mdhttp://www.cnblogs.com/zhengr ...

  4. python之运算符

    运算符的定义 运算符用于执行程序代码运算,会针对一个以上操作数项目来进行运算. 运算符类型 .算数运算符 .比较运算符 .赋值运算符 .位运算符 .逻辑运算符 .成员运算符 7身份运算符 详情介绍 1 ...

  5. Spring MVC 中的输入验证 Vlidator

    在 Spring MVC 中有两种方式可以验证输入:1. Spring 自带的验证框架:2. 利用 JSR 303 实现,即 Java Specification Requests Converter ...

  6. Delphi过程和函数中变量的作用域

    变量的作用域是指变量能被某一子程序识别的范围. 全局变量和局部变量.全局变量是指在程序的type区定义的变量,而局部变量是在过程或函数的定义部分声明的变量.全局变量在整个程序中都有意义,局部变量只在它 ...

  7. java 组件开发中的日志记录问题

    今天帮别人写封装几个url 请求,打成jar 包,可以以java接口的方式提供给外部访问. 遇到两个问题: 1. 是否把依赖的jar包也 打入 我要生成的jar包中,如果你不打入,别人直接调用接口会报 ...

  8. Scrum冲刺阶段2

    成员今日完成的任务 人员 任务 何承华 后端设计 陈宇 后端设计 丁培辉 后端设计 温志铭 主页面的设计 杨宇潇 主页面的设计 张主强 服务器构建 成员遇到的问题 人员 问题 何承华 暂无 陈宇 暂无 ...

  9. eclipse配置servlet错误

    可能是因为你的web.xml里的<url>映射的名字和servlet相同

  10. django的母板系统

    一.母板渲染语法 1.变量 {{ 变量 }} 2.逻辑 {% 逻辑语 %} 二.变量 在母板中有变量时,母板引擎会去反向解析找到这个传来的变量,然后替换掉. .(点),在母板中是深度查询据点符,它的查 ...