基于D3D11计算着色器并行计算的图像去雾算法

4 篇文章 0 订阅
4 篇文章 0 订阅

       这一个多月来,主要完成了基于D3D11计算着色器实现图像去雾的并行计算这个小项目。为什么选择D3D11而不是CUDA更通用的并行计算平台?是因为想把图像去雾的功能加在基于D3D11的播放器中,这时输入的图像时DX里面的纹理数据。

       之前完成了对去雾算法的测试和效果验证,可见这篇博客:一种去雾算法的实现。该CPU版本总的运行时间在160ms一帧(彩色1080P),达不到实时的要求。在花时间学习了DitectX 11的新特性:计算着色器,简单使用方法见D3D11计算着色器的配置和编程。我用了一段时间阅读了一些并行算法的设计相关的书籍,由于数学欠缺,所以理解不够深刻,但是我的要求也是对这些内容有个大体的认识。随后,我把之前去雾算法改编为基于计算着色器的并行算法,进行了测试和修改。完成了一个demo。

去雾算法步骤与并行化分析

        并行化算法的设计思想可以参考并行算法的设计与分析,电子书资源很好找,如果有读者需要,也可以找我。

        对于没有接触过并行化编程的人来说,初次进行并行化编程困难是思维上面的转变。比如下面算法中一个求出整张图像中最小值,基于CPU的串行方法肯定是依次比较,而在并行计算中,应该分别求出不同区域的最小值,再合并,再求出合并后的最小值,直到区域为1。串行方法对于一幅1920*1080图像,要进行1920*1080次指针移位,和对应次数的比较,时间成本上面是很大的。对于GPU来说,有多个线程可以同时处理,那么就要考虑如何让不同的线程完成不同的比较任务,也就是不同线程处理不同区域,再合并。

        我认为学习并行化编程的第一步,应该学习几种CPU下很简单的问题,比如求和,最小值等基础问题,在并行化编程要求下的几种算法,详细可以参考上面的参考书籍。

                                                                                            图1 算法步骤

          回到我使用的去雾算法,在并行化的编程框架下,将以上算法分为两部分:

         a. 像素独立可以计算。     b. 需要读取其它像素点值的

根据上面两种分类,算法步骤中2 5 7是可以实现像素独立计算的,3 4 6需要图像中其它像素才可以计算得到结果。对于像素可以独立计算的就比较简单,直接分配线程并行计算就好。对于b类,3 4 6 步骤分别就是图像卷积计算图像均值最值求解。这些都涉及到读取其它位置的像素,需要不同于CPU计算的算法去计算。

图像卷积计算

        利用计算着色器实现图像的卷积,在《Practical Rendering and Computatiojn with Direct3D 11》一书中有比较详细的介绍,包括了高斯滤波和双边滤波两种卷积核。实现卷积的方法,也有两种:2D和1D。书中表明2D会比1D用到更多的计算次数,这也是很容易计算的。假设卷积核大小为p*q,图像大小m*n,2D操作次数为p*q*m*n(每个像素点计算p*q次),1D只需要(p+q)*m*n。1D滤波器又叫做分离滤波器,即将原始的2D卷积模版分离为两个一维向量,分别是行和列,分开对图像进行计算,得到卷积后的图像。

        卷积的数学基础不再赘述,下面卷积的一般公式为。分离滤波器将模版分离再计算。

        下面给出着色器的代码,因为我将两个方向的滤波代码写在了一个函数中,所以利用了CPU传入GPU的常量缓冲来区别不同方向的计算。需要注意的是,我在对原图像的暗通道进行模糊处理时,只是将纹理这个类型作为一个数据的容器,并不关心其几通道对应哪个颜色。而不同方向的滤波计算时,我存入了不同的通道,所以要分开计算,否则使用一样的代码就可以计算两个方向。

Texture2D g_TexA : register(t0);

RWTexture2D<float4> g_Output : register(u0);

cbuffer CB: register(b0)
{
	uint dir_flag;        // 方向
	uint level;          // minmean 参数
	uint useless1;      // 未使用
	uint useless2;     //未使用
}

        随后,代码做一些宏定义,主要是滤波器模版参数和图像大小相关。在本文使用的去雾算法中,图像卷积模版是均值滤波。基于图像大小为1920*1080,选取模版大小为65*65,2D则每个点的值为1/(65*65),分离滤波器分别为行列向量,长度为65,每个点的值大小为1/65。

#define RADIUS 32
#define size_filter (RADIUS*2+1)
#define filterV 1/size_filter
#define size_x 1920
#define size_y 1080

         基于计算着色器的组内共享特性,我使用了groupshared,这使得读入图像数据后,每次使用一行(即一个组)内的数据时,不必访问纹理,这样速度更快。

groupshared float GSM[size_x];
[numthreads(size_x/2, 1, 1)]   //线程不能超过最大线程数1024

         下面就是滤波器的计算着色器主函数了,除了输入输出的通道不同,两个方向的计算一样。在这里我对超界进行了简单处理,即如果超界,只计算在范围内的值。据说计算着色器具有坐标超界处理的机制,我没有尝试验证,留一个坑以后再研究。

         在这段卷积的代码中,对于每个线程,我计算了两个像素点的模糊值,因为这样才能使  线程数/2 小于 最大线程数1024。

void BlurCS(uint3 DTid : SV_DispatchThreadID)
{

	//两个方向,分开处理
	if (dir_flag == 0)
	{
		//x方向
		int2 index1 = int2(DTid.x * 2, DTid.y);
		int2 index2 = int2(DTid.x * 2 + 1, DTid.y);
		float4 data1 = g_TexA[index1];
		float4 data2 = g_TexA[index2];
		float4 outV1 = data1;
		float4 outV2 = data2;
		GSM[DTid.x * 2] = data1[0]* filterV;
		GSM[DTid.x * 2 + 1] = data2[0] * filterV;

		//等待同步
		GroupMemoryBarrierWithGroupSync();

		float out1 = 0, out2 = 0;
		//滤波计算
		if (index1.x < RADIUS)
		{
			for (int x = 0; x <= (index1.x + RADIUS); x++)
			{
				out1 += GSM[x] ;
			}
		}
		else if ((size_x - index1.x) < RADIUS)
		{
			for (int x = (index1.x - RADIUS); x <=(size_x); x++)
			{
				out1 += GSM[x] ;
			}
		}
		else 
		{
			for (int x = (index1.x - RADIUS); x <= (index1.x + RADIUS); x++)
			{
				out1 += GSM[x] ;
			}

		}
	
		//滤波计算
		if (index2.x < RADIUS)
		{
			for (int x = 0; x <= (index2.x + RADIUS); x++)
			{
				out2 += GSM[x] ;
			}
		}
		else if ((size_x - index2.x) < RADIUS)
		{
			for (int x = (index2.x-RADIUS); x <= size_x; x++)
			{
				out2 += GSM[x];
			}
		}
		else
		 {
			for (int x = (index2.x - RADIUS); x <= (index2.x + RADIUS); x++)
			{
				out2 += GSM[x] ;
			}

		}
		outV1[1] = out1;
		outV2[1] = out2;
		 //outV1[0] = 0;
		 //outV2[0] = 0;
		g_Output[index1] = outV1;
		g_Output[index2] = outV2;

	}


	else if (dir_flag == 1)
	{
		//y方向
		int2 index1 = int2(DTid.y , DTid.x * 2);
		int2 index2 = int2(DTid.y , DTid.x * 2 + 1);
		float4 data1 = g_TexA[index1];
		float4 data2 = g_TexA[index2];
		float4 outV1 = data1;
		float4 outV2 = data2;
		GSM[DTid.x * 2] = data1[1] * filterV;
		GSM[DTid.x * 2 + 1] = data2[1] * filterV;

		//等待同步
		GroupMemoryBarrierWithGroupSync();

		float out1 = 0, out2 = 0;

		//滤波计算
		if (index1.y < RADIUS)
		{
			for (int x = 0; x <= (index1.y + RADIUS); x++)
			{
				out1 += GSM[x] ;
			}
		}
		else if ((size_y - index1.y) < RADIUS)
		{
			for (int x = (index1.y - RADIUS); x <= size_x; x++)
			{
				out1 += GSM[x];
			}
		}
		else
		{
			for (int x = (index1.y - RADIUS); x <= (index1.y + RADIUS); x++)
			{
				out1 += GSM[x];
			}

		}

		//滤波计算
		if (index2.y < RADIUS)
		{
			for (int x = 0; x <= (index2.y + RADIUS); x++)
			{
				out2 += GSM[x] ;
			}
		}
		else if ((size_y - index2.y) < RADIUS)
		{
			for (int x = (index2.y - RADIUS); x <= size_x; x++)
			{
				out2 += GSM[x];
			}
		}
		else
		{
			for (int x = (index2.y - RADIUS); x <= (index2.y + RADIUS); x++)
			{
				out2 += GSM[x] ;
			}

		}

		outV1[1] = out1;
		outV2[1] = out2;
		//outV1[0] = 0;
		//outV2[0] = 0;
		g_Output[index1] = outV1;
		g_Output[index2] = outV2;
	}
}

二维数据的均值和最值并行计算

        在串行计算中,我们可以很简单的利用依次累加得到均值,通过依次比较得到最值。当我们利用GPU计算时,有若干个运算单元来计算,如果还是依次计算,则无法体现多核的计算优势。此时,我们需要重新根据算法的原理,重设计算法在GPU下的运行步骤。均值和最值的计算作为两个最典型问题,在实现并行化时,必须修改,有很多算法值得参考。在《并行算法的设计与分析中》,有很多不同的数学模型和算法值得参考。理解了这些最基本运算的问题的并行化法方法,在后面编写其它算法的并行程序,会有很大的帮助。

        在我的demo中,我需要计算均值滤波后原图像最大值,暗通道图像的最大值和暗通道图像的均值。其中第二个值,在一般图像中都接近255,所以设置为255对于很多场景都适用,这一点我也在基于CPU的程序中验证过。第二个值和第三个值则需要求解,很明显,它们都与暗通道图像有关,并且均值需要对每一个值累加,最值需要对每个值比较,两者计算过程中,访问的像素数据量相等,所以我将两个值的计算放在了一起。

         先假设图像大小长宽相等,并且都是2的次方,计算时我取每四个像素点为一个小单元,在线程内计算最大值和均值,然后存入输入纹理坐标1/2处。用画图花了个简图,见下图。最左边的输出纹理的[0,0]坐标就是我们要求的最大值和均值。

         

         下面讨论一下图像大小不是以上规则形状时的情况,对于结果会有什么影响。对于最大值,以上思路会利用两个纹理来回反复作为输入输出,当计算到后面,计算的范围不再是正方形时,可以补齐为正方形,由于是求最值,这些已经经过比较的值,仍然会小于最大值,所以没有影响。对于均值,存在一样的情况,也是到接近求出值时,此时补齐的值实际上很接近全局均值,所以两者再求均值,影响也不大。

          接下来给出着色器代码,这里限制计算的范围是通过CPU程序中参数level来限制,如果线程坐标在范围外,就不计算。

Texture2D g_TexA : register(t0);

RWTexture2D<float4> g_Output : register(u0);

cbuffer CB: register(b0)
{
	uint dir_flag;        // 方向
	uint level;          // minmean 参数
	uint useless1;      // 未使用
	uint useless2;     //未使用
}


#define size_x 2048
#define size_y 1024

[numthreads(32, 32, 1)]   //线程不能超过最大线程数1024

void MinMean_CS(uint3 DTid : SV_DispatchThreadID)
{   
	int2 p1 = int2(DTid.x*2, DTid.y *2);
	int2 p2 = int2(DTid.x * 2, DTid.y * 2 + 1);
	int2 p3 = int2(DTid.x * 2 + 1, DTid.y * 2);
	int2 p4 = int2(DTid.x * 2 + 1, DTid.y * 2 + 1);
	int2 p5 = int2(DTid.x, DTid.y);


	if (DTid.x < (size_x / (level*2)-0.5) && DTid.y < (size_y / (level * 2) -0.5))
	{
		float Mean=0;
		float Max=0;

		if (level == 1) 
		{
			float4 v1 = g_TexA[p1][1];
			float4 v2 = g_TexA[p2][1];
			float4 v3 = g_TexA[p3][1];
			float4 v4 = g_TexA[p4][1];
			Mean = (v1[0] + v2[0] + v3[0] + v4[0]) / 4.0;
			Max = v1[1];
		
			if (v2[1] > Max)   Max = v2[1];
			if (v3[1] > Max)   Max = v3[1];
			if (v4[1] > Max)   Max = v4[1];

			float4 outV = float4(g_TexA[p5][0], g_TexA[p5][1], Max, Mean);
			g_Output[p5] = outV;
		}
		else
		{
			float4 v1 = g_TexA[p1];
			float4 v2 = g_TexA[p2];
			float4 v3 = g_TexA[p3];
			float4 v4 = g_TexA[p4];
			Mean = (v1[3] + v2[3] + v3[3] + v4[3]) / 4.0;
			Max = v1[2];
			if (v2[2] > Max)   Max = v2[2];
			if (v3[2] > Max)   Max = v3[2];
			if (v4[2] > Max)   Max = v4[2];

			float4 outV = float4(g_TexA[p5][0], g_TexA[p5][1], Max,Mean);
			g_Output[p5] = outV;
		}


	}
	if(p1[0]>255||p1[1]>255)
	{
		if (level == 1)
		{
			g_Output[p1] = g_TexA[p1];
			g_Output[p2] = g_TexA[p2];
			g_Output[p3] = g_TexA[p3];
			g_Output[p4] = g_TexA[p4];
		}
	}

}

         接下来给出C++代码, ChangeComputeShader()就是一个改变着色器的函数,因为写在这儿太复杂,所以我另写了一下函数。

        int p_level = 0;
	UINT level = 1;
	bool outF;

        ChangeComputeShader(1);//改变着色器
        m_pd3dImmediateContext->Dispatch(64, 32, 1);

	//均值滤波,模版大小见hlsl文件
	SetConstants(BLUR_DIR_X, 0, 0, 0);
	ChangeComputeShader(31);
	m_pd3dImmediateContext->Dispatch(1, Image_H, 1);


	ChangeComputeShader(32);
	SetConstants(BLUR_DIR_Y, 0, 0, 0);
	m_pd3dImmediateContext->Dispatch(1, Image_W, 1);


    //最大值和均值求解
	for (p_level=0,level=1; level < Image_W; p_level++)
		{
			if (p_level % 2 == 0)
			{
				ChangeComputeShader(22);
				SetConstants(0, level, 0, 0);
				m_pd3dImmediateContext->Dispatch(32, 32, 1);   //输出是B

				if ((level * 2) >= Image_H)
				{
					outF = 0;
				}
			}
			else
			{
				ChangeComputeShader(21);
				SetConstants(0, level, 0, 0);
				m_pd3dImmediateContext->Dispatch(32, 32, 1);//输出是A
				if ((level * 2) >= Image_H)
				{
					outF = 1;
				}
			}
			level = level * 2;
		}



		if (outF == false)
		{
			ChangeComputeShader(42);
			m_pd3dImmediateContext->Dispatch(64, 32, 1);//输出是A
		}
		else if (outF == true)
		{
			ChangeComputeShader(41);
			m_pd3dImmediateContext->Dispatch(64, 32, 1);//输出是B

		}

        下图就是出来的效果图,和CPU计算的结果一致。但是有个小bug就是,运行时间不稳定,目前认为是GPU计算完后,返回CPU时有时候会等待。这种情况,每1000次计算,会有15次左右时间超过1s,其它时间均远小于10ms。这个问题目前尚未找到解决办法,等待下一步融合如播放器时,再看效果如何。(当我在每次计算前,加一次系统延时sleep函数,1000次中就一次异常)。

2019.10.10 update:

用win10系统重新跑了程序,每一帧处理时间均小于0.001s,时间很满意,潜在的问题是GPU利用率达到了100%,如果将程序移植到其它平台,这会引起其它问题,后续需要处理。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值