PS

这篇文章似乎写歪了啊,因为我测试SSE优化的代码,发现和原始版本的C代码速度是一致?不知道是有什么核心的地方没注意到?我再研究研究吧。

前言

本算法的原理以及优化过程都是偷师ImageShop,也可以把这篇文章理解为https://www.cnblogs.com/Imageshop/p/6376028.html 这篇文章的一篇阅读总结。

原理

首先,高斯滤波是可以用递归算法来实现的。这最早见于《Recursive implementation of the Gaussian filter》论文中:
在forward的过程中,n是递增的,所以,我们在进行forward之前将in数据复制给w,然后上式即可变形为:
w [ n ] = B w [ n ] + ( b 1 w [ n 1 ] + b 2 w [ n 2 ] + b 3 w [ n 3 ] ) / b 0 w[n]=Bw[n]+(b_1w[n-1]+b_2w[n-2]+b_3w[n-3])/b_0 w[n]=Bw[n]+(b1w[n1]+b2w[n2]+b3w[n3])/b0-------(1a)。
同理backward过程中n是递减的,因此在backward前将w的数据完整的拷贝到out中,则式9b则变为:
o u t [ n ] = B o u t [ n ] + ( b 1 o u t [ n + 1 ] + b 2 o u t [ n + 2 ] + b 3 o u t [ n + 3 ] ) / b 0 out[n] = B out[n] + (b_1out[n+1] + b_2out[n+2] + b_3 out[n+3]) / b0 out[n]=Bout[n]+(b1out[n+1]+b2out[n+2]+b3out[n+3])/b0 -------(1b)。
从编程实现来看,这个拷贝是没有必要的,所以1b可以写成:
w [ n ] = B w [ n ] + ( b 1 w [ n + 1 ] + b 2 w [ n + 2 ] + b 3 w [ n + 3 ] ) / b 0 w[n] = B w[n] + (b_1 w[n+1] + b_2 w[n+2] + b_3 w[n+3]) / b0 w[n]=Bw[n]+(b1w[n+1]+b2w[n+2]+b3w[n+3])/b0------(1c)。
从速度上看,最好是能够去掉1a和1b的除法,所以这里重新定义b1 = b1 / b0, b2 = b2 / b0, b3 = b3 / b0, 最终得到我们使用的递归公式:
w [ n ] = B w [ n ] + b 1 w [ n 1 ] + b 2 w [ n 2 ] + b 3 w [ n 3 ] w[n] = B w[n] + b_1 w[n-1] + b_2 w[n-2] + b_3 w[n-3] w[n]=Bw[n]+b1w[n1]+b2w[n2]+b3w[n3]---------> (a)
w [ n ] = B w [ n ] + b 1 w [ n + 1 ] + b 2 w [ n + 2 ] + b 3 w [ n + 3 ] w[n] = Bw[n] + b_1w[n+1] + b_2w[n+2] + b_3w[n+3] w[n]=Bw[n]+b1w[n+1]+b2w[n+2]+b3w[n+3] <---------(b)
\quad 这个递推公式的意义就是通过forward和backward来完成一维的高斯滤波。而二维的高斯滤波就是先行后列或者先列后行进行一维的高斯滤波。

算法步骤

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

代码实现步骤

对于每一个函数,我都会给出相应的代码实现和我的一些想法,总结,ImageShop博主只给了一部分实现,我依靠自己的理解把某些函数补全了。

CalGaussCof 计算高斯模糊中使用到的系数

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(1 - 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;
}

这个公式是论文原文给出的,然后计算过程可以理解为O(1)的,没有优化空间,也没有对应的SSE优化。

ConvertBGR8U2BGRAF 将字节数据转换为浮点数据

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

这个也没有什么技巧可言,就是整数强制转浮点数。这里是普通的C语言实现,所以输出的float数组仍然是3通道的,之后会看到SSE优化后会是4通道的。

GaussBlurFromLeftToRight 水平方向的前向传播

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

按照前向传播Forward公式来。

GaussBlurFromRightToLeft 水平方向的反向传播

void GaussBlurFromRightToLeft(float *Data, int Width, int Height, float B0, float B1, float B2, float B3) {
	for (int Y = 0; Y < Height; Y++) {
		//w[n+1], w[n+2], w[n+3]
		float *LinePD = Data + Y * Width * 3 + (Width * 3);
		float BS1 = LinePD[0], BS2 = LinePD[0], BS3 = LinePD[0]; //边缘处使用重复像素的方案
		float GS1 = LinePD[1], GS2 = LinePD[1], GS3 = LinePD[1];
		float RS1 = LinePD[2], RS2 = LinePD[2], RS3 = LinePD[2];
		for (int X = Width - 1; X >= 0; X--, LinePD -= 3)
		{
			LinePD[0] = LinePD[0] * B0 + BS3 * B1 + BS2 * B2 + BS1 * B3;
			LinePD[1] = LinePD[1] * B0 + GS3 * B1 + GS2 * B2 + GS1 * B3;         // 进行反向迭代
			LinePD[2] = LinePD[2] * B0 + RS3 * B1 + RS2 * B2 + RS1 * B3;
			BS1 = BS2, BS2 = BS3, BS3 = LinePD[0];
			GS1 = GS2, GS2 = GS3, GS3 = LinePD[1];
			RS1 = RS2, RS2 = RS3, RS3 = LinePD[2];
		}
	}
}

按照反向传播公式Backward来。

剩下的代码普通的C语言可以在我的github上查看:https://github.com/BBuf/Image-processing-algorithm-Speed/blob/master/speed_gaussian_filter_sse.cpp

SSE优化

这篇文章的重点在于SSE优化,所以接下来会挨个挨个函数分析SSE优化的要点。

对ConvertBGR8U2BGRAF的SSE优化

这里需要对BGR图像新增一个通道,新的通道值这里直接填0。

void ConvertBGR8U2BGRAF_SSE(unsigned char *Src, float *Dest, int Width, int Height, int Stride) {
	const int BlockSize = 4;
	int Block = (Width - 2) / BlockSize;
	__m128i Mask = _mm_setr_epi8(0, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8, -1, 9, 10, 11, -1);
	__m128i Zero = _mm_setzero_si128();
	for (int Y = 0; Y < Height; Y++) {
		unsigned char *LinePS = Src + Y * Stride;
		float *LinePD = Dest + Y * Width * 4;
		int X = 0;
		for (; X < Block * BlockSize; X += BlockSize, LinePS += BlockSize * 3, LinePD += BlockSize * 4) {
			__m128i SrcV = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i*)LinePS), Mask);
			__m128i Src16L = _mm_unpacklo_epi8(SrcV, Zero);
			__m128i Src16H = _mm_unpackhi_epi8(SrcV, Zero);
			_mm_store_ps(LinePD + 0, _mm_cvtepi32_ps(_mm_unpacklo_epi16(Src16L, Zero)));
			_mm_store_ps(LinePD + 4, _mm_cvtepi32_ps(_mm_unpackhi_epi16(Src16L, Zero)));
			_mm_store_ps(LinePD + 8, _mm_cvtepi32_ps(_mm_unpacklo_epi16(Src16H, Zero)));
			_mm_store_ps(LinePD + 12, _mm_cvtepi32_ps(_mm_unpackhi_epi16(Src16H, Zero)));
		}
		for (; X < Width; X++, LinePS += 3, LinePD += 4) {
			LinePD[0] = LinePS[0];    LinePD[1] = LinePS[1];    LinePD[2] = LinePS[2];    LinePD[3] = 0;
		}
	}
}

首先定义了一个BlockSize=4,这是因为SSE是128位,每次可以处理128/8=16个uchar类型的数,其中每一个像素都是4个值,因为这里要新增一个通道。而int Block = (Width - 2) / BlockSize这行代码是因为_mm_loadu_si128一次性加载了5 + 1 / 3个像素值(这里指的是Src),当在处理最后一行像素时,如果Block取Width / BlockSize则有可能会超出像素范围内的内存,而-2则是因为额外的1/3像素起作用。循环挺好理解的,其中_mm_loadu_si128一次性加载16个字节的数据到SSE寄存器中,对于RGB图像 ,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函数把整形数据转换为浮点型。

对GaussBlurFromLeftToRight 水平方向的前向传播的SSE优化

void GaussBlurFromLeftToRight_SSE(float *Data, int Width, int Height, float B0, float B1, float B2, float B3) {
	const __m128 CofB0 = _mm_set_ps(0, B0, B0, B0);
	const __m128 CofB1 = _mm_set_ps(0, B1, B1, B1);
	const __m128 CofB2 = _mm_set_ps(0, B2, B2, B2);
	const __m128 CofB3 = _mm_set_ps(0, B3, B3, B3);
	for (int Y = 0; Y < Height; Y++) {
		float *LinePD = Data + Y * Width * 4;
		__m128 V1 = _mm_set_ps(LinePD[3], LinePD[2], LinePD[1], LinePD[0]);
		__m128 V2 = V1, V3 = V1;
		for (int X = 0; X < Width; X++, LinePD += 4) {
			__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);
		}
	}
}

这里ImageShop提到了之所以没有使用_mm_storeu_ps而是直接使用_mm_store_ps是因为分配Data内存时,采用了_mm_malloc分配了16字节对齐的内存,而Data每行元素的个数又是4的倍数,因此每行起始位置处的内存也是16字节对齐的,所以字节_mm_store_ps是可行的。
之后的水平方向的反向传播,垂直方向的传播代码均可以在我的github找到:https://github.com/BBuf/Image-processing-algorithm-Speed/blob/master/speed_gaussian_filter_sse.cpp

还有作者没有提供ConvertBGRAF2BGR8U的SSE的代码,这一段要进行SSE优化的难度确实是蛮大的,我实现的瓶颈在于我不知道如何将SSE向量映射为只保存3个值的一个像素,这里我还需要再继续探索下去,我先给一份我的普通实现(没有SSE优化):

void ConvertBGRAF2BGR8U_SSE(float *Src, unsigned char *Dest, int Width, int Height, int Stride) {
	for (int Y = 0; Y < Height; Y++) {
		float *LinePS = Src + Y * Width * 4;
		unsigned char *LinePD = Dest + Y * Stride;
		for (int X = 0; X < Width; X++, LinePS += 4, LinePD += 3) {
			LinePD[0] = LinePS[0]; LinePD[1] = LinePS[1];    LinePD[2] = LinePS[2];
		}
	}
}

时间测试

普通版和SSE优化版(没有最后一个颜色通道转换的优化)完整的代码可以在我的github找到,现在我给出我的时间测试表,Corei7 8770单核,图片分辨率为4032*3024:

优化技巧编号 优化技巧名称 时间 半径
1 无优化 290.43ms 11
2 SSE优化 265.96ms 11