使用CUDA和OpenCV将图像进行分块处理
1、概述
前面两篇文章介绍了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)。因此一个线程块需要执行两次才可以计算完一个图像块。
我们的目的是得到线程块中每个线程对应图像中的像素坐标索引值,然后将该索引值对应的像素值保存到对应的共享内存中,便于后面的计算。包括以下过程:
- 计算线程块在图像中的绝对索引,blockindex1和blockindex2。blockindex2是第一个线程块在第二次运行时对应图像中的索引。
- 计算线程块中的线程在图像中的索引,index1和index2。index2是第一个线程在第二次运行时对应图像中的索引。
- 计算线程在线程块中的索引,作为后续共享内存保存数据的索引值,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