基于OpenCL的直方图均衡化图像增强

基于OpenCL的直方图均衡化图像增强

  • 一、背景介绍
  • 二、直方图均衡化简介
    • 2.1 直方图归一化
    • 2.2 直方图均衡化数学思想
    • 2.3 直方图均衡化离散算法
  • 三、OpenCL实现
    • 3.1 kernel函数文件
    • 3.2 cpp文件程序
    • 3.3 运行效果图
    • 3.4 程序说明

一、背景介绍

偶然一次机会,帮助一个留学生做一个期末大作业吧,之前一直用CUDA,毕竟CUDA的生态环境还是好一些,然后CUDA的API也相对高层一些,OpenCL的的API真的是太底层了,写了半天代码还没开始分配内存,选择一个平台下的设备、查询一个设备参数都好复杂. 当然不是说API底层些就不好,只是说开发效率效率相对低些. 比如,C语言比Python底层多了吧,不依然活得好好的,没有死掉瑟,而且貌似变异型语言的工程师都有点鄙视Python工程师(不是我说的哈,勿喷)。

话说回来,要不是这帮他做作业,我也不知道啥时候能用上OpenCL,反正钱也挣了,也学习到了,一举两得。OpenCL当然相比CUDA也是有优势的,比如我写这个程序的时候发现OpenCL竟然支持全局同步,这真的是让第一次写OpenCL的我,垂死病中惊坐起啊。当然OpenCL的跨平台,估计也是很多粉丝选择它的原因之一吧,不光支持各厂家的GPU,还支持CPU和FPGA,貌似搞金融的人使用OpenCL+FPGA是个不错的选择. 当然这个说法不准确,不是说支持各家GPU和FPGA,只能说各硬件厂商根据OpenCL标准进行了给予各自硬件的实现。因此OpenCL更多的是一个标准和规范而不是一个编程语言活一个变成框架. 好吧,废话就不多说了。

二、直方图均衡化简介

图像增强有很多方法,基于空域的、基于频域的、基于特征工程的以及基于深度学习的等等。我就简单讲解下基于直方图均衡化的算法理论吧,有点班门弄斧请莫见笑。

首先还是说一下图像增强的初衷吧。很多时候,我们获取到的图像,由于光线、角度、镜头物理状况、镜头像素、分辨率等各种原因,导致我们得到的图像不是特别清晰,这时我们需要使用数学的方法将图像进行增强,使得图像更清晰逼真。图像不清晰往往是由于图像的对比度不够,因为图像的灰度值集中在了某一个区间,而不是分布在一个较大的范围内。因此,我们要设计一种算法,把原本图像的灰度范围扩大,而不改变图像本身要展示给我们的内容信息。

2.1 直方图归一化

图像的灰度级别(彩色图也同理,只是有多个颜色通道)取值通常在0255之间共256个取值,为了方便处理,我们通常将其约束到01的范围内,具体算法很简单,如下:
c o u n t s = c o u n t s M ∗ N counts=\frac{counts}{M*N} counts=MNcounts

2.2 直方图均衡化数学思想

为了便于分析,我们首先考虑灰度范围为0~1的情况,此时归一化的直方图即为概率密度函数:
p ( x ) , 0 ≤ x ≤ 1 p(x), \quad 0\le x\le 1 p(x),0x1
由概率密度函数的性质,有一下关系
∫ x = 0 1 p ( x ) d x = 1 \int_{x=0}^{1}p(x)dx=1 x=01p(x)dx=1
设均衡化前的概率密度函数为 p r ( r ) p_{r}(r) pr(r),转换后的概率密度函数为 p s ( s ) p_{s}(s) ps(s),转换函数(映射关系)为 s = f ( r ) s=f(r) s=f(r),由概率理论知识得到
p s ( s ) = p r ( r ) d r d s p_{s}(s)=p_{r}(r)\frac{dr}{ds} ps(s)=pr(r)dsdr
因为我们想让转换后的图像灰度值在0~255区间比较均匀的分布,因此最理想的状态就是每个灰度级别下的像素点个数相近(概率相等),因此我们假设 p s ( s ) = 1 p_{s}(s)=1 ps(s)=1,则原式必须满足:
KaTeX parse error: Undefined control sequence: \farc at position 9: p_r(r)=\̲f̲a̲r̲c̲{ds}{ds}
等式两边积分,得
s = f ( r ) = ∫ 0 r p r ( u ) d u s=f(r)=\int_{0}^{r}p_{r}(u)du s=f(r)=0rpr(u)du
该式被称为图像的累积分布函数(CDF)。

2.3 直方图均衡化离散算法

上式实在灰度值取[0,1]范围内的情况下推导出来的,对于[0,255]的情况,只要乘以最大灰度值 D m a x D_{max} Dmax(对于灰度图即255)即可,此时灰度均衡的转换公式变为
D B = f ( D A ) = D m a x ∫ 0 D A P D A ( u ) d u D_B=f(D_A)=D_{max}\int_{0}^{D_A} P_{D_A}(u)du DB=f(DA)=Dmax0DAPDA(u)du
其中, D B D_B DB为转换后的灰度值, D A D_A DA为转换前的灰度值。对于离散灰度级,相应的公式转换为
D B = f ( D A ) = D m a x A 0 ∑ i = 0 D A H i D_B=f(D_A)=\frac{D_{max}}{A_0}\sum_{i=0}^{D_A}H_i DB=f(DA)=A0Dmaxi=0DAHi
式中, H i H_i Hi为第 i i i级灰度的像素个数, A 0 A_0 A0为图像的面积,即图像像素的总数。

三、OpenCL实现

程序主要有两个文件组成,一个是cpp文件,一个是OpenCL kenrel核函数文件,这里直接上代码。

3.1 kernel函数文件

/*
hist          总的直方图
local_hist    每个工作组内的局部直方图
data_per_item 每个工作项处理的像素个数
all_byte_size 图像大小,byte级别
*/
#pragma OPENCL EXTENSION cl_khr_global_uchar_base_atomics:enable      // 开启原子操作(基本原子操作在本地内存中的32位整数)
#pragma OPENCL EXTENSION cl_khr_local_uchar_base_atomics:enable       // 这个还是很重要的,不然会提示不支持原子操作的函数
#pragma OPENCL EXTENSION cl_khr_global_int32_base_atomics:enable
#pragma OPENCL EXTENSION cl_khr_local_int32_base_atomics:enable
__kernel void imgToHist(__global const uchar* imgMat, __global int* hist,
	__local int* local_hist, uint data_per_item, uint all_byte_size)
{
	int l_idx = get_local_id(0);
	int g_idx = get_global_id(0);
	local_hist[l_idx] = 0;                     // 局部直方图数据初始化,这个需要工作组大小为256
	barrier(CLK_LOCAL_MEM_FENCE);              // 局部同步,即等到共享(局部)内存更新完毕

	int item_offset = g_idx * data_per_item;   // 每个工作项处理的像素点位置偏移
	for (unsigned int i = item_offset; i < item_offset + data_per_item && i < all_byte_size; i++) {
		atomic_inc(local_hist + imgMat[i]);
		//atomic_add(local_hist+imgMat[i], 1);
	}

	// 全局同步,等待全部线程都执行到这里
	// 这个功能还是很牛逼的,因为CUDA就不支持全局同步,为了全局同步只能重启kernel,这个会有不少的性能影响
	// 这也是我目前发现的唯一OpenCL比CUDA牛逼的地方
	// 另外OpenCL的原子操作好像比CUDA功能全一些
	barrier(CLK_GLOBAL_MEM_FENCE);            

	// 将每个工作组中的共享内存中的局部直方图合并,整理成为全局直方图
	if (l_idx < 256) {                        
		atomic_add(hist + l_idx, local_hist[l_idx]);
	}
}

// 直方图均衡化,这个地方因为问题本身可并行性不强,所以这个函数的执行效率跟串行一致,甚至更低一些
/*
   hist     全局直方图
   hist_eq  均衡化后的直方图
   imgSize  图像像素点个数
*/
__kernel void histEq(__global const int* hist, __global int* hist_eq, const int imgSize)
{
	int l_idx = get_local_id(0);
	//if (l_idx >= 256) return;
	for (int i = 0; i <= l_idx; i++)
		hist_eq[l_idx] += hist[i];
	hist_eq[l_idx] = hist_eq[l_idx] * 255 / imgSize;
}

// 将均衡化的直方图用到图像上
__kernel void histEqToImg(__global uchar* imgMat, __global int* hist_eq)
{
	int g_idx = get_global_id(0);
	imgMat[g_idx] = hist_eq[imgMat[g_idx]];
}

3.2 cpp文件程序

#include 
#include 
#include "Utils.h"                  //   大神写的一个工具库,对OpenCL常用工具的一层warp,地址:https://github.com/gcielniak/OpenCL-Tutorials/tree/master/include
#include "CImg.h"                   //  一个跨平台的图像处理库工具

using namespace cimg_library;
using namespace std;

int platform_id = 0;                // 平台序号:如Intel CPU/GPU平台,NVIDIA CUDA平台等
int device_id = 0;                  // 设备id,因为一个系统中,一个平台下可能有几个设备,比如NVIDIA平台下我就有3张显卡
string img_filename = "test.ppm";   // 图像数据

void print_help();                  // 帮助函数声明
cl_uint histEqual();                // 基于OpenCL的直方图均衡化实现
cl_device_id getdevice(int platform_id, int device_id);     // 通过平台id和设备id获取一个OpenCL设备对象

int main(int argc, char **argv)
{
	for (int i = 1; i < argc; i++) {
		if ((strcmp(argv[i], "-p") == 0) && (i < (argc - 1))) { platform_id = atoi(argv[++i]); }
		else if ((strcmp(argv[i], "-d") == 0) && (i < (argc - 1))) { device_id = atoi(argv[++i]); }
		else if (strcmp(argv[i], "-l") == 0) { std::cout << ListPlatformsDevices() << std::endl; }
		else if ((strcmp(argv[i], "-f") == 0) && (i < (argc - 1))) { img_filename = argv[++i]; }
		else if (strcmp(argv[i], "-h") == 0) { print_help(); return 0; }
	}

	histEqual();
	return 0;
}

void print_help() {
	std::cerr << "Application usage:" << std::endl;
	std::cerr << "  -p : select platform " << std::endl;
	std::cerr << "  -d : select device" << std::endl;
	std::cerr << "  -l : list all platforms and devices" << std::endl;
	std::cerr << "  -f : input image file (default: test.ppm)" << std::endl;
	std::cerr << "  -h : print this message" << std::endl;
}

cl_uint histEqual()
{
	cl_int err = 0;
	cl_device_id dev = getdevice(platform_id, device_id);
	size_t max_item_per_group = 0;
	// 获取单个工作组内工作项的最大取值
	clGetDeviceInfo(dev, CL_DEVICE_MAX_WORK_GROUP_SIZE, sizeof(max_item_per_group), &max_item_per_group, NULL);

	// 从图像数据文件中读取数据,注意这里使用的类型为unsigned char
	// 另外,CImg图像除了有x,y,depth之外,还有一个spectrum维度,我到现在都不知道是啥,
	// 当初不知道这个时写程序还走了许多弯路,现在直接把图转化为灰度图
	CImg<unsigned char> img_input_spectrum(img_filename.c_str());
	int h = img_input_spectrum.height();
	int w = img_input_spectrum.width();

	int size_per_item = 32;                                                // 设置让每个工作项处理32个像素点                                    
	int global_work_item_size = ceil(w*h / (float)size_per_item);
	global_work_item_size = ceil(global_work_item_size / 256.0f) * 256;    // 设置全局工作项的大小

	CImg<unsigned char> img_input(w, h, 1, 1, 0);                          // 初始化一个灰度图
	cimg_forXY(img_input_spectrum, x, y)                                   // 设置灰度图像的灰度值,这个有点像匿名函数的节奏?
	{
		img_input(x, y) = img_input_spectrum(x, y);
	}

	//std::cout<< img_input.height() <<", "<< img_input.width() <<"," << img_input.size() << std::endl;
	//std::cout << img_input.depth() << std::endl;
	//std::cout << img_input.spectrum() << std::endl;
	CImgDisplay disp_input(img_input, "input");                            // 显示图像

	cimg::exception_mode(0);                                               // 设置CImg的异常处理模式
	try {

		cl::Context context = GetContext(platform_id, device_id);          // 根据平台和设备获取上下文
		std::cout << "Runing on " << GetPlatformName(platform_id) << ", " << GetDeviceName(platform_id, device_id) << std::endl;
		cl::CommandQueue queue(context);                                   // 创建队列
		cl::Program::Sources sources;
		AddSources(sources, "img_hist_equal.cl");                          // 获取源码
		cl::Program program(context, sources);                             // 创建程序

		try {
			program.build();                                               // 编译程序
		}
		catch (const cl::Error& err) {                                     // 编译异常处理
			std::cout << "Build Status: " << program.getBuildInfo<CL_PROGRAM_BUILD_STATUS>(context.getInfo<CL_CONTEXT_DEVICES>()[0]) << std::endl;
			std::cout << "Build Options:\t" << program.getBuildInfo<CL_PROGRAM_BUILD_OPTIONS>(context.getInfo<CL_CONTEXT_DEVICES>()[0]) << std::endl;
			std::cout << "Build Log:\t " << program.getBuildInfo<CL_PROGRAM_BUILD_LOG>(context.getInfo<CL_CONTEXT_DEVICES>()[0]) << std::endl;
			throw err;
		}

		// 声明设备内存,用于存放输入图像,处理结果(输出)也存在这里
		cl::Buffer dev_img_input(context, CL_MEM_READ_ONLY, sizeof(unsigned char)*w*h);   
		cl::Buffer hist(context, CL_MEM_READ_WRITE, sizeof(int) * 256);     // 全局直方图分配内存
		cl::Buffer hist_eq(context, CL_MEM_READ_WRITE, sizeof(int) * 256);  // 局部直方图分配空间,局部内存类似CUDA的共享内存
		queue.enqueueWriteBuffer(dev_img_input, CL_TRUE, 0, w*h * sizeof(unsigned char), &img_input.data()[0]);   // 数据从host拷贝到OpenCL设备(这里是GPU)


		// Step-1 计算灰度图像的直方图
		cl::Kernel kernel1 = cl::Kernel(program, "imgToHist");              // 加载程序中指定名字的kernel函数
		err = kernel1.setArg(0, dev_img_input);
		err = kernel1.setArg(1, hist);
		err = kernel1.setArg(2, sizeof(int) * 256, NULL);                   // 局部内存(共享内存)不需要传递值
		err = kernel1.setArg(3, size_per_item);
		kernel1.setArg(4, w*h);
		err = queue.enqueueNDRangeKernel(kernel1, cl::NullRange, cl::NDRange(global_work_item_size), cl::NDRange(256));

		// Step-2 将直方图均衡化,即通过直方图均衡化算法,将每个像素原本的灰度值变为均衡化后的灰度值
		cl::Kernel kernel2 = cl::Kernel(program, "histEq");
		kernel2.setArg(0, hist);
		kernel2.setArg(1, hist_eq);
		kernel2.setArg(2, w*h);
		err = queue.enqueueNDRangeKernel(kernel2, cl::NullRange, cl::NDRange(256), cl::NDRange(256));

		// Step-3 通过均衡化后的直方图得到输出图像
		cl::Kernel kernel3 = cl::Kernel(program, "histEqToImg");
		kernel3.setArg(0, dev_img_input);
		kernel3.setArg(1, hist_eq);
		err = queue.enqueueNDRangeKernel(kernel3, cl::NullRange, cl::NDRange(img_input.size()), cl::NullRange);

		// 数据的传输和转换,方便生成图像格式
		vector<unsigned char> output_buffer(img_input.size());
		err = queue.enqueueReadBuffer(dev_img_input, CL_TRUE, 0, w*h * sizeof(unsigned char), &output_buffer.data()[0]);

		CImg<unsigned char> output_img(output_buffer.data(), w, h, 1, 1);
		CImgDisplay disp_output(output_img, "output");

		while (!disp_input.is_closed() && !disp_output.is_closed() && !disp_input.is_keyESC() && !disp_output.is_keyESC()) {
			disp_input.wait(5000);                                       // 图像显示窗口暂留时间
			disp_output.wait(5000);
		}
		return 0;
	}
	catch (const cl::Error& err) {
		std::cerr << "ERROR: " << err.what() << ", " << getErrorString(err.err()) << std::endl;
		return -1;
	}
	catch (CImgException& err) {
		std::cerr << "ERROR: " << err.what() << std::endl;
		return -1;
	}
}


cl_device_id getdevice(int platform_id, int device_id)
{
	cl_platform_id *platforms;
	cl_device_id *devices;
	cl_uint platform_size, device_size;
	int err;
	err = clGetPlatformIDs(0, NULL, &platform_size);
	platforms = (cl_platform_id*)malloc(sizeof(platform_id)*platform_size);
	err = clGetPlatformIDs(1, platforms, NULL);

	err = clGetDeviceIDs(platforms[platform_id], CL_DEVICE_TYPE_ALL, 0, NULL, &device_size);
	devices = (cl_device_id*)malloc(sizeof(cl_device_id)*device_size);
	err = clGetDeviceIDs(platforms[platform_id], CL_DEVICE_TYPE_ALL, sizeof(cl_device_id)*device_size, devices, NULL);
	cl_device_id dev(devices[0]); // = devices[device_id];
	free(platforms); free(devices);

	return dev;
}

3.3 运行效果图

运行结果如下:

3.4 程序说明

  1. 程序中会用到两个库工具,一个是对OpenCL的包装,一个是CImg跨平台图像处理库,两者均可在网上下载.
    下载地址

  2. 程序中会用到一个数据文件,也可以从上述地址对应的项目去下载;

  3. 另外,我也没有做根CPU串行的执行效率相对比,因为我写的CUDA并行程序真的是太多了,真的是不想再弄了,有热心的读者可以自己测试,然后提交给我,万分感激!

你可能感兴趣的:(高性能计算)