那些年CUDA编程那些事(三)

1、概述

前面两篇文章介绍了CUDA的基本概念,以及对数组、矩阵的简单求和操作:

那些年CUDA编程那些事(一)
那些年CUDA编程那些事(二)

CUDA用的最广泛之一的地方,还是在二维图像的处理上,如特征提取,立体匹配,三维重建,深度学习训练与检测等等。因此,这里艺一个简单的例子展示CUDA如何通过线程块与线程的组合,达到处理图像的目的:

目的:将一个8000*1000的单通道图分割为40x40 (可调整)的块, 计算每个块内的像素值的和、最大最小、均值,将计算结果保存在CPU端。

以上描述与后面的程序参考于:这篇博客

2、实现步骤

由于每个线程块的最大线程数为1024,因此考虑选择通过两次运算(每次计算数为40*20)来完成,运用共享内存保存每个块中传入图像的像素数据,最后利用归约算法对加法进行优化。

2.1 使用OpenCV输入一张8000*1000的单通道图像

什么?没有8000*1000的单通道图像?直接使用resize函数就可以了。

	Mat image = imread("2.jpg", 0);   //读取待检测图片,0表示以灰度图读入
	cv::resize(image, image, cv::Size(1000, 8000));

2.2 为CUDA数组分配内存

包括待处理的图像数据,处理后的各个块的最大、最小像素值以及块内的像素值总和。具体实现如下所示:

	//图像的所有字节数
	size_t memSize = image.cols*image.rows * sizeof(uchar);
	int size = 5000 * sizeof(int);

	//分配内存:GPU图像数据、求和结果数组、最大值结果数组、最小值结果数组
	uchar *d_src = NULL;
	int *d_sum = NULL;
	int *d_max = NULL;
	int *d_min = NULL;
	cudaMalloc((void**)&d_src, memSize);
	cudaMalloc((void**)&d_sum, size);
	cudaMalloc((void**)&d_max, size);
	cudaMalloc((void**)&d_min, size);

	//图像数据拷贝到GPU
	cudaMemcpy(d_src, image.data, memSize, cudaMemcpyHostToDevice);

2.3 分配线程和线程,执行核函数

这一部分先给出核函数大致的执行过程,下一小节再详细介绍核函数的具体实现过程,也是本篇文章的核心部分。

	//图像宽、高
	int imgWidth = image.cols;
	int imgHeight = image.rows;

	//dim3 threadsPerBlock(20, 40); //每个block大小为20*40,对应matSum2核函数
	dim3 threadsPerBlock(40, 20); //每个block大小为40*20,对应matSum核函数

	dim3 blockPerGrid(25, 200); //将8000*1000的图片分为25*200个小图像块

	double time0 = static_cast<double>(getTickCount()); //计时器开始
	matSum << <blockPerGrid, threadsPerBlock, 3200 * sizeof(int) >> > (d_src, d_sum, d_max, d_min, imgHeight, imgWidth);

	//等待所有线程执行完毕
	cudaDeviceSynchronize();

	time0 = ((double)getTickCount() - time0) / getTickFrequency(); //计时器结束
	cout << "The Run Time is :" << time0 << "s" << endl; //输出运行时间

上面程序中有两种线程的分配方式,都可以实现相应的功能,只是在线程的执行方式上略有不同。先放在这里,后面会详细讨论。

此外,上面程序中还涉及到3200 * sizeof(int)这个参数。该参数的具体含义是指核函数执行过程中,各个线程块之间共享内存的字节大小。共享内存:简而言之就是线程块内部各个线程都可以共同使用的内存。本文中知道这一点解足够了。

共享内存的声明方式为静态设动态。静态声明为:

//声明一个二维浮点数共享内存数组
__shared__ float a[size_x][size_y];

这里的size_x,size_y和声明c++数组一样,要是一个编译时确定的数字,不能是变量。

动态声明为:

extern __shared__ int tile[];

注意,动态声明只支持一维数组。关于共享内存的更多信息,参考这篇博客

2.4 结果输出与程序结束

主要是将CUDA的结果输出到CPU,然后释放内存,重置CUDA,结束程序。

	//将数据拷贝到CPU
	cudaMemcpy(sum, d_sum, size, cudaMemcpyDeviceToHost);
	cudaMemcpy(max, d_max, size, cudaMemcpyDeviceToHost);
	cudaMemcpy(min, d_min, size, cudaMemcpyDeviceToHost);

	//输出
	cout << "The sum is :" << sum[0] << endl;
	cout << "The max is :" << max[0] << endl;
	cout << "The min is :" << min[0] << endl;

	//释放内存
	cudaFree(d_src);
	cudaFree(d_sum);
	cudaFree(d_max);
	cudaFree(d_min);

	//重置设备
	cudaDeviceReset();

3、核函数的具体实现过程

此小节为算法执行的最关键部分,也是CUDA加速的核心所在。核函数的运行过程是:当核函数启后,所有线程块同步运行(不一定完全同步),每个线程块独立运行,互不影响,但线程块内部的各个线程可以进行数据交互。

3.1 定义共享内存

通过共享内存实现线程块内各个线程的数据交互,从而进行最大值,最小值以及求和操作的实现。

	//定义线程块中各个线程的共享数组:40*40=1600
	const int number = 1600;
	extern __shared__ int _sum[];  //动态共享数组, 用于求和
	__shared__ int _max[number];   //静态共享数组, 用于最大值的求取
	__shared__ int _min[number];   //静态共享数组, 用于最小值的求取

3.2 计算每个线程对应在图像中的索引

包括下面三个方面:
1、计算线程块在图像中的绝对索引值。
2、线程块中的线程在图像中的绝对索引值。
3、线程在线程块中的索引值。

	//根据线程块和线程的索引, 依次对应到图像数组中
	//1、线程块在图像中的索引值
	int blockindex1 = blockIdx.x*blockDim.x + 2*blockIdx.y*blockDim.y*imgWidth;
	int blockindex2 = blockIdx.x*blockDim.x + (2*blockIdx.y*blockDim.y + blockDim.y)*imgWidth;

	//2、线程块中的线程在图像中的索引值
	int index1 = threadIdx.x + threadIdx.y*imgWidth + blockindex1;
	int index2 = threadIdx.x + threadIdx.y*imgWidth + blockindex2;

	//3、线程在线程块中的索引值
	int thread = threadIdx.y*blockDim.x + threadIdx.x;

为了清楚了展示以上的计算过程,绘制了以下说明图:
在这里插入图片描述
对上图做一个简单的解释:由于分配的线程块(40,20)不足以计算一个图像块(40,40)。因此一个线程块需要执行两次才可以计算完一个图像块。

我们的目的是得到线程块中每个线程对应图像中的像素坐标索引值,然后将该索引值对应的像素值保存到对应的共享内存中,便于后面的计算。包括以下过程:

  1. 计算线程块在图像中的绝对索引,blockindex1和blockindex2。blockindex2是第一个线程块在第二次运行时对应图像中的索引。
  2. 计算线程块中的线程在图像中的索引,index1和index2。index2是第一个线程在第二次运行时对应图像中的索引。
  3. 计算线程在线程块中的索引,作为后续共享内存保存数据的索引值,thread1和thread2。thread2是第一个线程在第二次运行时对应线程块中的索引。

3.3 保存图像块的每个像素值

将待计算的40*40小图像块中的所有像素值分两次传送到共享数组中

	//4、将待计算的40*40小图像块中的所有像素值分两次传送到共享数组中
	//将上半部分的40*20中所有数据赋值到共享数组中
	//将下半部分的40*20中所有数据赋值到共享数组中
	_sum[thread] = dataIn[index1];
	_sum[thread + blockDim.x*blockDim.y] = dataIn[index2];

	_max[thread] = dataIn[index1];
	_max[thread + blockDim.x*blockDim.y] = dataIn[index2];

	_min[thread] = dataIn[index1];
	_min[thread + blockDim.x*blockDim.y] = dataIn[index2];

3.4 使用归约算法计算最终结果

利用归约算法求出40*40小图像块中1600个像素值中的和、最大值以及最小值

	//利用归约算法求出40*40小图像块中1600个像素值中的和、最大值以及最小值
	//使用归约算法后,最大值,最小值以及各像素之和,均保存在线程块中第一个线程对应的索引中。
	for (size_t s=number/2; s>0; s>>=1 )
	{
    
    
		_sum[thread] = _sum[thread] + _sum[thread + s];
		if (_max[thread] < _max[thread + s]) _max[thread] = _max[thread + s];
		if (_min[thread] > _min[thread + s]) _min[thread] = _min[thread + s];

		__syncthreads(); //所有线程同步
	}

	//将每个线程块的第一个数据结果保存
	if (thread == 0)
	{
    
    
		dataOutSum[blockIdx.x + blockIdx.y*gridDim.x] = _sum[0];
		dataOutMax[blockIdx.x + blockIdx.y*gridDim.x] = _max[0];
		dataOutMin[blockIdx.x + blockIdx.y*gridDim.x] = _min[0];
	}

3.5 另一种线程模式

计算的方式与上面相似,不再过多的描述,见下一节完整代码。

4、完整工程代码

#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <cuda.h>
#include <device_functions.h>
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

//threadsPerBlock(40, 20)
__global__ void matSum(uchar* dataIn, int *dataOutSum, int *dataOutMax, int *dataOutMin, int imgHeight, int imgWidth)
{
    
    
	//定义线程块中各个线程的共享数组:40*40=1600
	const int number = 1600;
	extern __shared__ int _sum[];  //动态共享数组, 用于求和
	__shared__ int _max[number];   //静态共享数组, 用于最大值的求取
	__shared__ int _min[number];   //静态共享数组, 用于最小值的求取

	//根据线程块和线程的索引, 依次对应到图像数组中
	//1、线程块在图像中的索引值
	int blockindex1 = blockIdx.x*blockDim.x + 2*blockIdx.y*blockDim.y*imgWidth;
	int blockindex2 = blockIdx.x*blockDim.x + (2*blockIdx.y*blockDim.y + blockDim.y)*imgWidth;

	//2、线程块中的线程在图像中的索引值
	int index1 = threadIdx.x + threadIdx.y*imgWidth + blockindex1;
	int index2 = threadIdx.x + threadIdx.y*imgWidth + blockindex2;

	//3、线程在线程块中的索引值
	int thread = threadIdx.y*blockDim.x + threadIdx.x;

	//4、将待计算的40*40小图像块中的所有像素值分两次传送到共享数组中
	//将上半部分的40*20中所有数据赋值到共享数组中
	//将下半部分的40*20中所有数据赋值到共享数组中
	_sum[thread] = dataIn[index1];
	_sum[thread + blockDim.x*blockDim.y] = dataIn[index2];

	_max[thread] = dataIn[index1];
	_max[thread + blockDim.x*blockDim.y] = dataIn[index2];

	_min[thread] = dataIn[index1];
	_min[thread + blockDim.x*blockDim.y] = dataIn[index2];

	//利用归约算法求出40*40小图像块中1600个像素值中的和、最大值以及最小值
	//使用归约算法后,最大值,最小值以及各像素之和,均保存在线程块中第一个线程对应的索引中。
	for (size_t s=number/2; s>0; s>>=1 )
	{
    
    
		_sum[thread] = _sum[thread] + _sum[thread + s];
		if (_max[thread] < _max[thread + s]) _max[thread] = _max[thread + s];
		if (_min[thread] > _min[thread + s]) _min[thread] = _min[thread + s];

		__syncthreads(); //所有线程同步
	}

	//将每个线程块的第一个数据结果保存
	if (thread == 0)
	{
    
    
		dataOutSum[blockIdx.x + blockIdx.y*gridDim.x] = _sum[0];
		dataOutMax[blockIdx.x + blockIdx.y*gridDim.x] = _max[0];
		dataOutMin[blockIdx.x + blockIdx.y*gridDim.x] = _min[0];
	}
}

//threadsPerBlock(20, 40)
__global__ void matSum2(uchar* dataIn, int *dataOutSum, int *dataOutMax, int *dataOutMin, int imgHeight, int imgWidth)
{
    
    
	//定义线程块中各个线程的共享数组:40*40=1600
	const int number = 1600;
	extern __shared__ int _sum[];  //动态共享数组, 用于求和
	__shared__ int _max[number];   //静态共享数组, 用于最大值的求取
	__shared__ int _min[number];   //静态共享数组, 用于最小值的求取

	int blockIndex1 = blockIdx.y*blockDim.y*imgWidth + 2 * blockIdx.x*blockDim.x;
	int blockIndex2 = blockIdx.y*blockDim.y*imgWidth + (2 * blockIdx.x + 1)*blockDim.x;

	int threadIndex1 = blockIndex1 + threadIdx.y*imgWidth + threadIdx.x;
	int threadIndex2 = blockIndex2 + threadIdx.y*imgWidth + threadIdx.x;

	int thread = threadIdx.x + threadIdx.y*blockDim.x;

	_sum[thread] = dataIn[threadIndex1];
	_sum[thread + blockDim.x*blockDim.y] = dataIn[threadIndex2];

	_max[thread] = dataIn[threadIndex1];
	_max[thread + blockDim.x*blockDim.y] = dataIn[threadIndex2];

	_min[thread] = dataIn[threadIndex1];
	_min[thread + blockDim.x*blockDim.y] = dataIn[threadIndex2];

	for (size_t i = number / 2; i > 0; i >>= 1)
	{
    
    
		_sum[thread] = _sum[thread] + _sum[thread + i];
		if (_min[thread] > _min[thread + i]) _min[thread] = _min[thread + i];
		if (_max[thread] < _max[thread + i]) _max[thread] = _max[thread + i];

		__syncthreads(); //所有线程同步
	}

	if (thread == 0)
	{
    
    
		dataOutSum[blockIdx.x + blockIdx.y*gridDim.x] = _sum[0];
		dataOutMax[blockIdx.x + blockIdx.y*gridDim.x] = _max[0];
		dataOutMin[blockIdx.x + blockIdx.y*gridDim.x] = _min[0];
	}
}

int main()
{
    
    
	Mat image = imread("2.jpg", 0); //读取待检测图片
	cv::resize(image, image, cv::Size(1000, 8000));

	int sum[5000];       //求和结果数组
	int max[5000];       //最大值结果数组
	int min[5000];       //最小值结果数组

	//图像的所有字节数
	size_t memSize = image.cols*image.rows * sizeof(uchar);
	int size = 5000 * sizeof(int);

	//分配内存:GPU图像数据、求和结果数组、最大值结果数组、最小值结果数组
	uchar *d_src = NULL;
	int *d_sum = NULL;
	int *d_max = NULL;
	int *d_min = NULL;
	cudaMalloc((void**)&d_src, memSize);
	cudaMalloc((void**)&d_sum, size);
	cudaMalloc((void**)&d_max, size);
	cudaMalloc((void**)&d_min, size);

	//图像数据拷贝到GPU
	cudaMemcpy(d_src, image.data, memSize, cudaMemcpyHostToDevice);

	//图像宽、高
	int imgWidth = image.cols;
	int imgHeight = image.rows;

	//dim3 threadsPerBlock(20, 40); //每个block大小为20*40,对应matSum2核函数
	dim3 threadsPerBlock(40, 20); //每个block大小为40*20,对应matSum核函数

	dim3 blockPerGrid(25, 200); //将8000*1000的图片分为25*200个小图像块

	double time0 = static_cast<double>(getTickCount()); //计时器开始
	matSum << <blockPerGrid, threadsPerBlock, 3200 * sizeof(int) >> > (d_src, d_sum, d_max, d_min, imgHeight, imgWidth);

	//等待所有线程执行完毕
	cudaDeviceSynchronize();

	time0 = ((double)getTickCount() - time0) / getTickFrequency(); //计时器结束
	cout << "The Run Time is :" << time0 << "s" << endl; //输出运行时间

	//将数据拷贝到CPU
	cudaMemcpy(sum, d_sum, size, cudaMemcpyDeviceToHost);
	cudaMemcpy(max, d_max, size, cudaMemcpyDeviceToHost);
	cudaMemcpy(min, d_min, size, cudaMemcpyDeviceToHost);

	//输出
	cout << "The sum is :" << sum[0] << endl;
	cout << "The max is :" << max[0] << endl;
	cout << "The min is :" << min[0] << endl;

	//释放内存
	cudaFree(d_src);
	cudaFree(d_sum);
	cudaFree(d_max);
	cudaFree(d_min);

	//重置设备
	cudaDeviceReset();

	waitKey(0);
	return 0;
}

5、实验结果

在这里插入图片描述
由于线程块的运行顺序不一致,每次运行时,求和结果的第一个数据可能不同,但是最大值和最小值每次的运行结果应该一致。

6、其他

参考博客:https://blog.csdn.net/MGotze/article/details/75268746?spm=1001.2014.3001.5501

猜你喜欢

转载自blog.csdn.net/qq_38589460/article/details/120317152