偶然一次机会,帮助一个留学生做一个期末大作业吧,之前一直用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更多的是一个标准和规范而不是一个编程语言活一个变成框架. 好吧,废话就不多说了。
图像增强有很多方法,基于空域的、基于频域的、基于特征工程的以及基于深度学习的等等。我就简单讲解下基于直方图均衡化的算法理论吧,有点班门弄斧请莫见笑。
首先还是说一下图像增强的初衷吧。很多时候,我们获取到的图像,由于光线、角度、镜头物理状况、镜头像素、分辨率等各种原因,导致我们得到的图像不是特别清晰,这时我们需要使用数学的方法将图像进行增强,使得图像更清晰逼真。图像不清晰往往是由于图像的对比度不够,因为图像的灰度值集中在了某一个区间,而不是分布在一个较大的范围内。因此,我们要设计一种算法,把原本图像的灰度范围扩大,而不改变图像本身要展示给我们的内容信息。
图像的灰度级别(彩色图也同理,只是有多个颜色通道)取值通常在0255之间共256个取值,为了方便处理,我们通常将其约束到01的范围内,具体算法很简单,如下:
c o u n t s = c o u n t s M ∗ N counts=\frac{counts}{M*N} counts=M∗Ncounts
为了便于分析,我们首先考虑灰度范围为0~1的情况,此时归一化的直方图即为概率密度函数:
p ( x ) , 0 ≤ x ≤ 1 p(x), \quad 0\le x\le 1 p(x),0≤x≤1
由概率密度函数的性质,有一下关系
∫ 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)。
上式实在灰度值取[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)=Dmax∫0DAPDA(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=0∑DAHi
式中, H i H_i Hi为第 i i i级灰度的像素个数, A 0 A_0 A0为图像的面积,即图像像素的总数。
程序主要有两个文件组成,一个是cpp文件,一个是OpenCL kenrel核函数文件,这里直接上代码。
/*
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]];
}
#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;
}
运行结果如下:
程序中会用到两个库工具,一个是对OpenCL的包装,一个是CImg跨平台图像处理库,两者均可在网上下载.
下载地址
程序中会用到一个数据文件,也可以从上述地址对应的项目去下载;
另外,我也没有做根CPU串行的执行效率相对比,因为我写的CUDA并行程序真的是太多了,真的是不想再弄了,有热心的读者可以自己测试,然后提交给我,万分感激!